1070 lines
31 KiB
Markdown
1070 lines
31 KiB
Markdown
# 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<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 构建 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<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: 前端 — 改造 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<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"
|
||
```
|