37 KiB
Agent 对话页面体验优化 实施计划
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 优化 agent 对话页面的 6 个体验问题:Skill 视觉跳变、Token 不准、Token UI、缺少任务规划、思考不流畅、思考级别控制。
Architecture: 后端扩展 thinking.ts 适配层 + 新增 TodoStore/todo 工具 + WS 协议新增 4 个事件;前端用 skill-card 替代双组件、新增 todo-card/thinking-block/token-stats/thinking-level-selector 5 个组件,store 统一管理新状态。
Tech Stack: Midway.js 3.20 + TypeORM + Socket.IO (后端) / Vue 3.5 + Element Plus 2.9 + Pinia (前端)
设计文档: docs/superpowers/specs/2026-04-13-agent-chat-ux-overhaul-design.md
文件结构
后端 (packages/backend/src/modules/netaclaw/)
| 文件 | 操作 | 职责 |
|---|---|---|
runtime/todo_store.ts |
新建 | TodoStore 类:write/read/merge/hydrate/formatForInjection |
tools/todo_tool.ts |
新建 | todo 工具 Schema + handler |
runtime/thinking.ts |
修改 | 扩展:buildThinkingParams、getModelThinkingCapability、预算映射 |
plugins/llm_providers/anthropic.ts |
修改 | 传递 thinking 参数 + 流式 thinking_delta |
plugins/llm_providers/openai.ts |
修改 | 传递 reasoning_effort 参数 |
runtime/agent.ts |
修改 | 会话级 thinkLevel 优先级链 + todo 工具注册 |
gateway/server.ts |
修改 | 新增 WS 事件 + set_thinking_level 处理 |
前端 (packages/frontend/src/modules/agent/)
| 文件 | 操作 | 职责 |
|---|---|---|
components/skill-card.vue |
新建 | 统一 Skill 卡片(替代 skill-indicator + message-item 内嵌) |
components/todo-card.vue |
新建 | Todo 任务规划卡片 |
components/token-stats.vue |
新建 | 紧凑 Token 统计组件 |
components/thinking-block.vue |
新建 | 思考内容流式展示 |
components/thinking-level-selector.vue |
新建 | 思考级别下拉选择器 |
types/index.d.ts |
修改 | 新增 TodoItem/TodoSummary/ThinkLevel/ModelThinkingCapability 类型 |
store/chat.ts |
修改 | 新增 todo/thinking/thinkLevel 状态 + 事件处理 |
views/chat.vue |
修改 | 布局重构:插入新组件 + token 统计移位 |
components/message-item.vue |
修改 | 删除旧 skill 历史 + 旧 thinking 区块,改用新组件 |
components/chat-sidebar.vue |
修改 | 会话项增加 todo 进度 |
hooks/websocket.ts |
修改 | 类型增加新事件 |
components/skill-indicator.vue |
删除 | 被 skill-card.vue 替代 |
任务依赖顺序
Task 1: 前端类型定义(无依赖)
Task 2: 后端 TodoStore(无依赖)
Task 3: 后端 todo 工具(依赖 Task 2)
Task 4: 后端 thinking.ts 扩展(无依赖)
Task 5: 后端 Anthropic 适配器(依赖 Task 4)
Task 6: 后端 OpenAI 适配器(依赖 Task 4)
Task 7: 后端 agent.ts 改造(依赖 Task 2, 3, 4)
Task 8: 后端 gateway WS 协议(依赖 Task 2, 3, 7)
Task 9: 前端 store/chat.ts 改造(依赖 Task 1)
Task 10: 前端 skill-card.vue(依赖 Task 9)
Task 11: 前端 todo-card.vue(依赖 Task 9)
Task 12: 前端 thinking-block.vue(依赖 Task 9)
Task 13: 前端 token-stats.vue(依赖 Task 9)
Task 14: 前端 thinking-level-selector.vue(依赖 Task 9)
Task 15: 前端 chat.vue 布局重构(依赖 Task 10-14)
Task 16: 前端 message-item.vue 改造(依赖 Task 10, 12)
Task 17: 前端 chat-sidebar.vue 适配(依赖 Task 9)
Task 18: 删除 skill-indicator.vue + 清理(依赖 Task 15, 16)
Task 1: 前端类型定义
Files:
-
Modify:
packages/frontend/src/modules/agent/types/index.d.ts:119-169 -
Step 1: 在 types/index.d.ts 中新增类型定义
在文件末尾追加:
// === Todo 系统类型 ===
export interface TodoItem {
id: string;
content: string;
status: 'pending' | 'in_progress' | 'completed' | 'cancelled';
}
export interface TodoSummary {
total: number;
pending: number;
in_progress: number;
completed: number;
cancelled: number;
}
// === 思考级别类型 ===
export type ThinkLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'adaptive';
export interface ModelThinkingCapability {
supported: boolean;
adaptive: boolean;
levels: ThinkLevel[];
defaultLevel: ThinkLevel;
}
// === Token 统计类型 ===
export interface TokenUpdateEvent {
current: {
inputTokens: number;
outputTokens: number;
totalTokens: number;
apiCalls: number;
};
context: {
usedTokens: number;
maxTokens: number;
percent: number;
};
}
- Step 2: 更新 WSServerEvent 类型,增加新事件
找到 WSServerEvent 类型定义(约第119行),在 type 联合类型中追加:
// 在现有 type 联合中追加:
| 'todo_update'
| 'thinking_delta'
| 'thinking_done'
- Step 3: 更新 hooks/websocket.ts 中的 WSClientMessage 类型
在 hooks/websocket.ts 中找到客户端消息类型,追加:
| 'set_thinking_level'
- Step 4: 提交
git add packages/frontend/src/modules/agent/types/index.d.ts packages/frontend/src/modules/agent/hooks/websocket.ts
git commit -m "feat(agent): 新增 Todo/Thinking/Token 类型定义和 WS 事件类型"
Task 2: 后端 TodoStore
Files:
-
Create:
packages/backend/src/modules/netaclaw/runtime/todo_store.ts -
Step 1: 创建 todo_store.ts
/**
* TodoStore - 会话级任务列表管理
* 参考 hermes-agent-main 的 TodoStore 设计
*/
export interface TodoItem {
id: string;
content: string;
status: 'pending' | 'in_progress' | 'completed' | 'cancelled';
}
export interface TodoSummary {
total: number;
pending: number;
in_progress: number;
completed: number;
cancelled: number;
}
const VALID_STATUSES = ['pending', 'in_progress', 'completed', 'cancelled'];
export class TodoStore {
private items: TodoItem[] = [];
/**
* 写入 todo 列表
* @param todos 任务项数组
* @param merge false=全量替换,true=按 id 增量更新+追加新项
*/
write(todos: TodoItem[], merge = false): TodoItem[] {
if (!merge) {
this.items = todos.map(t => this.validate(t));
} else {
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: TodoSummary = { 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 */
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 状态 */
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(typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content));
if (data.todos) {
store.write(data.todos, false);
}
} catch { /* 忽略解析失败 */ }
break;
}
}
return store;
}
private validate(item: Partial<TodoItem>): TodoItem {
return {
id: item.id || '?',
content: item.content || '(无描述)',
status: VALID_STATUSES.includes(item.status!) ? item.status! as TodoItem['status'] : 'pending',
};
}
}
- Step 2: 提交
git add packages/backend/src/modules/netaclaw/runtime/todo_store.ts
git commit -m "feat(netaclaw): 新增 TodoStore 类 - 支持 merge/replace/hydrate/压缩注入"
Task 3: 后端 todo 工具
Files:
-
Create:
packages/backend/src/modules/netaclaw/tools/todo_tool.ts -
Step 1: 创建 todo_tool.ts
import { TodoStore } from '../runtime/todo_store.js';
export 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' as const,
properties: {
todos: {
type: 'array' as const,
description: '任务项数组。省略则读取当前列表。',
items: {
type: 'object' as const,
properties: {
id: { type: 'string' as const, description: '唯一标识' },
content: { type: 'string' as const, description: '任务描述' },
status: {
type: 'string' as const,
enum: ['pending', 'in_progress', 'completed', 'cancelled'],
description: '当前状态',
},
},
required: ['id', 'content', 'status'],
},
},
merge: {
type: 'boolean' as const,
description: 'true=按 id 增量更新+追加新项。false(默认)=全量替换整个列表。',
default: false,
},
},
required: [] as string[],
},
};
/** 执行 todo 工具 */
export function executeTodo(
store: TodoStore,
args: { todos?: any[]; merge?: boolean }
): { todos: any[]; summary: any } {
if (args.todos && args.todos.length > 0) {
store.write(args.todos, args.merge ?? false);
}
return {
todos: store.read(),
summary: store.getSummary(),
};
}
- Step 2: 提交
git add packages/backend/src/modules/netaclaw/tools/todo_tool.ts
git commit -m "feat(netaclaw): 新增 todo 工具 Schema 和 handler"
Task 4: 后端 thinking.ts 扩展
Files:
-
Modify:
packages/backend/src/modules/netaclaw/runtime/thinking.ts(当前12行) -
Step 1: 扩展 thinking.ts,新增预算映射和多模型适配
保留现有的 resolveThinkingDefault 和 isThinkingSupported,在文件末尾追加:
import type { ThinkLevel } from '../plugins/plugin_entry.js';
// Anthropic 思考预算映射(参考 hermes THINKING_BUDGET)
export const ANTHROPIC_BUDGET_MAP: Record<string, number> = {
minimal: 2000,
low: 4000,
medium: 8000,
high: 16000,
};
// Anthropic adaptive effort 映射
export const ANTHROPIC_ADAPTIVE_MAP: Record<string, string> = {
minimal: 'low',
low: 'low',
medium: 'medium',
high: 'high',
adaptive: 'medium',
};
// OpenAI reasoning effort 映射
export const OPENAI_EFFORT_MAP: Record<string, string> = {
minimal: 'low',
low: 'low',
medium: 'medium',
high: 'high',
};
/** 检测模型是否支持 adaptive thinking(Claude 4.6+) */
export function supportsAdaptiveThinking(model: string): boolean {
return /4[-.]6/.test(model);
}
/** 模型思考能力描述 */
export interface ModelThinkingCapability {
supported: boolean;
adaptive: boolean;
levels: ThinkLevel[];
defaultLevel: ThinkLevel;
}
/** 获取模型的思考能力 */
export function getModelThinkingCapability(supplier: string, model: string): ModelThinkingCapability {
if (supplier === 'anthropic' && supportsAdaptiveThinking(model)) {
return { supported: true, adaptive: true, levels: ['off', 'minimal', 'low', 'medium', 'high', 'adaptive'], defaultLevel: 'adaptive' };
}
if (supplier === 'anthropic') {
return { supported: true, adaptive: false, levels: ['off', 'low', 'medium', 'high'], defaultLevel: 'medium' };
}
if (supplier === 'openai' && /^(o1|o3)/.test(model)) {
return { supported: true, adaptive: false, levels: ['off', 'low', 'medium', 'high'], defaultLevel: 'medium' };
}
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' };
}
/** 构建 Anthropic 思考参数 */
export function buildAnthropicThinkingParams(model: string, level: ThinkLevel) {
if (level === 'off') return null;
if (supportsAdaptiveThinking(model)) {
return {
thinking: { type: 'adaptive' as const },
betas: ['interleaved-thinking-2025-05-14'],
};
}
const budget = ANTHROPIC_BUDGET_MAP[level] || 8000;
return {
thinking: { type: 'enabled' as const, budget_tokens: budget },
temperature: 1.0,
maxTokensAdjust: budget + 4096,
betas: ['interleaved-thinking-2025-05-14'],
};
}
/** 构建 OpenAI/DeepSeek 思考参数 */
export function buildOpenAIThinkingParams(level: ThinkLevel) {
if (level === 'off') return null;
return {
reasoning_effort: OPENAI_EFFORT_MAP[level] || 'medium',
};
}
- Step 2: 提交
git add packages/backend/src/modules/netaclaw/runtime/thinking.ts
git commit -m "feat(netaclaw): 扩展 thinking.ts - 多模型预算映射和能力检测"
Task 5: 后端 Anthropic 适配器改造
Files:
-
Modify:
packages/backend/src/modules/netaclaw/plugins/llm_providers/anthropic.ts:23-30,66 -
Step 1: 导入 buildAnthropicThinkingParams 并传递思考参数
在文件顶部导入:
import { buildAnthropicThinkingParams } from '../../runtime/thinking.js';
在 chat() 方法中 client.messages.create() 调用处(约第23-30行),在构建请求参数时加入思考参数:
const thinkParams = buildAnthropicThinkingParams(config.model, config.thinkLevel || 'off');
const createParams: any = {
model: config.model,
max_tokens: config.maxTokens || 4096,
messages,
tools,
// ...现有参数
};
if (thinkParams) {
createParams.thinking = thinkParams.thinking;
if (thinkParams.temperature !== undefined) {
createParams.temperature = thinkParams.temperature;
}
if (thinkParams.maxTokensAdjust) {
createParams.max_tokens = Math.max(createParams.max_tokens, thinkParams.maxTokensAdjust);
}
}
// 添加 beta headers
const headers: Record<string, string> = {};
if (thinkParams?.betas?.length) {
headers['anthropic-beta'] = thinkParams.betas.join(',');
}
- Step 2: 提交
git add packages/backend/src/modules/netaclaw/plugins/llm_providers/anthropic.ts
git commit -m "feat(netaclaw): Anthropic 适配器传递 thinking 参数和 beta headers"
Task 6: 后端 OpenAI 适配器改造
Files:
-
Modify:
packages/backend/src/modules/netaclaw/plugins/llm_providers/openai.ts:18-24 -
Step 1: 导入 buildOpenAIThinkingParams 并传递参数
在文件顶部导入:
import { buildOpenAIThinkingParams } from '../../runtime/thinking.js';
在 chat() 方法中 client.chat.completions.create() 调用处(约第18-24行),加入:
const thinkParams = buildOpenAIThinkingParams(config.thinkLevel || 'off');
const createParams: any = {
model: config.model,
messages,
tools,
// ...现有参数
};
if (thinkParams) {
createParams.reasoning_effort = thinkParams.reasoning_effort;
}
- Step 2: 提交
git add packages/backend/src/modules/netaclaw/plugins/llm_providers/openai.ts
git commit -m "feat(netaclaw): OpenAI 适配器传递 reasoning_effort 参数"
Task 7: 后端 agent.ts 改造
Files:
-
Modify:
packages/backend/src/modules/netaclaw/runtime/agent.ts:36-45,57-66 -
Step 1: 导入 TodoStore 和 todo 工具,修改 thinkLevel 优先级链
在文件顶部导入:
import { TodoStore } from './todo_store.js';
import { todoToolSchema, executeTodo } from '../tools/todo_tool.js';
修改 runAgent() 函数中 thinkLevel 的获取逻辑(约第36行):
// 优先级链:会话级 > agent 配置 > 模型默认
const thinkLevel = params.sessionThinkLevel
?? config.defaultThinkLevel
?? resolveThinkingDefault(config.model);
在工具列表构建处,将 todo 工具加入:
// 在现有工具列表中追加 todo 工具
const todoStore = params.todoStore ?? new TodoStore();
const todoTool = {
...todoToolSchema,
execute: async (args: any) => {
const result = executeTodo(todoStore, args);
// 通过回调通知前端
params.onTodoUpdate?.(result);
return JSON.stringify(result);
},
};
- Step 2: 在 AgentConfig 接口中新增 defaultThinkLevel 字段
interface AgentConfig {
// ...现有字段(name, systemPrompt, model, apiKey, baseUrl, temperature, maxTokens, maxToolRounds, skills)
defaultThinkLevel?: ThinkLevel; // agent 配置的默认思考级别
}
- Step 3: 在 AgentRunParams 接口中新增字段
interface AgentRunParams {
// ...现有字段
sessionThinkLevel?: ThinkLevel; // 会话级思考级别
todoStore?: TodoStore; // TodoStore 实例
onTodoUpdate?: (data: { todos: any[]; summary: any }) => void; // todo 更新回调
onThinkingDelta?: (text: string) => void; // 思考流式回调(替代 onThinking)
}
- Step 4: 提交
git add packages/backend/src/modules/netaclaw/runtime/agent.ts
git commit -m "feat(netaclaw): agent.ts 集成 TodoStore + 会话级思考级别"
Task 8: 后端 gateway WS 协议改造
Files:
-
Modify:
packages/backend/src/modules/netaclaw/gateway/server.ts:77-251 -
Step 1: 处理 set_thinking_level 客户端消息
在 @OnWSMessage('message') 处理函数中(约第77行),增加对 set_thinking_level 消息的处理:
case 'set_thinking_level': {
const { level } = parsed.data;
// 更新当前 session 的思考级别
session.thinkLevel = level;
break;
}
- Step 2: 修改 handleChat 中的 runAgent 调用,新增回调
在 handleChat() 方法中(约第193-213行),修改 runAgent() 调用:
const todoStore = session.todoStore ?? new TodoStore();
session.todoStore = todoStore;
await runAgent({
// ...现有参数
sessionThinkLevel: session.thinkLevel,
todoStore,
onTodoUpdate: (data) => {
this.send({ type: 'todo_update', sessionId: sid, data });
},
onThinkingDelta: (text) => {
this.send({ type: 'thinking_delta', sessionId: sid, data: { text } });
},
onThinking: (text) => {
// 保留完整思考文本用于存储
thinkingText += text;
// 不再发送旧的 'thinking' 事件
},
});
// 思考结束标记
if (thinkingText) {
this.send({ type: 'thinking_done', sessionId: sid, data: {} });
}
- Step 3: 新增 token_update 推送逻辑(含 context 字段)
当前后端没有中间 token_update 推送,只在 done 事件的 usage 字段里带。需要在 runAgent 回调中新增推送。
首先在 thinking.ts 中新增上下文估算工具函数:
/** 模型上下文窗口上限映射 */
const MODEL_MAX_TOKENS: Record<string, number> = {
'claude-opus-4-6': 200000,
'claude-sonnet-4-6': 200000,
'gpt-4o': 128000,
'gpt-4o-mini': 128000,
'o1': 200000,
'o3': 200000,
'deepseek-r1': 64000,
'deepseek-chat': 64000,
};
export function getModelMaxTokens(model: string): number {
for (const [key, val] of Object.entries(MODEL_MAX_TOKENS)) {
if (model.includes(key)) return val;
}
return 128000; // 默认
}
然后在 gateway handleChat() 中,在 runAgent 调用后、done 事件发送前,推送 token_update:
// runAgent 完成后,推送最终 token_update
if (result.usage) {
const inp = result.usage.inputTokens || 0;
const out = result.usage.outputTokens || 0;
// 粗估上下文:历史消息 token + 本轮输入
const historyTokens = history.reduce((sum, m) => sum + (m.content?.length || 0) / 3, 0);
const usedTokens = Math.round(historyTokens + inp);
const maxTokens = getModelMaxTokens(agentConfig.model);
this.send({
type: 'token_update',
sessionId: sid,
data: {
current: { inputTokens: inp, outputTokens: out, totalTokens: inp + out, apiCalls: result.usage.apiCalls || 1 },
context: {
usedTokens,
maxTokens,
percent: Math.min(100, Math.round((usedTokens / maxTokens) * 100)),
},
},
});
}
- Step 4: 提交
git add packages/backend/src/modules/netaclaw/gateway/server.ts
git commit -m "feat(netaclaw): gateway 新增 todo_update/thinking_delta/thinking_done WS 事件"
Task 9: 前端 store/chat.ts 改造
Files:
-
Modify:
packages/frontend/src/modules/agent/store/chat.ts:142-337 -
Step 1: 新增状态变量
在 store 的状态定义区域新增:
// Todo 状态
const todoItems = ref<TodoItem[]>([]);
const todoSummary = ref<TodoSummary | null>(null);
// 思考流式状态
const isThinking = ref(false);
const thinkingStream = ref('');
// 思考级别
const thinkLevel = ref<ThinkLevel>('medium');
const modelThinkingCapability = ref<ModelThinkingCapability>({
supported: true, adaptive: false, levels: ['off', 'low', 'medium', 'high'], defaultLevel: 'medium',
});
// Token(简化,删除本地估算)
const tokenUsage = ref<TokenUpdateEvent | null>(null);
- Step 2: 删除旧的
case 'thinking'和旧的case 'token_update'
删除 store/chat.ts 中现有的 case 'thinking': 区块(约第153-157行)和现有的 case 'token_update': 区块(约第224-230行),替换为新事件处理。
- Step 3: 在 handleWSEvent 中新增事件处理
在 handleWSEvent() 函数的 switch 中追加:
case 'todo_update': {
todoItems.value = event.data.todos;
todoSummary.value = event.data.summary;
break;
}
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;
}
case 'token_update': {
tokenUsage.value = event.data;
break;
}
- Step 3: 修改 'token' 事件处理,自动结束思考状态
在现有的 case 'token' 处理中,开头加入:
case 'token': {
if (isThinking.value) {
isThinking.value = false;
if (assistantMsg) {
assistantMsg.thinking = thinkingStream.value;
}
}
// ...原有追加 content 逻辑
}
- Step 4: 修改 'done' 事件处理
修改约第237-269行的 case 'done' 处理:
case 'done': {
if (event.sessionId) {
sessionId.value = event.sessionId;
}
if (assistantMsg) {
if (!assistantMsg.metadata) assistantMsg.metadata = {};
// 将 skillProgress 写入 metadata(用于历史恢复),但不清空
assistantMsg.metadata.skillExecutions = [...skillProgress.value];
// 写入最终 token 数据
if (tokenUsage.value) {
assistantMsg.metadata.tokenUsage = tokenUsage.value.current;
} else if (event.usage) {
// 兼容:如果没有 token_update 推送,从 done 事件中取
const inp = event.usage.inputTokens || 0;
const out = event.usage.outputTokens || 0;
assistantMsg.metadata.tokenUsage = { input: inp, output: out, total: inp + out, apiCalls: event.usage.apiCalls };
sessionTotalTokens.value += inp + out;
}
}
tokenUsage.value = null;
loading.value = false;
loadSessions(); // 保留:刷新会话列表
// 注意:不再执行 setTimeout(() => { skillProgress.value = []; }, 3000)
// skillProgress 在切换会话时才清空
break;
}
- Step 5: 简化 recalcSessionTokens,删除本地 token 估算逻辑
修改 recalcSessionTokens() 函数(约第317-337行),删除中文字符估算代码(chineseChars/otherChars/ctxEstimate 相关行),只保留从 metadata.tokenUsage 累计 sessionTotalTokens 的逻辑。同时删除 contextTokens ref(上下文数据改由后端 token_update.context 推送)。
function recalcSessionTokens() {
let total = 0;
for (const msg of messages.value) {
const meta = typeof msg.metadata === 'string'
? (() => { try { return JSON.parse(msg.metadata); } catch { return null; } })()
: msg.metadata;
if (meta?.tokenUsage?.total) {
total += typeof meta.tokenUsage.total === 'number' ? meta.tokenUsage.total : (meta.tokenUsage.total.total || 0);
}
}
sessionTotalTokens.value = total;
}
- Step 6: 新增会话切换恢复函数
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;
}
function restoreSkillProgress(messages: ChatMessage[]) {
skillProgress.value = [];
for (const msg of messages) {
if (msg.metadata?.skillExecutions) {
skillProgress.value.push(...msg.metadata.skillExecutions);
}
}
}
function setThinkLevel(level: ThinkLevel) {
thinkLevel.value = level;
sendWSMessage({ type: 'set_thinking_level', data: { level } });
}
- Step 7: 在 switchSession 中调用恢复函数
在现有的会话切换逻辑中加入:
function switchSession(newSessionId: string) {
skillProgress.value = [];
todoItems.value = [];
todoSummary.value = null;
thinkingStream.value = '';
isThinking.value = false;
tokenUsage.value = null;
// ...加载新会话消息后
restoreTodoFromHistory(messages);
restoreSkillProgress(messages);
thinkLevel.value = session.thinkLevel ?? 'medium';
}
- Step 8: 导出新状态和函数
在 store 的 return 中追加导出:
return {
// ...现有导出
todoItems, todoSummary,
isThinking, thinkingStream,
thinkLevel, modelThinkingCapability, tokenUsage,
restoreTodoFromHistory, restoreSkillProgress, setThinkLevel,
};
- Step 9: 提交
git add packages/frontend/src/modules/agent/store/chat.ts
git commit -m "feat(agent): store 新增 todo/thinking/token 状态管理和 WS 事件处理"
Task 10: 前端 skill-card.vue
Files:
-
Create:
packages/frontend/src/modules/agent/components/skill-card.vue -
Step 1: 创建 skill-card.vue
完整组件代码见设计文档第 4.2 节。关键点:
-
Props: 接收
SkillProgress的所有字段(name, label, status, percent, step, detail, result, tokens) -
状态过渡:
<transition name="fade">包裹进度条和结果区域 -
CSS:
.skill-card--running/done/error通过 CSS 变量控制边框颜色 -
内部复用现有的
skill-result-viewer.vue展示结果详情 -
Step 2: 提交
git add packages/frontend/src/modules/agent/components/skill-card.vue
git commit -m "feat(agent): 新增 skill-card.vue 统一 Skill 卡片组件"
Task 11: 前端 todo-card.vue
Files:
-
Create:
packages/frontend/src/modules/agent/components/todo-card.vue -
Step 1: 创建 todo-card.vue
完整组件代码见设计文档第 6.2 节。关键点:
-
Props:
items: TodoItem[],summary: TodoSummary -
可折叠头部:📋 图标 + 标题 + 进度摘要 + 微型进度条
-
<transition-group name="todo-item">包裹列表项实现增删动画 -
状态图标:✅ completed / spinner in_progress / ❌ cancelled / ⬚ pending
-
CSS:
.todo-card__item--in_progress高亮背景,.is-done删除线 -
Step 2: 提交
git add packages/frontend/src/modules/agent/components/todo-card.vue
git commit -m "feat(agent): 新增 todo-card.vue 任务规划卡片组件"
Task 12: 前端 thinking-block.vue
Files:
-
Create:
packages/frontend/src/modules/agent/components/thinking-block.vue -
Step 1: 创建 thinking-block.vue
完整组件代码见设计文档第 7.4 节。关键点:
-
Props:
content: string,isStreaming: boolean -
流式时默认展开 + 呼吸动画(
thinking-pulse1.5s)+ 光标闪烁(blink1s) -
完成后自动折叠(watch isStreaming 变化触发)
-
内容渲染为 Markdown(使用项目现有的 markdown 渲染工具)
-
CSS:虚线边框 + 半透明背景 + 12px 小字号 + 柔和色
-
Step 2: 提交
git add packages/frontend/src/modules/agent/components/thinking-block.vue
git commit -m "feat(agent): 新增 thinking-block.vue 思考内容流式展示组件"
Task 13: 前端 token-stats.vue
Files:
-
Create:
packages/frontend/src/modules/agent/components/token-stats.vue -
Step 1: 创建 token-stats.vue
完整组件代码见设计文档第 5.3 节。关键点:
-
Props:
tokenUsage: TokenUpdateEvent | null,isRunning: boolean -
执行中显示:
↑1.2K ↓0.8K · 3次 -
始终显示:
上下文 32%+ 颜色圆点(≤50%绿/50-80%黄/>80%红) -
formatTokens()工具函数:1234→1.2K, 12345→12.3K -
hover tooltip 显示详细数据
-
Step 2: 提交
git add packages/frontend/src/modules/agent/components/token-stats.vue
git commit -m "feat(agent): 新增 token-stats.vue 紧凑 Token 统计组件"
Task 14: 前端 thinking-level-selector.vue
Files:
-
Create:
packages/frontend/src/modules/agent/components/thinking-level-selector.vue -
Step 1: 创建 thinking-level-selector.vue
完整组件代码见设计文档第 9.2 节。关键点:
-
Props:
modelValue: ThinkLevel,disabled: boolean,capability: ModelThinkingCapability -
el-select下拉,选项根据capability.levels动态生成 -
label 映射:off→关闭, minimal→极简, low→低, medium→中等, high→高, adaptive→自适应
-
emit
update:modelValue和change事件 -
Step 2: 提交
git add packages/frontend/src/modules/agent/components/thinking-level-selector.vue
git commit -m "feat(agent): 新增 thinking-level-selector.vue 思考级别选择器"
Task 15: 前端 chat.vue 布局重构
Files:
-
Modify:
packages/frontend/src/modules/agent/views/chat.vue:16-218 -
Step 1: 删除顶部 token 统计条
删除约第16-37行的 <div class="token-stats-bar"> 整个区块。
- Step 2: 重构消息列表区域
修改消息列表区域(约第40-89行),插入新组件:
<div class="chat-messages" ref="messagesRef">
<!-- 历史消息 -->
<message-item v-for="msg in displayMessages" :key="msg.id" :message="msg" />
<!-- Todo 卡片 -->
<todo-card v-if="todoItems.length" :items="todoItems" :summary="todoSummary" />
<!-- Skill 执行卡片(替代 skill-indicator) -->
<skill-card v-for="sp in skillProgress" :key="sp.name" v-bind="sp" />
<!-- 流式 assistant 消息(含 thinking-block) -->
<message-item v-if="streamingMsg" :message="streamingMsg">
<template #before-content>
<thinking-block
v-if="thinkingStream || streamingMsg?.thinking"
:content="isThinking ? thinkingStream : (streamingMsg?.thinking || '')"
:is-streaming="isThinking"
/>
</template>
</message-item>
</div>
- Step 3: 在输入区域添加思考级别选择器和 token 统计
在输入区域(约第92-218行)的工具栏中加入:
<div class="chat-input__toolbar">
<!-- 现有的 Agent 选择器、附件按钮 -->
<!-- 新增:思考级别选择器 -->
<thinking-level-selector
v-model="thinkLevel"
:disabled="loading"
:capability="modelThinkingCapability"
@change="setThinkLevel"
/>
<!-- 右侧:token 统计 -->
<token-stats
class="chat-input__token-stats"
:token-usage="tokenUsage"
:is-running="loading"
/>
</div>
- Step 4: 导入新组件
在 <script setup> 中导入:
import SkillCard from '../components/skill-card.vue';
import TodoCard from '../components/todo-card.vue';
import ThinkingBlock from '../components/thinking-block.vue';
import TokenStats from '../components/token-stats.vue';
import ThinkingLevelSelector from '../components/thinking-level-selector.vue';
- Step 5: 删除 skill-indicator 的导入和使用
删除 import SkillIndicator 和模板中的 <skill-indicator> 标签。
- Step 6: 提交
git add packages/frontend/src/modules/agent/views/chat.vue
git commit -m "feat(agent): chat.vue 布局重构 - 集成新组件 + token 统计移位"
Task 16: 前端 message-item.vue 改造
Files:
-
Modify:
packages/frontend/src/modules/agent/components/message-item.vue:16-89 -
Step 1: 删除旧的 Skill 执行历史区块
删除约第31-89行的 <div class="message-item__skills"> 整个区块(skill 历史现在由 skill-card 在 chat.vue 中渲染)。
- Step 2: 替换旧的思考区块
删除约第16-29行的 <div class="message-item__thinking"> 区块,替换为:
<!-- 历史消息中的思考内容 -->
<thinking-block
v-if="message.thinking && message.role === 'assistant'"
:content="message.thinking"
:is-streaming="false"
/>
<!-- 插槽:流式消息的 thinking-block 从 chat.vue 传入 -->
<slot name="before-content" />
- Step 3: 导入 thinking-block
import ThinkingBlock from './thinking-block.vue';
- Step 4: 提交
git add packages/frontend/src/modules/agent/components/message-item.vue
git commit -m "refactor(agent): message-item 移除旧 skill 历史和 thinking 区块,改用新组件"
Task 17: 前端 chat-sidebar.vue 适配
Files:
-
Modify:
packages/frontend/src/modules/agent/components/chat-sidebar.vue:30-59 -
Step 1: 在会话项中增加摘要行
在每个会话项的标题下方(约第31-59行),追加:
<div class="session-item__subtitle">
<template v-if="session.todoSummary">
<span class="session-item__todo-badge">
📋 {{ session.todoSummary.completed }}/{{ session.todoSummary.total }}
</span>
</template>
<template v-else-if="session.isRunning">
<span class="session-item__running-dot" />
<span>执行中...</span>
</template>
<template v-else-if="session.lastMessage">
<span class="session-item__last-msg">{{ session.lastMessage }}</span>
</template>
</div>
- Step 2: 添加 CSS
.session-item__subtitle {
font-size: 12px;
color: var(--el-text-color-secondary);
margin-top: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.session-item__todo-badge {
font-size: 11px;
}
.session-item__running-dot {
display: inline-block;
width: 6px; height: 6px;
border-radius: 50%;
background: var(--el-color-success);
margin-right: 4px;
animation: pulse 1.5s infinite;
}
- Step 3: 提交
git add packages/frontend/src/modules/agent/components/chat-sidebar.vue
git commit -m "feat(agent): chat-sidebar 会话项增加 todo 进度和运行状态"
Task 18: 删除 skill-indicator.vue + 清理
Files:
-
Delete:
packages/frontend/src/modules/agent/components/skill-indicator.vue -
Step 1: 确认 skill-indicator 不再被引用
cd packages/frontend
grep -r "skill-indicator" src/ --include="*.vue" --include="*.ts"
预期:无结果(Task 15 已删除所有引用)。
- Step 2: 删除文件
rm packages/frontend/src/modules/agent/components/skill-indicator.vue
- Step 3: 全局搜索确认无遗漏引用
grep -r "SkillIndicator\|skill-indicator" packages/frontend/src/ --include="*.vue" --include="*.ts"
预期:无结果。
- Step 4: 提交
git add -A packages/frontend/src/modules/agent/components/skill-indicator.vue
git commit -m "refactor(agent): 删除 skill-indicator.vue,已被 skill-card.vue 替代"
验证清单
完成所有 Task 后,执行以下验证:
- 启动后端
pnpm --filter @neta/backend dev,确认无编译错误 - 启动前端
pnpm --filter @neta/frontend dev,确认无编译错误 - 打开对话页面,发送消息,验证:
- Skill 执行中→完成无视觉跳变(同一个卡片,CSS 过渡)
- Token 统计显示在输入区右下角,数据来自后端推送
- 思考内容流式展示(虚线边框 + 呼吸动画 + 光标闪烁)
- 思考完成后自动折叠
- 思考级别选择器可切换
- Todo 卡片在对话流中实时更新(需 agent 调用 todo 工具)
- 切换会话后 todo/skill 状态正确恢复
- 会话列表显示 todo 进度