fix(frontend): sync generated conversation title
This commit is contained in:
parent
16473c824d
commit
eaf270766f
@ -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:
|
||||
|
||||
@ -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', {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user