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

37 KiB
Raw Permalink Blame History

Agent 对话页面体验优化 实施计划

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 优化 agent 对话页面的 6 个体验问题Skill 视觉跳变、Token 不准、Token UI、缺少任务规划、思考不流畅、思考级别控制。

Architecture: 后端扩展 thinking.ts 适配层 + 新增 TodoStore/todo 工具 + WS 协议新增 4 个事件;前端用 skill-card 替代双组件、新增 todo-card/thinking-block/token-stats/thinking-level-selector 5 个组件store 统一管理新状态。

Tech Stack: Midway.js 3.20 + TypeORM + Socket.IO (后端) / Vue 3.5 + Element Plus 2.9 + Pinia (前端)

设计文档: docs/superpowers/specs/2026-04-13-agent-chat-ux-overhaul-design.md


文件结构

后端 (packages/backend/src/modules/netaclaw/)

文件 操作 职责
runtime/todo_store.ts 新建 TodoStore 类write/read/merge/hydrate/formatForInjection
tools/todo_tool.ts 新建 todo 工具 Schema + handler
runtime/thinking.ts 修改 扩展buildThinkingParams、getModelThinkingCapability、预算映射
plugins/llm_providers/anthropic.ts 修改 传递 thinking 参数 + 流式 thinking_delta
plugins/llm_providers/openai.ts 修改 传递 reasoning_effort 参数
runtime/agent.ts 修改 会话级 thinkLevel 优先级链 + todo 工具注册
gateway/server.ts 修改 新增 WS 事件 + set_thinking_level 处理

前端 (packages/frontend/src/modules/agent/)

文件 操作 职责
components/skill-card.vue 新建 统一 Skill 卡片(替代 skill-indicator + message-item 内嵌)
components/todo-card.vue 新建 Todo 任务规划卡片
components/token-stats.vue 新建 紧凑 Token 统计组件
components/thinking-block.vue 新建 思考内容流式展示
components/thinking-level-selector.vue 新建 思考级别下拉选择器
types/index.d.ts 修改 新增 TodoItem/TodoSummary/ThinkLevel/ModelThinkingCapability 类型
store/chat.ts 修改 新增 todo/thinking/thinkLevel 状态 + 事件处理
views/chat.vue 修改 布局重构:插入新组件 + token 统计移位
components/message-item.vue 修改 删除旧 skill 历史 + 旧 thinking 区块,改用新组件
components/chat-sidebar.vue 修改 会话项增加 todo 进度
hooks/websocket.ts 修改 类型增加新事件
components/skill-indicator.vue 删除 被 skill-card.vue 替代

任务依赖顺序

