GPU_GUARD_MONOREPO/docs/superpowers/plans/2026-04-13-agent-chat-ux-overhaul.md

1278 lines
37 KiB
Markdown
Raw Normal View History

2026-05-20 21:39:12 +08:00
# 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 中新增类型定义**
在文件末尾追加:
```typescript
// === 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 联合类型中追加:
```typescript
// 在现有 type 联合中追加:
| 'todo_update'
| 'thinking_delta'
| 'thinking_done'
```
- [ ] **Step 3: 更新 hooks/websocket.ts 中的 WSClientMessage 类型**
`hooks/websocket.ts` 中找到客户端消息类型,追加:
```typescript
| 'set_thinking_level'
```
- [ ] **Step 4: 提交**
```bash
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**
```typescript
/**
* 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: 提交**
```bash
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**
```typescript
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: 提交**
```bash
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`,在文件末尾追加:
```typescript
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 thinkingClaude 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: 提交**
```bash
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 并传递思考参数**
在文件顶部导入:
```typescript
import { buildAnthropicThinkingParams } from '../../runtime/thinking.js';
```
`chat()` 方法中 `client.messages.create()` 调用处约第23-30行在构建请求参数时加入思考参数
```typescript
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: 提交**
```bash
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 并传递参数**
在文件顶部导入:
```typescript
import { buildOpenAIThinkingParams } from '../../runtime/thinking.js';
```
`chat()` 方法中 `client.chat.completions.create()` 调用处约第18-24行加入
```typescript
const thinkParams = buildOpenAIThinkingParams(config.thinkLevel || 'off');
const createParams: any = {
model: config.model,
messages,
tools,
// ...现有参数
};
if (thinkParams) {
createParams.reasoning_effort = thinkParams.reasoning_effort;
}
```
- [ ] **Step 2: 提交**
```bash
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 优先级链**
在文件顶部导入:
```typescript
import { TodoStore } from './todo_store.js';
import { todoToolSchema, executeTodo } from '../tools/todo_tool.js';
```
修改 `runAgent()` 函数中 thinkLevel 的获取逻辑约第36行
```typescript
// 优先级链:会话级 > agent 配置 > 模型默认
const thinkLevel = params.sessionThinkLevel
?? config.defaultThinkLevel
?? resolveThinkingDefault(config.model);
```
在工具列表构建处,将 todo 工具加入:
```typescript
// 在现有工具列表中追加 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 字段**
```typescript
interface AgentConfig {
// ...现有字段name, systemPrompt, model, apiKey, baseUrl, temperature, maxTokens, maxToolRounds, skills
defaultThinkLevel?: ThinkLevel; // agent 配置的默认思考级别
}
```
- [ ] **Step 3: 在 AgentRunParams 接口中新增字段**
```typescript
interface AgentRunParams {
// ...现有字段
sessionThinkLevel?: ThinkLevel; // 会话级思考级别
todoStore?: TodoStore; // TodoStore 实例
onTodoUpdate?: (data: { todos: any[]; summary: any }) => void; // todo 更新回调
onThinkingDelta?: (text: string) => void; // 思考流式回调(替代 onThinking
}
```
- [ ] **Step 4: 提交**
```bash
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` 消息的处理:
```typescript
case 'set_thinking_level': {
const { level } = parsed.data;
// 更新当前 session 的思考级别
session.thinkLevel = level;
break;
}
```
- [ ] **Step 2: 修改 handleChat 中的 runAgent 调用,新增回调**
`handleChat()` 方法中约第193-213行修改 `runAgent()` 调用:
```typescript
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` 中新增上下文估算工具函数:
```typescript
/** 模型上下文窗口上限映射 */
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`
```typescript
// 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: 提交**
```bash
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 的状态定义区域新增:
```typescript
// 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 中追加:
```typescript
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'` 处理中,开头加入:
```typescript
case 'token': {
if (isThinking.value) {
isThinking.value = false;
if (assistantMsg) {
assistantMsg.thinking = thinkingStream.value;
}
}
// ...原有追加 content 逻辑
}
```
- [ ] **Step 4: 修改 'done' 事件处理**
修改约第237-269行的 `case 'done'` 处理:
```typescript
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` 推送)。
```typescript
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: 新增会话切换恢复函数**
```typescript
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 中调用恢复函数**
在现有的会话切换逻辑中加入:
```typescript
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 中追加导出:
```typescript
return {
// ...现有导出
todoItems, todoSummary,
isThinking, thinkingStream,
thinkLevel, modelThinkingCapability, tokenUsage,
restoreTodoFromHistory, restoreSkillProgress, setThinkLevel,
};
```
- [ ] **Step 9: 提交**
```bash
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: 提交**
```bash
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: 提交**
```bash
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-pulse` 1.5s+ 光标闪烁(`blink` 1s
- 完成后自动折叠watch isStreaming 变化触发)
- 内容渲染为 Markdown使用项目现有的 markdown 渲染工具)
- CSS虚线边框 + 半透明背景 + 12px 小字号 + 柔和色
- [ ] **Step 2: 提交**
```bash
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: 提交**
```bash
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: 提交**
```bash
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行插入新组件
```vue
<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行的工具栏中加入
```vue
<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>` 中导入:
```typescript
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: 提交**
```bash
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">` 区块,替换为:
```vue
<!-- 历史消息中的思考内容 -->
<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**
```typescript
import ThinkingBlock from './thinking-block.vue';
```
- [ ] **Step 4: 提交**
```bash
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行追加
```vue
<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**
```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: 提交**
```bash
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 不再被引用**
```bash
cd packages/frontend
grep -r "skill-indicator" src/ --include="*.vue" --include="*.ts"
```
预期无结果Task 15 已删除所有引用)。
- [ ] **Step 2: 删除文件**
```bash
rm packages/frontend/src/modules/agent/components/skill-indicator.vue
```
- [ ] **Step 3: 全局搜索确认无遗漏引用**
```bash
grep -r "SkillIndicator\|skill-indicator" packages/frontend/src/ --include="*.vue" --include="*.ts"
```
预期:无结果。
- [ ] **Step 4: 提交**
```bash
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 进度