fix(frontend): sync generated conversation title

This commit is contained in:
JOJO 2026-05-25 22:52:03 +08:00
parent 16473c824d
commit eaf270766f
2 changed files with 146 additions and 2 deletions

View File

@ -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:

View File

@ -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', {