From ed34dc554cfcc8a72efcbc68b16744c5e3b39af1 Mon Sep 17 00:00:00 2001 From: shaohuzhang1 Date: Wed, 2 Sep 2026 19:03:53 +0800 Subject: [PATCH] fix: Form submission cannot have a conversation --- apps/chat/serializers/chat.py | 26 +-- .../impl/response/system_to_response.py | 2 +- .../conversation/chat-panel/index.vue | 152 +++++++++++------- .../common/use-chat-store/chat/index.ts | 48 ++---- .../common/use-chat-store/debug/index.ts | 33 ++-- .../conversation/content/items/form.vue | 50 ++---- .../components/conversation/sidebar/index.vue | 11 +- 7 files changed, 162 insertions(+), 160 deletions(-) diff --git a/apps/chat/serializers/chat.py b/apps/chat/serializers/chat.py index c01a0a9f6aa..06d2bcf3029 100644 --- a/apps/chat/serializers/chat.py +++ b/apps/chat/serializers/chat.py @@ -58,7 +58,6 @@ from models_provider.tools import get_model_instance_by_model_workspace_id from system_manage.models.chat_user_token_quota import ChatUserTokenQuota -# 「Chat 行尚未查询」的哨兵,区别于「查询过但不存在(None)」 _CHAT_UNSET = object() @@ -200,13 +199,10 @@ def ensure_chat_row(self, question, asker): self._chat_cache = chat return chat - def save_chat_record(self, chat_record_id, question, asker, new_record): - """插入一条占位 ChatRecord(workflow 完成后由 update_chat_record 回填)。""" - chat_id = self.data.get("chat_id") - q_text = question.get("content", "") if isinstance(question, dict) else question - self.ensure_chat_row(q_text, asker) - defaults = { - "chat_id": chat_id, + def get_defaults_record(self, question): + """构造一条占位 ChatRecord 的字段(workflow 完成后由 update_chat_record 回填)。""" + return { + "chat_id": self.data.get("chat_id"), "problem_text": "", "answer_text": "", "details": {}, @@ -222,12 +218,6 @@ def save_chat_record(self, chat_record_id, question, asker, new_record): "question": question, "messages": [], } - if new_record: - # 全新记录:直接插入,省掉 update_or_create 的一次 SELECT - ChatRecord(id=chat_record_id, **defaults).save(force_insert=True) - else: - # Form 提交 / 重答:复用同一 chat_record_id - QuerySet(ChatRecord).update_or_create(id=chat_record_id, defaults=defaults) @staticmethod def _usage_from_context(workflow_context): @@ -288,6 +278,9 @@ def chat_work_flow(self, application, instance: dict, base_to_response): workflow = new_instance(work_flow, WorkflowType.APPLICATION) chat_record_id_str = str(uuid.uuid7()) if chat_record_id is None else str(chat_record_id) + self.ensure_chat_row(message, chat_user) + if chat_record_id is None: + ChatRecord(id=chat_record_id_str, **self.get_defaults_record(message_dict)).save(force_insert=True) parameters = { "history_chat_record": history_chat_record, @@ -317,12 +310,10 @@ def chat_work_flow(self, application, instance: dict, base_to_response): result_queue = queue.Queue() aggregation = AggregationManager() - self.save_chat_record(chat_record_id_str, message_dict, chat_user, new_record=chat_record_id is None) def on_next(wf_manage, content): aggregation.aggregate(content) block = content.to_dict() - # 持久化(resume)与 live 队列都放裸内容块,格式化统一交给消费端的 base_to_response get_message_queue().produce(chat_record_id_str, block) result_queue.put(("chunk", block)) @@ -337,9 +328,7 @@ def on_complete(wf_manage, error): ) messages = aggregation.get_contents() self.update_chat_record(chat_user_id, chat_record_id_str, wf_manage.context, messages) - # 计数统计放到内容落库之后再更新(挪出进对话前的关键路径) ChatCountSerializer(data={"chat_id": chat_id}).update_chat() - # 定稿后把本轮记录追加进历史缓存(question 已在建占位时确定,messages 为聚合结果) ChatHistory(chat_id).append( ChatRecord( id=chat_record_id_str, @@ -403,7 +392,6 @@ def generate(): yield "data: [DONE]\n\n" break if msg_type == "chunk": - # data 是裸内容块(content.to_dict()),格式化交给 base_to_response frame = base_to_response.to_stream(chat_id, chat_record_id_str, data) if frame is not None: yield "data: " + frame + "\n\n" diff --git a/apps/common/handle/impl/response/system_to_response.py b/apps/common/handle/impl/response/system_to_response.py index 29243613ae4..daabc6cc1b5 100644 --- a/apps/common/handle/impl/response/system_to_response.py +++ b/apps/common/handle/impl/response/system_to_response.py @@ -23,7 +23,7 @@ def to_stream(self, chat_id, chat_record_id, block: dict): { "chat_id": str(chat_id), "chat_record_id": str(chat_record_id), - "content": [block], + "content": [{**block, "chat_id": str(chat_id), "chat_record_id": str(chat_record_id)}], }, ensure_ascii=False, ) diff --git a/ui/src/components/conversation/chat-panel/index.vue b/ui/src/components/conversation/chat-panel/index.vue index 43e93d5b71a..92dfa0617a7 100644 --- a/ui/src/components/conversation/chat-panel/index.vue +++ b/ui/src/components/conversation/chat-panel/index.vue @@ -236,7 +236,7 @@ diff --git a/ui/src/components/conversation/common/use-chat-store/chat/index.ts b/ui/src/components/conversation/common/use-chat-store/chat/index.ts index 35d1807c9b8..dee38731556 100644 --- a/ui/src/components/conversation/common/use-chat-store/chat/index.ts +++ b/ui/src/components/conversation/common/use-chat-store/chat/index.ts @@ -10,6 +10,9 @@ import type { ChatMessage } from '../../types' // ── 共享状态(单例) ───────────────────────────────────── const appInfo = ref<{ name: string; icon: string } | null>(null) const currentChatId = ref('') +// composer 重置信号:新建/切换会话时自增,chat-panel 监听后清空暂存文件与输入, +// 避免上传文件(source_id 绑旧 chat_id)在切上下文后与新 chat_id 对不上。 +const composerResetSignal = ref(0) // ── 会话 CRUD ───────────────────────────────────────── const { conversations, loadConversations, loadMore } = useConversationCrud({ @@ -78,16 +81,13 @@ export function useChatStore() { }) // ── 会话操作 ───────────────────────────────────────── - // 本地新建:前端直接生成 chat_id(草稿),不预先请求后端 open; - // 首次发消息 / 上传时后端会按该 id 现开会话(open-if-missing)。 - const newChat = (): string => { - const id = crypto.randomUUID() - currentChatId.value = id - resetMsgState() - if (!conversations.value.some((c) => c.id === id)) { - conversations.value.unshift({ id, abstract: '新建对话' }) - } - return id + // 惰性获取当前 chat_id:已有则原样返回;为空才前端生成一个并赋值。 + // 幂等——反复调用返回同一个 id,从而上传文件的 source_id 与后续发送的 chat_id 始终一致。 + // 不请求后端 open,不往左侧列表 push(直到首次发消息才入列表,见 chat-panel send)。 + const getChatId = (): string => { + if (currentChatId.value) return currentChatId.value + currentChatId.value = crypto.randomUUID() + return currentChatId.value } const openChat = async (appId?: string) => { @@ -114,26 +114,6 @@ export function useChatStore() { return { url: res, name: file.name } } - // ── 发送消息 ───────────────────────────────────────── - const sendMessage = (cid: string, payload: any, aiMsg: ChatMessage) => { - loading.value = true - streamManager.startStream({ - cid, - request: () => chat(cid, payload), - onStream: (chunk) => { - appendChunk(aiMsg, chunk) - }, - onFinish: () => { - aiMsg.write_ed = true - loading.value = false - }, - onFailure: () => { - aiMsg.write_ed = true - loading.value = false - } - }) - } - // ── 切换对话 ───────────────────────────────────────── const switchConversation = async (cid: string) => { await streamManager.switchConversation({ @@ -169,7 +149,7 @@ export function useChatStore() { // 会话 loadConversations, loadMore, - newChat, + getChatId, openChat, deleteChat, renameChat, @@ -183,7 +163,6 @@ export function useChatStore() { appendChunk, // 流式 chat, - sendMessage, startStream: streamManager.startStream, switchConversation, closeStream: streamManager.closeStream, @@ -191,5 +170,10 @@ export function useChatStore() { cancelWorkflow: (cid: string) => streamManager.cancelWorkflow(cid, 'chat'), // 文件 uploadFile, + // composer 重置 + composerResetSignal, + resetComposer: () => { + composerResetSignal.value++ + }, } } diff --git a/ui/src/components/conversation/common/use-chat-store/debug/index.ts b/ui/src/components/conversation/common/use-chat-store/debug/index.ts index 90ec0982995..6ffea95fdd8 100644 --- a/ui/src/components/conversation/common/use-chat-store/debug/index.ts +++ b/ui/src/components/conversation/common/use-chat-store/debug/index.ts @@ -10,6 +10,9 @@ import type { ChatMessage } from '../../types' // ── 共享状态(单例) ───────────────────────────────────── const appInfo = ref<{ name: string; icon: string } | null>(null) const currentChatId = ref('') +// composer 重置信号:新建/切换会话时自增,chat-panel 监听后清空暂存文件与输入, +// 避免上传文件(source_id 绑旧 chat_id)在切上下文后与新 chat_id 对不上。 +const composerResetSignal = ref(0) let currentApplicationId = '' const loading = ref(false) @@ -87,16 +90,13 @@ export function useDebugStore() { }) // ── 会话操作 ───────────────────────────────────────── - // 本地新建:前端直接生成 chat_id(草稿),不预先请求后端 open; - // 首次发消息 / 上传时后端会按该 id 现开会话(open-if-missing)。 - const newChat = (): string => { - const id = crypto.randomUUID() - currentChatId.value = id - resetMsgState() - if (!conversations.value.some((c) => c.id === id)) { - conversations.value.unshift({ id, abstract: '新建对话' }) - } - return id + // 惰性获取当前 chat_id:已有则原样返回;为空才前端生成一个并赋值。 + // 幂等——反复调用返回同一个 id,从而上传文件的 source_id 与后续发送的 chat_id 始终一致。 + // 不请求后端 open,不往左侧列表 push(直到首次发消息才入列表,见 chat-panel send)。 + const getChatId = (): string => { + if (currentChatId.value) return currentChatId.value + currentChatId.value = crypto.randomUUID() + return currentChatId.value } const openChat = async (appId?: string) => { @@ -120,10 +120,8 @@ export function useDebugStore() { // ── 文件上传 ───────────────────────────────────────── const uploadFile = async (file: File, chatId: string): Promise<{ url: string; name: string }> => { loading.value = true - let cid = chatId - if (!cid) { - cid = await openChat(applicationId.value) - } + // 无会话时前端生成草稿 chat_id(不调后端 open),与发送保持同一个 id。 + const cid = chatId || getChatId() const res = await debugApi.uploadFile(file, cid) loading.value = false return { url: res, name: file.name } @@ -163,7 +161,7 @@ export function useDebugStore() { // 会话 loadConversations, loadMore, - newChat, + getChatId, openChat, deleteChat, renameChat, @@ -184,5 +182,10 @@ export function useDebugStore() { cancelWorkflow: (cid: string) => streamManager.cancelWorkflow(cid, 'debug', currentApplicationId), // 文件 uploadFile, + // composer 重置 + composerResetSignal, + resetComposer: () => { + composerResetSignal.value++ + }, } } diff --git a/ui/src/components/conversation/content/items/form.vue b/ui/src/components/conversation/content/items/form.vue index 65944120234..14783f55ca6 100644 --- a/ui/src/components/conversation/content/items/form.vue +++ b/ui/src/components/conversation/content/items/form.vue @@ -1,11 +1,6 @@