GPU_GUARD_MONOREPO/docs/superpowers/plans/2026-04-13-agent-chat-ux-overhaul.md
2026-05-20 21:39:12 +08:00

1278 lines
37 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 进度