1278 lines
37 KiB
Markdown
1278 lines
37 KiB
Markdown
# 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 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: 提交**
|
||
|
||
```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 进度
|
||
|