GPU_GUARD_MONOREPO/docs/superpowers/plans/2026-04-12-netaclaw-agent-skill-migration.md
2026-05-20 21:39:12 +08:00

31 KiB
Raw Permalink Blame History

NetaClaw Agent/Skill 管理迁移实施计划

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 管理、Skill 管理、Agent 对话页面从旧接口迁移到 NetaClaw 运行时,删除旧 agent 模块

Architecture: 后端在 NetaClaw 模块新增 Entitynetaclaw_agent, netaclaw_skill和 Controlleragent CRUD, skill 管理, 会话管理),增强 WS Gateway 支持 agentId。前端 store 从 SSE 改为 WebSocket所有页面 API 路径统一切到 /admin/netaclaw/*/open/netaclaw/*

Tech Stack: Midway.js 3.20 + TypeORM + Socket.IO / Vue 3 + Pinia + Element Plus

Spec: docs/superpowers/specs/2026-04-12-netaclaw-agent-skill-migration-design.md


Task 1: 新建 NetaClaw Agent Entity

Files:

  • Create: packages/backend/src/modules/netaclaw/entity/agent.ts

  • Step 1: 创建 NetaClawAgentEntity

// packages/backend/src/modules/netaclaw/entity/agent.ts
import { BaseEntity } from '../../base/entity/base.js';
import { Column, Entity, Index } from 'typeorm';

/**
 * NetaClaw Agent 配置
 */
@Entity('netaclaw_agent')
export class NetaClawAgentEntity extends BaseEntity {
  @Column({ comment: '唯一标识', length: 100, unique: true })
  name: string;

  @Column({ comment: '显示名称', length: 200 })
  label: string;

  @Column({ comment: '描述', type: 'text', nullable: true })
  description: string;

  @Column({ comment: '图标', length: 100, nullable: true })
  icon: string;

  @Column({ comment: '系统提示词', type: 'text', nullable: true })
  systemPrompt: string;

  @Column({ type: 'json', comment: '关联Skill名称列表', nullable: true })
  skills: string[];

  @Column({ type: 'json', comment: '模型配置', nullable: true })
  modelConfig: { apiUrl?: string; apiKey?: string; modelId?: string; contextWindow?: number };

  @Column({ type: 'json', comment: 'Agent配置', nullable: true })
  config: Record<string, unknown>;

  @Index()
  @Column({ comment: '状态: 0=草稿 1=已发布', default: 0 })
  status: number;
}
  • Step 2: 启动后端验证表自动创建

Run: cd packages/backend && npx midway-bin dev Expected: 启动成功,数据库中出现 netaclaw_agent

  • Step 3: 用 MCP 工具验证表结构

用 MySQL MCP 工具执行: DESCRIBE netaclaw_agent Expected: 包含 id, createTime, updateTime, tenantId, name, label, description, icon, systemPrompt, skills, modelConfig, config, status 字段

  • Step 4: Commit
git add packages/backend/src/modules/netaclaw/entity/agent.ts
git commit -m "feat(netaclaw): add NetaClawAgentEntity for agent CRUD management"

Task 2: 新建 NetaClaw Skill Entity

Files:

  • Create: packages/backend/src/modules/netaclaw/entity/skill.ts

  • Step 1: 创建 NetaClawSkillEntity

// packages/backend/src/modules/netaclaw/entity/skill.ts
import { BaseEntity } from '../../base/entity/base.js';
import { Column, Entity, Index } from 'typeorm';

/**
 * NetaClaw Skill 状态管理(配合 SKILL.md 文件使用)
 */
@Entity('netaclaw_skill')
export class NetaClawSkillEntity extends BaseEntity {
  @Column({ comment: 'Skill名称', length: 100, unique: true })
  name: string;

  @Column({ comment: '显示名称', length: 200 })
  label: string;

  @Column({ comment: '描述', type: 'text', nullable: true })
  description: string;

  @Column({ comment: '图标', length: 100, nullable: true })
  icon: string;

  @Column({ comment: '分类', length: 50, nullable: true })
  category: string;

  @Column({ comment: 'Skill类型: compute/llm/multimodal', length: 20, nullable: true })
  skillType: string;

