# 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 模块新增 Entity(netaclaw_agent, netaclaw_skill)和 Controller(agent 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** ```typescript // 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; @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** ```bash 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** ```typescript // 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; @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** ```bash 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` 字段后面添加: ```typescript @Column({ comment: 'Agent ID(关联 netaclaw_agent)', nullable: true }) agentId: number; ``` - [ ] **Step 2: 验证字段添加** 启动后端(synchronize: true 会自动加字段),用 MySQL MCP 工具执行: `DESCRIBE netaclaw_session` Expected: 出现 agentId 字段 - [ ] **Step 3: Commit** ```bash 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** ```typescript // 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; /** 分页查询 */ 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) { const entity = this.agentRepo.create(data); const saved = await this.agentRepo.save(entity); return { id: saved.id }; } /** 更新 */ async update(data: Partial) { 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** ```typescript // 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** ```bash 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 存在但数据库无记录,自动创建记录(默认启用) ```typescript // 在现有 SkillLoaderService 中新增以下导入和方法: import { InjectEntityModel } from '@midwayjs/typeorm'; import { Repository } from 'typeorm'; import { NetaClawSkillEntity } from '../entity/skill.js'; // 在类中新增: @InjectEntityModel(NetaClawSkillEntity) skillRepo: Repository; /** 获取所有 Skill 元数据(SKILL.md + 数据库状态合并) */ async getSkillMetas(): Promise { 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 { 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** ```typescript // 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** ```bash 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` 中新增: ```typescript /** 会话列表 */ async listSessions(userId?: string, agentId?: number): Promise { 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 { await this.messageRepo.delete({ sessionId }); await this.sessionRepo.delete({ sessionId }); } /** 删除用户所有会话 */ async deleteAllSessions(userId?: string): Promise { 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** ```typescript // 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** ```bash 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` 字段: ```typescript export interface ClientChatMessage { type: 'chat'; sessionId: string; content: string; agentName?: string; agentId?: number; // 新增:从数据库加载 Agent 配置 } ``` 新增服务端事件类型: ```typescript 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 构建 agentConfig(apiUrl, apiKey, modelId) 5. 用 Agent 的 systemPrompt + skills 构建系统提示 6. 用 Agent 的 config.middleware.maxToolRounds 设置最大工具轮次 关键改动: ```typescript // 注入 @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`,发送: ```json {"type":"chat","sessionId":"","content":"你好","agentId":null} ``` Expected: 收到 token/done 事件 - [ ] **Step 4: Commit** ```bash 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): ```typescript /** * 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** ```typescript // 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 | null = null; let reconnectTimer: ReturnType | 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** ```bash 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: 前端 — 改造 Store(SSE → 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` → 追加 content(`event.content`) - WS `thinking` → 追加 thinking(`event.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` 关键代码结构: ```typescript const BASE_OPEN = '/open/netaclaw'; const BASE_ADMIN = '/admin/netaclaw'; // WS 实例(store 级别单例) let wsInstance: ReturnType | 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** ```bash 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** ```bash 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** ```bash 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** ```bash 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_info`、`agent_skill`、`agent_session`、`agent_message` 表名的 Entity 文件,删除它们。 同时搜索是否有旧的 Controller/Service 引用这些 Entity,一并删除。 - [ ] **Step 2: 删除旧数据库表** 用 MySQL MCP 工具执行: ```sql 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** ```bash 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** ```bash git add -A git commit -m "feat(netaclaw): complete agent/skill management migration from old agent module" ```