# Agent 对话页面体验优化设计 > 日期:2026-04-13 > 状态:待实施 > 范围:前端 agent 模块 + 后端 netaclaw 模块 ## 1. 背景与问题 当前 Neta 项目的 agent 对话页面存在 4 个体验问题: 1. **Skill 视觉跳变**:执行中由 `skill-indicator.vue` 渲染进度条,完成后 `skillProgress` 被清空,Skill 记录转存到 `message.metadata.skillExecutions`,由 `message-item.vue` 重新渲染为"查看结果"按钮。两个完全不同的组件、不同的位置,造成视觉断裂。 2. **Token 计算不准**:前端用字符数粗估 token(中文 1.5 字符=1 token),与实际偏差大。 3. **Token UI 不直观**:统计条占据对话区顶部整行,信息密度低。 4. **缺少任务规划**:agent 每次裸跑,没有 todo/plan 工具,不会主动规划任务步骤。 5. **思考内容不流畅**:思考内容不是流式展示,等全部完成才一次性显示;默认折叠用户容易忽略;没有思考级别控制。 6. **LLM 适配器思考参数缺失**:后端已有 `thinking.ts` 基础框架和 `ThinkLevel` 类型,但 Anthropic/OpenAI 适配器未将思考参数传递给 API。 参考项目: - **openclaw-main**:`update_plan` 工具;思考块虚线边框+半透明背景+呼吸动画;思考级别下拉选择器 - **hermes-agent-main**:`TodoStore` + `todo` 工具;多模型统一思考适配层;thinking_delta 流式推送;思考预算分级 用户选择 hermes 的 TodoStore 方案作为主要参考,思考 UI 参考 openclaw 的视觉设计。 ## 2. 设计目标 - Skill 执行中→完成:同一个组件、同一个位置、CSS 过渡动画,无跳变 - Token 数据:前端零估算,100% 由后端推送真实数据 - Token UI:紧凑,不抢眼,hover 查看详情 - Todo 系统:后端 `todo` 工具 + 前端内嵌卡片,agent 主动规划,实时更新 - 思考内容:流式展示 + 呼吸动画 + 光标闪烁,思考→正文平滑过渡 - 思考级别:前端可选(off/minimal/low/medium/high/adaptive),会话级持久化 - 多模型适配:统一思考参数传递层,Anthropic/OpenAI/DeepSeek 各自映射 ## 3. Todo 工具系统(后端) ### 3.1 TodoStore 类 文件:`packages/backend/src/modules/netaclaw/runtime/todo_store.ts` 完全参考 hermes 的 TodoStore 设计: ```typescript interface TodoItem { id: string; // agent 自选的唯一标识 content: string; // 任务描述 status: 'pending' | 'in_progress' | 'completed' | 'cancelled'; } interface TodoSummary { total: number; pending: number; in_progress: number; completed: number; cancelled: number; } class TodoStore { private items: TodoItem[] = []; /** * 写入 todo 列表 * @param todos - 任务项数组 * @param merge - false(默认)=全量替换,true=按 id 增量更新+追加新项 * @returns 写入后的完整列表 */ write(todos: TodoItem[], merge: boolean = false): TodoItem[] { if (!merge) { // Replace 模式:全量替换,agent 重新规划时使用 this.items = todos.map(t => this.validate(t)); } else { // Merge 模式:按 id 匹配更新,未匹配的追加 for (const todo of todos) { const validated = this.validate(todo); const existing = this.items.find(i => i.id === validated.id); if (existing) { Object.assign(existing, validated); } else { this.items.push(validated); } } } return this.read(); } /** 读取当前列表(返回拷贝,防止外部篡改) */ read(): TodoItem[] { return JSON.parse(JSON.stringify(this.items)); } hasItems(): boolean { return this.items.length > 0; } getSummary(): TodoSummary { const s = { total: 0, pending: 0, in_progress: 0, completed: 0, cancelled: 0 }; for (const item of this.items) { s.total++; s[item.status]++; } return s; } /** * 上下文压缩后注入,只保留 pending + in_progress 项 * 返回人类可读格式,作为 user message 注入压缩后的历史 */ formatForInjection(): string | null { const active = this.items.filter(i => i.status === 'pending' || i.status === 'in_progress' ); if (active.length === 0) return null; const lines = active.map(i => { const marker = i.status === 'in_progress' ? '[>]' : '[ ]'; return `- ${marker} ${i.id}. ${i.content} (${i.status})`; }); return '[你的活跃任务列表在上下文压缩后被保留]\n' + lines.join('\n'); } /** * 从历史消息中恢复 todo 状态(hydrate) * 倒序扫描消息,找到最近一次 todo 工具调用的返回值 */ static hydrateFromHistory(messages: any[]): TodoStore { const store = new TodoStore(); for (let i = messages.length - 1; i >= 0; i--) { const msg = messages[i]; if (msg.role === 'tool' && msg.toolName === 'todo') { try { const data = JSON.parse(msg.content); if (data.todos) { store.write(data.todos, false); } } catch {} break; } } return store; } private validate(item: Partial): TodoItem { return { id: item.id || '?', content: item.content || '(无描述)', status: ['pending', 'in_progress', 'completed', 'cancelled'].includes(item.status!) ? item.status! : 'pending', }; } } ``` ### 3.2 todo 工具定义 文件:`packages/backend/src/modules/netaclaw/tools/todo_tool.ts` ```typescript const todoToolSchema = { name: 'todo', description: '管理当前会话的任务列表。用于3步以上的复杂任务或用户提供多个任务时。\n' + '不传参数=读取当前列表。\n' + '传 todos 数组=写入。merge=false(默认)全量替换整个计划,merge=true 按 id 增量更新。\n' + '每个项: { id: string, content: string, status: pending|in_progress|completed|cancelled }\n' + '列表顺序=优先级。同一时间只能有一个 in_progress。\n' + '完成立即标记 completed。失败则 cancel 并添加修正项。\n' + '始终返回完整列表。', parameters: { type: 'object', properties: { todos: { type: 'array', description: '任务项数组。省略则读取当前列表。', items: { type: 'object', properties: { id: { type: 'string', description: '唯一标识' }, content: { type: 'string', description: '任务描述' }, status: { type: 'string', enum: ['pending', 'in_progress', 'completed', 'cancelled'], description: '当前状态', }, }, required: ['id', 'content', 'status'], }, }, merge: { type: 'boolean', description: 'true=按 id 增量更新+追加新项。false(默认)=全量替换整个列表。', default: false, }, }, required: [], }, }; ``` 工具返回值格式: ```json { "todos": [ { "id": "1", "content": "分析需求文档", "status": "completed" }, { "id": "2", "content": "实现用户认证模块", "status": "in_progress" }, { "id": "3", "content": "编写单元测试", "status": "pending" }, { "id": "4", "content": "更新API文档", "status": "pending" } ], "summary": { "total": 4, "pending": 2, "in_progress": 1, "completed": 1, "cancelled": 0 } } ``` ### 3.3 集成到 ReAct 循环 - 每个 agent session 维护一个 `TodoStore` 实例 - 新建 session:空 TodoStore - 恢复 session(gateway 模式):`TodoStore.hydrateFromHistory(messages)` 从历史恢复 - agent 调用 `todo` 工具后:通过 WS 推送 `todo_update` 事件 - 上下文压缩时:调用 `todoStore.formatForInjection()`,非空则作为 user message 注入压缩后历史 ## 4. Skill 统一组件(前端) ### 4.1 核心改动 废弃双组件方案(`skill-indicator.vue` + `message-item.vue` 内嵌历史),合并为 `skill-card.vue`。 **数据流对比**: 改造前(有跳变): ``` skill_start → skillProgress[] 添加 → skill-indicator.vue 渲染 skill_end → skillProgress[] 更新 status=done done → skillProgress[] 清空 → metadata.skillExecutions 接管 → message-item.vue 重新渲染(跳变!) ``` 改造后(无跳变): ``` skill_start → skillProgress[] 添加 → skill-card.vue 渲染(status=running) skill_end → skillProgress[] 更新 status=done → skill-card.vue CSS 过渡(无跳变) done → skillProgress[] 保留不清空,同时写入 metadata 用于历史恢复 ``` 关键变化:`done` 事件**不再清空** `skillProgress`,组件始终由 `skillProgress` 驱动。 ### 4.2 skill-card.vue 文件:`packages/frontend/src/modules/agent/components/skill-card.vue` ```vue ``` CSS 过渡: ```css .skill-card { transition: all 0.3s ease; border-left: 3px solid var(--skill-border-color); } .skill-card--running { --skill-border-color: var(--el-color-primary); } .skill-card--done { --skill-border-color: var(--el-color-success); } .skill-card--error { --skill-border-color: var(--el-color-danger); } .fade-enter-active, .fade-leave-active { transition: opacity 0.3s ease; } .fade-enter-from, .fade-leave-to { opacity: 0; } ``` ### 4.3 store/chat.ts 改动 ```typescript // done 事件:不再清空 skillProgress case 'done': { // 将 skillProgress 写入 metadata 用于历史恢复 if (assistantMsg) { assistantMsg.metadata.skillExecutions = [...skillProgress.value]; // 不再执行 skillProgress.value = [] } break; } // 切换会话时才清空 function switchSession(newSessionId: string) { skillProgress.value = []; // 切换时清空 // ...加载新会话消息 } // 加载历史消息时从 metadata 恢复 function restoreSkillProgress(messages: ChatMessage[]) { skillProgress.value = []; for (const msg of messages) { if (msg.metadata?.skillExecutions) { skillProgress.value.push(...msg.metadata.skillExecutions); } } } ``` ## 5. Token 统计系统改造 ### 5.1 后端改动 `token_update` WS 事件增加 `context` 字段: ```typescript interface TokenUpdateEvent { // 本轮对话的实时累计 current: { inputTokens: number; outputTokens: number; totalTokens: number; apiCalls: number; }; // 上下文窗口状态(新增) context: { usedTokens: number; // 当前上下文已用 token maxTokens: number; // 模型上下文窗口上限 percent: number; // 使用百分比(后端算好) }; } ``` 推送时机:每次 LLM API 返回后推送。 ### 5.2 前端改动 删除本地 token 估算逻辑: ```typescript // 删除:estimateContextTokens 相关代码 // 删除:中文字符/其他字符的 token 估算公式 // 简化为: const tokenUsage = ref(null); case 'token_update': { tokenUsage.value = event.data; break; } case 'done': { if (assistantMsg && tokenUsage.value) { assistantMsg.metadata.tokenUsage = tokenUsage.value.current; } tokenUsage.value = null; break; } ``` ### 5.3 Token UI 重设计 从顶部统计条改为输入区右下角紧凑展示: ``` ┌─ 对话区 ────────────────────────────────────┐ │ │ │ 消息列表... │ │ │ ├──────────────────────────────────────────────┤ │ 输入框 │ │ │ │ [Agent选择] [附件] 上下文 32% ● 2K │ └──────────────────────────────────────────────┘ ``` 文件:`packages/frontend/src/modules/agent/components/token-stats.vue` ```vue ``` 格式化规则:`1234` → `1.2K`,`12345` → `12.3K` 颜色规则:≤50% 绿,50-80% 黄,>80% 红。 ## 6. Todo 卡片前端组件 ### 6.1 store/chat.ts 新增 ```typescript const todoItems = ref([]); const todoSummary = ref(null); case 'todo_update': { todoItems.value = event.data.todos; todoSummary.value = event.data.summary; break; } // 切换会话时从历史恢复 function restoreTodoFromHistory(messages: ChatMessage[]) { for (let i = messages.length - 1; i >= 0; i--) { const msg = messages[i]; if (msg.role === 'tool' && msg.metadata?.toolName === 'todo') { try { const data = JSON.parse(msg.content); todoItems.value = data.todos; todoSummary.value = data.summary; } catch {} return; } } todoItems.value = []; todoSummary.value = null; } ``` ### 6.2 todo-card.vue 文件:`packages/frontend/src/modules/agent/components/todo-card.vue` ```vue ``` CSS 要点: ```css .todo-card { margin: 8px 0; border-radius: 8px; border: 1px solid var(--el-border-color-lighter); background: var(--el-fill-color-lighter); overflow: hidden; } .todo-card__item { transition: all 0.3s ease; } .todo-card__item--in_progress { background: var(--el-color-primary-light-9); } .todo-card__content.is-done { text-decoration: line-through; color: var(--el-text-color-placeholder); } .todo-card__content.is-active { font-weight: 600; color: var(--el-color-primary); } .todo-card__spinner { display: inline-block; width: 14px; height: 14px; border: 2px solid var(--el-color-primary); border-top-color: transparent; border-radius: 50%; animation: spin 1s linear infinite; } ``` ### 6.3 chat.vue 布局 ```vue
``` ## 7. 思考内容流式展示 ### 7.1 问题现状 当前 Neta 后端已有的基础: - `runtime/thinking.ts`:`resolveThinkingDefault(model)`、`isThinkingSupported(model)`、`ThinkLevel` 类型 - `plugins/plugin_entry.ts`:`LLMProviderConfig` 包含 `thinkLevel?: ThinkLevel` - `plugins/llm_providers/anthropic.ts`:能提取 `block.type === 'thinking'` 块 - `plugins/llm_providers/openai.ts`:能提取 `reasoning_content` 字段 - `runtime/attempt.ts`:有 `onThinking?.(response.thinking)` 回调 缺失的部分: - 适配器未将思考参数传递给 LLM API(Anthropic 没传 `thinking_budget`,OpenAI 没传 `reasoning_effort`) - 前端不是流式展示思考内容 - 没有思考级别控制 UI - 没有多模型统一适配层 ### 7.2 后端:思考流式推送 将当前的一次性 `thinking` 事件改为流式 `thinking_delta`: ```typescript // 新增 WS 事件 type: 'thinking_delta' // 增量文本片段 data: { text: string } type: 'thinking_done' // 思考结束标记 data: {} // 废弃 type: 'thinking' // 被 thinking_delta + thinking_done 替代 ``` **当前限制:** 现有 LLM 适配器(anthropic.ts/openai.ts)使用非流式 API 调用,`attempt.ts:38` 的 `onThinking?.(response.thinking)` 是一次性返回完整 thinking 文本。因此本次实现中 `thinking_delta` 实际为"收到即推"(一次性推送完整文本),而非逐字流式。前端组件已按流式设计,后续迭代将 LLM 适配器改为 streaming 模式后可无缝升级为真正逐字流式。 ### 7.3 前端 store 改动 ```typescript // 新增状态 const isThinking = ref(false); const thinkingStream = ref(''); case 'thinking_delta': { if (!isThinking.value) { isThinking.value = true; thinkingStream.value = ''; } thinkingStream.value += event.data.text; break; } case 'thinking_done': { isThinking.value = false; if (assistantMsg) { assistantMsg.thinking = thinkingStream.value; } break; } // 正文 token 到达时自动结束思考状态 case 'token': { if (isThinking.value) { isThinking.value = false; if (assistantMsg) { assistantMsg.thinking = thinkingStream.value; } } // ...原有追加 content 逻辑 } ``` ### 7.4 thinking-block.vue 组件 参考 openclaw 的视觉设计(虚线边框 + 半透明背景 + 呼吸动画): ```vue ``` 交互行为: - 流式思考中:默认展开,内容逐字出现,末尾闪烁光标 - 思考完成后:自动折叠(CSS transition 0.3s),用户可点击重新展开 - 历史消息中的思考:默认折叠 CSS 样式(参考 openclaw `chat-thinking`): ```css .thinking-block { margin: 8px 0; padding: 10px 12px; border-radius: 8px; border: 1px dashed var(--el-border-color); background: var(--el-fill-color-lighter); font-size: 12px; line-height: 1.5; color: var(--el-text-color-secondary); transition: all 0.3s ease; } .thinking-block.is-streaming { animation: thinking-pulse 1.5s ease-in-out infinite; } @keyframes thinking-pulse { 0%, 100% { border-color: var(--el-border-color); } 50% { border-color: var(--el-color-primary-light-3); } } .thinking-block__cursor { display: inline-block; width: 2px; height: 14px; background: var(--el-color-primary); animation: blink 1s step-end infinite; vertical-align: text-bottom; margin-left: 2px; } @keyframes blink { 50% { opacity: 0; } } ``` ### 7.5 在 chat.vue 中的位置 思考块嵌入在流式 assistant 消息内部,位于正文之前: ```vue ``` 历史消息中同理,`message-item.vue` 检测 `msg.thinking` 字段渲染 `thinking-block`(默认折叠)。 ## 8. 多模型思考适配层(后端) 参考 hermes 的多模型统一适配架构,补全 Neta 后端的思考参数传递链路。 ### 8.1 统一思考配置接口 文件:`packages/backend/src/modules/netaclaw/runtime/thinking.ts`(已有,需扩展) ```typescript // 已有的 ThinkLevel 类型 type ThinkLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'adaptive'; // 新增:各提供商的思考预算映射(参考 hermes THINKING_BUDGET) const ANTHROPIC_BUDGET_MAP: Record = { minimal: 2000, low: 4000, medium: 8000, high: 16000, }; // 新增:Anthropic adaptive effort 映射(参考 hermes ADAPTIVE_EFFORT_MAP) const ANTHROPIC_ADAPTIVE_MAP: Record = { minimal: 'low', low: 'low', medium: 'medium', high: 'high', adaptive: 'medium', // adaptive 模式下由模型自行决定 }; // 新增:检测模型是否支持 adaptive thinking(Claude 4.6+) function supportsAdaptiveThinking(model: string): boolean { return /4[-.]6/.test(model); } // 新增:构建各提供商的思考参数 interface ThinkingParams { anthropic?: { thinking: { type: string; budget_tokens?: number; effort?: string }; temperature?: number; maxTokensAdjust?: number; betas?: string[]; }; openai?: { reasoning_effort?: string; reasoning?: { enabled: boolean; effort?: string }; }; } function buildThinkingParams( supplier: string, model: string, level: ThinkLevel ): ThinkingParams { if (level === 'off') return {}; const params: ThinkingParams = {}; switch (supplier) { case 'anthropic': { if (supportsAdaptiveThinking(model)) { // Claude 4.6:adaptive thinking params.anthropic = { thinking: { type: 'adaptive' }, betas: ['interleaved-thinking-2025-05-14'], }; // adaptive 模式下通过 output_config.effort 控制 } else { // 旧模型:手动 budget const budget = ANTHROPIC_BUDGET_MAP[level] || 8000; params.anthropic = { thinking: { type: 'enabled', budget_tokens: budget }, temperature: 1.0, maxTokensAdjust: budget + 4096, betas: ['interleaved-thinking-2025-05-14'], }; } break; } case 'openai': case 'deepseek': { // OpenAI 兼容:reasoning_effort 直接传递 // 支持 o1/o3/deepseek-r1 等模型 const effortMap: Record = { minimal: 'low', low: 'low', medium: 'medium', high: 'high', }; params.openai = { reasoning_effort: effortMap[level] || 'medium', reasoning: { enabled: true, effort: effortMap[level] || 'medium' }, }; break; } default: { // 其他提供商:不传思考参数,依赖模型自身行为 break; } } return params; } ``` ### 8.2 Anthropic 适配器改动 文件:`plugins/llm_providers/anthropic.ts` ```typescript // 当前:只提取思考块,没传思考参数 // 改为:根据 config.thinkLevel 构建并传递参数 import { buildThinkingParams, supportsAdaptiveThinking } from '../../runtime/thinking'; // 在构建 API 请求时: const thinkParams = buildThinkingParams('anthropic', model, config.thinkLevel || 'off'); if (thinkParams.anthropic) { requestBody.thinking = thinkParams.anthropic.thinking; if (thinkParams.anthropic.temperature !== undefined) { requestBody.temperature = thinkParams.anthropic.temperature; } if (thinkParams.anthropic.maxTokensAdjust) { requestBody.max_tokens = Math.max(requestBody.max_tokens, thinkParams.anthropic.maxTokensAdjust); } // Beta headers if (thinkParams.anthropic.betas?.length) { headers['anthropic-beta'] = thinkParams.anthropic.betas.join(','); } } // 流式响应中提取 thinking_delta: // 当收到 type === 'thinking_delta' 的 SSE 事件时 // 立即通过 onThinkingDelta?.(delta.thinking) 回调推送给 WS 层 ``` ### 8.3 OpenAI 适配器改动 文件:`plugins/llm_providers/openai.ts` ```typescript import { buildThinkingParams } from '../../runtime/thinking'; const thinkParams = buildThinkingParams(config.supplier, model, config.thinkLevel || 'off'); if (thinkParams.openai) { // OpenAI / DeepSeek:通过 extra_body 传递 requestBody.reasoning_effort = thinkParams.openai.reasoning_effort; // 或通过 reasoning 对象(部分提供商) // requestBody.reasoning = thinkParams.openai.reasoning; } // 流式响应中提取 reasoning delta: // 检查 delta.reasoning_content 或 delta.reasoning 字段 // 通过 onThinkingDelta?.(text) 回调推送 ``` ### 8.4 Agent 运行时:会话级思考级别 文件:`runtime/agent.ts` ```typescript // 当前:只用 resolveThinkingDefault(model) 硬编码默认值 // 改为:优先级链 const thinkLevel = session.thinkLevel // 1. 用户在前端选择的(会话级) ?? agent.defaultThinkLevel // 2. agent 配置的默认值 ?? resolveThinkingDefault(model); // 3. 模型默认值 // 传入 LLMProviderConfig const llmConfig: LLMProviderConfig = { ...baseConfig, thinkLevel, }; ``` ### 8.5 模型能力检测增强 ```typescript // thinking.ts 扩展 interface ModelThinkingCapability { supported: boolean; adaptive: boolean; // 是否支持 adaptive thinking levels: ThinkLevel[]; // 支持的级别列表 defaultLevel: ThinkLevel; // 默认级别 } function getModelThinkingCapability(supplier: string, model: string): ModelThinkingCapability { // Anthropic Claude 4.6+ if (supplier === 'anthropic' && supportsAdaptiveThinking(model)) { return { supported: true, adaptive: true, levels: ['off', 'minimal', 'low', 'medium', 'high', 'adaptive'], defaultLevel: 'adaptive', }; } // Anthropic Claude 3.5/4.5(非 adaptive) if (supplier === 'anthropic') { return { supported: true, adaptive: false, levels: ['off', 'low', 'medium', 'high'], defaultLevel: 'medium', }; } // OpenAI o1/o3 if (supplier === 'openai' && /^(o1|o3)/.test(model)) { return { supported: true, adaptive: false, levels: ['off', 'low', 'medium', 'high'], defaultLevel: 'medium', }; } // DeepSeek R1 if (supplier === 'deepseek' && model.includes('r1')) { return { supported: true, adaptive: false, levels: ['off', 'low', 'medium', 'high'], defaultLevel: 'medium', }; } // 不支持思考的模型 return { supported: false, adaptive: false, levels: ['off'], defaultLevel: 'off', }; } ``` ## 9. 思考级别控制(前端) ### 9.1 WS 协议:动态切换思考级别 ```typescript // 新增客户端→服务端消息 { type: 'set_thinking_level', data: { level: ThinkLevel } } // 后端收到后更新 session.thinkLevel,下次 LLM 调用生效 ``` ### 9.2 thinking-level-selector.vue 放在输入区域,与 Agent 选择器、附件按钮同行: ``` ┌─ 对话区 ────────────────────────────────────────┐ │ 消息列表... │ ├──────────────────────────────────────────────────┤ │ 输入框 │ │ │ │ [Agent] [附件] [💭 中等 ▾] 上下文 32% ● 2K │ └──────────────────────────────────────────────────┘ ``` ```vue ``` ### 9.3 store/chat.ts 新增 ```typescript const thinkLevel = ref('medium'); const modelThinkingCapability = ref({ ... }); function setThinkLevel(level: ThinkLevel) { thinkLevel.value = level; sendWSMessage({ type: 'set_thinking_level', data: { level } }); } // 切换会话时恢复 function switchSession(sessionId: string) { // ... thinkLevel.value = session.thinkLevel ?? 'medium'; } // 切换模型/Agent 时更新可选级别 function updateModelCapability(supplier: string, model: string) { // 通过 API 或本地映射获取模型思考能力 modelThinkingCapability.value = getModelThinkingCapability(supplier, model); // 如果当前级别不在新模型支持范围内,回退到默认值 if (!modelThinkingCapability.value.levels.includes(thinkLevel.value)) { setThinkLevel(modelThinkingCapability.value.defaultLevel); } } ``` ## 10. 对话列表适配 `chat-sidebar.vue` 会话项增加摘要行: ```vue
``` ## 11. WS 事件协议变更汇总 | 事件类型 | 方向 | 变更 | 数据结构 | |---------|------|------|---------| | `todo_update` | 服务端→客户端 | **新增** | `{ todos: TodoItem[], summary: TodoSummary }` | | `token_update` | 服务端→客户端 | **修改** | 增加 `context: { usedTokens, maxTokens, percent }` | | `thinking_delta` | 服务端→客户端 | **新增** | `{ text: string }` | | `thinking_done` | 服务端→客户端 | **新增** | `{}` | | `set_thinking_level` | 客户端→服务端 | **新增** | `{ level: ThinkLevel }` | | `thinking` | 服务端→客户端 | **废弃** | 被 `thinking_delta` + `thinking_done` 替代 | | `skill_start` | 服务端→客户端 | 不变 | — | | `skill_end` | 服务端→客户端 | 不变 | — | | `done` | 服务端→客户端 | **修改** | 前端不再清空 skillProgress | ## 12. 完整文件改动清单 ### 后端新增 | 文件 | 说明 | |------|------| | `modules/netaclaw/runtime/todo_store.ts` | TodoStore 类 | | `modules/netaclaw/tools/todo_tool.ts` | todo 工具定义 + Schema + handler | ### 后端修改 | 文件 | 说明 | |------|------| | `modules/netaclaw/runtime/thinking.ts` | 扩展:`buildThinkingParams()`、`getModelThinkingCapability()`、预算映射常量 | | `modules/netaclaw/plugins/llm_providers/anthropic.ts` | 传递 `thinking` 参数 + beta headers + 流式 `thinking_delta` 提取 | | `modules/netaclaw/plugins/llm_providers/openai.ts` | 传递 `reasoning_effort` 参数 + 流式 reasoning delta 提取 | | `modules/netaclaw/runtime/agent.ts` | 会话级思考级别优先级链、todo 工具注册 | | `modules/netaclaw/runtime/attempt.ts` | `onThinkingDelta` 回调改为流式推送 | | ReAct 循环主文件 | todo hydrate、压缩注入、推送 `todo_update` 事件 | | WS gateway | 处理 `set_thinking_level` 客户端消息、`token_update` 增加 context 字段 | ### 前端新增 | 文件 | 说明 | |------|------| | `components/skill-card.vue` | 统一 Skill 卡片 | | `components/todo-card.vue` | Todo 任务规划卡片 | | `components/token-stats.vue` | 紧凑 Token 统计组件 | | `components/thinking-block.vue` | 思考内容流式展示组件 | | `components/thinking-level-selector.vue` | 思考级别下拉选择器 | ### 前端修改 | 文件 | 说明 | |------|------| | `store/chat.ts` | 新增 todo/thinking/thinkLevel 状态、`todo_update`/`thinking_delta`/`thinking_done`/`set_thinking_level` 事件处理、删除本地 token 估算、`done` 不清空 skillProgress | | `views/chat.vue` | 布局:插入 todo-card + skill-card + thinking-block、token 统计移到输入区、思考级别选择器 | | `components/message-item.vue` | 删除 Skill 执行历史渲染代码、删除旧思考折叠区块、改用 thinking-block 组件 | | `components/chat-sidebar.vue` | 会话项增加 todo 进度和运行状态 | | `hooks/websocket.ts` | 类型定义增加 `todo_update`、`thinking_delta`、`thinking_done`、`set_thinking_level` | | `types/index.d.ts` | 新增 TodoItem、TodoSummary、ThinkLevel、ModelThinkingCapability 类型 | ### 前端删除 | 文件 | 说明 | |------|------| | `components/skill-indicator.vue` | 被 skill-card.vue 替代 | ## 13. 不在本次范围 - 上下文压缩/截断机制(后续迭代) - 后端 ReAct 循环核心逻辑(只注册新工具 + 接入思考参数,不改执行流程) - skill-result-viewer 及子组件(skill-card 内部复用) - 语音对话功能 - Agent 列表和编辑页面 - 思考块签名管理(Anthropic 签名验证/剥离,参考 hermes 但复杂度高,后续迭代)