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

1070 lines
31 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

# 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**
```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<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**
```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<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**
```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<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**
```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<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**
```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<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**
```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 构建 agentConfigapiUrl, 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<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**
```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: 前端 — 改造 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` → 追加 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<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**
```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"
```