  @Column({ type: 'json', comment: '标签', nullable: true })
  tags: string[];

  @Column({ type: 'json', comment: '配置', nullable: true })
  config: Record<string, unknown>;

  @Index()
  @Column({ comment: '状态: 0=禁用 1=启用', default: 1 })
  status: number;

  @Column({ comment: '版本号', length: 20, nullable: true })
  version: string;
}
  • Step 2: 验证表创建

启动后端,用 MySQL MCP 工具执行: DESCRIBE netaclaw_skill Expected: 包含所有定义的字段

  • Step 3: Commit
git add packages/backend/src/modules/netaclaw/entity/skill.ts
git commit -m "feat(netaclaw): add NetaClawSkillEntity for skill status management"

Task 3: 给 netaclaw_session 表增加 agentId 字段

Files:

  • Modify: packages/backend/src/modules/netaclaw/entity/session.ts

  • Step 1: 在 NetaClawSessionEntity 中增加 agentId 字段

agentName 字段后面添加:

  @Column({ comment: 'Agent ID关联 netaclaw_agent', nullable: true })
  agentId: number;
  • Step 2: 验证字段添加

启动后端synchronize: true 会自动加字段),用 MySQL MCP 工具执行: DESCRIBE netaclaw_session Expected: 出现 agentId 字段

  • Step 3: Commit
git add packages/backend/src/modules/netaclaw/entity/session.ts
git commit -m "feat(netaclaw): add agentId field to session entity"

Task 4: 新建 Agent 管理 Controller + Service

Files:

  • Create: packages/backend/src/modules/netaclaw/service/agent.ts

  • Create: packages/backend/src/modules/netaclaw/controller/agent.ts

  • Step 1: 创建 AgentService

// packages/backend/src/modules/netaclaw/service/agent.ts
import { Provide, Inject, Scope, ScopeEnum } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository, Like } from 'typeorm';
import { NetaClawAgentEntity } from '../entity/agent.js';

@Provide()
@Scope(ScopeEnum.Singleton)
export class NetaClawAgentService {
  @InjectEntityModel(NetaClawAgentEntity)
  agentRepo: Repository<NetaClawAgentEntity>;

  /** 分页查询 */
  async page(params: { page: number; size: number; keyWord?: string }) {
    const { page = 1, size = 20, keyWord } = params;
    const where: any = {};
    if (keyWord) {
      where.label = Like(`%${keyWord}%`);
    }
    const [list, total] = await this.agentRepo.findAndCount({
      where,
      order: { createTime: 'DESC' },
      skip: (page - 1) * size,
      take: size,
    });
    return { list, pagination: { page, size, total } };
  }

  /** 创建 */
  async add(data: Partial<NetaClawAgentEntity>) {
    const entity = this.agentRepo.create(data);
    const saved = await this.agentRepo.save(entity);
    return { id: saved.id };
  }

  /** 更新 */
  async update(data: Partial<NetaClawAgentEntity>) {
    await this.agentRepo.save(data);
  }

  /** 删除 */
  async delete(ids: number[]) {
    await this.agentRepo.delete(ids);
  }

  /** 详情 */
  async info(id: number) {
    return this.agentRepo.findOneBy({ id });
  }

  /** 已发布列表 */
  async publishedList() {
    return this.agentRepo.find({ where: { status: 1 }, order: { createTime: 'DESC' } });
  }
}
  • Step 2: 创建 Agent Controller
// packages/backend/src/modules/netaclaw/controller/agent.ts
import { Provide, Inject, Post, Get, Body, Query, Controller, Logger } from '@midwayjs/core';
import { ILogger } from '@midwayjs/logger';
import { Context } from '@midwayjs/koa';
import { NetaClawAgentService } from '../service/agent.js';

@Provide()
@Controller('/admin/netaclaw/agent')
export class NetaClawAgentAdminController {
  @Inject()
  ctx: Context;

  @Logger()
  logger: ILogger;

  @Inject()
  agentService: NetaClawAgentService;

  @Post('/page')
  async page(@Body() body: { page?: number; size?: number; keyWord?: string }) {
    return { code: 1000, data: await this.agentService.page(body) };
  }

