diff --git a/static/src/app.ts b/static/src/app.ts index 6220354..a2c01fb 100644 --- a/static/src/app.ts +++ b/static/src/app.ts @@ -301,17 +301,18 @@ const appOptions = { console.warn('CSRF token 初始化失败:', err); }); } - await this.bootstrapRoute(); - await this.initSocket(); + // 并行启动路由解析与实时连接,避免等待路由加载阻塞思考模式同步 + const routePromise = this.bootstrapRoute(); + const socketPromise = this.initSocket(); + await routePromise; + await socketPromise; this.$nextTick(() => { this.ensureScrollListener(); }); setupShowImageObserver(); - // 延迟加载初始数据 - setTimeout(() => { - this.loadInitialData(); - }, 500); + // 立即加载初始数据(并行获取状态,优先同步运行模式) + this.loadInitialData(); document.addEventListener('click', this.handleClickOutsideQuickMenu); document.addEventListener('click', this.handleClickOutsidePanelMenu); @@ -1288,54 +1289,56 @@ const appOptions = { async loadInitialData() { try { debugLog('加载初始数据...'); - - await this.fileFetchTree(); - await this.focusFetchFiles(); - await this.fileFetchTodoList(); - + + // 并行拉取文件/待办,优先获取状态以尽快同步运行模式 + const treePromise = this.fileFetchTree(); + const focusPromise = this.focusFetchFiles(); + const todoPromise = this.fileFetchTodoList(); + const statusResponse = await fetch('/api/status'); const statusData = await statusResponse.json(); this.projectPath = statusData.project_path || ''; this.agentVersion = statusData.version || this.agentVersion; this.thinkingMode = !!statusData.thinking_mode; this.applyStatusSnapshot(statusData); - await this.fetchUsageQuota(); - + // 立即更新配额和运行模式,避免等待其他慢接口 + this.fetchUsageQuota(); + // 获取当前对话信息 const statusConversationId = statusData.conversation && statusData.conversation.current_id; - if (statusConversationId) { - if (!this.currentConversationId) { - this.skipConversationHistoryReload = true; - // 首次从状态恢复对话时,避免 socket 的 conversation_loaded 再次触发历史加载 - this.skipConversationLoadedEvent = true; - this.currentConversationId = statusConversationId; + if (statusConversationId && !this.currentConversationId) { + this.skipConversationHistoryReload = true; + // 首次从状态恢复对话时,避免 socket 的 conversation_loaded 再次触发历史加载 + this.skipConversationLoadedEvent = true; + this.currentConversationId = statusConversationId; - // 如果有当前对话,尝试获取标题和历史 - try { - const convResponse = await fetch(`/api/conversations/current`); - const convData = await convResponse.json(); - if (convData.success && convData.data) { - this.currentConversationTitle = convData.data.title; - } - // 初始化时调用一次,因为 skipConversationHistoryReload 会阻止 watch 触发 - if ( - this.lastHistoryLoadedConversationId !== this.currentConversationId || - !Array.isArray(this.messages) || - this.messages.length === 0 - ) { - await this.fetchAndDisplayHistory(); - } - // 获取当前对话的Token统计 - this.fetchConversationTokenStatistics(); - this.updateCurrentContextTokens(); - } catch (e) { - console.warn('获取当前对话标题失败:', e); + // 如果有当前对话,尝试获取标题和历史 + try { + const convResponse = await fetch(`/api/conversations/current`); + const convData = await convResponse.json(); + if (convData.success && convData.data) { + this.currentConversationTitle = convData.data.title; } + // 初始化时调用一次,因为 skipConversationHistoryReload 会阻止 watch 触发 + if ( + this.lastHistoryLoadedConversationId !== this.currentConversationId || + !Array.isArray(this.messages) || + this.messages.length === 0 + ) { + await this.fetchAndDisplayHistory(); + } + // 获取当前对话的Token统计 + this.fetchConversationTokenStatistics(); + this.updateCurrentContextTokens(); + } catch (e) { + console.warn('获取当前对话标题失败:', e); } } - + + // 等待其他加载项完成(允许部分失败不阻塞模式切换) + await Promise.allSettled([treePromise, focusPromise, todoPromise]); await this.loadToolSettings(true); - + debugLog('初始数据加载完成'); } catch (error) { console.error('加载初始数据失败:', error); @@ -1835,17 +1838,19 @@ const appOptions = { } if (toolAction) { - // 解析工具结果 + // 解析工具结果(优先使用JSON,其次使用元数据的 tool_payload,以保证搜索结果在刷新后仍可展示) let result; try { - // 尝试解析为JSON result = JSON.parse(message.content); } catch (e) { - // 如果不是JSON,就作为纯文本 - result = { - output: message.content, - success: true - }; + if (message.metadata && message.metadata.tool_payload) { + result = message.metadata.tool_payload; + } else { + result = { + output: message.content, + success: true + }; + } } toolAction.tool.status = 'completed'; diff --git a/web_server.py b/web_server.py index 2579d3a..2113afb 100644 --- a/web_server.py +++ b/web_server.py @@ -545,6 +545,38 @@ def format_tool_result_notice(tool_name: str, tool_call_id: Optional[str], conte body = "(无附加输出)" return f"{header}\n{body}" + +def compact_web_search_result(result_data: Dict[str, Any]) -> Dict[str, Any]: + """提取 web_search 结果中前端展示所需的关键字段,避免持久化时丢失列表。""" + if not isinstance(result_data, dict): + return {"success": False, "error": "invalid search result"} + + compact: Dict[str, Any] = { + "success": bool(result_data.get("success")), + "summary": result_data.get("summary"), + "query": result_data.get("query"), + "filters": result_data.get("filters") or {}, + "total_results": result_data.get("total_results", 0) + } + + # 仅保留前端需要渲染的字段,避免巨大正文导致历史加载时缺失 + items: List[Dict[str, Any]] = [] + for item in result_data.get("results") or []: + if not isinstance(item, dict): + continue + items.append({ + "index": item.get("index"), + "title": item.get("title") or item.get("name"), + "url": item.get("url") + }) + + compact["results"] = items + + if not compact.get("success") and result_data.get("error"): + compact["error"] = result_data.get("error") + + return compact + # 创建调试日志文件 DEBUG_LOG_FILE = Path(LOGS_DIR).expanduser().resolve() / "debug_stream.log" CHUNK_BACKEND_LOG_FILE = Path(LOGS_DIR).expanduser().resolve() / "chunk_backend.log" @@ -4751,7 +4783,14 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac # ===== 增量保存:立即保存工具结果 ===== metadata_payload = None if isinstance(result_data, dict): - tool_result_content = format_tool_result_for_context(function_name, result_data, tool_result) + # 特殊处理 web_search:保留可供前端渲染的精简结构,以便历史记录复现搜索结果 + if function_name == "web_search": + try: + tool_result_content = json.dumps(compact_web_search_result(result_data), ensure_ascii=False) + except Exception: + tool_result_content = tool_result + else: + tool_result_content = format_tool_result_for_context(function_name, result_data, tool_result) metadata_payload = {"tool_payload": result_data} else: tool_result_content = tool_result