Task 1: 前端类型定义(无依赖)
Task 2: 后端 TodoStore无依赖
Task 3: 后端 todo 工具(依赖 Task 2
Task 4: 后端 thinking.ts 扩展(无依赖)
Task 5: 后端 Anthropic 适配器(依赖 Task 4
Task 6: 后端 OpenAI 适配器(依赖 Task 4
Task 7: 后端 agent.ts 改造(依赖 Task 2, 3, 4
Task 8: 后端 gateway WS 协议(依赖 Task 2, 3, 7
Task 9: 前端 store/chat.ts 改造(依赖 Task 1
Task 10: 前端 skill-card.vue依赖 Task 9
Task 11: 前端 todo-card.vue依赖 Task 9
Task 12: 前端 thinking-block.vue依赖 Task 9
Task 13: 前端 token-stats.vue依赖 Task 9
Task 14: 前端 thinking-level-selector.vue依赖 Task 9
Task 15: 前端 chat.vue 布局重构(依赖 Task 10-14
Task 16: 前端 message-item.vue 改造(依赖 Task 10, 12
Task 17: 前端 chat-sidebar.vue 适配(依赖 Task 9
Task 18: 删除 skill-indicator.vue + 清理(依赖 Task 15, 16

Task 1: 前端类型定义

Files:

  • Modify: packages/frontend/src/modules/agent/types/index.d.ts:119-169

  • Step 1: 在 types/index.d.ts 中新增类型定义

在文件末尾追加:

// === Todo 系统类型 ===
export interface TodoItem {
  id: string;
  content: string;
  status: 'pending' | 'in_progress' | 'completed' | 'cancelled';
}

export interface TodoSummary {
  total: number;
  pending: number;
  in_progress: number;
  completed: number;
  cancelled: number;
}

// === 思考级别类型 ===
export type ThinkLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'adaptive';

export interface ModelThinkingCapability {
  supported: boolean;
  adaptive: boolean;
  levels: ThinkLevel[];
  defaultLevel: ThinkLevel;
}

// === Token 统计类型 ===
export interface TokenUpdateEvent {
  current: {
    inputTokens: number;
    outputTokens: number;
    totalTokens: number;
    apiCalls: number;
  };
  context: {
    usedTokens: number;
    maxTokens: number;
    percent: number;
  };
}
  • Step 2: 更新 WSServerEvent 类型,增加新事件

找到 WSServerEvent 类型定义约第119行在 type 联合类型中追加:

// 在现有 type 联合中追加:
| 'todo_update'
| 'thinking_delta'
| 'thinking_done'
  • Step 3: 更新 hooks/websocket.ts 中的 WSClientMessage 类型

hooks/websocket.ts 中找到客户端消息类型,追加:

| 'set_thinking_level'
  • Step 4: 提交
git add packages/frontend/src/modules/agent/types/index.d.ts packages/frontend/src/modules/agent/hooks/websocket.ts
git commit -m "feat(agent): 新增 Todo/Thinking/Token 类型定义和 WS 事件类型"

Task 2: 后端 TodoStore

Files:

  • Create: packages/backend/src/modules/netaclaw/runtime/todo_store.ts

  • Step 1: 创建 todo_store.ts

/**
 * TodoStore - 会话级任务列表管理
 * 参考 hermes-agent-main 的 TodoStore 设计
 */

export interface TodoItem {
  id: string;
  content: string;
  status: 'pending' | 'in_progress' | 'completed' | 'cancelled';
}

export interface TodoSummary {
  total: number;
  pending: number;
  in_progress: number;
  completed: number;
  cancelled: number;
}

const VALID_STATUSES = ['pending', 'in_progress', 'completed', 'cancelled'];

export class TodoStore {
  private items: TodoItem[] = [];

  /**
   * 写入 todo 列表
   * @param todos 任务项数组
   * @param merge false=全量替换true=按 id 增量更新+追加新项
   */
  write(todos: TodoItem[], merge = false): TodoItem[] {
    if (!merge) {
      this.items = todos.map(t => this.validate(t));
    } else {
      for (const todo of todos) {
        const validated = this.validate(todo);
        const existing = this.items.find(i => i.id === validated.id);
        if (existing) {
          Object.assign(existing, validated);
        } else {
          this.items.push(validated);
        }
      }
    }
    return this.read();
  }

  read(): TodoItem[] {
    return JSON.parse(JSON.stringify(this.items));
  }

  hasItems(): boolean {
    return this.items.length > 0;
  }

  getSummary(): TodoSummary {
    const s: TodoSummary = { total: 0, pending: 0, in_progress: 0, completed: 0, cancelled: 0 };
    for (const item of this.items) {
      s.total++;
      s[item.status]++;
    }
    return s;
  }

  /** 上下文压缩后注入,只保留 pending + in_progress */
  formatForInjection(): string | null {
    const active = this.items.filter(i => i.status === 'pending' || i.status === 'in_progress');
    if (active.length === 0) return null;
    const lines = active.map(i => {
      const marker = i.status === 'in_progress' ? '[>]' : '[ ]';
      return `- ${marker} ${i.id}. ${i.content} (${i.status})`;
    });
    return '[你的活跃任务列表在上下文压缩后被保留]\n' + lines.join('\n');
  }

  /** 从历史消息中恢复 todo 状态 */
  static hydrateFromHistory(messages: any[]): TodoStore {
    const store = new TodoStore();
    for (let i = messages.length - 1; i >= 0; i--) {
      const msg = messages[i];
      if (msg.role === 'tool' && msg.toolName === 'todo') {
        try {
          const data = JSON.parse(typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content));
          if (data.todos) {
            store.write(data.todos, false);
          }
        } catch { /* 忽略解析失败 */ }
        break;
      }
    }
    return store;
  }

  private validate(item: Partial<TodoItem>): TodoItem {
    return {
      id: item.id || '?',
      content: item.content || '(无描述)',
      status: VALID_STATUSES.includes(item.status!) ? item.status! as TodoItem['status'] : 'pending',
    };
  }
}
  • Step 2: 提交
git add packages/backend/src/modules/netaclaw/runtime/todo_store.ts
git commit -m "feat(netaclaw): 新增 TodoStore 类 - 支持 merge/replace/hydrate/压缩注入"

Task 3: 后端 todo 工具

Files:

  • Create: packages/backend/src/modules/netaclaw/tools/todo_tool.ts

  • Step 1: 创建 todo_tool.ts

import { TodoStore } from '../runtime/todo_store.js';

export const todoToolSchema = {
  name: 'todo',
  description:
    '管理当前会话的任务列表。用于3步以上的复杂任务或用户提供多个任务时。\n'
    + '不传参数=读取当前列表。\n'
    + '传 todos 数组=写入。merge=false(默认)全量替换整个计划merge=true 按 id 增量更新。\n'
    + '每个项: { id: string, content: string, status: pending|in_progress|completed|cancelled }\n'
    + '列表顺序=优先级。同一时间只能有一个 in_progress。\n'
    + '完成立即标记 completed。失败则 cancel 并添加修正项。\n'
    + '始终返回完整列表。',
  parameters: {
    type: 'object' as const,
    properties: {
      todos: {
        type: 'array' as const,
        description: '任务项数组。省略则读取当前列表。',
        items: {
          type: 'object' as const,
          properties: {
            id: { type: 'string' as const, description: '唯一标识' },
            content: { type: 'string' as const, description: '任务描述' },
            status: {
              type: 'string' as const,
              enum: ['pending', 'in_progress', 'completed', 'cancelled'],
              description: '当前状态',
            },
          },
          required: ['id', 'content', 'status'],
        },
      },
      merge: {
        type: 'boolean' as const,
        description: 'true=按 id 增量更新+追加新项。false(默认)=全量替换整个列表。',
        default: false,
      },
    },
    required: [] as string[],
  },
};

/** 执行 todo 工具 */
export function executeTodo(
  store: TodoStore,
  args: { todos?: any[]; merge?: boolean }
): { todos: any[]; summary: any } {
  if (args.todos && args.todos.length > 0) {
    store.write(args.todos, args.merge ?? false);
  }
  return {
    todos: store.read(),
    summary: store.getSummary(),
  };
}
  • Step 2: 提交
git add packages/backend/src/modules/netaclaw/tools/todo_tool.ts
git commit -m "feat(netaclaw): 新增 todo 工具 Schema 和 handler"

Task 4: 后端 thinking.ts 扩展

Files:

  • Modify: packages/backend/src/modules/netaclaw/runtime/thinking.ts (当前12行)

  • Step 1: 扩展 thinking.ts新增预算映射和多模型适配

保留现有的 resolveThinkingDefaultisThinkingSupported,在文件末尾追加:

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: 提交
git add packages/backend/src/modules/netaclaw/runtime/thinking.ts
git commit -m "feat(netaclaw): 扩展 thinking.ts - 多模型预算映射和能力检测"

Task 5: 后端 Anthropic 适配器改造

Files:

  • Modify: packages/backend/src/modules/netaclaw/plugins/llm_providers/anthropic.ts:23-30,66

  • Step 1: 导入 buildAnthropicThinkingParams 并传递思考参数

在文件顶部导入:

import { buildAnthropicThinkingParams } from '../../runtime/thinking.js';

chat() 方法中 client.messages.create() 调用处约第23-30行在构建请求参数时加入思考参数

const thinkParams = buildAnthropicThinkingParams(config.model, config.thinkLevel || 'off');

const createParams: any = {
  model: config.model,
  max_tokens: config.maxTokens || 4096,
  messages,
  tools,
  // ...现有参数
};

if (thinkParams) {
  createParams.thinking = thinkParams.thinking;
  if (thinkParams.temperature !== undefined) {
    createParams.temperature = thinkParams.temperature;
  }
  if (thinkParams.maxTokensAdjust) {
    createParams.max_tokens = Math.max(createParams.max_tokens, thinkParams.maxTokensAdjust);
  }
}

// 添加 beta headers
const headers: Record<string, string> = {};
if (thinkParams?.betas?.length) {
  headers['anthropic-beta'] = thinkParams.betas.join(',');
}
  • Step 2: 提交
git add packages/backend/src/modules/netaclaw/plugins/llm_providers/anthropic.ts
git commit -m "feat(netaclaw): Anthropic 适配器传递 thinking 参数和 beta headers"

Task 6: 后端 OpenAI 适配器改造

Files:

  • Modify: packages/backend/src/modules/netaclaw/plugins/llm_providers/openai.ts:18-24

  • Step 1: 导入 buildOpenAIThinkingParams 并传递参数

在文件顶部导入:

import { buildOpenAIThinkingParams } from '../../runtime/thinking.js';

chat() 方法中 client.chat.completions.create() 调用处约第18-24行加入

const thinkParams = buildOpenAIThinkingParams(config.thinkLevel || 'off');

const createParams: any = {
  model: config.model,
  messages,
  tools,
  // ...现有参数
};

if (thinkParams) {
  createParams.reasoning_effort = thinkParams.reasoning_effort;
}
  • Step 2: 提交
git add packages/backend/src/modules/netaclaw/plugins/llm_providers/openai.ts
git commit -m "feat(netaclaw): OpenAI 适配器传递 reasoning_effort 参数"

Task 7: 后端 agent.ts 改造

Files:

  • Modify: packages/backend/src/modules/netaclaw/runtime/agent.ts:36-45,57-66

  • Step 1: 导入 TodoStore 和 todo 工具,修改 thinkLevel 优先级链

在文件顶部导入:

import { TodoStore } from './todo_store.js';
import { todoToolSchema, executeTodo } from '../tools/todo_tool.js';

修改 runAgent() 函数中 thinkLevel 的获取逻辑约第36行

// 优先级链:会话级 > agent 配置 > 模型默认
const thinkLevel = params.sessionThinkLevel
  ?? config.defaultThinkLevel
  ?? resolveThinkingDefault(config.model);

在工具列表构建处,将 todo 工具加入:

// 在现有工具列表中追加 todo 工具
const todoStore = params.todoStore ?? new TodoStore();
const todoTool = {
  ...todoToolSchema,
  execute: async (args: any) => {
    const result = executeTodo(todoStore, args);
    // 通过回调通知前端
    params.onTodoUpdate?.(result);
    return JSON.stringify(result);
  },
};
  • Step 2: 在 AgentConfig 接口中新增 defaultThinkLevel 字段
interface AgentConfig {
  // ...现有字段name, systemPrompt, model, apiKey, baseUrl, temperature, maxTokens, maxToolRounds, skills
  defaultThinkLevel?: ThinkLevel;  // agent 配置的默认思考级别
}
  • Step 3: 在 AgentRunParams 接口中新增字段
interface AgentRunParams {
  // ...现有字段
  sessionThinkLevel?: ThinkLevel;   // 会话级思考级别
  todoStore?: TodoStore;            // TodoStore 实例
  onTodoUpdate?: (data: { todos: any[]; summary: any }) => void;  // todo 更新回调
  onThinkingDelta?: (text: string) => void;  // 思考流式回调(替代 onThinking
}
  • Step 4: 提交
git add packages/backend/src/modules/netaclaw/runtime/agent.ts
git commit -m "feat(netaclaw): agent.ts 集成 TodoStore + 会话级思考级别"

Task 8: 后端 gateway WS 协议改造

Files:

  • Modify: packages/backend/src/modules/netaclaw/gateway/server.ts:77-251

  • Step 1: 处理 set_thinking_level 客户端消息

@OnWSMessage('message') 处理函数中约第77行增加对 set_thinking_level 消息的处理:

case 'set_thinking_level': {
  const { level } = parsed.data;
  // 更新当前 session 的思考级别
  session.thinkLevel = level;
  break;
}
  • Step 2: 修改 handleChat 中的 runAgent 调用,新增回调

handleChat() 方法中约第193-213行修改 runAgent() 调用:

const todoStore = session.todoStore ?? new TodoStore();
session.todoStore = todoStore;

await runAgent({
  // ...现有参数
  sessionThinkLevel: session.thinkLevel,
  todoStore,
  onTodoUpdate: (data) => {
    this.send({ type: 'todo_update', sessionId: sid, data });
  },
  onThinkingDelta: (text) => {
    this.send({ type: 'thinking_delta', sessionId: sid, data: { text } });
  },
  onThinking: (text) => {
    // 保留完整思考文本用于存储
    thinkingText += text;
    // 不再发送旧的 'thinking' 事件
  },
});

// 思考结束标记
if (thinkingText) {
  this.send({ type: 'thinking_done', sessionId: sid, data: {} });
}
  • Step 3: 新增 token_update 推送逻辑(含 context 字段)

当前后端没有中间 token_update 推送,只在 done 事件的 usage 字段里带。需要在 runAgent 回调中新增推送。

首先在 thinking.ts 中新增上下文估算工具函数:

/** 模型上下文窗口上限映射 */
const MODEL_MAX_TOKENS: Record<string, number> = {
  'claude-opus-4-6': 200000,
  'claude-sonnet-4-6': 200000,
  'gpt-4o': 128000,
  'gpt-4o-mini': 128000,
  'o1': 200000,
  'o3': 200000,
  'deepseek-r1': 64000,
  'deepseek-chat': 64000,
};

export function getModelMaxTokens(model: string): number {
  for (const [key, val] of Object.entries(MODEL_MAX_TOKENS)) {
    if (model.includes(key)) return val;
  }
  return 128000; // 默认
}

然后在 gateway handleChat() 中,在 runAgent 调用后、done 事件发送前,推送 token_update

// runAgent 完成后,推送最终 token_update
if (result.usage) {
  const inp = result.usage.inputTokens || 0;
  const out = result.usage.outputTokens || 0;
  // 粗估上下文:历史消息 token + 本轮输入
  const historyTokens = history.reduce((sum, m) => sum + (m.content?.length || 0) / 3, 0);
  const usedTokens = Math.round(historyTokens + inp);
  const maxTokens = getModelMaxTokens(agentConfig.model);
  this.send({
    type: 'token_update',
    sessionId: sid,
    data: {
      current: { inputTokens: inp, outputTokens: out, totalTokens: inp + out, apiCalls: result.usage.apiCalls || 1 },
      context: {
        usedTokens,
        maxTokens,
        percent: Math.min(100, Math.round((usedTokens / maxTokens) * 100)),
      },
    },
  });
}
  • Step 4: 提交
git add packages/backend/src/modules/netaclaw/gateway/server.ts
git commit -m "feat(netaclaw): gateway 新增 todo_update/thinking_delta/thinking_done WS 事件"

Task 9: 前端 store/chat.ts 改造

Files:

  • Modify: packages/frontend/src/modules/agent/store/chat.ts:142-337

  • Step 1: 新增状态变量

在 store 的状态定义区域新增:

// Todo 状态
const todoItems = ref<TodoItem[]>([]);
const todoSummary = ref<TodoSummary | null>(null);

// 思考流式状态
const isThinking = ref(false);
const thinkingStream = ref('');

// 思考级别
const thinkLevel = ref<ThinkLevel>('medium');
const modelThinkingCapability = ref<ModelThinkingCapability>({
  supported: true, adaptive: false, levels: ['off', 'low', 'medium', 'high'], defaultLevel: 'medium',
});

// Token简化删除本地估算
const tokenUsage = ref<TokenUpdateEvent | null>(null);
  • Step 2: 删除旧的 case 'thinking' 和旧的 case 'token_update'

删除 store/chat.ts 中现有的 case 'thinking': 区块约第153-157行和现有的 case 'token_update': 区块约第224-230行替换为新事件处理。

  • Step 3: 在 handleWSEvent 中新增事件处理

handleWSEvent() 函数的 switch 中追加:

case 'todo_update': {
  todoItems.value = event.data.todos;
  todoSummary.value = event.data.summary;
  break;
}

case 'thinking_delta': {
  if (!isThinking.value) {
    isThinking.value = true;
    thinkingStream.value = '';
  }
  thinkingStream.value += event.data.text;
  break;
}

case 'thinking_done': {
  isThinking.value = false;
  if (assistantMsg) {
    assistantMsg.thinking = thinkingStream.value;
  }
  break;
}

case 'token_update': {
  tokenUsage.value = event.data;
  break;
}
  • Step 3: 修改 'token' 事件处理,自动结束思考状态

在现有的 case 'token' 处理中,开头加入:

case 'token': {
  if (isThinking.value) {
    isThinking.value = false;
    if (assistantMsg) {
      assistantMsg.thinking = thinkingStream.value;
    }
  }
  // ...原有追加 content 逻辑
}
  • Step 4: 修改 'done' 事件处理

修改约第237-269行的 case 'done' 处理:

case 'done': {
  if (event.sessionId) {
    sessionId.value = event.sessionId;
  }
  if (assistantMsg) {
    if (!assistantMsg.metadata) assistantMsg.metadata = {};
    // 将 skillProgress 写入 metadata用于历史恢复但不清空
    assistantMsg.metadata.skillExecutions = [...skillProgress.value];
    // 写入最终 token 数据
    if (tokenUsage.value) {
      assistantMsg.metadata.tokenUsage = tokenUsage.value.current;
    } else if (event.usage) {
      // 兼容:如果没有 token_update 推送,从 done 事件中取
      const inp = event.usage.inputTokens || 0;
      const out = event.usage.outputTokens || 0;
      assistantMsg.metadata.tokenUsage = { input: inp, output: out, total: inp + out, apiCalls: event.usage.apiCalls };
      sessionTotalTokens.value += inp + out;
    }
  }
  tokenUsage.value = null;
  loading.value = false;
  loadSessions();  // 保留:刷新会话列表
  // 注意:不再执行 setTimeout(() => { skillProgress.value = []; }, 3000)
  // skillProgress 在切换会话时才清空
  break;
}
  • Step 5: 简化 recalcSessionTokens删除本地 token 估算逻辑

修改 recalcSessionTokens() 函数约第317-337行删除中文字符估算代码chineseChars/otherChars/ctxEstimate 相关行),只保留从 metadata.tokenUsage 累计 sessionTotalTokens 的逻辑。同时删除 contextTokens ref上下文数据改由后端 token_update.context 推送)。

function recalcSessionTokens() {
  let total = 0;
  for (const msg of messages.value) {
    const meta = typeof msg.metadata === 'string'
      ? (() => { try { return JSON.parse(msg.metadata); } catch { return null; } })()
      : msg.metadata;
    if (meta?.tokenUsage?.total) {
      total += typeof meta.tokenUsage.total === 'number' ? meta.tokenUsage.total : (meta.tokenUsage.total.total || 0);
    }
  }
  sessionTotalTokens.value = total;
}
  • Step 6: 新增会话切换恢复函数
function restoreTodoFromHistory(messages: ChatMessage[]) {
  for (let i = messages.length - 1; i >= 0; i--) {
    const msg = messages[i];
    if (msg.role === 'tool' && msg.metadata?.toolName === 'todo') {
      try {
        const data = JSON.parse(msg.content);
        todoItems.value = data.todos;
        todoSummary.value = data.summary;
      } catch {}
      return;
    }
  }
  todoItems.value = [];
  todoSummary.value = null;
}

function restoreSkillProgress(messages: ChatMessage[]) {
  skillProgress.value = [];
  for (const msg of messages) {
    if (msg.metadata?.skillExecutions) {
      skillProgress.value.push(...msg.metadata.skillExecutions);
    }
  }
}

function setThinkLevel(level: ThinkLevel) {
  thinkLevel.value = level;
  sendWSMessage({ type: 'set_thinking_level', data: { level } });
}
  • Step 7: 在 switchSession 中调用恢复函数

在现有的会话切换逻辑中加入:

function switchSession(newSessionId: string) {
  skillProgress.value = [];
  todoItems.value = [];
  todoSummary.value = null;
  thinkingStream.value = '';
  isThinking.value = false;
  tokenUsage.value = null;
  // ...加载新会话消息后
  restoreTodoFromHistory(messages);
  restoreSkillProgress(messages);
  thinkLevel.value = session.thinkLevel ?? 'medium';
}
  • Step 8: 导出新状态和函数

在 store 的 return 中追加导出:

return {
  // ...现有导出
  todoItems, todoSummary,
  isThinking, thinkingStream,
  thinkLevel, modelThinkingCapability, tokenUsage,
  restoreTodoFromHistory, restoreSkillProgress, setThinkLevel,
};
  • Step 9: 提交
git add packages/frontend/src/modules/agent/store/chat.ts
git commit -m "feat(agent): store 新增 todo/thinking/token 状态管理和 WS 事件处理"

Task 10: 前端 skill-card.vue

Files:

  • Create: packages/frontend/src/modules/agent/components/skill-card.vue

  • Step 1: 创建 skill-card.vue

完整组件代码见设计文档第 4.2 节。关键点:

  • Props: 接收 SkillProgress 的所有字段name, label, status, percent, step, detail, result, tokens

  • 状态过渡:<transition name="fade"> 包裹进度条和结果区域

  • CSS.skill-card--running/done/error 通过 CSS 变量控制边框颜色

  • 内部复用现有的 skill-result-viewer.vue 展示结果详情

  • Step 2: 提交

git add packages/frontend/src/modules/agent/components/skill-card.vue
git commit -m "feat(agent): 新增 skill-card.vue 统一 Skill 卡片组件"

Task 11: 前端 todo-card.vue

Files:

  • Create: packages/frontend/src/modules/agent/components/todo-card.vue

  • Step 1: 创建 todo-card.vue

完整组件代码见设计文档第 6.2 节。关键点:

  • Props: items: TodoItem[], summary: TodoSummary

  • 可折叠头部:📋 图标 + 标题 + 进度摘要 + 微型进度条

  • <transition-group name="todo-item"> 包裹列表项实现增删动画

  • 状态图标: completed / spinner in_progress / cancelled / ⬚ pending

  • CSS.todo-card__item--in_progress 高亮背景,.is-done 删除线

  • Step 2: 提交

git add packages/frontend/src/modules/agent/components/todo-card.vue
git commit -m "feat(agent): 新增 todo-card.vue 任务规划卡片组件"

Task 12: 前端 thinking-block.vue

Files:

  • Create: packages/frontend/src/modules/agent/components/thinking-block.vue

  • Step 1: 创建 thinking-block.vue

完整组件代码见设计文档第 7.4 节。关键点:

  • Props: content: string, isStreaming: boolean

  • 流式时默认展开 + 呼吸动画(thinking-pulse 1.5s+ 光标闪烁(blink 1s

  • 完成后自动折叠watch isStreaming 变化触发)

  • 内容渲染为 Markdown使用项目现有的 markdown 渲染工具)

  • CSS虚线边框 + 半透明背景 + 12px 小字号 + 柔和色

  • Step 2: 提交

git add packages/frontend/src/modules/agent/components/thinking-block.vue
git commit -m "feat(agent): 新增 thinking-block.vue 思考内容流式展示组件"

Task 13: 前端 token-stats.vue

Files:

  • Create: packages/frontend/src/modules/agent/components/token-stats.vue

  • Step 1: 创建 token-stats.vue

完整组件代码见设计文档第 5.3 节。关键点:

  • Props: tokenUsage: TokenUpdateEvent | null, isRunning: boolean

  • 执行中显示:↑1.2K ↓0.8K · 3次

  • 始终显示:上下文 32% + 颜色圆点≤50%绿/50-80%黄/>80%红)

  • formatTokens() 工具函数1234→1.2K, 12345→12.3K

  • hover tooltip 显示详细数据

  • Step 2: 提交

git add packages/frontend/src/modules/agent/components/token-stats.vue
git commit -m "feat(agent): 新增 token-stats.vue 紧凑 Token 统计组件"

Task 14: 前端 thinking-level-selector.vue

Files:

  • Create: packages/frontend/src/modules/agent/components/thinking-level-selector.vue

  • Step 1: 创建 thinking-level-selector.vue

完整组件代码见设计文档第 9.2 节。关键点:

  • Props: modelValue: ThinkLevel, disabled: boolean, capability: ModelThinkingCapability

  • el-select 下拉,选项根据 capability.levels 动态生成

  • label 映射off→关闭, minimal→极简, low→低, medium→中等, high→高, adaptive→自适应

  • emit update:modelValuechange 事件

  • Step 2: 提交

git add packages/frontend/src/modules/agent/components/thinking-level-selector.vue
git commit -m "feat(agent): 新增 thinking-level-selector.vue 思考级别选择器"

Task 15: 前端 chat.vue 布局重构

Files:

  • Modify: packages/frontend/src/modules/agent/views/chat.vue:16-218

  • Step 1: 删除顶部 token 统计条

删除约第16-37行的 <div class="token-stats-bar"> 整个区块。

  • Step 2: 重构消息列表区域

修改消息列表区域约第40-89行插入新组件

<div class="chat-messages" ref="messagesRef">
  <!-- 历史消息 -->
  <message-item v-for="msg in displayMessages" :key="msg.id" :message="msg" />

  <!-- Todo 卡片 -->
  <todo-card v-if="todoItems.length" :items="todoItems" :summary="todoSummary" />

  <!-- Skill 执行卡片(替代 skill-indicator -->
  <skill-card v-for="sp in skillProgress" :key="sp.name" v-bind="sp" />

  <!-- 流式 assistant 消息(含 thinking-block -->
  <message-item v-if="streamingMsg" :message="streamingMsg">
    <template #before-content>
      <thinking-block
        v-if="thinkingStream || streamingMsg?.thinking"
        :content="isThinking ? thinkingStream : (streamingMsg?.thinking || '')"
        :is-streaming="isThinking"
      />
    </template>
  </message-item>
</div>
  • Step 3: 在输入区域添加思考级别选择器和 token 统计

在输入区域约第92-218行的工具栏中加入

<div class="chat-input__toolbar">
  <!-- 现有的 Agent 选择器附件按钮 -->
  
  <!-- 新增思考级别选择器 -->
  <thinking-level-selector
    v-model="thinkLevel"
    :disabled="loading"
    :capability="modelThinkingCapability"
    @change="setThinkLevel"
  />

  <!-- 右侧token 统计 -->
  <token-stats
    class="chat-input__token-stats"
    :token-usage="tokenUsage"
    :is-running="loading"
  />
</div>
  • Step 4: 导入新组件

<script setup> 中导入:

import SkillCard from '../components/skill-card.vue';
import TodoCard from '../components/todo-card.vue';
import ThinkingBlock from '../components/thinking-block.vue';
import TokenStats from '../components/token-stats.vue';
import ThinkingLevelSelector from '../components/thinking-level-selector.vue';
  • Step 5: 删除 skill-indicator 的导入和使用

删除 import SkillIndicator 和模板中的 <skill-indicator> 标签。

  • Step 6: 提交
git add packages/frontend/src/modules/agent/views/chat.vue
git commit -m "feat(agent): chat.vue 布局重构 - 集成新组件 + token 统计移位"

Task 16: 前端 message-item.vue 改造

Files:

  • Modify: packages/frontend/src/modules/agent/components/message-item.vue:16-89

  • Step 1: 删除旧的 Skill 执行历史区块

删除约第31-89行的 <div class="message-item__skills"> 整个区块skill 历史现在由 skill-card 在 chat.vue 中渲染)。

  • Step 2: 替换旧的思考区块

删除约第16-29行的 <div class="message-item__thinking"> 区块,替换为:

<!-- 历史消息中的思考内容 -->
<thinking-block
  v-if="message.thinking && message.role === 'assistant'"
  :content="message.thinking"
  :is-streaming="false"
/>

<!-- 插槽流式消息的 thinking-block  chat.vue 传入 -->
<slot name="before-content" />
  • Step 3: 导入 thinking-block
import ThinkingBlock from './thinking-block.vue';
  • Step 4: 提交
git add packages/frontend/src/modules/agent/components/message-item.vue
git commit -m "refactor(agent): message-item 移除旧 skill 历史和 thinking 区块,改用新组件"

Task 17: 前端 chat-sidebar.vue 适配

Files:

  • Modify: packages/frontend/src/modules/agent/components/chat-sidebar.vue:30-59

  • Step 1: 在会话项中增加摘要行

在每个会话项的标题下方约第31-59行追加

<div class="session-item__subtitle">
  <template v-if="session.todoSummary">
    <span class="session-item__todo-badge">
      📋 {{ session.todoSummary.completed }}/{{ session.todoSummary.total }}
    </span>
  </template>
  <template v-else-if="session.isRunning">
    <span class="session-item__running-dot" />
    <span>执行中...</span>
  </template>
  <template v-else-if="session.lastMessage">
    <span class="session-item__last-msg">{{ session.lastMessage }}</span>
  </template>
</div>
  • Step 2: 添加 CSS
.session-item__subtitle {
  font-size: 12px;
  color: var(--el-text-color-secondary);
  margin-top: 4px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.session-item__todo-badge {
  font-size: 11px;
}
.session-item__running-dot {
  display: inline-block;
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--el-color-success);
  margin-right: 4px;
  animation: pulse 1.5s infinite;
}
  • Step 3: 提交
git add packages/frontend/src/modules/agent/components/chat-sidebar.vue
git commit -m "feat(agent): chat-sidebar 会话项增加 todo 进度和运行状态"

Task 18: 删除 skill-indicator.vue + 清理

Files:

  • Delete: packages/frontend/src/modules/agent/components/skill-indicator.vue

  • Step 1: 确认 skill-indicator 不再被引用

cd packages/frontend
grep -r "skill-indicator" src/ --include="*.vue" --include="*.ts"

预期无结果Task 15 已删除所有引用)。

  • Step 2: 删除文件
rm packages/frontend/src/modules/agent/components/skill-indicator.vue
  • Step 3: 全局搜索确认无遗漏引用
grep -r "SkillIndicator\|skill-indicator" packages/frontend/src/ --include="*.vue" --include="*.ts"

预期:无结果。

  • Step 4: 提交
git add -A packages/frontend/src/modules/agent/components/skill-indicator.vue
git commit -m "refactor(agent): 删除 skill-indicator.vue已被 skill-card.vue 替代"

验证清单

完成所有 Task 后,执行以下验证:

  • 启动后端 pnpm --filter @neta/backend dev,确认无编译错误
  • 启动前端 pnpm --filter @neta/frontend dev,确认无编译错误
  • 打开对话页面,发送消息,验证:
    • Skill 执行中→完成无视觉跳变同一个卡片CSS 过渡)
    • Token 统计显示在输入区右下角,数据来自后端推送
    • 思考内容流式展示(虚线边框 + 呼吸动画 + 光标闪烁)
    • 思考完成后自动折叠
    • 思考级别选择器可切换
    • Todo 卡片在对话流中实时更新(需 agent 调用 todo 工具)
    • 切换会话后 todo/skill 状态正确恢复
    • 会话列表显示 todo 进度