  @Post('/add')
  async add(@Body() body: any) {
    return { code: 1000, data: await this.agentService.add(body) };
  }

  @Post('/update')
  async update(@Body() body: any) {
    await this.agentService.update(body);
    return { code: 1000, message: 'success' };
  }

  @Post('/delete')
  async delete(@Body() body: { ids: number[] }) {
    await this.agentService.delete(body.ids);
    return { code: 1000, message: 'success' };
  }

  @Get('/info')
  async info(@Query('id') id: number) {
    return { code: 1000, data: await this.agentService.info(id) };
  }
}

@Provide()
@Controller('/open/netaclaw/agent')
export class NetaClawAgentOpenController {
  @Inject()
  agentService: NetaClawAgentService;

  @Post('/list')
  async list() {
    return { code: 1000, data: await this.agentService.publishedList() };
  }
}
  • Step 3: 启动后端验证接口

Run: cd packages/backend && npx midway-bin dev 用 curl 测试: curl -X POST http://localhost:8003/admin/netaclaw/agent/page -H "Content-Type: application/json" -d '{"page":1,"size":10}' Expected: 返回 {"code":1000,"data":{"list":[],"pagination":{"page":1,"size":10,"total":0}}}

  • Step 4: Commit
git add packages/backend/src/modules/netaclaw/service/agent.ts packages/backend/src/modules/netaclaw/controller/agent.ts
git commit -m "feat(netaclaw): add agent CRUD controller and service"

Task 5: 新建 Skill 管理 Controller合并 SKILL.md + 数据库状态)

Files:

  • Modify: packages/backend/src/modules/netaclaw/service/skill_loader.ts

  • Create: packages/backend/src/modules/netaclaw/controller/skill.ts

  • Step 1: 增强 SkillLoaderService增加数据库状态合并

packages/backend/src/modules/netaclaw/service/skill_loader.ts 中:

  • 注入 NetaClawSkillEntity 的 Repository
  • 新增 getSkillMetas() 方法:扫描 SKILL.md + 合并数据库 status
  • 新增 setSkillStatus(name, status) 方法:更新数据库中的启用/禁用状态
  • 如果 SKILL.md 存在但数据库无记录,自动创建记录(默认启用)
// 在现有 SkillLoaderService 中新增以下导入和方法:
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { NetaClawSkillEntity } from '../entity/skill.js';

// 在类中新增:
@InjectEntityModel(NetaClawSkillEntity)
skillRepo: Repository<NetaClawSkillEntity>;

/** 获取所有 Skill 元数据SKILL.md + 数据库状态合并) */
async getSkillMetas(): Promise<any[]> {
  const fileSkills = this.getAllSkills();
  const dbSkills = await this.skillRepo.find();
  const dbMap = new Map(dbSkills.map(s => [s.name, s]));

  const result = [];
  for (const fs of fileSkills) {
    let dbRecord = dbMap.get(fs.name);
    if (!dbRecord) {
      // 自动创建数据库记录
      dbRecord = await this.skillRepo.save({
        name: fs.name,
        label: fs.name,
        description: fs.description,
        status: 1,
      });
    }
    result.push({
      name: fs.name,
      label: dbRecord.label || fs.name,
      description: fs.description || dbRecord.description,
      icon: dbRecord.icon,
      category: dbRecord.category,
      skillType: dbRecord.skillType,
      tags: dbRecord.tags,
      config: dbRecord.config,
      status: dbRecord.status,
      version: dbRecord.version,
    });
  }
  return result;
}

/** 设置 Skill 启用/禁用状态 */
async setSkillStatus(name: string, status: number): Promise<void> {
  const existing = await this.skillRepo.findOneBy({ name });
  if (existing) {
    await this.skillRepo.update({ name }, { status });
  } else {
    await this.skillRepo.save({ name, label: name, status });
  }
}
  • Step 2: 创建 Skill Controller
// packages/backend/src/modules/netaclaw/controller/skill.ts
import { Provide, Inject, Get, Post, Body, Controller } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
import { SkillLoaderService } from '../service/skill_loader.js';

