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({
+
+
+
+
+ 编辑对话标题
+
+
+
+
+
+ 取消
+ 保存
+
+
+