From eaf270766fead02e313e74cc64ec66013b0a7b88 Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Mon, 25 May 2026 22:52:03 +0800 Subject: [PATCH] fix(frontend): sync generated conversation title --- server/conversation.py | 7 +- static/src/app/methods/taskPolling.ts | 141 ++++++++++++++++++++++++++ 2 files changed, 146 insertions(+), 2 deletions(-) diff --git a/server/conversation.py b/server/conversation.py index 35ede1c..b8bd74f 100644 --- a/server/conversation.py +++ b/server/conversation.py @@ -1675,7 +1675,8 @@ def get_current_conversation(terminal: WebTerminal, workspace: UserWorkspace, us "id": current_id, "title": "新对话", "messages_count": 0, - "is_temporary": True + "is_temporary": True, + "title_locked": False } }) @@ -1683,13 +1684,15 @@ def get_current_conversation(terminal: WebTerminal, workspace: UserWorkspace, us try: conversation_data = terminal.context_manager.conversation_manager.load_conversation(current_id) if conversation_data: + metadata = conversation_data.get("metadata", {}) or {} return jsonify({ "success": True, "data": { "id": current_id, "title": conversation_data.get("title", "未知对话"), "messages_count": len(conversation_data.get("messages", [])), - "is_temporary": False + "is_temporary": False, + "title_locked": bool(metadata.get("title_locked", False)) } }) else: diff --git a/static/src/app/methods/taskPolling.ts b/static/src/app/methods/taskPolling.ts index 6509787..94d75a6 100644 --- a/static/src/app/methods/taskPolling.ts +++ b/static/src/app/methods/taskPolling.ts @@ -1322,6 +1322,141 @@ export const taskPollingMethods = { } }, + isPlaceholderConversationTitle(title: any) { + const normalized = String(title || '').trim(); + return !normalized || normalized === '新对话'; + }, + + applyConversationTitleUpdate(conversationId: string, title: string, source = 'unknown') { + const normalizedTitle = String(title || '').trim(); + const normalizedConversationId = String(conversationId || '').trim(); + if (!normalizedConversationId || this.isPlaceholderConversationTitle(normalizedTitle)) { + return false; + } + + let changed = false; + if (normalizedConversationId === this.currentConversationId) { + if (this.currentConversationTitle !== normalizedTitle) { + // 确保 watcher 能从“新对话”切到真实标题并播放现有标题打字动画。 + this.suppressTitleTyping = false; + this.titleReady = true; + this.currentConversationTitle = normalizedTitle; + changed = true; + } + } + + if (Array.isArray(this.conversations)) { + let listChanged = false; + const nextConversations = this.conversations.map((conv: any) => { + if (!conv || conv.id !== normalizedConversationId || conv.title === normalizedTitle) { + return conv; + } + listChanged = true; + return { + ...conv, + title: normalizedTitle + }; + }); + if (listChanged) { + this.conversations = nextConversations; + changed = true; + } + } + + if (changed) { + debugLog('[TaskPolling] 已同步生成后的对话标题:', { + conversationId: normalizedConversationId, + title: normalizedTitle, + source + }); + this.$forceUpdate(); + } + return changed; + }, + + async fetchGeneratedConversationTitle(conversationId: string, source = 'unknown') { + const normalizedConversationId = String(conversationId || '').trim(); + if (!normalizedConversationId || normalizedConversationId !== this.currentConversationId) { + return false; + } + try { + const response = await fetch('/api/conversations/current', { cache: 'no-store' }); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + const result = await response.json(); + const data = result?.data || {}; + if (!result?.success || data.id !== normalizedConversationId) { + return false; + } + const normalizedTitle = String(data.title || '').trim(); + const titleLocked = Boolean(data.title_locked || data.metadata?.title_locked); + if (this.isPlaceholderConversationTitle(normalizedTitle) || !titleLocked) { + // Unlocked titles are usually first-message fallback titles; keep waiting for the final generated title. + return false; + } + this.applyConversationTitleUpdate(normalizedConversationId, normalizedTitle, source); + return true; + } catch (error) { + console.warn('[TaskPolling] 同步生成后的对话标题失败:', error); + return false; + } + }, + + scheduleGeneratedTitleRefresh(reason = 'unknown', options: any = {}) { + const conversationId = String(options.conversationId || this.currentConversationId || '').trim(); + if (!conversationId) { + return; + } + + // 标题模型是后台独立请求,慢时可能在主任务完成后几十秒才落盘。 + // 这里用“总等待窗口”而不是少量固定次数,避免刚好错过最终 title_locked=true 的标题。 + const deadlineMs = Math.max(15000, Number(options.deadlineMs || 90000)); + const maxAttempts = Math.max(1, Number(options.maxAttempts || 60)); + const delays = Array.isArray(options.delays) + ? options.delays + : [0, 800, 1200, 1600, 2200, 3000, 4000, 5000, 6000]; + const startedAt = Date.now(); + const seq = Number(this.generatedTitleRefreshSeq || 0) + 1; + this.generatedTitleRefreshSeq = seq; + if (this.generatedTitleRefreshTimer) { + clearTimeout(this.generatedTitleRefreshTimer); + this.generatedTitleRefreshTimer = null; + } + + const run = async (attempt: number) => { + if (this.generatedTitleRefreshSeq !== seq || this.currentConversationId !== conversationId) { + return; + } + this.generatedTitleRefreshTimer = null; + const updated = await this.fetchGeneratedConversationTitle( + conversationId, + `${reason}:attempt-${attempt}` + ); + if (updated) { + return; + } + const elapsedMs = Date.now() - startedAt; + if (attempt >= maxAttempts || elapsedMs >= deadlineMs) { + debugLog('[TaskPolling] 停止等待生成标题', { + reason, + conversationId, + attempt, + elapsedMs, + deadlineMs + }); + return; + } + const delay = Number(delays[Math.min(attempt, delays.length - 1)] || 1000); + const nextDelay = Math.min(delay, Math.max(0, deadlineMs - elapsedMs)); + this.generatedTitleRefreshTimer = window.setTimeout(() => { + void run(attempt + 1); + }, nextDelay); + }; + + void run(1); + }, + handleTaskComplete(data: any) { const pendingToolsBefore = typeof this.hasPendingToolActions === 'function' ? this.hasPendingToolActions() : null; @@ -1410,6 +1545,12 @@ export const taskPollingMethods = { }, 500); } this.scheduleTodoListRefresh(100); + // 标题生成可能晚于主任务完成;主轮询停止后主动短轮询当前会话标题,避免必须刷新页面。 + this.scheduleGeneratedTitleRefresh('task_complete', { + conversationId: data?.conversation_id || this.currentConversationId, + deadlineMs: 90000, + maxAttempts: 60 + }); const pendingToolsAfter = typeof this.hasPendingToolActions === 'function' ? this.hasPendingToolActions() : null; jsonDebug('handleTaskComplete:after', {