@Provide()
@Controller('/admin/netaclaw/skill')
export class NetaClawSkillController {
  @Inject()
  ctx: Context;

  @Inject()
  skillLoader: SkillLoaderService;

  @Get('/metas')
  async metas() {
    return { code: 1000, data: await this.skillLoader.getSkillMetas() };
  }

  @Post('/setStatus')
  async setStatus(@Body() body: { name: string; status: number }) {
    await this.skillLoader.setSkillStatus(body.name, body.status);
    return { code: 1000, message: 'success' };
  }
}
  • Step 3: 验证接口

用 curl 测试: curl http://localhost:8003/admin/netaclaw/skill/metas Expected: 返回 {"code":1000,"data":[...]}(如果 skills/ 目录有 SKILL.md 文件则有数据)

  • Step 4: Commit
git add packages/backend/src/modules/netaclaw/service/skill_loader.ts packages/backend/src/modules/netaclaw/controller/skill.ts
git commit -m "feat(netaclaw): add skill management controller with SKILL.md + DB merge"

Task 6: 新建会话管理 Controller

Files:

  • Create: packages/backend/src/modules/netaclaw/controller/session.ts

  • Modify: packages/backend/src/modules/netaclaw/gateway/session.ts

  • Step 1: 在 NetaClawSessionService 中新增会话管理方法

packages/backend/src/modules/netaclaw/gateway/session.ts 中新增:

/** 会话列表 */
async listSessions(userId?: string, agentId?: number): Promise<NetaClawSessionEntity[]> {
  const where: any = { status: 'active' };
  if (userId) where.userId = userId;
  if (agentId) where.agentId = agentId;
  return this.sessionRepo.find({ where, order: { updateTime: 'DESC' } });
}

/** 获取会话消息历史 */
async getMessages(sessionId: string) {
  return this.messageRepo.find({
    where: { sessionId },
    order: { createTime: 'ASC' },
  });
}

/** 删除会话及其消息 */
async deleteSession(sessionId: string): Promise<void> {
  await this.messageRepo.delete({ sessionId });
  await this.sessionRepo.delete({ sessionId });
}

/** 删除用户所有会话 */
async deleteAllSessions(userId?: string): Promise<void> {
  const where: any = {};
  if (userId) where.userId = userId;
  const sessions = await this.sessionRepo.find({ where });
  for (const s of sessions) {
    await this.messageRepo.delete({ sessionId: s.sessionId });
  }
  await this.sessionRepo.delete(where);
}
  • Step 2: 创建 Session Controller
// packages/backend/src/modules/netaclaw/controller/session.ts
import { Provide, Inject, Post, Body, Controller } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
import { NetaClawSessionService } from '../gateway/session.js';

@Provide()
@Controller('/open/netaclaw/session')
export class NetaClawSessionController {
  @Inject()
  ctx: Context;

  @Inject()
  sessionService: NetaClawSessionService;

  @Post('/list')
  async list(@Body() body: { userId?: string; agentId?: number }) {
    return { code: 1000, data: await this.sessionService.listSessions(body.userId, body.agentId) };
  }

  @Post('/messages')
  async messages(@Body() body: { sessionId: string }) {
    return { code: 1000, data: await this.sessionService.getMessages(body.sessionId) };
  }

  @Post('/delete')
  async delete(@Body() body: { sessionId: string }) {
    await this.sessionService.deleteSession(body.sessionId);
    return { code: 1000, message: 'success' };
  }

  @Post('/deleteAll')
  async deleteAll(@Body() body: { userId?: string }) {
    await this.sessionService.deleteAllSessions(body.userId);
    return { code: 1000, message: 'success' };
  }
}
  • Step 3: 验证接口

用 curl 测试: curl -X POST http://localhost:8003/open/netaclaw/session/list -H "Content-Type: application/json" -d '{}' Expected: 返回 {"code":1000,"data":[]}

  • Step 4: Commit
git add packages/backend/src/modules/netaclaw/controller/session.ts packages/backend/src/modules/netaclaw/gateway/session.ts
git commit -m "feat(netaclaw): add session management controller (list/messages/delete)"

Task 7: 增强 WebSocket Gateway 支持 agentId

