feat: 支持自动生成webchat title

This commit is contained in:
Soulter
2025-06-10 10:58:49 +08:00
parent 725a841a3b
commit a6d91933dc
3 changed files with 166 additions and 14 deletions
@@ -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,
+29 -12
View File
@@ -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)
+78 -1
View File
@@ -27,9 +27,14 @@ marked.setOptions({
<template v-slot:prepend>
<v-icon size="small" icon="mdi-message-text-outline"></v-icon>
</template>
<v-list-item-title class="conversation-title">新对话</v-list-item-title>
<v-list-item-title class="conversation-title">{{ item.title || '新对话'}}</v-list-item-title>
<v-list-item-subtitle class="timestamp">{{ formatDate(item.updated_at)
}}</v-list-item-subtitle>
<template v-slot:append>
<v-btn icon="mdi-pencil" size="x-small" variant="text"
class="edit-title-btn" @click.stop="showEditTitleDialog(item.cid, item.title)" />
</template>
</v-list-item>
</v-list>
</v-card>
@@ -195,6 +200,29 @@ marked.setOptions({
</div>
</v-card-text>
</v-card>
<!-- 编辑对话标题对话框 -->
<v-dialog v-model="editTitleDialog" max-width="400">
<v-card>
<v-card-title class="dialog-title">编辑对话标题</v-card-title>
<v-card-text>
<v-text-field
v-model="editingTitle"
label="对话标题"
variant="outlined"
hide-details
class="mt-2"
@keyup.enter="saveTitle"
autofocus
/>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text @click="editTitleDialog = false" color="grey-darken-1">取消</v-btn>
<v-btn text @click="saveTitle" color="primary">保存</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
@@ -230,6 +258,11 @@ export default {
ctrlKeyLongPressThreshold: 300, // 长按阈值,单位毫秒
mediaCache: {}, // Add a cache to store media blobs
// 添加对话标题编辑相关变量
editTitleDialog: false,
editingTitle: '',
editingCid: '',
}
},
@@ -264,6 +297,34 @@ export default {
},
methods: {
// 显示编辑对话标题对话框
showEditTitleDialog(cid, title) {
this.editingCid = cid;
this.editingTitle = title || ''; // 如果标题为空,则设置为空字符串
this.editTitleDialog = true;
},
// 保存对话标题
saveTitle() {
if (!this.editingCid) return;
axios.post('/api/chat/rename_conversation', {
conversation_id: this.editingCid,
title: this.editingTitle
})
.then(response => {
// 更新本地对话列表中的标题
const conversation = this.conversations.find(c => c.cid === this.editingCid);
if (conversation) {
conversation.title = this.editingTitle;
}
this.editTitleDialog = false;
})
.catch(err => {
console.error('重命名对话失败:', err);
});
},
async getMediaFile(filename) {
if (this.mediaCache[filename]) {
return this.mediaCache[filename];
@@ -374,6 +435,14 @@ export default {
} else if (chunk_json.type === 'end') {
in_streaming = false;
continue;
} else if (chunk_json.type === 'update_title') {
// 更新对话标题
const conversation = this.conversations.find(c => c.cid === chunk_json.cid);
if (conversation) {
conversation.title = chunk_json.data;
}
} else {
console.warn('未知数据类型:', chunk_json.type);
}
this.scrollToBottom();
}
@@ -799,6 +868,7 @@ export default {
height: auto !important;
min-height: 56px;
padding: 8px 12px !important;
position: relative; /* 确保相对定位,便于添加编辑按钮 */
}
.conversation-item:hover {
@@ -1152,4 +1222,11 @@ export default {
.fade-in {
animation: fadeIn 0.3s ease-in-out;
}
/* 对话框标题样式 */
.dialog-title {
font-size: 18px;
font-weight: 500;
padding-bottom: 8px;
}
</style>