From 4e9f609c5bc09ef2ad3dd57d8fd2b660fe11b858 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E5=8B=A4=E6=B0=91?= Date: Thu, 16 Apr 2026 16:01:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(chat):=20=E5=AE=9E=E7=8E=B0=E8=81=8A?= =?UTF-8?q?=E5=A4=A9=E7=95=8C=E9=9D=A2=E5=8A=9F=E8=83=BD=E5=A2=9E=E5=BC=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加继续命令到离线命令列表 - 将聊天Activity重构为列表布局支持多条消息显示 - 集成Markwon库实现Markdown格式内容渲染 - 实现聊天消息数据模型和列表适配器 - 添加滚动到最新消息功能 - 实现循环问题轮询机制支持连续对话 - 优化SSE流处理和异常处理逻辑 - 更新应用基础URL配置 - 移除旧的单消息布局改为RecyclerView列表布局 - 添加聊天项点击触发新问题功能 --- .../com/nova/brain/glass/MyApplication.java | 4 +- .../glass/helper/OfflineCmdServiceHelper.kt | 1 + .../com/nova/brain/glass/ui/ChatActivity.kt | 53 ++++++----- .../com/nova/brain/glass/viewmodel/ChatVM.kt | 90 +++++++++++++++---- app/src/main/res/layout/activity_chat.xml | 27 ++---- app/src/main/res/layout/item_chat.xml | 1 + 6 files changed, 110 insertions(+), 66 deletions(-) diff --git a/app/src/main/java/com/nova/brain/glass/MyApplication.java b/app/src/main/java/com/nova/brain/glass/MyApplication.java index 46e175e..c867a10 100644 --- a/app/src/main/java/com/nova/brain/glass/MyApplication.java +++ b/app/src/main/java/com/nova/brain/glass/MyApplication.java @@ -13,8 +13,8 @@ import com.xuqm.base.di.manager.HttpManager; */ public class MyApplication extends App { - public static String baseUrl = "http://192.168.6.20"; -// public static String baseUrl = "http://22fs132201.imwork.net"; +// public static String baseUrl = "http://192.168.6.20"; + public static String baseUrl = "http://22fs132201.imwork.net"; @Override public void onCreate() { diff --git a/app/src/main/java/com/nova/brain/glass/helper/OfflineCmdServiceHelper.kt b/app/src/main/java/com/nova/brain/glass/helper/OfflineCmdServiceHelper.kt index 1367fb2..17cb4a3 100644 --- a/app/src/main/java/com/nova/brain/glass/helper/OfflineCmdServiceHelper.kt +++ b/app/src/main/java/com/nova/brain/glass/helper/OfflineCmdServiceHelper.kt @@ -15,6 +15,7 @@ object OfflineCmdServiceHelper { // 所有页面通用关键词(常量,只分配一次) private val COMMON_CMDS = listOf( + OfflineCmdBean("继续", "ji xu"), OfflineCmdBean("退出", "tui chu"), OfflineCmdBean("返回", "fan hui") ) diff --git a/app/src/main/java/com/nova/brain/glass/ui/ChatActivity.kt b/app/src/main/java/com/nova/brain/glass/ui/ChatActivity.kt index 6201d04..2c37d98 100644 --- a/app/src/main/java/com/nova/brain/glass/ui/ChatActivity.kt +++ b/app/src/main/java/com/nova/brain/glass/ui/ChatActivity.kt @@ -1,47 +1,56 @@ package com.nova.brain.glass.ui import android.widget.TextView -import androidx.lifecycle.ViewModelProvider import com.nova.brain.glass.R import com.nova.brain.glass.databinding.ActivityChatBinding import com.nova.brain.glass.helper.OfflineCmdListener import com.nova.brain.glass.helper.OfflineCmdServiceHelper import com.nova.brain.glass.model.ChatItem import com.nova.brain.glass.viewmodel.ChatVM -import com.nova.brain.glass.viewmodel.WelcomeVM import com.xuqm.base.adapter.CommonPagedAdapter import com.xuqm.base.adapter.ViewHolder -import com.xuqm.base.ui.BaseActivity +import com.xuqm.base.ui.BaseListFormLayoutNormalActivity import io.noties.markwon.Markwon -class ChatActivity : BaseActivity() { +class ChatActivity : BaseListFormLayoutNormalActivity() { override fun getLayoutId(): Int = R.layout.activity_chat override fun fullscreen(): Boolean = true + + private val markwon: Markwon by lazy { Markwon.create(this) } + private val listener = object : OfflineCmdListener { override fun onOfflineCmd(cmd: String) { runOnUiThread { when (cmd) { - "退出", "返回", "退回" -> { - finish() - } + "退出", "返回", "退回" -> finish() + "继续" -> viewModel.demoPostSse() } } } } - private lateinit var markwon: Markwon - private lateinit var vm: ChatVM + override fun initData() { super.initData() - vm = ViewModelProvider(this)[ChatVM::class.java] - markwon = Markwon.create(this) - - binding.title.text = "我的代办任务有哪些?" - vm.result.observe( this){ - binding.content.text = it + viewModel.result.observe(this) { + val lastIndex = (recyclerView.adapter?.itemCount ?: 1) - 1 + if (lastIndex >= 0) recyclerView.scrollToPosition(lastIndex) } - vm.demoPostSse() + viewModel.demoPostSse() + } + + override fun adapter() = object : CommonPagedAdapter(R.layout.item_chat) { + override fun convert(holder: ViewHolder, item: ChatItem, position: Int) { + val chatItems = viewModel.chatItems + val liveItem = if (position < chatItems.size) chatItems[position] else item + holder.setText(R.id.title, liveItem.title) + val tv = holder.getView(R.id.content) + markwon.setMarkdown(tv, liveItem.content) + holder.setClickListener(R.id.root, { + viewModel.demoPostSse() + }) + } } override fun onResume() { @@ -53,14 +62,4 @@ class ChatActivity : BaseActivity() { super.onPause() OfflineCmdServiceHelper.removeOnLineListener(listener) } - -// private val adapter = object : CommonPagedAdapter(R.layout.item_chat) { -// override fun convert(holder: ViewHolder, item: ChatItem, position: Int) { -// holder.setText(R.id.title, item.title) -// -// val tv = holder.getView(R.id.content) -// markwon.setMarkdown(tv, item.content); -// } -// } - -} +} \ No newline at end of file diff --git a/app/src/main/java/com/nova/brain/glass/viewmodel/ChatVM.kt b/app/src/main/java/com/nova/brain/glass/viewmodel/ChatVM.kt index 50cc107..aecfcdf 100644 --- a/app/src/main/java/com/nova/brain/glass/viewmodel/ChatVM.kt +++ b/app/src/main/java/com/nova/brain/glass/viewmodel/ChatVM.kt @@ -1,6 +1,7 @@ package com.nova.brain.glass.viewmodel import androidx.lifecycle.MutableLiveData +import com.nova.brain.glass.model.ChatItem import com.nova.brain.glass.model.ChatModel import com.nova.brain.glass.model.ChatModel1 import com.nova.brain.glass.model.ChatModel2 @@ -8,45 +9,97 @@ import com.nova.brain.glass.model.data.ChatData import com.nova.brain.glass.repository.Service import com.xuqm.base.common.GsonImplHelp import com.xuqm.base.di.manager.HttpManager -import com.xuqm.sdhbwfu.core.viewModel.BaseViewModel +import com.xuqm.base.viewmodel.BaseListViewModel +import com.xuqm.base.viewmodel.callback.Response +import android.os.Handler +import android.os.Looper +import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers - -class ChatVM : BaseViewModel() { +class ChatVM : BaseListViewModel() { val result = MutableLiveData() - private var t = "string" + val chatItems: MutableList = mutableListOf() + + private var currentTask: Disposable? = null + + private val questions = listOf( + "我的代办任务有哪些?", + "今天天气情况如何?", + "当前设备状态如何?", + "今日巡检计划是什么?", + "有哪些待处理的告警?", + "最近的维修记录有哪些?", + "当前区域的安全评分是多少?", + "有哪些设备需要维护?", + "今天有哪些会议安排?", + "当前库存状态如何?" + ) + private var questionIndex = 0 + private var dataSourceReady = false + private val mainHandler = Handler(Looper.getMainLooper()) + + override fun loadData(page: Int, onResponse: Response) { + dataSourceReady = true + if (page == 0) { + onResponse.onResponse(ArrayList(chatItems)) + } else { + onResponse.onResponse(ArrayList()) + } + } + fun demoPostSse() { - t = "" - HttpManager.getApi(Service::class.java).chat(ChatData("我的代办任务有哪些?")) + currentTask?.dispose() + currentTask = null + + val question = questions[questionIndex % questions.size] + questionIndex++ + + chatItems.add(ChatItem(question, "")) + if (dataSourceReady) invalidate() + + currentTask = HttpManager.getApi(Service::class.java).chat(ChatData(question)) .subscribeOn(Schedulers.io()) .subscribe({ body -> - var sb = "" + var content = "" + var type = "" body.charStream().buffered().use { reader -> try { var line: String? while (reader.readLine().also { line = it } != null) { val l = line!! if (l.isNotEmpty()) { + if (l.trimStart().startsWith("<")) { + // HTML响应(隧道断开),回退问题索引并在主线程重试 + mainHandler.post { + questionIndex-- + if (chatItems.isNotEmpty()) chatItems.removeAt(chatItems.size - 1) + demoPostSse() + } + return@use + } val json = if (l.startsWith("data:")) l.removePrefix("data:").trim() else l val model = GsonImplHelp.get().toObject(json, ChatModel::class.java) if (model.type == null) { result.postValue(model.msg ?: json) return@use } - if (t != model.type) { - sb = "" + if (type != model.type) { + content = "" } - t = model.type + type = model.type if (model.type == "string") { - val model1 = - GsonImplHelp.get().toObject(json, ChatModel1::class.java) - sb+=model1.data + val model1 = GsonImplHelp.get().toObject(json, ChatModel1::class.java) + content += model1.data } else { - val model2 = - GsonImplHelp.get().toObject(json, ChatModel2::class.java) - sb+=model2.data.content + val model2 = GsonImplHelp.get().toObject(json, ChatModel2::class.java) + content += model2.data.content } - result.postValue(sb) + val lastIndex = chatItems.size - 1 + if (lastIndex >= 0) { + chatItems[lastIndex] = chatItems[lastIndex].copy(content = content) + notifyItem(lastIndex) + } + result.postValue(content) } } } catch (e: Exception) { @@ -55,6 +108,7 @@ class ChatVM : BaseViewModel() { } }, { e -> result.postValue("AI反馈异常: ${e.message}") - }).adds() + }) + currentTask?.also { add(it) } } } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_chat.xml b/app/src/main/res/layout/activity_chat.xml index a2eaa94..6f76d3f 100644 --- a/app/src/main/res/layout/activity_chat.xml +++ b/app/src/main/res/layout/activity_chat.xml @@ -1,26 +1,15 @@ - + android:background="@color/app_color_black"> - + android:layout_height="match_parent" + android:overScrollMode="never" /> - - - + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_chat.xml b/app/src/main/res/layout/item_chat.xml index ea71c0a..f4bbf3f 100644 --- a/app/src/main/res/layout/item_chat.xml +++ b/app/src/main/res/layout/item_chat.xml @@ -3,6 +3,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="30dp" + android:id="@+id/root" android:paddingHorizontal="29dp" android:paddingVertical="10dp" android:orientation="vertical">