Files:

  • Modify: packages/backend/src/modules/netaclaw/gateway/protocol.ts

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

  • Step 1: 更新协议定义,增加新事件类型

packages/backend/src/modules/netaclaw/gateway/protocol.ts 中:

ClientChatMessage 增加 agentId 字段:

export interface ClientChatMessage {
  type: 'chat';
  sessionId: string;
  content: string;
  agentName?: string;
  agentId?: number;  // 新增:从数据库加载 Agent 配置
}

新增服务端事件类型:

export interface ServerSkillStartEvent {
  type: 'skill_start';
  sessionId: string;
  name: string;
  label: string;
}

export interface ServerSkillEndEvent {
  type: 'skill_end';
  sessionId: string;
  name: string;
  status: string;
  result?: any;
  tokens?: any;
}

export interface ServerProgressEvent {
  type: 'progress';
  sessionId: string;
  name: string;
  step?: string;
  detail?: string;
  percent?: number;
}

export interface ServerTokenUpdateEvent {
  type: 'token_update';
  sessionId: string;
  input: number;
  output: number;
  total: number;
  apiCalls: number;
}

将新类型加入 ServerEvent 联合类型。

  • Step 2: 更新 Gateway 的 handleChat 方法

packages/backend/src/modules/netaclaw/gateway/server.ts 中:

  1. 注入 NetaClawAgentService
  2. handleChat 方法签名增加 agentId?: number 参数
  3. 如果有 agentId从数据库读取 Agent 配置systemPrompt, modelConfig, skills, config
  4. 用 Agent 的 modelConfig 构建 agentConfigapiUrl, apiKey, modelId
  5. 用 Agent 的 systemPrompt + skills 构建系统提示
  6. 用 Agent 的 config.middleware.maxToolRounds 设置最大工具轮次

关键改动:

// 注入
@Inject()
agentService: NetaClawAgentService;

// handleChat 中
private async handleChat(sessionId: string, content: string, agentName?: string, agentId?: number) {
  // ... 现有的会话创建逻辑 ...

  let agentConfig: AgentConfig;
  if (agentId) {
    const agentInfo = await this.agentService.info(agentId);
    if (agentInfo) {
      const mc = agentInfo.modelConfig || {};
      const cfg = (agentInfo.config || {}) as any;
      const skillNames = agentInfo.skills || [];
      const skillPrompt = this.skillLoader.getSkillPrompt(skillNames);
      agentConfig = {
        name: agentInfo.name,
        systemPrompt: (agentInfo.systemPrompt || '') + skillPrompt,
        model: mc.modelId ? `openai:${mc.modelId}` : (process.env.NETACLAW_MODEL ?? 'anthropic:claude-sonnet-4-20250514'),
        apiKey: mc.apiKey || process.env.NETACLAW_API_KEY || '',
        baseUrl: mc.apiUrl,
        maxToolRounds: cfg?.middleware?.maxToolRounds ?? 20,
      };
    }
  }
  // 如果没有 agentId 或找不到 Agent使用默认配置现有逻辑
  if (!agentConfig) {
    agentConfig = { /* 现有默认配置 */ };
  }
  // ... 后续 runAgent 调用不变 ...
}

同时更新 onMessage 中传递 msg.agentId

  • Step 3: 验证 WS 连接

用 WebSocket 客户端工具连接 ws://localhost:8003/netaclaw,发送:

{"type":"chat","sessionId":"","content":"你好","agentId":null}

Expected: 收到 token/done 事件

  • Step 4: Commit
git add packages/backend/src/modules/netaclaw/gateway/protocol.ts packages/backend/src/modules/netaclaw/gateway/server.ts
git commit -m "feat(netaclaw): enhance WS gateway with agentId support and new event types"

Task 8: 前端 — 更新类型定义和 WebSocket 工具

Files:

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

  • Create: packages/frontend/src/modules/agent/hooks/websocket.ts

  • Step 1: 更新类型定义

packages/frontend/src/modules/agent/types/index.d.ts 中:

