diff --git a/astrbot/core/pipeline/process_stage/method/llm_request.py b/astrbot/core/pipeline/process_stage/method/llm_request.py index d22a1f453..c2f91399c 100644 --- a/astrbot/core/pipeline/process_stage/method/llm_request.py +++ b/astrbot/core/pipeline/process_stage/method/llm_request.py @@ -33,6 +33,7 @@ from mcp.types import ( TextResourceContents, BlobResourceContents, ) +from astrbot.core import web_chat_back_queue class LLMRequestSubStage(Stage): @@ -287,7 +288,64 @@ class LLMRequestSubStage(Stage): if img_b64 := event.get_extra("tool_call_img_respond"): await event.send(MessageChain(chain=[Image.fromBase64(img_b64)])) event.set_extra("tool_call_img_respond", None) - yield + + if event.get_platform_name() == "webchat": + # 异步处理 WebChat 特殊情况 + asyncio.create_task(self._handle_webchat(event, req)) + + async def _handle_webchat(self, event: AstrMessageEvent, req: ProviderRequest): + """处理 WebChat 平台的特殊情况,包括第一次 LLM 对话时总结对话内容生成 title""" + conversation = await self.conv_manager.get_conversation( + event.unified_msg_origin, req.conversation.cid + ) + if conversation and not req.conversation.title: + messages = json.loads(conversation.history) + latest_pair = messages[-2:] + if not latest_pair: + return + provider = self.ctx.plugin_manager.context.get_using_provider() + cleaned_text = "User: " + latest_pair[0].get("content", "").strip() + # if len(latest_pair) > 1: + # cleaned_text += ( + # "\nAssistant: " + latest_pair[1].get("content", "").strip() + # ) + logger.debug(f"WebChat 对话标题生成请求,清理后的文本: {cleaned_text}") + llm_resp = await provider.text_chat( + system_prompt="You are expert in summarizing user's query.", + prompt=( + f"Please summarize the following query of user:\n" + f"{cleaned_text}\n" + "Only output the summary within 10 words, DO NOT INCLUDE any other text." + "You must use the same language as the user." + "If you think the dialog is too short to summarize, just output a special mark: `_None_`" + ), + ) + if llm_resp and llm_resp.completion_text: + logger.debug( + f"WebChat 对话标题生成响应: {llm_resp.completion_text.strip()}" + ) + title = llm_resp.completion_text.strip() + if not title or "_None_" in title: + return + await self.conv_manager.update_conversation_title( + event.unified_msg_origin, title=title + ) + # 由于 WebChat 平台特殊性,其有两个对话,因此我们要更新两个对话的标题 + # webchat adapter 中,session_id 的格式是 f"webchat!{username}!{cid}" + # TODO: 优化 WebChat 适配器的对话管理 + if event.session_id: + username, cid = event.session_id.split("!")[1:3] + db_helper = self.ctx.plugin_manager.context._db + db_helper.update_conversation_title( + user_id=username, + cid=cid, + title=title, + ) + web_chat_back_queue.put_nowait({ + "type": "update_title", + "cid": cid, + "data": title, + }) async def _handle_llm_response( self, diff --git a/astrbot/dashboard/routes/chat.py b/astrbot/dashboard/routes/chat.py index 95ac98da6..24627a6e9 100644 --- a/astrbot/dashboard/routes/chat.py +++ b/astrbot/dashboard/routes/chat.py @@ -26,6 +26,7 @@ class ChatRoute(Route): "/chat/conversations": ("GET", self.get_conversations), "/chat/get_conversation": ("GET", self.get_conversation), "/chat/delete_conversation": ("GET", self.delete_conversation), + "/chat/rename_conversation": ("POST", self.rename_conversation), "/chat/get_file": ("GET", self.get_file), "/chat/post_image": ("POST", self.post_image), "/chat/post_file": ("POST", self.post_file), @@ -100,7 +101,6 @@ class ChatRoute(Route): file = post_data["file"] filename = f"{str(uuid.uuid4())}" - print(file) # 通过文件格式判断文件类型 if file.content_type.startswith("audio"): filename += ".wav" @@ -135,22 +135,24 @@ class ChatRoute(Route): self.curr_user_cid[username] = conversation_id - await web_chat_queue.put(( - username, - conversation_id, - { - "message": message, - "image_url": image_url, # list - "audio_url": audio_url, - }, - )) + await web_chat_queue.put( + ( + username, + conversation_id, + { + "message": message, + "image_url": image_url, # list + "audio_url": audio_url, + }, + ) + ) # 持久化 conversation = self.db.get_conversation_by_user_id(username, conversation_id) try: history = json.loads(conversation.history) except BaseException as e: - print(e) + logger.error(f"Failed to parse conversation history: {e}") history = [] new_his = {"type": "user", "message": message} if image_url: @@ -204,6 +206,9 @@ class ChatRoute(Route): if streaming and type != "end": continue + if type == "update_title": + continue + if result_text: conversation = self.db.get_conversation_by_user_id( username, cid @@ -211,7 +216,7 @@ class ChatRoute(Route): try: history = json.loads(conversation.history) except BaseException as e: - print(e) + logger.error(f"Failed to parse conversation history: {e}") history = [] history.append({"type": "bot", "message": result_text}) self.db.update_conversation( @@ -249,6 +254,18 @@ class ChatRoute(Route): self.db.new_conversation(username, conversation_id) return Response().ok(data={"conversation_id": conversation_id}).__dict__ + async def rename_conversation(self): + username = g.get("username", "guest") + post_data = await request.json + if "conversation_id" not in post_data or "title" not in post_data: + return Response().error("Missing key: conversation_id or titke").__dict__ + + conversation_id = post_data["conversation_id"] + title = post_data["title"] + + self.db.update_conversation_title(username, conversation_id, title=title) + return Response().ok(message="重命名成功!").__dict__ + async def get_conversations(self): username = g.get("username", "guest") conversations = self.db.get_conversations(username) diff --git a/dashboard/src/views/ChatPage.vue b/dashboard/src/views/ChatPage.vue index e0ad762ac..5368a6668 100644 --- a/dashboard/src/views/ChatPage.vue +++ b/dashboard/src/views/ChatPage.vue @@ -27,9 +27,14 @@ marked.setOptions({ - 新对话 + {{ item.title || '新对话'}} {{ formatDate(item.updated_at) }} + + @@ -195,6 +200,29 @@ marked.setOptions({ + + + + + 编辑对话标题 + + + + + + 取消 + 保存 + + +