fix(frontend): 修复刷新后AI等待提示与终端侧边栏重复问题
- 修复模型调用工具后刷新页面,AI回复头部等待提示文字再次出现的bug - restoreTaskState 根据 hasAssistantContentEvent 精确设置 awaitingFirstContent - handleAiMessageStart 从头重建时保留 restoreTaskState 的设置 - handleToolStart 兜底清除 awaitingFirstContent - 修复刷新页面后终端侧边栏已有内容重复显示绿色箭头与输入内容的bug - 后端 get_snapshot/get_terminal_output 返回 last_event_time - 前端 TerminalPanel 基于 last_event_time 对 terminal_output 事件去重
This commit is contained in:
parent
2eed4d5b86
commit
e90610ed05
@ -130,9 +130,16 @@ class LifecycleMixin:
|
|||||||
"seconds_since_last_output": self._seconds_since_last_output(),
|
"seconds_since_last_output": self._seconds_since_last_output(),
|
||||||
"last_command": self.last_command,
|
"last_command": self.last_command,
|
||||||
"buffer_size": self.total_output_size,
|
"buffer_size": self.total_output_size,
|
||||||
"timestamp": datetime.now().isoformat()
|
"timestamp": datetime.now().isoformat(),
|
||||||
|
"last_event_time": self._get_last_event_time()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _get_last_event_time(self) -> Optional[float]:
|
||||||
|
"""获取 io_history 中最后一条事件的时间戳,用于前端去重"""
|
||||||
|
if self.io_history:
|
||||||
|
return self.io_history[-1][1]
|
||||||
|
return None
|
||||||
|
|
||||||
def get_status(self) -> Dict:
|
def get_status(self) -> Dict:
|
||||||
"""获取终端状态"""
|
"""获取终端状态"""
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -672,7 +672,8 @@ class TerminalManager:
|
|||||||
"seconds_since_last_output": snapshot.get("seconds_since_last_output", terminal._seconds_since_last_output()),
|
"seconds_since_last_output": snapshot.get("seconds_since_last_output", terminal._seconds_since_last_output()),
|
||||||
"echo_loop_detected": snapshot.get("echo_loop_detected", terminal.echo_loop_detected),
|
"echo_loop_detected": snapshot.get("echo_loop_detected", terminal.echo_loop_detected),
|
||||||
"lines_returned": snapshot.get("lines_returned"),
|
"lines_returned": snapshot.get("lines_returned"),
|
||||||
"truncated": snapshot.get("truncated", False)
|
"truncated": snapshot.get("truncated", False),
|
||||||
|
"last_event_time": snapshot.get("last_event_time")
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_terminal_snapshot(
|
def get_terminal_snapshot(
|
||||||
|
|||||||
@ -228,7 +228,8 @@ def handle_get_terminal_output(data):
|
|||||||
'session': session_name,
|
'session': session_name,
|
||||||
'output': result['output'],
|
'output': result['output'],
|
||||||
'is_interactive': result.get('is_interactive', False),
|
'is_interactive': result.get('is_interactive', False),
|
||||||
'last_command': result.get('last_command', '')
|
'last_command': result.get('last_command', ''),
|
||||||
|
'last_event_time': result.get('last_event_time')
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
emit('error', {'message': result['error']})
|
emit('error', {'message': result['error']})
|
||||||
|
|||||||
@ -101,7 +101,12 @@ export const aiStreamMethods = {
|
|||||||
|
|
||||||
// 确保 awaitingFirstContent 被正确设置
|
// 确保 awaitingFirstContent 被正确设置
|
||||||
const hasContent = lastMessage.actions && lastMessage.actions.length > 0;
|
const hasContent = lastMessage.actions && lastMessage.actions.length > 0;
|
||||||
if (!hasContent) {
|
if (this._rebuildingFromScratch) {
|
||||||
|
// 从头重建时,restoreTaskState 已根据事件流分析(hasAssistantContentEvent)
|
||||||
|
// 正确设置了 awaitingFirstContent,此处不覆盖。
|
||||||
|
// actions 被清空不代表没有内容,事件重放会恢复。
|
||||||
|
console.log('[AiMessageStart] 从头重建,保留 awaitingFirstContent:', lastMessage.awaitingFirstContent);
|
||||||
|
} else if (!hasContent) {
|
||||||
lastMessage.awaitingFirstContent = true;
|
lastMessage.awaitingFirstContent = true;
|
||||||
lastMessage.generatingLabel = lastMessage.generatingLabel || '思考中...';
|
lastMessage.generatingLabel = lastMessage.generatingLabel || '思考中...';
|
||||||
console.log('[AiMessageStart] 设置等待动画', {
|
console.log('[AiMessageStart] 设置等待动画', {
|
||||||
|
|||||||
@ -365,9 +365,14 @@ export const compressionMethods = {
|
|||||||
// 让 handleAiMessageStart 检测到 isRefreshRestore 并复用现有消息容器。
|
// 让 handleAiMessageStart 检测到 isRefreshRestore 并复用现有消息容器。
|
||||||
// 这样避免视觉闪回(消息不会突然消失)+ 新事件通过轮询增量追加,不会一次性同步处理几百个事件导致卡死。
|
// 这样避免视觉闪回(消息不会突然消失)+ 新事件通过轮询增量追加,不会一次性同步处理几百个事件导致卡死。
|
||||||
if (isAssistantMessage) {
|
if (isAssistantMessage) {
|
||||||
debugLog('[TaskPolling] 复用现有 assistant 消息容器,清空 actions 等待事件重建');
|
debugLog('[TaskPolling] 复用现有 assistant 消息容器,清空 actions 等待事件重建', {
|
||||||
|
hasAssistantContentEvent
|
||||||
|
});
|
||||||
lastMessage.actions = [];
|
lastMessage.actions = [];
|
||||||
lastMessage.awaitingFirstContent = true;
|
// 如果事件流中已经有内容事件(tool/thinking/text),说明回复已经开始过,
|
||||||
|
// 重放事件会恢复内容,不显示等待提示。
|
||||||
|
// 如果还没有内容事件,说明回复还没开始,应该显示等待提示。
|
||||||
|
lastMessage.awaitingFirstContent = !hasAssistantContentEvent;
|
||||||
lastMessage.generatingLabel = lastMessage.generatingLabel || '思考中...';
|
lastMessage.generatingLabel = lastMessage.generatingLabel || '思考中...';
|
||||||
// 清理旧的流式标记,确保新事件能正确设置
|
// 清理旧的流式标记,确保新事件能正确设置
|
||||||
if (typeof lastMessage.streaming === 'boolean') {
|
if (typeof lastMessage.streaming === 'boolean') {
|
||||||
|
|||||||
@ -80,6 +80,18 @@ export const toolMethods = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 兜底:工具开始执行时关闭等待动画(正常流程由 tool_preparing 关闭,
|
||||||
|
// 但从头重建等场景可能跳过 tool_preparing 直接收到 tool_start)
|
||||||
|
const msgForAwaiting = this.chatEnsureAssistantMessage();
|
||||||
|
if (msgForAwaiting && msgForAwaiting.awaitingFirstContent) {
|
||||||
|
console.log('[ToolStart] 关闭等待动画', {
|
||||||
|
beforeAwaitingFirstContent: msgForAwaiting.awaitingFirstContent,
|
||||||
|
toolName: data.name
|
||||||
|
});
|
||||||
|
msgForAwaiting.awaitingFirstContent = false;
|
||||||
|
msgForAwaiting.generatingLabel = '';
|
||||||
|
}
|
||||||
|
|
||||||
let action = null;
|
let action = null;
|
||||||
if (data.preparing_id && this.preparingTools.has(data.preparing_id)) {
|
if (data.preparing_id && this.preparingTools.has(data.preparing_id)) {
|
||||||
action = this.preparingTools.get(data.preparing_id);
|
action = this.preparingTools.get(data.preparing_id);
|
||||||
|
|||||||
@ -69,6 +69,8 @@ let term: Terminal | null = null;
|
|||||||
let themeObserver: MutationObserver | null = null;
|
let themeObserver: MutationObserver | null = null;
|
||||||
let resizeObserver: ResizeObserver | null = null;
|
let resizeObserver: ResizeObserver | null = null;
|
||||||
const _historyReady = ref<Record<string, boolean>>({});
|
const _historyReady = ref<Record<string, boolean>>({});
|
||||||
|
// 历史加载完成后记录最后一条事件的时间戳,用于去重刷新后回放的旧 terminal_output 事件
|
||||||
|
const _historyLastEventTime = ref<Record<string, number>>({});
|
||||||
|
|
||||||
const sessionKeys = computed(() => Object.keys(sessions.value));
|
const sessionKeys = computed(() => Object.keys(sessions.value));
|
||||||
|
|
||||||
@ -211,6 +213,9 @@ function switchToSession(name: string) {
|
|||||||
activeSession.value = name;
|
activeSession.value = name;
|
||||||
if (!sessionHydrated.value[name]) {
|
if (!sessionHydrated.value[name]) {
|
||||||
_historyReady.value = { ..._historyReady.value, [name]: false };
|
_historyReady.value = { ..._historyReady.value, [name]: false };
|
||||||
|
const updatedTimes = { ..._historyLastEventTime.value };
|
||||||
|
delete updatedTimes[name];
|
||||||
|
_historyLastEventTime.value = updatedTimes;
|
||||||
if (socket?.connected) {
|
if (socket?.connected) {
|
||||||
console.log('[TerminalPanel] requesting history for:', name);
|
console.log('[TerminalPanel] requesting history for:', name);
|
||||||
socket.emit('get_terminal_output', { session: name, lines: 0 });
|
socket.emit('get_terminal_output', { session: name, lines: 0 });
|
||||||
@ -246,6 +251,7 @@ watch(() => props.workspaceId, (newId, oldId) => {
|
|||||||
sessionLogs.value = {};
|
sessionLogs.value = {};
|
||||||
sessionHydrated.value = {};
|
sessionHydrated.value = {};
|
||||||
_historyReady.value = {};
|
_historyReady.value = {};
|
||||||
|
_historyLastEventTime.value = {};
|
||||||
if (term) term.clear();
|
if (term) term.clear();
|
||||||
initSocket();
|
initSocket();
|
||||||
}
|
}
|
||||||
@ -333,6 +339,17 @@ async function initSocket() {
|
|||||||
const s = data.session;
|
const s = data.session;
|
||||||
if (!s) return;
|
if (!s) return;
|
||||||
const delta = (data.data || '') as string;
|
const delta = (data.data || '') as string;
|
||||||
|
|
||||||
|
// 去重:刷新页面后后端会回放历史快照(terminal_output_history)
|
||||||
|
// 同时可能回放最近的 terminal_output 事件,导致已有内容被重复写入。
|
||||||
|
// 用历史快照的 last_event_time 作为截止线,跳过更早的事件。
|
||||||
|
const historyTime = _historyLastEventTime.value[s] || 0;
|
||||||
|
const eventTime = typeof data.timestamp === 'number' ? data.timestamp : 0;
|
||||||
|
if (historyTime > 0 && eventTime > 0 && eventTime <= historyTime) {
|
||||||
|
console.log('[TerminalPanel] terminal_output DEDUP skip:', s, 'eventTime:', eventTime, '<= historyTime:', historyTime);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 先把输出暂存到 sessionLogs;历史加载完成后再写入终端,
|
// 先把输出暂存到 sessionLogs;历史加载完成后再写入终端,
|
||||||
// 避免新终端历史为空时 _historyReady 始终为 false 导致永远写不进去。
|
// 避免新终端历史为空时 _historyReady 始终为 false 导致永远写不进去。
|
||||||
sessionLogs.value[s] = (sessionLogs.value[s] || '') + delta;
|
sessionLogs.value[s] = (sessionLogs.value[s] || '') + delta;
|
||||||
@ -369,6 +386,9 @@ async function initSocket() {
|
|||||||
if (target) {
|
if (target) {
|
||||||
sessionLogs.value = { ...sessionLogs.value, [target]: '' };
|
sessionLogs.value = { ...sessionLogs.value, [target]: '' };
|
||||||
sessionHydrated.value = { ...sessionHydrated.value, [target]: true };
|
sessionHydrated.value = { ...sessionHydrated.value, [target]: true };
|
||||||
|
const updatedTimes = { ..._historyLastEventTime.value };
|
||||||
|
delete updatedTimes[target];
|
||||||
|
_historyLastEventTime.value = updatedTimes;
|
||||||
if (target === activeSession.value) renderSessionLog();
|
if (target === activeSession.value) renderSessionLog();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -397,6 +417,9 @@ async function initSocket() {
|
|||||||
// _historyReady 一直为 false,所有实时输出都会被跳过,终端 seemingly 卡住。
|
// _historyReady 一直为 false,所有实时输出都会被跳过,终端 seemingly 卡住。
|
||||||
sessionHydrated.value = { ...sessionHydrated.value, [s]: true };
|
sessionHydrated.value = { ...sessionHydrated.value, [s]: true };
|
||||||
_historyReady.value = { ..._historyReady.value, [s]: true };
|
_historyReady.value = { ..._historyReady.value, [s]: true };
|
||||||
|
// 记录历史快照最后一条事件的时间戳,用于后续 terminal_output 去重
|
||||||
|
const lastEventTime = typeof data.last_event_time === 'number' ? data.last_event_time : 0;
|
||||||
|
_historyLastEventTime.value = { ..._historyLastEventTime.value, [s]: lastEventTime };
|
||||||
console.log('[TerminalPanel] handleHistory SAVED:', s, 'finalLen:', payload.length, 'calling render:', s === activeSession.value);
|
console.log('[TerminalPanel] handleHistory SAVED:', s, 'finalLen:', payload.length, 'calling render:', s === activeSession.value);
|
||||||
if (s === activeSession.value) {
|
if (s === activeSession.value) {
|
||||||
renderSessionLog();
|
renderSessionLog();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user