新增 WebSocket 事件类型(替代 SSEEvent

/**
 * WebSocket 客户端消息
 */
export interface WSClientMessage {
  type: 'chat' | 'ping';
  sessionId?: string;
  content?: string;
  agentId?: number;
}

/**
 * WebSocket 服务端事件
 */
export interface WSServerEvent {
  type: 'token' | 'thinking' | 'tool_call' | 'tool_result' | 'skill_start' | 'skill_end' | 'progress' | 'token_update' | 'done' | 'error' | 'pong';
  sessionId?: string;
  [key: string]: any;
}
  • Step 2: 创建 WebSocket 连接管理 Hook
// packages/frontend/src/modules/agent/hooks/websocket.ts
import { ref, onUnmounted } from 'vue';
import { config } from '/@/config';
import type { WSClientMessage, WSServerEvent } from '../types/index.d';

/**
 * NetaClaw WebSocket 连接管理
 */
export function useNetaClawWS() {
  const connected = ref(false);
  let ws: WebSocket | null = null;
  let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
  let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
  let reconnectAttempts = 0;
  const MAX_RECONNECT_ATTEMPTS = 10;
  const handlers = new Set<(event: WSServerEvent) => void>();

  function getWsUrl(): string {
    const base = config.baseUrl || `http://localhost:8003`;
    const url = new URL(base);
    const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
    return `${protocol}//${url.host}/netaclaw`;
  }

  function connect() {
    if (ws?.readyState === WebSocket.OPEN) return;
    try {
      ws = new WebSocket(getWsUrl());
      ws.onopen = () => {
        connected.value = true;
        reconnectAttempts = 0;
        startHeartbeat();
      };
      ws.onmessage = (e) => {
        try {
          const event: WSServerEvent = JSON.parse(e.data);
          if (event.type === 'pong') return;
          handlers.forEach(h => h(event));
        } catch { /* ignore */ }
      };
      ws.onclose = () => {
        connected.value = false;
        stopHeartbeat();
        scheduleReconnect();
      };
      ws.onerror = () => {
        ws?.close();
      };
    } catch { /* ignore */ }
  }

  function send(msg: WSClientMessage) {
    if (ws?.readyState === WebSocket.OPEN) {
      ws.send(JSON.stringify(msg));
    }
  }

  function onEvent(handler: (event: WSServerEvent) => void) {
    handlers.add(handler);
    return () => handlers.delete(handler);
  }

  function startHeartbeat() {
    stopHeartbeat();
    heartbeatTimer = setInterval(() => {
      send({ type: 'ping' });
    }, 30000);
  }

  function stopHeartbeat() {
    if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; }
  }

  function scheduleReconnect() {
    if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) return;
    const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
    reconnectAttempts++;
    reconnectTimer = setTimeout(() => connect(), delay);
  }

  function disconnect() {
    stopHeartbeat();
    if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
    reconnectAttempts = MAX_RECONNECT_ATTEMPTS; // 阻止自动重连
    ws?.close();
    ws = null;
    connected.value = false;
  }

  return { connected, connect, disconnect, send, onEvent };
}
  • Step 3: Commit
git add packages/frontend/src/modules/agent/types/index.d.ts packages/frontend/src/modules/agent/hooks/websocket.ts
git commit -m "feat(frontend): add WebSocket hook and types for NetaClaw migration"

Task 9: 前端 — 改造 StoreSSE → WebSocket

Files:

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

  • Step 1: 重写 store将 SSE 替换为 WebSocket

核心改动:

  1. 导入 useNetaClawWS 替代 fetch SSE
  2. API 路径从 /open/agent/* 改为 /open/netaclaw/*(管理接口从 /admin/agent/* 改为 /admin/netaclaw/*
  3. sendMessage() 改为通过 WS 发送 {type:'chat', sessionId, content, agentId}
  4. 新增 initWS() 方法建立 WS 连接并注册事件处理
  5. WS 事件处理复用现有的 handleSSEEvent 逻辑(重命名为 handleWSEvent),映射关系:
    • WS token → 追加 contentevent.content
    • WS thinking → 追加 thinkingevent.content
    • WS tool_call → 记录(event.toolName, event.args
    • WS skill_start → 添加 skillProgress
    • WS skill_end → 完成 skillProgress
    • WS progress → 更新 skillProgress
    • WS token_update → 更新 runningTokenUsage
    • WS done → 标记完成(event.sessionId, event.usage
    • WS error → 显示错误(event.message
  6. 移除 checkExecutionStatus()reconnectToExecution()loadContextTokens() 方法WS 自动处理)
  7. stopGeneration() 改为发送 WS 取消消息或直接断开重连
  8. 会话管理 API 路径替换:
    • loadMessages: /open/netaclaw/session/messages
    • loadSessions: /open/netaclaw/session/list
    • deleteSession: /open/netaclaw/session/delete
    • deleteAllSessions: /open/netaclaw/session/deleteAll
    • loadAgents: /open/netaclaw/agent/list

关键代码结构:

const BASE_OPEN = '/open/netaclaw';
const BASE_ADMIN = '/admin/netaclaw';

// WS 实例store 级别单例)
let wsInstance: ReturnType<typeof useNetaClawWS> | null = null;
let wsCleanup: (() => void) | null = null;

function initWS() {
  if (wsInstance) return;
  wsInstance = useNetaClawWS();
  wsInstance.connect();
  wsCleanup = wsInstance.onEvent(handleWSEvent);
}

async function sendMessage(content: string) {
  if (!content.trim() || loading.value) return;
  initWS();
  // 添加用户消息 + 空助手消息占位(同现有逻辑)
  // ...
  loading.value = true;
  wsInstance.send({
    type: 'chat',
    sessionId: sessionId.value || undefined,
    content: content.trim(),
    agentId: currentAgentId.value || undefined,
  });
}

function handleWSEvent(event: WSServerEvent) {
  if (!loading.value && event.type !== 'done') return;
  const assistantMsg = messages.value[messages.value.length - 1];
  if (!assistantMsg || assistantMsg.role !== 'assistant') return;

  switch (event.type) {
    case 'token':
      assistantMsg.content += event.content;
      _onTokenCbs.forEach(cb => cb());
      break;
    case 'thinking':
      if (!assistantMsg.thinking) assistantMsg.thinking = '';
      assistantMsg.thinking += event.content;
      _onTokenCbs.forEach(cb => cb());
      break;
    case 'done':
      if (event.sessionId) sessionId.value = event.sessionId;
      loading.value = false;
      loadSessions();
      recalcSessionTokens();
      setTimeout(() => { skillProgress.value = []; }, 3000);
      break;
    case 'error':
      assistantMsg.content += `\n\n**错误**: ${event.message}`;
      loading.value = false;
      break;
    // skill_start, skill_end, progress, token_update 同现有逻辑
  }
}
  • Step 2: 验证 Store 编译通过

Run: cd packages/frontend && npx vue-tsc --noEmit Expected: 无类型错误

  • Step 3: Commit
git add packages/frontend/src/modules/agent/store/chat.ts
git commit -m "feat(frontend): migrate chat store from SSE to WebSocket"

Task 10: 前端 — 改造 hooks/chat.ts

Files:

  • Modify: packages/frontend/src/modules/agent/hooks/chat.ts

  • Step 1: 同步改造 chat hook

与 store 相同的改造逻辑:

  1. API 路径从 /open/agent/* 改为 /open/netaclaw/*
  2. sendMessage() 改为通过 WS 发送
  3. 事件处理从 SSE 改为 WS
  4. 移除 SSE 相关的 fetch/reader/decoder 逻辑

注意:这个 hook 和 store 功能重叠,如果 store 已经覆盖所有场景,可以将 hook 简化为直接调用 store 的方法。

  • Step 2: Commit
git add packages/frontend/src/modules/agent/hooks/chat.ts
git commit -m "feat(frontend): migrate chat hook from SSE to WebSocket"

Task 11: 前端 — 改造管理页面 API 路径

Files:

  • Modify: packages/frontend/src/modules/agent/views/agent-list.vue

  • Modify: packages/frontend/src/modules/agent/views/agent-edit.vue

  • Modify: packages/frontend/src/modules/agent/views/skills.vue

  • Step 1: 改造 agent-list.vue

全局替换 API 路径:

  • /admin/agent/info/page/admin/netaclaw/agent/page

  • /admin/agent/info/update/admin/netaclaw/agent/update

  • /admin/agent/info/delete/admin/netaclaw/agent/delete

  • Step 2: 改造 agent-edit.vue

全局替换 API 路径:

  • /admin/agent/info/add/admin/netaclaw/agent/add

  • /admin/agent/info/update/admin/netaclaw/agent/update

  • /admin/agent/info/info/admin/netaclaw/agent/info

  • /admin/agent/skill/metas/admin/netaclaw/skill/metas

  • Step 3: 改造 skills.vue

全局替换 API 路径:

  • /admin/agent/skill/metas/admin/netaclaw/skill/metas

  • /admin/agent/skill/setStatus/admin/netaclaw/skill/setStatus

  • Step 4: Commit

git add packages/frontend/src/modules/agent/views/agent-list.vue packages/frontend/src/modules/agent/views/agent-edit.vue packages/frontend/src/modules/agent/views/skills.vue
git commit -m "feat(frontend): migrate agent/skill management pages to netaclaw API paths"

Task 12: 前端 — 改造 chat.vue 对话页面

Files:

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

  • Step 1: 更新 API 路径

  • /admin/agent/info/info/admin/netaclaw/agent/info

  • /admin/base/comm/upload → 保持不变(通用上传接口)

  • Step 2: 确保 WS 连接在页面加载时建立

onMounted 中调用 store 的 initWS() 或确保 store 初始化时自动连接。

  • Step 3: 验证对话功能

启动前后端,在浏览器中:

  1. 打开 Agent 对话页面
  2. 发送一条消息
  3. 确认收到流式回复token 逐字输出)
  4. 确认会话列表正确更新
  • Step 4: Commit
git add packages/frontend/src/modules/agent/views/chat.vue
git commit -m "feat(frontend): migrate chat page to WebSocket communication"

Task 13: 删除旧 Agent 模块代码和数据库表

Files:

  • Delete: 旧 agent 相关 Entity 文件(如果存在)

  • Database: DROP 旧表

  • Step 1: 查找并删除旧 agent Entity 文件

搜索 packages/backend/src/modules/ 下所有引用 agent_infoagent_skillagent_sessionagent_message 表名的 Entity 文件,删除它们。

同时搜索是否有旧的 Controller/Service 引用这些 Entity一并删除。

  • Step 2: 删除旧数据库表

用 MySQL MCP 工具执行:

DROP TABLE IF EXISTS agent_info;
DROP TABLE IF EXISTS agent_skill;
DROP TABLE IF EXISTS agent_session;
DROP TABLE IF EXISTS agent_message;
DROP TABLE IF EXISTS agent_checkpoints;
DROP TABLE IF EXISTS agent_checkpoint_writes;
DROP TABLE IF EXISTS agent_configs;
DROP TABLE IF EXISTS skill_configs;
  • Step 3: 验证后端启动正常

Run: cd packages/backend && npx midway-bin dev Expected: 启动成功,无报错

  • Step 4: Commit
git add -A
git commit -m "chore: remove old agent module entities and drop legacy database tables"

Task 14: 端到端联调验证

Files: 无新文件

  • Step 1: 验证 Agent 管理页面
  1. 打开 Agent 管理页面
  2. 创建一个新 Agent填写名称、描述、模型配置
  3. 编辑 Agent
  4. 发布 Agent
  5. 删除 Agent
  • Step 2: 验证 Skill 管理页面
  1. 打开 Skill 管理页面
  2. 查看 Skill 列表
  3. 启用/禁用 Skill
  • Step 3: 验证对话页面
  1. 选择一个已发布的 Agent
  2. 发送消息,确认 WS 流式回复正常
  3. 查看会话列表
  4. 切换会话
  5. 删除会话
  • Step 4: 验证数据库

用 MySQL MCP 工具确认:

  • netaclaw_agent 表有数据

  • netaclaw_session 表有新会话

  • netaclaw_message 表有消息

  • agent_* 表已不存在

  • Step 5: 最终 Commit

git add -A
git commit -m "feat(netaclaw): complete agent/skill management migration from old agent module"