初始化提交
This commit is contained in:
commit
cc660d19d1
8
.env.example
Normal file
8
.env.example
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Neta Monorepo 环境变量配置示例
|
||||||
|
# 注意:后端数据库配置在 packages/backend/src/config/config.local.ts 中
|
||||||
|
|
||||||
|
# 后端端口
|
||||||
|
NODE_ENV=local
|
||||||
|
|
||||||
|
# AI 配置 (AI Flow 后端使用)
|
||||||
|
OPENAI_API_KEY=your_openai_key
|
||||||
69
.gitignore
vendored
Normal file
69
.gitignore
vendored
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
.pnpm-store/
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
out/
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
packages/backend/src/index.ts
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Data directories
|
||||||
|
data/
|
||||||
|
!packages/frontend/src/plugins/distpicker/data/
|
||||||
|
.netabrowser-data/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
server.log
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Tauri
|
||||||
|
packages/desktop/src-tauri/target/
|
||||||
|
packages/desktop/src-tauri/Cargo.lock
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
.Python
|
||||||
|
*.so
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
.cache/
|
||||||
|
.worktrees/
|
||||||
|
|
||||||
|
# Claude Code
|
||||||
|
.claude/settings.local.json
|
||||||
|
.claude/
|
||||||
|
.husky/
|
||||||
|
.superpowers/
|
||||||
30
AGENTS.md
Normal file
30
AGENTS.md
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Repository Guidelines
|
||||||
|
|
||||||
|
## 项目结构与模块组织
|
||||||
|
本仓库是一个 `pnpm` monorepo。核心代码位于 `packages/`:`backend` 为 NestJS API,`frontend` 为 Vue 3 + Vite 前端,`desktop` 为 Tauri 桌面端,`ai-core` 为 AI 能力核心,`shared` 为跨包复用的类型、常量和工具,`skills/node` 为技能运行时。补充文档位于 `docs/`,脚本位于 `scripts/`,环境变量示例见 `.env.example`。`packages/backend/dist`、`packages/desktop/src-tauri/target` 等目录均为生成产物,不应手动修改。
|
||||||
|
|
||||||
|
## 构建、测试与开发命令
|
||||||
|
请在仓库根目录使用 `pnpm`。
|
||||||
|
|
||||||
|
- `pnpm dev`:先检查端口,再同时启动 `frontend` 和 `backend`。
|
||||||
|
- `pnpm dev:all`:同时启动 `frontend`、`backend` 和 `desktop`。
|
||||||
|
- `pnpm build`:构建 `packages/*` 下的工作区包。
|
||||||
|
- `pnpm test`:执行各包的 `test` 脚本;当前主要对 `packages/backend` 有效。
|
||||||
|
- `pnpm lint`:执行已定义的 lint 脚本。
|
||||||
|
- `pnpm --filter @neta/frontend build`:仅构建指定包。
|
||||||
|
- `pnpm --dir art-design-pro-main dev`:运行独立的设计原型前端。
|
||||||
|
|
||||||
|
## 编码风格与命名约定
|
||||||
|
仓库根目录的 TypeScript 使用严格模式。前端使用 ESLint、Prettier、Stylelint,统一 2 空格缩进、单引号、无分号。保持现有命名方式:Vue 页面或组件通常使用 `PascalCase` 目录或 `index.vue`;NestJS 模块按 `src/modules/<domain>` 组织;DTO 文件命名使用 `create-*.dto.ts`、`update-*.dto.ts`、`query-*.dto.ts`。新增类型、常量或工具前,优先复用 `packages/shared` 中的已有导出。
|
||||||
|
|
||||||
|
## 测试规范
|
||||||
|
后端测试通过 `pnpm --filter @neta/backend test` 运行,底层框架为 `jest`。当前仓库尚未配置统一的前端测试框架,也没有强制覆盖率门槛,因此新增后端功能时应补充自动化测试;前端或桌面端改动需在 PR 中说明手动验证步骤。新增测试文件优先使用 `*.spec.ts` 命名,并遵循所在包的现有习惯。
|
||||||
|
|
||||||
|
## 提交与 Pull Request 规范
|
||||||
|
提交历史采用带作用域的 Conventional Commits,例如 `feat(frontend): add Skill dialog component`、`docs: add Agent management system design spec`。建议格式为 `type(scope): summary`,常用类型包括 `feat`、`fix`、`docs`。PR 应包含变更说明、影响范围、关联任务或问题、已执行命令(如 `pnpm lint`、`pnpm test`),涉及界面变更时附上截图。
|
||||||
|
|
||||||
|
## 协作与回复语言
|
||||||
|
仓库内的自动化代理、协作者文档和交互说明统一使用中文。无论提问者使用中文、英文或其他语言,回复都应使用中文,除非任务明确要求保留某段原文或代码片段的原始语言。
|
||||||
|
|
||||||
|
## 安全与配置提示
|
||||||
|
建议使用 Node 20 及以上版本,尤其是 `art-design-pro-main` 对版本要求更高。请从 `.env.example` 复制本地配置,禁止提交密钥、日志文件或本地产生的二进制产物。
|
||||||
248
CLAUDE.md
Normal file
248
CLAUDE.md
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
本文件为 Claude Code 在此代码仓库中工作时提供指导。
|
||||||
|
|
||||||
|
## 重要规定
|
||||||
|
|
||||||
|
**所有回复必须使用中文。**
|
||||||
|
|
||||||
|
**架构边界规则:** 实现功能前必须遵循 `.claude/rules/architecture-boundaries.md`。
|
||||||
|
|
||||||
|
**需要操作数据库时使用 MCP 工具。**
|
||||||
|
|
||||||
|
## 项目概述
|
||||||
|
|
||||||
|
Neta AI电商 是 AI 驱动的电商自动化运营平台,采用 Monorepo 架构。后端基于 Midway.js + Cool Admin 框架,前端基于 Vue 3 + Element Plus,AI 引擎采用自研 NetaClaw(ReAct 循环 + WebSocket 实时通信)。
|
||||||
|
|
||||||
|
**核心架构:**
|
||||||
|
- **Monorepo**: pnpm workspace 管理多个包
|
||||||
|
- **后端**: Midway.js 3.20 + Cool Admin 8.x,端口 8003
|
||||||
|
- **前端**: Vue 3.5 + Vite 5.4 + Element Plus 2.9,端口 9001
|
||||||
|
- **AI 引擎**: NetaClaw (ReAct + TypeBox Schema + Socket.IO)
|
||||||
|
- **数据库**: MySQL 8.0+ (`neta_test`)
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
Neta-monorepo/
|
||||||
|
├── packages/
|
||||||
|
│ ├── backend/ # Midway.js 后端 (Cool Admin 框架)
|
||||||
|
│ ├── frontend/ # Vue 3 前端 (Cool Admin UI)
|
||||||
|
│ └── shared/ # 共享 TypeScript 类型
|
||||||
|
├── docs/superpowers/ # 设计文档与实施计划
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常用命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install # 安装依赖
|
||||||
|
pnpm dev # 同时启动后端+前端
|
||||||
|
pnpm --filter @neta/backend dev # 单独启动后端 (8003)
|
||||||
|
pnpm --filter @neta/frontend dev # 单独启动前端 (9001)
|
||||||
|
pnpm build # 构建所有包
|
||||||
|
pnpm lint # 代码检查
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cool Admin 框架规范
|
||||||
|
|
||||||
|
本项目基于 Cool Admin 开源框架二次开发,**必须遵循以下约定**。
|
||||||
|
|
||||||
|
### 后端 CRUD 模式 (@CoolController)
|
||||||
|
|
||||||
|
`@CoolController` 装饰器自动生成标准 CRUD 接口:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@CoolController({
|
||||||
|
api: ['add', 'delete', 'update', 'info', 'list', 'page'],
|
||||||
|
entity: ProjectInfoEntity,
|
||||||
|
service: ProjectInfoService,
|
||||||
|
pageQueryOp: {
|
||||||
|
keyWordLikeFields: ['name'], // 模糊搜索字段
|
||||||
|
fieldEq: ['status'], // 精确匹配字段
|
||||||
|
addOrderBy: { createTime: 'DESC' }, // 默认排序
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export class AdminProjectInfoController extends BaseController {}
|
||||||
|
```
|
||||||
|
|
||||||
|
**自动生成的接口:**
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| POST | `/admin/{module}/{entity}/add` | 新增 |
|
||||||
|
| POST | `/admin/{module}/{entity}/delete` | 删除 (body: {ids: [1,2]}) |
|
||||||
|
| POST | `/admin/{module}/{entity}/update` | 更新 |
|
||||||
|
| GET | `/admin/{module}/{entity}/info` | 详情 (query: id) |
|
||||||
|
| POST | `/admin/{module}/{entity}/list` | 列表 |
|
||||||
|
| POST | `/admin/{module}/{entity}/page` | 分页 (body: {page, size, keyWord, ...}) |
|
||||||
|
|
||||||
|
**自定义接口:** 在 Controller 类中用 `@Get`/`@Post` 装饰器添加:
|
||||||
|
```typescript
|
||||||
|
@Get('/ganttData', { summary: '甘特图数据' })
|
||||||
|
async ganttData(@Query('projectId') projectId: number) {
|
||||||
|
return this.ok(await this.service.ganttData(projectId));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
<!-- PLACEHOLDER_CLAUDE_CONTINUE -->
|
||||||
|
|
||||||
|
### BaseEntity 提供的字段
|
||||||
|
|
||||||
|
所有 Entity 继承 `BaseEntity`(来自 `../../base/entity/base.ts`),自动拥有:
|
||||||
|
- `id`: 自增主键
|
||||||
|
- `createTime`: 创建时间(自动填充)
|
||||||
|
- `updateTime`: 更新时间(自动更新)
|
||||||
|
- `tenantId`: 租户ID(多租户支持,nullable)
|
||||||
|
|
||||||
|
**创建 Entity 示例:**
|
||||||
|
```typescript
|
||||||
|
import { BaseEntity } from '../../base/entity/base.js';
|
||||||
|
import { Column, Entity } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('project_info')
|
||||||
|
export class ProjectInfoEntity extends BaseEntity {
|
||||||
|
@Column({ comment: '项目名称' })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column({ comment: '状态 0未开始 1进行中 2已完成', default: 0 })
|
||||||
|
status: number;
|
||||||
|
|
||||||
|
@Column({ comment: '开始日期', type: 'date', nullable: true })
|
||||||
|
startDate: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 前端 Service 代理
|
||||||
|
|
||||||
|
Cool Admin 根据后端 Controller 文件名自动生成 service 代理对象:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { service } = useCool();
|
||||||
|
|
||||||
|
// Controller 文件: modules/project/controller/admin/info.ts
|
||||||
|
// → service.project.info.page({...})
|
||||||
|
// → service.project.info.add({...})
|
||||||
|
// → service.project.info.update({...})
|
||||||
|
// → service.project.info.delete({ ids: [1] })
|
||||||
|
// → service.project.info.info({ id: 1 })
|
||||||
|
|
||||||
|
// Controller 文件: modules/project/controller/admin/task.ts
|
||||||
|
// → service.project.task.page({...})
|
||||||
|
|
||||||
|
// 自定义接口需要用 request:
|
||||||
|
// service.request({ url: '/admin/project/task/ganttData', params: { projectId: 1 } })
|
||||||
|
```
|
||||||
|
|
||||||
|
**重要:** service 路径由文件名决定,下划线文件名如 `time_log.ts` 对应 `service.project.time_log`(不是 timeLog)。
|
||||||
|
|
||||||
|
### 前端模块配置 (ModuleConfig)
|
||||||
|
|
||||||
|
每个前端模块必须有 `config.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export default (): ModuleConfig => {
|
||||||
|
return {
|
||||||
|
name: 'project',
|
||||||
|
label: '项目管理',
|
||||||
|
order: 50,
|
||||||
|
views: [
|
||||||
|
{
|
||||||
|
path: '/project/list',
|
||||||
|
meta: { label: '项目列表' },
|
||||||
|
component: () => import('./views/list.vue')
|
||||||
|
},
|
||||||
|
]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
## 菜单与权限系统
|
||||||
|
|
||||||
|
### 菜单配置 (base_sys_menu 表)
|
||||||
|
|
||||||
|
菜单通过数据库 `base_sys_menu` 表配置,**不在前端硬编码**。
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `parentId` | 父菜单ID(顶级为 null) |
|
||||||
|
| `name` | 菜单名称 |
|
||||||
|
| `router` | 路由路径(如 `/project/list`) |
|
||||||
|
| `viewPath` | Vue 组件路径(如 `modules/project/views/list.vue`) |
|
||||||
|
| `icon` | 图标 |
|
||||||
|
| `orderNum` | 排序号 |
|
||||||
|
| `type` | 0=目录, 1=菜单页面, 2=按钮权限 |
|
||||||
|
| `isShow` | 是否在侧边栏显示 |
|
||||||
|
| `keepAlive` | 是否缓存页面 |
|
||||||
|
| `perms` | 权限标识(按钮级,如 `project:info:add`) |
|
||||||
|
|
||||||
|
**新增菜单标准流程:**
|
||||||
|
```sql
|
||||||
|
-- 1. 新增目录
|
||||||
|
INSERT INTO base_sys_menu (parentId, name, icon, orderNum, type, isShow, createTime, updateTime)
|
||||||
|
VALUES (null, '项目管理', 'icon-project', 3, 0, 1, NOW(), NOW());
|
||||||
|
-- 记录返回的 id,假设为 158
|
||||||
|
|
||||||
|
-- 2. 新增菜单页面
|
||||||
|
INSERT INTO base_sys_menu (parentId, name, router, viewPath, orderNum, type, isShow, createTime, updateTime)
|
||||||
|
VALUES (158, '项目列表', '/project/list', 'modules/project/views/list.vue', 1, 1, 1, NOW(), NOW());
|
||||||
|
|
||||||
|
-- 3. 隐藏页面(详情页等)
|
||||||
|
INSERT INTO base_sys_menu (parentId, name, router, viewPath, orderNum, type, isShow, createTime, updateTime)
|
||||||
|
VALUES (158, '项目详情', '/project/detail', 'modules/project/views/detail.vue', 2, 1, 0, NOW(), NOW());
|
||||||
|
```
|
||||||
|
|
||||||
|
### 权限机制
|
||||||
|
|
||||||
|
- **admin 用户** 自动拥有所有权限,无需配置 role_menu 映射
|
||||||
|
- 普通用户通过 `base_sys_role_menu` 关联角色和菜单
|
||||||
|
- 权限中间件: JWT Token → 解析 userId → 查询权限列表 → 匹配请求 URL
|
||||||
|
- `/admin/*/comm/` 路径对所有登录用户开放
|
||||||
|
|
||||||
|
## 后端模块
|
||||||
|
|
||||||
|
| 模块 | 路径 | 用途 | 关键 Entity |
|
||||||
|
|------|------|------|------------|
|
||||||
|
| **base** | `modules/base/` | 用户、角色、菜单、权限 | BaseSysUser, BaseSysRole, BaseSysMenu |
|
||||||
|
| **netaclaw** | `modules/netaclaw/` | AI Agent 引擎 | NetaClawAgent, NetaClawSession, NetaClawMessage, NetaClawSkill, NetaClawModelChannel |
|
||||||
|
| **project** | `modules/project/` | 项目管理 | ProjectInfo, ProjectPhase, ProjectTask, ProjectTaskDependency, ProjectTimeLog |
|
||||||
|
| **data** | `modules/data/` | 药品目录数据 | DataDrugItem, DataCatalog, DataMedicalItem |
|
||||||
|
| **dict** | `modules/dict/` | 字典管理 | DictType, DictInfo |
|
||||||
|
| **task** | `modules/task/` | 定时任务 | TaskInfo, TaskLog |
|
||||||
|
| **space** | `modules/space/` | 文件空间 | SpaceInfo, SpaceType |
|
||||||
|
| **user** | `modules/user/` | 应用用户 | UserInfo, UserWx |
|
||||||
|
| **notification** | `modules/notification/` | 通知服务 | NotificationMessageLog |
|
||||||
|
| **plugin** | `modules/plugin/` | 插件系统 | PluginInfo |
|
||||||
|
| **recycle** | `modules/recycle/` | 回收站 | RecycleData |
|
||||||
|
| **demo** | `modules/demo/` | 演示 | DemoGoods |
|
||||||
|
| **swagger** | `modules/swagger/` | API 文档 | - |
|
||||||
|
|
||||||
|
## 前端模块
|
||||||
|
|
||||||
|
| 模块 | 路由前缀 | 用途 |
|
||||||
|
|------|---------|------|
|
||||||
|
| **base** | `/` | 登录(Canvas星河动画)、用户、角色、菜单、部门 |
|
||||||
|
| **agent** | `/agent/*` | Agent 对话(WebSocket)、Skill 管理、模型渠道管理 |
|
||||||
|
| **project** | `/project/*` | 项目管理(甘特图/日历/看板/列表) |
|
||||||
|
| **data** | `/data/*` | 药品目录数据管理 |
|
||||||
|
| **dict** | `/dict/*` | 字典管理 |
|
||||||
|
| **task** | `/task/*` | 定时任务 |
|
||||||
|
| **space** | `/space/*` | 文件空间 |
|
||||||
|
| **user** | `/user/*` | 应用用户 |
|
||||||
|
|
||||||
|
## 开发规范
|
||||||
|
|
||||||
|
- **后端文件名**: 下划线法 (`model_channel.ts`)
|
||||||
|
- **Entity 字段**: 驼峰法 (`modelConfig`)
|
||||||
|
- **注释**: 中文
|
||||||
|
- **Entity**: 继承 BaseEntity,不使用外键
|
||||||
|
- **Controller**: 使用 `@CoolController` 自动生成 CRUD
|
||||||
|
- **包管理器**: 必须使用 pnpm
|
||||||
|
- **数据库**: 开发环境 `synchronize: true` 自动建表,生产环境禁止
|
||||||
|
|
||||||
|
## 环境变量
|
||||||
|
|
||||||
|
```env
|
||||||
|
NODE_ENV=development
|
||||||
|
BACKEND_PORT=8003
|
||||||
|
DB_HOST=120.48.5.80
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_DATABASE=neta_test
|
||||||
|
```
|
||||||
83
README.md
Normal file
83
README.md
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
# Neta AI电商 - AI驱动的电商自动化运营平台
|
||||||
|
|
||||||
|
> 基于 Midway.js + Vue 3 + NetaClaw AI引擎 的智能电商运营平台,Monorepo 架构
|
||||||
|
|
||||||
|
## 技术架构
|
||||||
|
|
||||||
|
| 层级 | 技术选型 | 端口 |
|
||||||
|
|------|---------|------|
|
||||||
|
| **后端** | Midway.js 3.20 + TypeScript 5.9 + TypeORM + Cool Admin | 8003 |
|
||||||
|
| **前端** | Vue 3.5 + Vite 5.4 + Element Plus 2.9 + Pinia | 9001 |
|
||||||
|
| **AI 引擎** | NetaClaw (ReAct 循环 + TypeBox Schema + WebSocket) | - |
|
||||||
|
| **数据库** | MySQL 8.0+ | 3306 |
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
Neta-monorepo/
|
||||||
|
├── packages/
|
||||||
|
│ ├── backend/ # Midway.js 后端 (Cool Admin 框架)
|
||||||
|
│ ├── frontend/ # Vue 3 前端 (Cool Admin UI)
|
||||||
|
│ └── shared/ # 共享 TypeScript 类型
|
||||||
|
├── docs/ # 设计文档与实施计划
|
||||||
|
├── pnpm-workspace.yaml
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 环境要求
|
||||||
|
|
||||||
|
- **Node.js**: >= 24.0.0
|
||||||
|
- **pnpm**: >= 8.0.0
|
||||||
|
- **MySQL**: >= 8.0.0
|
||||||
|
|
||||||
|
### 安装与启动
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# 同时启动后端 + 前端
|
||||||
|
pnpm dev
|
||||||
|
|
||||||
|
# 单独启动
|
||||||
|
pnpm dev:backend # 后端 http://localhost:8003
|
||||||
|
pnpm dev:frontend # 前端 http://localhost:9001
|
||||||
|
```
|
||||||
|
|
||||||
|
## 核心模块
|
||||||
|
|
||||||
|
### 后端模块
|
||||||
|
|
||||||
|
| 模块 | 路径 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| **base** | `modules/base/` | 用户、角色、菜单、权限 (RBAC) |
|
||||||
|
| **netaclaw** | `modules/netaclaw/` | AI Agent 引擎 (ReAct + WebSocket) |
|
||||||
|
| **dict** | `modules/dict/` | 字典管理 |
|
||||||
|
| **task** | `modules/task/` | 定时任务 |
|
||||||
|
| **space** | `modules/space/` | 文件空间 |
|
||||||
|
| **user** | `modules/user/` | 应用用户 |
|
||||||
|
|
||||||
|
### 前端模块
|
||||||
|
|
||||||
|
| 模块 | 路由前缀 | 用途 |
|
||||||
|
|------|---------|------|
|
||||||
|
| **agent** | `/agent/*` | Agent 对话、Skill 管理、模型渠道管理 |
|
||||||
|
| **base** | `/` | 登录、用户、角色、菜单、部门 |
|
||||||
|
|
||||||
|
## 数据库
|
||||||
|
|
||||||
|
后端配置位于 `packages/backend/src/config/`:
|
||||||
|
- 开发环境: `config.local.ts`
|
||||||
|
- 生产环境: `config.prod.ts`
|
||||||
|
|
||||||
|
数据库名: `cpu_guard`,TypeORM 开发环境自动建表 (`synchronize: true`)。
|
||||||
|
|
||||||
|
## 开发规范
|
||||||
|
|
||||||
|
- **后端文件名**: 下划线法 (`model_channel.ts`)
|
||||||
|
- **Entity 字段**: 驼峰法 (`modelConfig`)
|
||||||
|
- **注释**: 中文
|
||||||
|
- **Entity**: 继承 BaseEntity,不使用外键
|
||||||
|
- **Controller**: 使用 `@CoolController` 自动生成 CRUD
|
||||||
|
- **包管理器**: 必须使用 pnpm
|
||||||
114
docs/code-wiki/SCHEMA.md
Normal file
114
docs/code-wiki/SCHEMA.md
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
---
|
||||||
|
title: Wiki Schema
|
||||||
|
created: 2026-04-13
|
||||||
|
updated: 2026-04-13
|
||||||
|
---
|
||||||
|
|
||||||
|
# Wiki Schema
|
||||||
|
|
||||||
|
## 领域
|
||||||
|
|
||||||
|
Neta AI 电商平台代码知识库 — 覆盖 Neta-monorepo 项目的架构设计、模块职责、数据流、技术选型和开发规范。项目基于 Midway.js + Vue 3 的 Monorepo 架构,核心是 NetaClaw AI Agent 引擎。
|
||||||
|
|
||||||
|
## 约定
|
||||||
|
|
||||||
|
- 文件名:小写、连字符、无空格(如 `netaclaw-agent-runtime.md`)
|
||||||
|
- 所有 wiki 页面以 YAML frontmatter 开头(见下方模板)
|
||||||
|
- 页面之间使用 `[[wikilinks]]` 互相链接(每页至少 2 个出站链接)
|
||||||
|
- 更新页面时必须更新 `updated` 日期
|
||||||
|
- 每个新页面必须添加到 `index.md` 对应分区下
|
||||||
|
- 每次操作必须追加到 `log.md`
|
||||||
|
- 所有内容使用中文编写
|
||||||
|
|
||||||
|
## Frontmatter 模板
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
title: 页面标题
|
||||||
|
created: YYYY-MM-DD
|
||||||
|
updated: YYYY-MM-DD
|
||||||
|
type: entity | concept | comparison | query
|
||||||
|
tags: [从下方标签体系中选择]
|
||||||
|
sources: [代码路径或参考来源]
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
## 标签体系
|
||||||
|
|
||||||
|
新增标签前必须先添加到此处,禁止随意创建标签。
|
||||||
|
|
||||||
|
### 架构层
|
||||||
|
- `architecture`: 系统架构、整体设计
|
||||||
|
- `module`: 业务模块
|
||||||
|
- `runtime`: 运行时核心逻辑
|
||||||
|
- `gateway`: 网关/通信层
|
||||||
|
|
||||||
|
### 技术层
|
||||||
|
- `backend`: 后端相关
|
||||||
|
- `frontend`: 前端相关
|
||||||
|
- `database`: 数据库/Entity
|
||||||
|
- `api`: 接口/协议
|
||||||
|
- `websocket`: 实时通信
|
||||||
|
- `auth`: 认证/权限
|
||||||
|
|
||||||
|
### AI 层
|
||||||
|
- `agent`: AI Agent 相关
|
||||||
|
- `llm`: 大语言模型
|
||||||
|
- `tool`: 工具系统
|
||||||
|
- `skill`: 技能系统
|
||||||
|
- `memory`: 记忆系统
|
||||||
|
|
||||||
|
### 业务层
|
||||||
|
- `project`: 项目管理
|
||||||
|
- `data`: 数据管理(药品/医保)
|
||||||
|
- `user`: 用户系统
|
||||||
|
- `dict`: 字典/配置
|
||||||
|
- `notification`: 通知
|
||||||
|
- `plugin`: 插件
|
||||||
|
|
||||||
|
### 开发层
|
||||||
|
- `convention`: 开发规范/约定
|
||||||
|
- `config`: 配置相关
|
||||||
|
- `deploy`: 部署/构建
|
||||||
|
- `tech-stack`: 技术栈选型
|
||||||
|
|
||||||
|
## 页面阈值
|
||||||
|
|
||||||
|
- **创建页面**:当一个实体/概念在项目中承担独立职责(独立的模块、服务、核心流程)
|
||||||
|
- **添加到已有页面**:当信息是某个已有实体的补充细节
|
||||||
|
- **不创建页面**:工具函数、辅助类、临时逻辑、与领域无关的内容
|
||||||
|
- **拆分页面**:超过 200 行时,按子主题拆分并用 wikilink 互联
|
||||||
|
- **归档页面**:代码已删除或完全重构后,移到 `_archive/`,从 index 移除
|
||||||
|
|
||||||
|
## Entity 页面规范
|
||||||
|
|
||||||
|
每个关键模块/服务/组件一个页面,包含:
|
||||||
|
- 概述 / 职责
|
||||||
|
- 目录结构和关键文件路径
|
||||||
|
- 核心 API / 接口
|
||||||
|
- 与其他模块的关系([[wikilinks]])
|
||||||
|
- 数据模型(如有 Entity)
|
||||||
|
|
||||||
|
## Concept 页面规范
|
||||||
|
|
||||||
|
每个架构模式/数据流/设计决策一个页面,包含:
|
||||||
|
- 定义 / 解释
|
||||||
|
- 工作流程(文字或图表)
|
||||||
|
- 关键代码路径
|
||||||
|
- 相关概念([[wikilinks]])
|
||||||
|
|
||||||
|
## Comparison 页面规范
|
||||||
|
|
||||||
|
并列分析,包含:
|
||||||
|
- 对比什么、为什么对比
|
||||||
|
- 对比维度(表格形式优先)
|
||||||
|
- 结论或综合判断
|
||||||
|
- 来源
|
||||||
|
|
||||||
|
## 更新策略
|
||||||
|
|
||||||
|
当代码变更导致 wiki 内容过时时:
|
||||||
|
1. 检查 git log 确认变更时间
|
||||||
|
2. 更新页面内容,标注变更日期
|
||||||
|
3. 如果是破坏性变更,标注 `breaking: true` 到 frontmatter
|
||||||
|
4. 在 lint 报告中标记需要用户确认的内容
|
||||||
121
docs/code-wiki/comparisons/database-entity-overview.md
Normal file
121
docs/code-wiki/comparisons/database-entity-overview.md
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
---
|
||||||
|
title: 数据库 Entity 全景
|
||||||
|
created: 2026-04-13
|
||||||
|
updated: 2026-05-15
|
||||||
|
type: comparison
|
||||||
|
tags: [database, architecture, module]
|
||||||
|
sources: [packages/backend/src/entities.ts, packages/backend/src/modules/]
|
||||||
|
---
|
||||||
|
|
||||||
|
# 数据库 Entity 全景
|
||||||
|
|
||||||
|
全部 50+ 个 Entity 按模块分组对比,快速定位数据表。2026-05-14 后新增 [[desktop-op-module]] 的配置/审计表,并为 [[agent-channel]] 增加群白名单表。2026-05-15 后,[[netaclaw-module]] 增加 MySQL 问数数据源和查询审计表。
|
||||||
|
|
||||||
|
## 按模块分布
|
||||||
|
|
||||||
|
| 模块 | 表数量 | 核心表 |
|
||||||
|
|------|--------|--------|
|
||||||
|
| base | 11 | user, role, menu, department |
|
||||||
|
| netaclaw | 19 | agent, session, message, skill, model_channel, memory, memory_type, tool, subagent_session, crew, crew_agent, crew_run, crew_task, agent_channel, agent_channel_group, agent_session, agent_session_entry, data_source, data_source_query_audit |
|
||||||
|
| desktop_op | 2 | config, action_log |
|
||||||
|
| geo | 2 | account, proxy_ip |
|
||||||
|
| project | 5 | info, phase, task, task_dependency, time_log |
|
||||||
|
| data | 6 | drug_item, catalog, category, medical_item, medical_catalog, medical_category |
|
||||||
|
| user | 3 | info, wx, address |
|
||||||
|
| dict | 2 | type, info |
|
||||||
|
| task | 2 | info, log |
|
||||||
|
| space | 2 | info, type |
|
||||||
|
| notification | 2 | message_log, user |
|
||||||
|
| plugin | 1 | info |
|
||||||
|
| demo | 1 | goods |
|
||||||
|
| recycle | 1 | data |
|
||||||
|
|
||||||
|
## NetaClaw 核心表字段速查
|
||||||
|
|
||||||
|
### netaclaw_agent
|
||||||
|
`id` | `name`(唯一) | `label` | `description` | `icon` | `systemPrompt` | `skills`(JSON数组) | `toolsets`(JSON数组) | `tools`(JSON: inheritCoreTools/enabled/disabled) | `subagentConfig`(JSON) | `modelConfig`(JSON) | `config`(JSON) | `auxiliaryModelChannelId` | `auxiliaryModelId` | `compactionThreshold` | `compactionKeepRecent` | `status`
|
||||||
|
|
||||||
|
### netaclaw_session
|
||||||
|
`id` | `sessionId`(唯一) | `userId` | `agentName` | `agentId` | `title` | `model` | `status` | `metadata`
|
||||||
|
|
||||||
|
### netaclaw_message
|
||||||
|
`id` | `sessionId` | `role`(user/assistant/tool/system) | `content` | `thinking` | `toolCalls`(JSON) | `toolCallId` | `skillName` | `metadata` | `compactedAt` | `compactedIntoId` | `isCompactionSummary`
|
||||||
|
|
||||||
|
### netaclaw_skill
|
||||||
|
`id` | `name`(唯一) | `label` | `description` | `skillType` | `tags`(JSON) | `status` | `version` | `source` | `sourceUrl` | `installSpec`(JSON) | `metadata`(JSON) | `fingerprint` | `installedAt` | `secrets`(AES-256-GCM密文) | `envSchema`(JSON)
|
||||||
|
|
||||||
|
### netaclaw_model_channel
|
||||||
|
`id` | `name`(唯一) | `supplier` | `baseUrl` | `apiKey` | `models`(JSON) | `description` | `isAuxiliary` | `status`
|
||||||
|
|
||||||
|
### netaclaw_memory
|
||||||
|
`id` | `agentName` | `userId` | `type`(user/project/feedback/reference/自定义) | `name` | `content` | `description` | `metadata`
|
||||||
|
|
||||||
|
### netaclaw_memory_type
|
||||||
|
`id` | `key`(唯一) | `name` | `description` | `icon` | `isSystem`
|
||||||
|
|
||||||
|
### netaclaw_tool
|
||||||
|
`id` | `name`(唯一) | `label` | `toolset` | `description` | `visibility` | `capability` | `status` | `isCore` | `canDisable` | `supportsPromptHint` | `promptHint` | `sort` | `extra`
|
||||||
|
|
||||||
|
### netaclaw_subagent_session
|
||||||
|
`id` | `sessionId` | `parentMessageId` | `parentToolCallId` | `parentAgentId` | `sourceType` | `presetAgentId` | `name` | `goal` | `context` | `status` | `model` | `toolNames`(JSON) | `summary` | `resultPayload`(JSON) | `error` | `tokenUsage`(JSON) | `sortOrder` | `startedAt` | `endedAt`
|
||||||
|
|
||||||
|
### netaclaw_crew
|
||||||
|
`id` | `name`(唯一) | `label` | `masterAgentId` | `canvasData`(JSON) | `triggerConfig`(JSON) | `delegateHints` | `status` | `maxConcurrent` | `taskTimeout` | `retryPolicy`(JSON)
|
||||||
|
|
||||||
|
### netaclaw_crew_agent
|
||||||
|
`id` | `crewId` | `agentId` | `role` | `canvasPosition`(JSON) | `groupName`
|
||||||
|
|
||||||
|
### netaclaw_crew_run
|
||||||
|
`id` | `crewId` | `triggerType` | `triggerInput`(JSON) | `status` | `masterSessionId` | `startTime` | `endTime` | `result`(JSON) | `error` | `pausedState`(JSON) | `tokenUsage`(JSON)
|
||||||
|
|
||||||
|
### netaclaw_crew_task
|
||||||
|
`id` | `runId` | `agentId` | `taskDescription` | `status` | `sessionId` | `startTime` | `endTime` | `result` | `error` | `retryCount` | `timeout` | `tokenUsage`(JSON) | `parentTaskId`
|
||||||
|
|
||||||
|
### netaclaw_agent_channel
|
||||||
|
`id` | `name`(唯一) | `type`(weixin/weixin-db) | `agentId` | `agentName` | `config`(JSON,含 group/weixinReply) | `credential`(JSON,含 wxid/wechatVersion) | `runtime`(JSON) | `loginStatus` | `lastError` | `lastConnectedAt`
|
||||||
|
|
||||||
|
### netaclaw_agent_channel_group
|
||||||
|
`id` | `channelId` | `roomId`(channel内唯一) | `roomName` | `status` | `triggerMode` | `triggerPrefix` | `boundAgentId`(weixin-db 群级必填) | `replyIdentityOverride`(weixin-db 群级必填) | `firstSeenAt` | `lastSeenAt` | `lastActiveAt`
|
||||||
|
|
||||||
|
### netaclaw_data_source
|
||||||
|
`id` | `name`(唯一) | `label` | `type`(mysql) | `host` | `port` | `database` | `username` | `passwordEncrypted`(AES-256-GCM密文) | `readonly` | `status` | `allowedAgentIds`(JSON) | `extra`(JSON: allowedTables/blockedTables/maskedColumns/schemaVisibility/maxRows/maxJoinTables/queryTimeoutMs/connectTimeout/poolConnectionLimit/SSL)
|
||||||
|
|
||||||
|
### netaclaw_data_source_query_audit
|
||||||
|
`id` | `dataSourceId` | `agentId` | `userId` | `toolCallId` | `sqlHash` | `sqlPreview` | `status`(success/rejected/failed) | `rejectReason` | `elapsedMs` | `rowCount` | `errorCode`
|
||||||
|
|
||||||
|
## Desktop Op 核心表字段速查
|
||||||
|
|
||||||
|
### desktop_op_config
|
||||||
|
`id` | `allowedApps`(JSON) | `extraDangerousKeys`(JSON) | `globalPerMin` | `globalPerDay` | `defaultWatermark`
|
||||||
|
|
||||||
|
### desktop_op_action_log
|
||||||
|
`id` | `taskId` | `appId` | `targetJson` | `actionType` | `paramsPreview` | `finalText` | `channelId` | `roomName` | `modelChannelId` | `modelCalls` | `steps` | `durationMs` | `status` | `error` | `abortedReason`
|
||||||
|
|
||||||
|
## Geo 核心表字段速查
|
||||||
|
|
||||||
|
### geo_account
|
||||||
|
`id` | `name` | `platform` | `loginAccount` | `sessionName`(唯一) | `agentId` | `fingerprintSeed` | `cookies` | `cookieCapturedAt` | `cookieExpiresAt` | `loginStatus` | `proxyId` | `lastActiveAt` | `extra`
|
||||||
|
|
||||||
|
### geo_proxy_ip
|
||||||
|
`id` | `name` | `provider` | `mode` | `host` | `port` | `protocol` | `username` | `password` | `region` | `isp` | `exitIp` | `city` | `packageId` | `externalId` | `bindAccountId`(唯一) | `status` | `latencyMs` | `lastCheckAt` | `expiresAt` | `extra`
|
||||||
|
|
||||||
|
## 通用字段(BaseEntity)
|
||||||
|
|
||||||
|
所有表继承:`id` | `createTime` | `updateTime` | `tenantId`
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [[netaclaw-module]] — NetaClaw 19 个核心表
|
||||||
|
- [[mysql-data-source]] — netaclaw_data_source 与 netaclaw_data_source_query_audit
|
||||||
|
- [[memory-system]] — netaclaw_memory 与 netaclaw_memory_type
|
||||||
|
- [[tool-governance]] — netaclaw_tool 治理表
|
||||||
|
- [[skill-runtime]] — netaclaw_skill 的 secrets 与 envSchema
|
||||||
|
- [[subagent-session]] — netaclaw_subagent_session 运行记录
|
||||||
|
- [[context-compaction]] — netaclaw_message 的压缩字段
|
||||||
|
- [[crew-orchestration]] — Crew 编排 4 个表
|
||||||
|
- [[agent-channel]] — 渠道配置表
|
||||||
|
- [[desktop-op-module]] — desktop_op_config 与 desktop_op_action_log
|
||||||
|
- [[geo-module]] — geo_account 与 geo_proxy_ip
|
||||||
|
- [[base-module]] — Base 11 个系统表
|
||||||
|
- [[project-module]] — Project 5 个业务表
|
||||||
|
- [[cool-admin-framework]] — BaseEntity 和自动 CRUD
|
||||||
94
docs/code-wiki/concepts/context-compaction.md
Normal file
94
docs/code-wiki/concepts/context-compaction.md
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
---
|
||||||
|
title: 上下文压缩与历史视图
|
||||||
|
created: 2026-04-19
|
||||||
|
updated: 2026-04-23
|
||||||
|
type: concept
|
||||||
|
tags: [runtime, agent, api, backend]
|
||||||
|
sources: [packages/backend/src/modules/netaclaw/runtime/compaction.ts, packages/backend/src/modules/netaclaw/session-tree/context_builder.ts, packages/backend/src/modules/netaclaw/session-tree/types.ts, packages/backend/src/modules/netaclaw/service/chat_orchestrator.ts, packages/frontend/src/modules/agent/store/chat.ts, packages/frontend/src/modules/agent/views/chat.vue]
|
||||||
|
---
|
||||||
|
|
||||||
|
# 上下文压缩与历史视图
|
||||||
|
|
||||||
|
上下文压缩用于长对话的 token 控制。早期实现更接近在线性消息表上标记 `compactedAt` 和摘要消息;当前架构下,压缩结果应进入 [[session-tree-runtime]],成为 `compaction` 节点,并由 active path 决定是否参与模型上下文。
|
||||||
|
|
||||||
|
## 核心目标
|
||||||
|
|
||||||
|
- 控制模型上下文窗口。
|
||||||
|
- 保留长会话的可读摘要。
|
||||||
|
- 避免切断 assistant/tool 成对消息。
|
||||||
|
- 支持前端在历史中展示压缩事件。
|
||||||
|
- 与分支、子 Agent 批次、模型切换共享同一会话树。
|
||||||
|
|
||||||
|
## 运行流程
|
||||||
|
|
||||||
|
```text
|
||||||
|
ChatOrchestrator 准备运行
|
||||||
|
-> SessionTreeContextBuilder 读取 active path
|
||||||
|
-> 估算上下文 token
|
||||||
|
-> 达到阈值时触发 compaction
|
||||||
|
-> CompactionService 生成摘要
|
||||||
|
-> appendCompaction 写入 session tree
|
||||||
|
-> runtimeContext 使用摘要后的上下文
|
||||||
|
-> 前端 snapshot/patch 展示 compaction 节点
|
||||||
|
```
|
||||||
|
|
||||||
|
## 与 Session Tree 的关系
|
||||||
|
|
||||||
|
压缩不应只作为消息 metadata 存在。新的表达方式是:
|
||||||
|
|
||||||
|
- `compaction` entry 保存摘要、`firstKeptEntryId`、`tokensBefore`、details。
|
||||||
|
- `activePath` 决定压缩摘要是否对当前分支生效。
|
||||||
|
- `runtimeContext.messages` 由 `context_builder.ts` 构造,负责把压缩节点转成模型可消费消息。
|
||||||
|
- 前端 `visibleEntries` 把压缩节点渲染为会话中的结构化提示。
|
||||||
|
|
||||||
|
这让压缩和分支天然兼容。
|
||||||
|
|
||||||
|
## 手动与自动触发
|
||||||
|
|
||||||
|
手动触发仍可通过对话命令或 UI 入口发起。自动触发由 Agent 配置中的上下文阈值控制,运行前由后端判断是否需要压缩。
|
||||||
|
|
||||||
|
配置页面需要注意:
|
||||||
|
|
||||||
|
- 压缩阈值是 Agent 运行配置的一部分。
|
||||||
|
- 辅助模型配置会影响压缩质量和成本。
|
||||||
|
- 压缩结果在刷新后应从 session tree 恢复。
|
||||||
|
|
||||||
|
当前还要注意一个过渡事实:
|
||||||
|
|
||||||
|
- legacy `netaclaw_message.compactedAt` 相关逻辑仍存在,主要用于兼容旧历史与压缩服务本身。
|
||||||
|
- 但 Agent Chat 的主展示和主恢复路径已经切到 Session Tree,压缩节点应该被理解为 `compaction entry`,不是“数据库里某几条 message 被隐藏了”这么简单。
|
||||||
|
|
||||||
|
## 前端展示
|
||||||
|
|
||||||
|
Agent 对话页需要展示:
|
||||||
|
|
||||||
|
- 压缩开始、完成、失败状态。
|
||||||
|
- 摘要内容。
|
||||||
|
- 压缩前 token 数和保留边界。
|
||||||
|
- 当前是否处于压缩后的 active path。
|
||||||
|
|
||||||
|
前端不应自行计算“哪些历史已经压缩”,而应依赖后端 snapshot 和 compaction entry。
|
||||||
|
|
||||||
|
## 与子 Agent 的关系
|
||||||
|
|
||||||
|
子 Agent 批次也写入会话树,因此压缩时需要明确哪些批次节点进入摘要、哪些仅用于 UI 展示。当前建议:
|
||||||
|
|
||||||
|
- `subagent_batch` 和 `subagent_result` 可作为可见节点保留。
|
||||||
|
- 进入模型上下文时使用其摘要信息,而不是完整工具日志。
|
||||||
|
- 压缩摘要应保留子任务结论和影响主任务的关键事实。
|
||||||
|
|
||||||
|
随着子 Agent 回放投影落地,这里的边界也更清楚:
|
||||||
|
|
||||||
|
- evidence summary、process replay、tool execution 主要属于 UI/回放层。
|
||||||
|
- 真正进入压缩摘要和后续 prompt 的,应该是对主任务有语义价值的结论,而不是完整过程事件。
|
||||||
|
|
||||||
|
详见 [[subagent-session]]。
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [[agent-runtime]]
|
||||||
|
- [[session-tree-runtime]]
|
||||||
|
- [[subagent-session]]
|
||||||
|
- [[frontend-architecture]]
|
||||||
|
- [[websocket-gateway]]
|
||||||
|
- [[prompt-builder]]
|
||||||
51
docs/code-wiki/concepts/development-conventions.md
Normal file
51
docs/code-wiki/concepts/development-conventions.md
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
title: 开发规范
|
||||||
|
created: 2026-04-13
|
||||||
|
updated: 2026-04-13
|
||||||
|
type: concept
|
||||||
|
tags: [convention, backend, frontend, database]
|
||||||
|
sources: [CLAUDE.md]
|
||||||
|
---
|
||||||
|
|
||||||
|
# 开发规范
|
||||||
|
|
||||||
|
项目统一的编码约定和开发流程。
|
||||||
|
|
||||||
|
## 后端规范
|
||||||
|
|
||||||
|
| 规则 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 文件名 | 下划线法:`model_channel.ts`、`time_log.ts` |
|
||||||
|
| Entity 字段 | 驼峰法:`sessionId`、`createTime` |
|
||||||
|
| Controller | 使用 `@CoolController` 自动生成 CRUD |
|
||||||
|
| 业务逻辑 | 放 Service 层,不在 Controller |
|
||||||
|
| 响应格式 | `this.ok(data)` 或 `this.fail('message')` |
|
||||||
|
| API 路径 | `POST /admin/{module}/{entity}/{action}` |
|
||||||
|
| Entity 注册 | 在 `src/entities.ts` 中添加 import |
|
||||||
|
|
||||||
|
## 前端规范
|
||||||
|
|
||||||
|
| 规则 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 模块配置 | 必须有 `config.ts` 导出 `ModuleConfig` |
|
||||||
|
| Service 调用 | 使用 `useCool()` 的 service 代理 |
|
||||||
|
| 组件风格 | Vue 3 Composition API + `<script lang="ts" setup>` |
|
||||||
|
| 样式 | Sass scoped |
|
||||||
|
| 菜单 | 通过 `base_sys_menu` 表配置,不硬编码 |
|
||||||
|
| 路由 | 动态路由由菜单驱动 |
|
||||||
|
|
||||||
|
## 数据库规范
|
||||||
|
|
||||||
|
| 规则 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 自动建表 | 开发环境 `synchronize: true`,生产禁用 |
|
||||||
|
| 索引 | 关键查询字段加 `@Index()` |
|
||||||
|
| JSON 字段 | 用 `type: 'json'` 存储配置和元数据 |
|
||||||
|
| 多租户 | 自动填充 `tenantId`(来自 JWT) |
|
||||||
|
| 时间戳 | 自动填充 `createTime`、`updateTime` |
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [[cool-admin-framework]] — 框架约定
|
||||||
|
- [[project-overview]] — 项目总览
|
||||||
|
- [[frontend-architecture]] — 前端架构
|
||||||
193
docs/code-wiki/concepts/frontend-architecture.md
Normal file
193
docs/code-wiki/concepts/frontend-architecture.md
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
---
|
||||||
|
title: 前端架构
|
||||||
|
created: 2026-04-13
|
||||||
|
updated: 2026-05-15
|
||||||
|
type: concept
|
||||||
|
tags: [frontend, architecture, convention, agent]
|
||||||
|
sources: [packages/frontend/src/, packages/frontend/src/modules/agent/store/chat.ts, packages/frontend/src/modules/agent/views/chat.vue, packages/frontend/src/modules/agent/components/chat/ChatComposer.vue, packages/frontend/src/modules/agent/views/memory.vue, packages/frontend/src/modules/agent/views/agent-edit.vue, packages/frontend/src/modules/agent/views/tools.vue, packages/frontend/src/modules/agent/views/skills.vue, packages/frontend/src/modules/agent/views/channel-management.vue, packages/frontend/src/modules/agent/components/channel-group-panel.vue, packages/frontend/src/modules/agent/tools/renderer-registry.ts, packages/frontend/src/modules/agent/components/tool-process-timeline.vue, packages/frontend/src/modules/agent/utils/tool-process-display.ts, packages/frontend/src/modules/geo/]
|
||||||
|
---
|
||||||
|
|
||||||
|
# 前端架构
|
||||||
|
|
||||||
|
前端基于 Vue 3 + Vite + Element Plus + Cool Admin。Agent 模块在近期重构后,重点从“普通聊天页面”升级为“Session Tree 快照消费 + 工具治理 projection + 子 Agent runtime 可视化”的管理和运行界面。
|
||||||
|
|
||||||
|
## 模块结构
|
||||||
|
|
||||||
|
业务模块仍遵循 Cool Admin 的模块化结构:
|
||||||
|
|
||||||
|
```text
|
||||||
|
modules/{name}/
|
||||||
|
config.ts
|
||||||
|
views/
|
||||||
|
components/
|
||||||
|
store/
|
||||||
|
hooks/
|
||||||
|
types/
|
||||||
|
static/
|
||||||
|
```
|
||||||
|
|
||||||
|
Agent 相关代码集中在 `packages/frontend/src/modules/agent/`。
|
||||||
|
|
||||||
|
## Agent Chat Store
|
||||||
|
|
||||||
|
`modules/agent/store/chat.ts` 是对话页状态核心。当前它不再只维护线性 message 数组,而是消费 [[session-tree-runtime]] 的 snapshot:
|
||||||
|
|
||||||
|
- `sessionMeta`: 当前 session 元信息。
|
||||||
|
- `entries`: session tree 节点列表。
|
||||||
|
- `entryById`: 节点索引。
|
||||||
|
- `childrenByParentId`: 父子索引。
|
||||||
|
- `activePathIds`: 当前活动路径。
|
||||||
|
- `visibleEntries`: 对话 UI 可展示节点。
|
||||||
|
- `subagentRuntimeByBatchId`: 子 Agent 批次运行状态。
|
||||||
|
- `toolRuntimeRoutesByBatchId`: 子 Agent 工具路由状态。
|
||||||
|
- `selectedEntryId`: 当前树面板选中的节点。
|
||||||
|
- `switchingLeafEntryId`: 正在切换为 leaf 的目标节点。
|
||||||
|
- `pendingLeafConfirmation`: 已选中但尚未真正切换的继续发送目标。
|
||||||
|
- `subagentEvidenceSummariesByEntryId`、`subagentProcessEventsByEntryId`、`subagentTaskPanelsByEntryId`、`subagentProjectionDiagnosticsByEntryId`: 统一消费后端 `subagentProjection` 后得到的展示态索引。
|
||||||
|
- 删除会话时,前端会优先从会话列表中取该会话自己的 `agentId`,再调用 `/open/netaclaw/session/delete`,避免 MySQL session-tree 会话因当前选中 Agent 不一致而走错 provider。
|
||||||
|
|
||||||
|
localStorage 只保存:
|
||||||
|
|
||||||
|
- `neta:agent-chat:last-tree-session`
|
||||||
|
- `neta:agent-chat:last-agent`
|
||||||
|
|
||||||
|
它不是完整历史存储。刷新后历史恢复依赖后端 session tree snapshot。
|
||||||
|
|
||||||
|
## Agent 对话页
|
||||||
|
|
||||||
|
`views/chat.vue` 的职责:
|
||||||
|
|
||||||
|
- 加载或恢复最近 Agent/session。
|
||||||
|
- 渲染普通消息、压缩摘要、分支摘要、自定义消息、子 Agent 批次和结果。
|
||||||
|
- 展示 token/thinking/tool 调用过程。
|
||||||
|
- 保证长对话区域可滚动,避免消息增多后底部不可见。
|
||||||
|
- 消费后端工具路由和 blocked reason,而不是在前端重新推导。
|
||||||
|
- 提供会话树节点选择、continue-from-entry、分支继续发送和 pending guard 交互。
|
||||||
|
- 对 `subagent_result` 节点优先渲染后端生成的 task panel、tool execution、evidence card、process timeline 和 projection diagnostics。
|
||||||
|
- 对 `subagent_batch` 节点只展示批次运行态和工具路由概览,不把它当作唯一回放来源。
|
||||||
|
- 流式工具卡片会保留工具参数和结构化 `rawResult`,例如 bash 工具会把命令展示在执行卡片里,流结束后仍能从 `skillExecutions` metadata 恢复。
|
||||||
|
- 当后端推送 `tool_confirmation_request` 时,`ChatComposer.vue` 会把输入区临时切换为工具确认 UI,显示工具名、命令/参数和风险原因;用户点击允许或拒绝后发送 `tool_confirmation_response`。这条链路主要保护 bash 删除、强制 git、数据库清空等高风险操作。
|
||||||
|
|
||||||
|
工具执行过程中的乱码问题通常来自命令执行 shell/编码和后端 stderr/stdout 处理;前端应只负责正确展示 UTF-8 文本和结构化结果。
|
||||||
|
|
||||||
|
## 工具管理页
|
||||||
|
|
||||||
|
`views/tools.vue` 面向全局 [[tool-governance]]:
|
||||||
|
|
||||||
|
- 查看工具 catalog 和 DB 同步结果。
|
||||||
|
- 调整全局启停、核心工具、是否可关闭、Prompt Hint、排序。
|
||||||
|
- 展示 `effectiveRuntimeProfile`、worker routing、blocked reason。
|
||||||
|
- 页面必须有可滚动容器,工具数量增加后不能裁掉底部内容。
|
||||||
|
|
||||||
|
## 记忆管理页
|
||||||
|
|
||||||
|
`views/memory.vue` 面向 [[memory-system]]:
|
||||||
|
|
||||||
|
- 左侧按 Agent 统计记忆数量,并标识 MySQL/SQLite 后端。
|
||||||
|
- 右侧支持 Agent、类型、关键词筛选和分页。
|
||||||
|
- 新增/编辑记忆时可写入 JSON metadata,编辑提交带 `updatedAt` 乐观锁。
|
||||||
|
- 类型管理弹窗读取 `memory_type` 服务,系统内置类型不可删除。
|
||||||
|
- Agent 下拉使用 `agent/page` 返回的 label,避免展示内部 name。
|
||||||
|
|
||||||
|
## Agent 编辑页
|
||||||
|
|
||||||
|
`views/agent-edit.vue` 面向单个 Agent 配置:
|
||||||
|
|
||||||
|
- 配置模型、Prompt、上下文压缩、辅助模型。
|
||||||
|
- 配置工具局部启停。
|
||||||
|
- 配置子 Agent 开关、并发、可用预设 Agent、可用工具。
|
||||||
|
- 展示后端 projection 中的 effective runtime profile 和 effective subagent allowed。
|
||||||
|
- 保存本地存储/workspace/shell/readonly 等配置时,应影响 [[tool-runtime-policy]] 推导。
|
||||||
|
|
||||||
|
Agent 编辑页不能只接入 i18n;核心是把后端新的工具治理和子 Agent 策略完整接进配置面。
|
||||||
|
|
||||||
|
## Skill 管理页
|
||||||
|
|
||||||
|
`views/skills.vue` 和 `components/skill-detail.vue` 已从简单列表升级为 [[skill-runtime]] 的管理入口:
|
||||||
|
|
||||||
|
- Skill 卡片展示 `classification`:`prompt`、`compute-entry`、`compute-toolkit`。
|
||||||
|
- 顶部加载 `/admin/netaclaw/skill/diagnostics`,按 error / warning 显示诊断横幅。
|
||||||
|
- 详情抽屉分为“基本信息 / 配置 / 诊断”。
|
||||||
|
- “配置”页读取 `/admin/netaclaw/skill/envSchema`,保存时调用 `/admin/netaclaw/skill/secrets`,只提交用户填写的新 secret。
|
||||||
|
- 安装 GitHub skill 后,如果元数据声明 setup 脚本,前端会弹窗确认后再调用 `installDeps`。
|
||||||
|
|
||||||
|
这意味着 Skill 页不再只是安装器 UI,也承担 compute skill 的运行前配置和健康检查职责。
|
||||||
|
|
||||||
|
## 数据源管理页
|
||||||
|
|
||||||
|
`packages/frontend/src/modules/base/views/data-source.vue` 是 [[mysql-data-source]] 的后台配置入口,挂在“系统管理 -> 数据源管理”。页面通过 `/admin/netaclaw/data-source/list|save|delete|test` 管理 MySQL 数据源。
|
||||||
|
|
||||||
|
页面职责包括:
|
||||||
|
|
||||||
|
- 配置 host/IP、端口、数据库、用户名、密码、SSL、连接超时和连接池上限。
|
||||||
|
- 端口使用普通输入框并校验 1-65535,避免 `el-input-number` 的步进控件和位数限制影响 MySQL 端口录入。
|
||||||
|
- 配置授权 Agent、表白名单、表黑名单、脱敏列、schema 可见性、最大返回行数、最大 JOIN 表数和查询超时。
|
||||||
|
- “测试连接”只把配置提交给后端测试,不在前端暴露密钥解密逻辑;密码展示使用 `hasPassword` 投影。
|
||||||
|
|
||||||
|
该页面依赖 [[base-module]] 的动态菜单路由。现有库如果没有“数据源管理”菜单,需要由后端启动同步 `base/menu.json` 与 `base/event/menu.ts`,而不是在 `modules/base/config.ts` 再加静态路由。
|
||||||
|
|
||||||
|
## 频道管理页
|
||||||
|
|
||||||
|
`views/channel-management.vue` 在 2026-05-14 后承担两类微信渠道配置:
|
||||||
|
|
||||||
|
- `weixin`:ClawBot 私聊助手,仍保留扫码登录、重连、断开和清空会话。
|
||||||
|
- `weixin-db`:本地 PC 微信数据库代理,要求 Windows + 已登录 PC 微信,表单要求填写 `wxid` 并做同 wxid 唯一性校验。
|
||||||
|
|
||||||
|
weixin-db 表单新增“微信自动回复(v4 桌面操作)”区块:
|
||||||
|
|
||||||
|
- `weixinReply.enabled` 控制是否启用双 Agent 自动回复。
|
||||||
|
- `desktopAgentId` 选择桌面操作 Agent;前端显示所有 Agent,后端保存时自动补齐 `weixin_desktop` toolset 和 `weixin_send_text` 路由。
|
||||||
|
- 可配置每群每分钟、每天上限、小号安全模式和水印策略。
|
||||||
|
- 频道卡片展示群聊启用数、微信版本、wxid 和连接状态。
|
||||||
|
|
||||||
|
`components/channel-group-panel.vue` 管理 channel 下群白名单、每群绑定 Agent 和每群回复身份。weixin-db 频道编辑页不再配置频道级默认 Agent、机器人别名或默认回复身份;每个群必须独立指定 reply agent 和回复身份。前端不直接处理本机微信数据库,消息读取、白名单缓存和自动发送都在 [[agent-channel]] 与 [[desktop-op-module]] 后端侧完成。
|
||||||
|
|
||||||
|
## 工具渲染
|
||||||
|
|
||||||
|
工具调用展示通过 `tools/renderer-registry.ts` 收敛,不建议在 chat 页面散落大量工具名判断。运行时策略文案通过 `tools/runtime-policy.ts` 收敛,保持与后端 projection 一致。
|
||||||
|
|
||||||
|
2026-05-07 后,长耗时工具和 compute-entry Skill 的 [[runtime-process-events]] 由 `tool-process-timeline.vue` 和 `tool-process-display.ts` 展示,可从实时事件或历史 metadata 恢复。例如 [[vehicle-damage-skill]] 的抽帧、检测、定位和复核阶段不再只能等待最终 JSON。
|
||||||
|
|
||||||
|
## Geo 前端模块
|
||||||
|
|
||||||
|
`packages/frontend/src/modules/geo/` 是新增的 [[geo-module]] 前端入口:
|
||||||
|
|
||||||
|
- `accounts.vue`:账号列表、平台选择、绑定 Agent/IP、启动浏览器、抓取 cookie、重置会话和切换 IP。
|
||||||
|
- `proxies.vue`:代理 IP 管理、绑定状态、区域/城市/套餐和健康信息。
|
||||||
|
- `dashboard.vue`:Geo 模块概览入口。
|
||||||
|
|
||||||
|
该模块不是 Agent 对话页的一部分,但会通过账号的 `agentId`、`sessionName` 和 cookie/profile 生命周期与 [[netabrowser-runtime]]、Agent 自动化任务发生关系。
|
||||||
|
|
||||||
|
对子 Agent 可视化还有一个新的边界约束:
|
||||||
|
|
||||||
|
- 前端 `store/subagent_projection.ts` 仍保留 legacy fallback 解析逻辑,但这是为了兼容历史数据。
|
||||||
|
- 当前实现的主路径是后端在 `session-tree/subagent_projection.ts` 生成 canonical projection,前端只做提取、排序、折叠和交互展示。
|
||||||
|
- 如果某次会话页面展示异常,应先排查后端 projection 是否缺失或 `fallbackUsed` 是否异常,而不是优先怀疑 Vue 组件渲染层。
|
||||||
|
|
||||||
|
这套结构使新增工具时需要同步考虑:
|
||||||
|
|
||||||
|
- 后端工具实现。
|
||||||
|
- catalog/governance/manifest。
|
||||||
|
- resolver projection。
|
||||||
|
- 前端工具管理页。
|
||||||
|
- Agent 编辑页。
|
||||||
|
- 对话页渲染。
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [[agent-runtime]]
|
||||||
|
- [[session-tree-runtime]]
|
||||||
|
- [[tool-governance]]
|
||||||
|
- [[tool-runtime-policy]]
|
||||||
|
- [[tool-operations]]
|
||||||
|
- [[runtime-process-events]]
|
||||||
|
- [[geo-module]]
|
||||||
|
- [[netabrowser-runtime]]
|
||||||
|
- [[desktop-op-module]]
|
||||||
|
- [[subagent-session]]
|
||||||
|
- [[memory-system]]
|
||||||
|
- [[skill-system]]
|
||||||
|
- [[skill-runtime]]
|
||||||
|
- [[mysql-data-source]]
|
||||||
|
- [[websocket-gateway]]
|
||||||
|
- [[cool-admin-framework]]
|
||||||
106
docs/code-wiki/concepts/prompt-builder.md
Normal file
106
docs/code-wiki/concepts/prompt-builder.md
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
---
|
||||||
|
title: Prompt Builder 分层注入系统
|
||||||
|
created: 2026-04-16
|
||||||
|
updated: 2026-04-19
|
||||||
|
type: concept
|
||||||
|
tags: [agent, llm, architecture]
|
||||||
|
sources: [packages/backend/src/modules/netaclaw/runtime/prompt_builder.ts, packages/backend/src/modules/netaclaw/runtime/prompt_guidance.ts, packages/backend/src/modules/netaclaw/controller/agent.ts]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Prompt Builder 分层注入系统
|
||||||
|
|
||||||
|
## 定义
|
||||||
|
|
||||||
|
8 层分层系统,将 Agent 的系统提示词从多个来源统一组装。它本身是纯函数层,但现在通常接收来自 [[tool-governance]] 的最终 `toolNames` 与 `toolPromptHints`,不再单独决定运行时工具可见性。
|
||||||
|
|
||||||
|
## 8 层结构
|
||||||
|
|
||||||
|
```text
|
||||||
|
Layer 1: Agent 身份(agentSystemPrompt)
|
||||||
|
Layer 2: 工具使用纪律(基于 availableToolNames 动态生成)
|
||||||
|
Layer 3: 模型特定指导(GPT/DeepSeek/MiniMax/豆包/通义千问/GLM)
|
||||||
|
Layer 4: 工具行为策略(基于工具列表 + Prompt Hint)
|
||||||
|
Layer 5: Skill 索引(如果有 Skill)
|
||||||
|
Layer 6: 记忆系统(如果启用记忆)
|
||||||
|
Layer 7: Crew 编排上下文(如果是 Crew 模式)
|
||||||
|
Layer 8: 元信息(时间戳、模型ID)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 核心文件
|
||||||
|
|
||||||
|
| 文件 | 职责 |
|
||||||
|
|------|------|
|
||||||
|
| `runtime/prompt_builder.ts` | `buildSystemPrompt()` 主函数,8层组装 |
|
||||||
|
| `runtime/prompt_guidance.ts` | 指导文本常量集中管理 |
|
||||||
|
| `controller/agent.ts` | 预览接口,展示最终 layers/toolNames/disabledReasons |
|
||||||
|
|
||||||
|
## 关键 API
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface BuildSystemPromptParams {
|
||||||
|
agentSystemPrompt: string;
|
||||||
|
modelId: string;
|
||||||
|
availableToolNames: string[];
|
||||||
|
skills: string[];
|
||||||
|
skillLoader: SkillLoaderService;
|
||||||
|
memoryEnabled: boolean;
|
||||||
|
memoryContext?: string;
|
||||||
|
crewContext?: string;
|
||||||
|
toolPromptHints?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildSystemPrompt(params): { prompt: string, layers: PromptLayer[] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## 关键变化
|
||||||
|
|
||||||
|
### 旧模式
|
||||||
|
|
||||||
|
Prompt Builder 可直接通过 `collectAvailableToolNames()` 结合 toolset 推导工具列表。
|
||||||
|
|
||||||
|
### 当前主路径
|
||||||
|
|
||||||
|
当前真正用于运行时和预览的主链路是:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Agent 配置 + 全局 Tool 配置 + 模型能力 + 上下文角色
|
||||||
|
→ tool_resolver.resolve()
|
||||||
|
→ 输出 toolNames / toolPromptHints / disabledReasons
|
||||||
|
→ buildSystemPrompt()
|
||||||
|
```
|
||||||
|
|
||||||
|
因此 Prompt Builder 更像“最终提示词组装器”,而不是“工具决策器”。
|
||||||
|
|
||||||
|
## 模型特定指导
|
||||||
|
|
||||||
|
`getModelGuidance(modelId)` 会为不同模型生成约束性指导,用于减少工具调用和 schema 输出偏差。
|
||||||
|
|
||||||
|
## Prompt Hint
|
||||||
|
|
||||||
|
Layer 4 现在支持从 [[tool-governance]] 注入 `toolPromptHints`,用于覆写单个工具的行为指令。这个能力使运营侧可以不改工具代码就微调模型使用方式。
|
||||||
|
|
||||||
|
## 预览接口
|
||||||
|
|
||||||
|
Agent 编辑页通过:
|
||||||
|
|
||||||
|
```text
|
||||||
|
POST /admin/netaclaw/agent/previewPrompt
|
||||||
|
```
|
||||||
|
|
||||||
|
直接返回:
|
||||||
|
|
||||||
|
- `layers`
|
||||||
|
- `toolNames`
|
||||||
|
- `disabledReasons`
|
||||||
|
|
||||||
|
这让前端可以同时看到最终提示词和为什么某些工具没生效。
|
||||||
|
|
||||||
|
## 关联页面
|
||||||
|
|
||||||
|
- [[tool-catalog]] — schema 注册来源
|
||||||
|
- [[tool-governance]] — 最终工具列表与 Prompt Hint 来源
|
||||||
|
- [[agent-runtime]] — 调用 buildSystemPrompt 的执行引擎
|
||||||
|
- [[llm-providers]] — 模型特定指导的适配对象
|
||||||
|
- [[skill-system]] — Layer 5 Skill 索引来源
|
||||||
|
- [[memory-system]] — Layer 6 记忆上下文来源
|
||||||
|
- [[crew-orchestration]] — Layer 7 Crew 编排上下文来源
|
||||||
44
docs/code-wiki/concepts/react-loop.md
Normal file
44
docs/code-wiki/concepts/react-loop.md
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
title: ReAct 循环
|
||||||
|
created: 2026-04-13
|
||||||
|
updated: 2026-04-13
|
||||||
|
type: concept
|
||||||
|
tags: [agent, runtime, architecture]
|
||||||
|
sources: [packages/backend/src/modules/netaclaw/runtime/agent.ts]
|
||||||
|
---
|
||||||
|
|
||||||
|
# ReAct 循环
|
||||||
|
|
||||||
|
ReAct(Reasoning + Acting)是 NetaClaw Agent 的核心推理模式。LLM 交替进行"思考"和"行动",直到任务完成。
|
||||||
|
|
||||||
|
## 工作原理
|
||||||
|
|
||||||
|
```
|
||||||
|
用户输入 → LLM 推理
|
||||||
|
├─ 直接回答(无需工具)→ 结束
|
||||||
|
└─ 需要工具 → 输出 tool_use block
|
||||||
|
→ 执行工具 → 获取 tool_result
|
||||||
|
→ 将结果追加到对话历史
|
||||||
|
→ 再次调用 LLM 推理(下一轮)
|
||||||
|
→ 重复直到 LLM 不再调用工具
|
||||||
|
```
|
||||||
|
|
||||||
|
## 关键约束
|
||||||
|
|
||||||
|
- **最大轮次**:`maxToolRounds` 限制循环次数,防止无限循环
|
||||||
|
- **流式输出**:每轮推理的 token 实时通过 WebSocket 推送
|
||||||
|
- **思考能力**:支持 Anthropic 的 `extended_thinking`,思考内容单独推送
|
||||||
|
|
||||||
|
## 与传统 Chain 的区别
|
||||||
|
|
||||||
|
| | ReAct | 固定 Chain |
|
||||||
|
|---|---|---|
|
||||||
|
| 决策方式 | LLM 自主决定是否调用工具 | 预定义的步骤序列 |
|
||||||
|
| 灵活性 | 高,可动态组合工具 | 低,固定流程 |
|
||||||
|
| 适用场景 | 开放式任务 | 结构化流程 |
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [[agent-runtime]] — 实现 ReAct 循环的代码
|
||||||
|
- [[tool-system]] — 循环中可调用的工具
|
||||||
|
- [[websocket-gateway]] — 流式推送通道
|
||||||
69
docs/code-wiki/concepts/runtime-process-events.md
Normal file
69
docs/code-wiki/concepts/runtime-process-events.md
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
title: 运行时过程事件
|
||||||
|
created: 2026-05-07
|
||||||
|
updated: 2026-05-07
|
||||||
|
type: concept
|
||||||
|
tags: [runtime, agent, tool, skill, websocket]
|
||||||
|
sources: [packages/backend/src/modules/netaclaw/runtime/process_events.ts, packages/backend/src/modules/netaclaw/runtime/attempt.ts, packages/backend/src/modules/netaclaw/service/skill_executor.ts, packages/backend/src/modules/netaclaw/tools/builtin/execute_skill.ts, packages/frontend/src/modules/agent/components/tool-process-timeline.vue, packages/frontend/src/modules/agent/utils/tool-process-display.ts]
|
||||||
|
---
|
||||||
|
|
||||||
|
# 运行时过程事件
|
||||||
|
|
||||||
|
运行时过程事件是 2026-05-07 新增的长耗时工具/Skill 进度表达机制。它把“工具正在做什么”从最终 `tool_result` 中拆出来,统一为可流式展示、可采样存档的 `RuntimeProcessEvent`,服务 [[skill-runtime]]、[[tool-system]]、[[vehicle-damage-skill]] 和前端对话页。
|
||||||
|
|
||||||
|
## 事件模型
|
||||||
|
|
||||||
|
`runtime/process_events.ts` 定义:
|
||||||
|
|
||||||
|
- `operationId`:一次工具、Skill、子 Agent 或 runtime 操作的稳定 ID。
|
||||||
|
- `parentOperationId`:父子操作关系。
|
||||||
|
- `targetType`:`tool`、`skill`、`subagent`、`task`、`runtime`。
|
||||||
|
- `source`:事件来源,当前包括 `skill`、`tool`、`subagent`、`runtime`。
|
||||||
|
- `stage`:阶段名,例如抽帧、检测、复核。
|
||||||
|
- `status`:`queued`、`started`、`running`、`completed`、`failed`、`cancelled`、`skipped`。
|
||||||
|
- `level`:`debug`、`info`、`warning`、`error`。
|
||||||
|
- `current/total/percent`:进度信息。
|
||||||
|
- `payload`:经过脱敏和截断的结构化补充信息。
|
||||||
|
|
||||||
|
`sanitizeProcessPayload()` 会过滤 key 中包含 api key、token、password、secret、authorization、credential 的字段,避免过程事件泄漏密钥。
|
||||||
|
|
||||||
|
## 缓冲与落盘
|
||||||
|
|
||||||
|
`ProcessEventBuffer` 对重复进度做节流,默认 400ms 聚合一次。终态事件会先 flush 同 operation 的待发送事件,再立即发出终态。
|
||||||
|
|
||||||
|
`appendRuntimeProcessLog()` 会把事件追加到:
|
||||||
|
|
||||||
|
```text
|
||||||
|
<dataDir>/netaclaw-process-events/<sessionId>/<operationId>.jsonl
|
||||||
|
```
|
||||||
|
|
||||||
|
同时返回 `runtime_process_log` 引用,便于 session metadata 保存抽样结果和完整日志位置。
|
||||||
|
|
||||||
|
## 工具与 Skill 接入
|
||||||
|
|
||||||
|
当前重点接入点:
|
||||||
|
|
||||||
|
- `execute_skill.ts`:把 compute-entry skill 的 process events 桥接为工具执行过程。
|
||||||
|
- `SkillExecutorService`:执行子进程时处理 stdout JSON 协议和过程事件。
|
||||||
|
- `runtime/attempt.ts`:在工具调用生命周期内携带 operationId、process log ref 和采样事件。
|
||||||
|
- `vehicle-damage-inspection`:在抽帧、检测、grounding、review 等长耗时阶段发出进度。
|
||||||
|
|
||||||
|
这使 compute-entry Skill 不再只能“运行结束后返回一个大 JSON”,而可以边运行边给用户可解释进度。
|
||||||
|
|
||||||
|
## 前端展示
|
||||||
|
|
||||||
|
前端新增:
|
||||||
|
|
||||||
|
- `components/tool-process-timeline.vue`:过程时间线组件。
|
||||||
|
- `utils/tool-process-display.ts`:事件文案、阶段和状态展示工具。
|
||||||
|
- `StreamingStatusCard.vue`、`message-item.vue`、`chat.ts`:消费流式/历史 metadata 中的 process events。
|
||||||
|
|
||||||
|
对话页可以从实时 WebSocket 事件展示进度,也可以从 session tree 节点 metadata 恢复历史过程回放。
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [[tool-system]]
|
||||||
|
- [[skill-runtime]]
|
||||||
|
- [[vehicle-damage-skill]]
|
||||||
|
- [[websocket-gateway]]
|
||||||
|
- [[frontend-architecture]]
|
||||||
154
docs/code-wiki/concepts/session-tree-runtime.md
Normal file
154
docs/code-wiki/concepts/session-tree-runtime.md
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
---
|
||||||
|
title: Session Tree 运行时
|
||||||
|
created: 2026-04-21
|
||||||
|
updated: 2026-04-26
|
||||||
|
type: concept
|
||||||
|
tags: [runtime, agent, backend, database]
|
||||||
|
sources: [packages/backend/src/modules/netaclaw/session-tree/provider.ts, packages/backend/src/modules/netaclaw/session-tree/types.ts, packages/backend/src/modules/netaclaw/session-tree/snapshot.ts, packages/backend/src/modules/netaclaw/session-tree/context_builder.ts, packages/backend/src/modules/netaclaw/session-tree/mysql_provider.ts, packages/backend/src/modules/netaclaw/session-tree/file_provider.ts, packages/backend/src/modules/netaclaw/gateway/session.ts, packages/frontend/src/modules/agent/store/chat.ts]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Session Tree 运行时
|
||||||
|
|
||||||
|
Session Tree 是 NetaClaw 新 Agent runtime 的会话状态模型。它把对话从线性消息列表升级为“节点树 + 当前叶子 + 活动路径 + 运行时上下文”的结构,使刷新恢复、分支、压缩、子 Agent 批次、标签、模型切换都能进入同一套持久化和投影机制。
|
||||||
|
|
||||||
|
它是 [[agent-runtime]]、[[context-compaction]]、[[subagent-session]] 和 [[frontend-architecture]] 的共同底座。
|
||||||
|
|
||||||
|
## Provider 抽象
|
||||||
|
|
||||||
|
`SessionTreeProvider` 定义统一接口,当前有 file 和 mysql 两种 provider。
|
||||||
|
|
||||||
|
主要能力包括:
|
||||||
|
|
||||||
|
- `createSession`
|
||||||
|
- `getSession`
|
||||||
|
- `updateSession`
|
||||||
|
- `deleteSession`
|
||||||
|
- `listEntries`
|
||||||
|
- `appendEntry`
|
||||||
|
- `appendMessage`
|
||||||
|
- `appendThinkingLevelChange`
|
||||||
|
- `appendModelChange`
|
||||||
|
- `appendCompaction`
|
||||||
|
- `appendBranchSummary`
|
||||||
|
- `appendLabelChange`
|
||||||
|
- `appendSessionInfo`
|
||||||
|
- `updateEntry`
|
||||||
|
- `switchLeaf`
|
||||||
|
- `resetLeaf`
|
||||||
|
- `createBranchedSession`
|
||||||
|
- `getActivePath`
|
||||||
|
- `getSnapshot`
|
||||||
|
|
||||||
|
这层抽象让运行时不直接绑定 MySQL 或本地文件,也为未来多 workspace、本地优先存储、导入导出保留空间。
|
||||||
|
|
||||||
|
## Snapshot 结构
|
||||||
|
|
||||||
|
`SessionTreeSnapshot` 是前后端对齐的核心投影:
|
||||||
|
|
||||||
|
| 字段 | 含义 |
|
||||||
|
| --- | --- |
|
||||||
|
| `session` | 会话元信息,包括 provider、rootEntryId、leafEntryId、cwd、agentId 等 |
|
||||||
|
| `entries` | 当前会话所有树节点 |
|
||||||
|
| `activePath` | 从 root 到 leaf 的当前上下文路径 |
|
||||||
|
| `childrenByParentId` | 前端重建树结构所需的父子索引 |
|
||||||
|
| `labelsByEntryId` | 节点标签状态 |
|
||||||
|
| `runtimeContext` | 供模型调用使用的消息、thinking level、模型引用 |
|
||||||
|
|
||||||
|
前端刷新恢复应优先加载 snapshot,而不是依赖 localStorage 保存完整聊天记录。
|
||||||
|
|
||||||
|
## Entry 类型
|
||||||
|
|
||||||
|
当前树节点类型包括:
|
||||||
|
|
||||||
|
| 类型 | 用途 |
|
||||||
|
| --- | --- |
|
||||||
|
| `message` | system/user/assistant/tool 模型消息 |
|
||||||
|
| `thinking_level_change` | 会话内 thinking level 切换 |
|
||||||
|
| `model_change` | 会话内模型切换 |
|
||||||
|
| `compaction` | 压缩摘要节点,详见 [[context-compaction]] |
|
||||||
|
| `branch_summary` | 分支摘要 |
|
||||||
|
| `custom` | 不直接展示的自定义数据 |
|
||||||
|
| `custom_message` | 可展示的自定义消息 |
|
||||||
|
| `label` | 对目标节点设置或清除标签 |
|
||||||
|
| `session_info` | 会话信息变更 |
|
||||||
|
| `subagent_batch` | 子 Agent 批次节点,详见 [[subagent-session]] |
|
||||||
|
| `subagent_result` | 子 Agent 批次结果节点 |
|
||||||
|
|
||||||
|
## Active Path 与上下文构建
|
||||||
|
|
||||||
|
模型上下文不再等于“所有历史消息”。运行时会从当前 `leafEntryId` 回溯得到 `activePath`,再由 `context_builder.ts` 转成 `runtimeContext.messages`。
|
||||||
|
|
||||||
|
这带来几个结果:
|
||||||
|
|
||||||
|
- 分支切换只需要切换 leaf。
|
||||||
|
- 压缩摘要可以替代被压缩的历史段。
|
||||||
|
- 子 Agent、分支摘要、custom message 可以参与展示,但不一定进入模型上下文。
|
||||||
|
- thinking/model 变更可以沿路径生效。
|
||||||
|
|
||||||
|
## 持久化位置
|
||||||
|
|
||||||
|
Session Tree 支持两类持久化:
|
||||||
|
|
||||||
|
- MySQL provider:适合生产和多端共享。
|
||||||
|
- File provider:适合本地优先、单机 workspace、调试和导入导出。
|
||||||
|
|
||||||
|
浏览器 localStorage 只保存最近 session/agent 之类的轻量指针,不是对话历史主存储。
|
||||||
|
|
||||||
|
## 删除语义
|
||||||
|
|
||||||
|
Session Tree 删除必须按会话实际所属的 provider 执行:
|
||||||
|
|
||||||
|
- 前端 `chat.ts` 删除请求优先携带会话列表中该 session 的 `agentId`。
|
||||||
|
- 后端 `gateway/session.ts` 在删除前先从 `netaclaw_agent_session` 反查 `sessionId` 所属 `agentId`,再解析该 Agent 的 session backend。
|
||||||
|
- MySQL provider 删除 `netaclaw_agent_session_entry` 和 `netaclaw_agent_session`;file provider 删除对应 JSONL 文件。
|
||||||
|
- 随后后端再清理 legacy session/message、subagent_session 和 session-tree 兼容表记录。
|
||||||
|
|
||||||
|
这避免了 MySQL 会话在当前选中 Agent 不一致、或前端缺少 `agentId` 时走默认 file provider,导致“点击删除没有效果”。
|
||||||
|
|
||||||
|
## 前端消费
|
||||||
|
|
||||||
|
`packages/frontend/src/modules/agent/store/chat.ts` 负责消费 snapshot:
|
||||||
|
|
||||||
|
- `sessionMeta` 保存会话元信息。
|
||||||
|
- `entries` 和 `entryById` 保存节点。
|
||||||
|
- `childrenByParentId` 支持树结构恢复。
|
||||||
|
- `activePathIds` 控制当前可见路径。
|
||||||
|
- `visibleEntries` 将树节点投影成对话 UI。
|
||||||
|
- `subagentRuntimeByBatchId` 和 `toolRuntimeRoutesByBatchId` 记录子 Agent 工具路由展示数据。
|
||||||
|
|
||||||
|
对话页滚动、刷新恢复、历史批次展示都应该围绕这些状态实现。
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [[agent-runtime]]
|
||||||
|
- [[subagent-session]]
|
||||||
|
- [[context-compaction]]
|
||||||
|
- [[frontend-architecture]]
|
||||||
|
- [[websocket-gateway]]
|
||||||
|
- [[netaclaw-module]]
|
||||||
|
- [[frontend-architecture]]
|
||||||
|
|
||||||
|
## 2026-04-22 Subagent Result Metadata
|
||||||
|
|
||||||
|
`subagent_result` carries durable metadata needed to replay subagent execution after refresh.
|
||||||
|
|
||||||
|
- `metadata.processEvents`: normalized worker event timeline for UI replay.
|
||||||
|
- `metadata.evidenceSummaries`: structured evidence projection built from subagent tool results.
|
||||||
|
- `metadata.toolRuntimeRoutes`: tool routing diagnostics for the delegated batch.
|
||||||
|
|
||||||
|
The frontend store should project these fields into dedicated maps keyed by entry id, rather than letting view components parse raw metadata directly.
|
||||||
|
|
||||||
|
## 2026-04-23 Continue-From-Entry And Projection Contract
|
||||||
|
|
||||||
|
Session Tree 现在不仅负责“存什么”,还决定“从哪里继续对话”:
|
||||||
|
|
||||||
|
- 前端 `chat.ts` 维护 `selectedEntryId`、`switchingLeafEntryId`、`pendingLeafConfirmation`,允许用户选中任意节点后继续发送,而不是只能沿当前 leaf 末尾追加。
|
||||||
|
- 如果选中的是普通 user 节点,继续发送会基于它的父节点重放该条 user message,形成新的分支。
|
||||||
|
- 如果选中的是 `branch_summary`、`compaction` 或非当前 leaf 节点,前端会先切换 leaf,再沿该路径继续。
|
||||||
|
- 这些交互都以 `session.leafEntryId`、`activePath` 和 snapshot 中的树结构为依据,不依赖前端自行缓存一份线性历史。
|
||||||
|
|
||||||
|
对子 Agent 相关节点,当前 snapshot 契约也更明确:
|
||||||
|
|
||||||
|
- `subagent_result.metadata.subagentProjection` 是会话树边界输出的 canonical projection。
|
||||||
|
- projection 内的 `diagnostics.selectedSource`、`inputSources`、`fallbackUsed` 用来说明当前展示究竟来自 `subagent_result`、旧 tool message,还是历史 metadata fallback。
|
||||||
|
- 这让 Session Tree 不再只是原始数据容器,而是把“兼容历史记录”和“为当前 UI 供给稳定形态”一起纳入运行时模型。
|
||||||
95
docs/code-wiki/concepts/skill-runtime.md
Normal file
95
docs/code-wiki/concepts/skill-runtime.md
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
---
|
||||||
|
title: Skill Runtime 执行体系
|
||||||
|
created: 2026-05-02
|
||||||
|
updated: 2026-05-07
|
||||||
|
type: concept
|
||||||
|
tags: [skill, runtime, agent, backend, config]
|
||||||
|
sources: [docs/superpowers/specs/2026-04-27-skill-system-evolution-design.md, packages/backend/src/modules/netaclaw/service/skill_config.ts, packages/backend/src/modules/netaclaw/service/skill_executor.ts, packages/backend/src/modules/netaclaw/service/skill_secret.ts, packages/backend/src/modules/netaclaw/tools/builtin/execute_skill.ts, packages/shared/types/skill.types.ts]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Skill Runtime 执行体系
|
||||||
|
|
||||||
|
Skill Runtime 是 2026-04-27 后对 [[skill-system]] 的一次架构升级:Skill 不再只是 `SKILL.md` prompt 指令,还可以声明运行时、入口脚本、依赖、环境变量和必读 references。它把 prompt skill、compute-entry skill、compute-toolkit skill 区分开,并由 [[tool-system]] 注入不同工具执行。
|
||||||
|
|
||||||
|
## 三种分类
|
||||||
|
|
||||||
|
分类由 `service/skill_config.ts` 根据 `skill.config.yaml` 推导:
|
||||||
|
|
||||||
|
| 分类 | 判定 | Agent 使用方式 | 例子 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `prompt` | 无 `skill.config.yaml`,或 config 无 runtime/entrypoint | `read_skill` 读取完整指令 | `llm-wiki` |
|
||||||
|
| `compute-entry` | config 有 `entrypoint` | `execute_skill` 直接执行 stdin/stdout JSON 协议 | OCR/API 封装类 skill |
|
||||||
|
| `compute-toolkit` | config 有 `runtime` 但无 `entrypoint` | `read_skill` 后按指令用 `bash` 执行脚本 | `minimax-pdf`、`minimax-xlsx`、`minimax-docx` |
|
||||||
|
|
||||||
|
`buildSkillsPrompt()` 会在 `<available_skills>` 中输出 `type`,compute-entry 还会摘要 `interface.input`,让模型知道何时可以直接调用 `execute_skill`。
|
||||||
|
|
||||||
|
## skill.config.yaml
|
||||||
|
|
||||||
|
`skill.config.yaml` 当前支持:
|
||||||
|
|
||||||
|
- `runtime`: `python`、`node`、`bash`、`dotnet`
|
||||||
|
- `entrypoint`: compute-entry 的执行入口
|
||||||
|
- `timeout`: 子进程超时
|
||||||
|
- `env`: skill scoped 环境变量 schema
|
||||||
|
- `dependencies`: system/python/node/dotnet 依赖声明
|
||||||
|
- `setup`: posix/win32 安装脚本
|
||||||
|
- `interface`: 输入输出字段说明
|
||||||
|
- `references`: required / optional / routes
|
||||||
|
|
||||||
|
这些字段同步到 `packages/shared/types/skill.types.ts`,供前后端共享。
|
||||||
|
|
||||||
|
## execute_skill
|
||||||
|
|
||||||
|
`tools/builtin/execute_skill.ts` 注册 `execute_skill` 工具。`tool_resolver.ts` 只在 Agent 有 skills 且包含 compute-entry skill 时注入它,避免没有执行型 skill 的 Agent 暴露多余工具。
|
||||||
|
|
||||||
|
执行链路:
|
||||||
|
|
||||||
|
```text
|
||||||
|
LLM -> execute_skill({ name, input })
|
||||||
|
-> SkillExecutorService.execute()
|
||||||
|
-> SkillConfigService 读取 runtime/entrypoint/interface
|
||||||
|
-> SkillSecretService.resolveEnv() 注入 skill scoped env
|
||||||
|
-> spawn skill 子进程,stdin 写 JSON
|
||||||
|
-> stdout 解析 JSON,返回工具结果
|
||||||
|
```
|
||||||
|
|
||||||
|
Python skill 需要先有 `.venv`;执行器会检查 venv 目录,缺失时返回诊断性错误。
|
||||||
|
|
||||||
|
2026-05-07 后,`execute_skill` 还支持桥接 compute-entry 子进程发出的 [[runtime-process-events]]。长耗时 Skill 可以在最终 JSON 之前持续输出阶段进度;运行时会做 payload 脱敏、采样和 JSONL 落盘。
|
||||||
|
|
||||||
|
## 密钥与环境变量
|
||||||
|
|
||||||
|
`service/skill_secret.ts` 负责 skill scoped secrets:
|
||||||
|
|
||||||
|
- `netaclaw_skill.secrets` 存 AES-256-GCM 加密后的 JSON。
|
||||||
|
- `netaclaw_skill.envSchema` 存环境变量声明。
|
||||||
|
- `resolveEnv(skillName)` 合并 env 默认值和已配置 secrets。
|
||||||
|
- `saveSecrets()` 只保存加密结果,前端不回显明文。
|
||||||
|
|
||||||
|
bash 工具还支持 `createSkillBashEnvProvider()`:当 cwd 落在某个 skill 目录内时,自动把该 skill 的 env 注入命令环境。这样 compute-toolkit 不需要把 API key 写入 prompt 或脚本参数。
|
||||||
|
|
||||||
|
## 诊断与兼容
|
||||||
|
|
||||||
|
`SkillLoaderService` 现在会收集诊断:
|
||||||
|
|
||||||
|
- `NAME_INVALID`、`NAME_MISMATCH`、`NAME_COLLISION`
|
||||||
|
- `DESC_MISSING`、`DESC_TOO_LONG`
|
||||||
|
- `CONFIG_PARSE_ERROR`
|
||||||
|
- `VENV_MISSING`
|
||||||
|
- `ENV_NOT_CONFIGURED`
|
||||||
|
|
||||||
|
`/admin/netaclaw/skill/diagnostics` 给前端技能页展示。旧的纯 prompt skill 不需要迁移;没有 config 的 skill 仍按原逻辑加载。
|
||||||
|
|
||||||
|
## 新增 compute-entry 示例
|
||||||
|
|
||||||
|
[[vehicle-damage-skill]] 是当前最典型的 compute-entry 示例:它通过 Node 脚本抽取汽车环车视频帧、调用视觉模型检测旧伤候选、定位标注、复核放大图并输出最终证据。该 Skill 同时验证了 `skill.config.yaml`、scoped env、过程事件和前端时间线展示的完整闭环。
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [[skill-system]]
|
||||||
|
- [[document-skills]]
|
||||||
|
- [[vehicle-damage-skill]]
|
||||||
|
- [[runtime-process-events]]
|
||||||
|
- [[tool-system]]
|
||||||
|
- [[tool-governance]]
|
||||||
|
- [[frontend-architecture]]
|
||||||
61
docs/code-wiki/concepts/thinking-system.md
Normal file
61
docs/code-wiki/concepts/thinking-system.md
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
title: Thinking/Reasoning 能力系统
|
||||||
|
created: 2026-04-14
|
||||||
|
updated: 2026-04-14
|
||||||
|
type: concept
|
||||||
|
tags: [llm, agent, runtime]
|
||||||
|
sources: [packages/backend/src/modules/netaclaw/runtime/thinking.ts, packages/backend/src/modules/netaclaw/plugins/llm_providers/anthropic.ts]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Thinking/Reasoning 能力系统
|
||||||
|
|
||||||
|
统一管理不同 LLM 提供商的思考/推理能力,支持会话级动态调节思考深度。
|
||||||
|
|
||||||
|
## 支持的模型
|
||||||
|
|
||||||
|
| 提供商 | 模型 | 机制 | 级别 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| Anthropic | claude-3-5-sonnet, claude-4* | extended_thinking (budget_tokens) | minimal(2k)/low(4k)/medium(8k)/high(16k) |
|
||||||
|
| Anthropic | claude-4.6+ | adaptive thinking | 自适应(无需指定预算) |
|
||||||
|
| OpenAI | o1, o3 | reasoning_effort | low/medium/high |
|
||||||
|
| DeepSeek | deepseek-r1 | reasoning_content 字段 | 无级别控制 |
|
||||||
|
|
||||||
|
## 核心函数(runtime/thinking.ts)
|
||||||
|
|
||||||
|
| 函数 | 用途 |
|
||||||
|
|------|------|
|
||||||
|
| `isThinkingSupported(model)` | 检查模型是否支持思考 |
|
||||||
|
| `supportsAdaptiveThinking(model)` | 检测 Claude 4.6+ 自适应思考 |
|
||||||
|
| `getModelThinkingCapability(supplier, model)` | 返回能力描述(supported, adaptive, levels, defaultLevel) |
|
||||||
|
| `buildAnthropicThinkingParams(model, level)` | 构建 Anthropic 参数(adaptive 或 budget_tokens) |
|
||||||
|
| `buildOpenAIThinkingParams(level)` | 构建 OpenAI reasoning_effort 参数 |
|
||||||
|
|
||||||
|
## 参数注入
|
||||||
|
|
||||||
|
**Anthropic 适配器**:
|
||||||
|
- 自适应模式:`thinking: { type: 'adaptive' }` + beta header `interleaved-thinking-2025-05-14`
|
||||||
|
- 固定预算:`thinking: { type: 'enabled', budget_tokens: N }` + 强制 `temperature: 1.0`
|
||||||
|
|
||||||
|
**OpenAI 适配器**:
|
||||||
|
- `reasoning_effort: 'low' | 'medium' | 'high'`
|
||||||
|
|
||||||
|
## 思考内容提取
|
||||||
|
|
||||||
|
| 提供商 | 提取方式 |
|
||||||
|
|--------|---------|
|
||||||
|
| Anthropic | response.content[] 中 `{ type: 'thinking' }` 块 |
|
||||||
|
| OpenAI/o1/o3 | `thinking` 字段 |
|
||||||
|
| DeepSeek-R1 | `reasoning_content` 字段 |
|
||||||
|
| 通用 | `<think>...</think>` 标签解析 |
|
||||||
|
|
||||||
|
## 会话级控制
|
||||||
|
|
||||||
|
优先级链:会话级 `sessionThinkLevel` > Agent 配置 `defaultThinkLevel` > 模型默认值
|
||||||
|
|
||||||
|
前端通过 `ClientSetThinkingLevelMessage` WebSocket 消息动态调节,服务端通过 `ServerThinkingEvent` / `ServerThinkingDeltaEvent` / `ServerThinkingDoneEvent` 流式推送思考内容。
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [[llm-providers]] — 提供商适配层实现
|
||||||
|
- [[agent-runtime]] — 运行时集成思考参数
|
||||||
|
- [[websocket-gateway]] — 思考事件的 WebSocket 协议
|
||||||
65
docs/code-wiki/concepts/tool-operations.md
Normal file
65
docs/code-wiki/concepts/tool-operations.md
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
title: Tool Operations 接口层
|
||||||
|
created: 2026-05-02
|
||||||
|
updated: 2026-05-02
|
||||||
|
type: concept
|
||||||
|
tags: [tool, runtime, backend, architecture]
|
||||||
|
sources: [docs/superpowers/specs/2026-05-01-tool-pluggable-operations-design.md, packages/backend/src/modules/netaclaw/tools/operations/, packages/backend/src/modules/netaclaw/tools/builtin/, packages/backend/src/modules/netaclaw/service/tool_resolver.ts, packages/backend/test/tool_operations.test.ts]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Tool Operations 接口层
|
||||||
|
|
||||||
|
Tool Operations 是 2026-05-01 后引入的工具执行后端抽象层。它把 [[tool-system]] 中 `bash`、`read_file`、`write_file`、`list_dir`、`find_files`、`grep`、`edit`、`patch` 这些内置工具的业务逻辑,与底层文件系统 / 子进程 / 搜索命令调用解耦。
|
||||||
|
|
||||||
|
它不是替代 [[tool-governance]] 或 [[tool-runtime-policy]]:治理层仍决定工具是否可见、是否能进子 Agent、走哪种 worker route;Operations 层只负责“被允许执行后,具体由哪个后端完成 I/O 和进程操作”。
|
||||||
|
|
||||||
|
## 三组接口
|
||||||
|
|
||||||
|
`tools/operations/types.ts` 定义三组接口,并聚合为 `ToolOperations`:
|
||||||
|
|
||||||
|
| 接口 | 核心方法 | 用途 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `FileOperations` | `readFile`、`writeFile`、`appendFile`、`access`、`readDir`、`mkdir`、`realpath` | 文件读写、目录枚举、写队列 realpath key |
|
||||||
|
| `ProcessOperations` | `exec(command, cwd, options)` | shell 命令执行、流式 stdout/stderr、超时和 abort |
|
||||||
|
| `SearchOperations` | `ripgrep`、`fd` | `grep` / `find_files` 的搜索后端 |
|
||||||
|
|
||||||
|
`tools/operations/local.ts` 是默认本地实现,使用 `fs/promises` 和 `comm/child_process.spawn`。`getDefaultOperations()` 通过 `tools/operations/index.ts` 提供单例,避免每次 resolver 重新创建实现对象。
|
||||||
|
|
||||||
|
## 当前落地范围
|
||||||
|
|
||||||
|
最近提交已完成四个阶段:
|
||||||
|
|
||||||
|
1. 建立 `ToolOperations` 接口、本地实现和 `tool_operations.test.ts` 契约测试。
|
||||||
|
2. 将 read/write/list、edit/patch、find/grep/bash 迁移到 factory + operations 注入模式。
|
||||||
|
3. 在 `tool_resolver.ts` 统一注入 `operations?: ToolOperations`,主进程默认使用 `getDefaultOperations()`。
|
||||||
|
4. 清理旧的 queue 签名和 bash 自有 operations,工具业务文件不再直接承担底层 spawn / fs 抽象职责。
|
||||||
|
|
||||||
|
因此新增或测试内置文件工具时,应优先 mock `ToolOperations`,而不是真实读写宿主机文件系统。
|
||||||
|
|
||||||
|
## 与子 Agent 的关系
|
||||||
|
|
||||||
|
[[subagent-session]] 仍以 [[tool-runtime-policy]] 的 manifest route 判断某个工具是 `worker-local`、`main-process-proxy` 还是 `disabled`。Operations 改造后的价值是让 worker / 主进程可以在构建工具实例时注入不同后端,而不用改工具的参数校验、diff 计算、输出截断等业务逻辑。
|
||||||
|
|
||||||
|
当前实现先保持原有工具粒度路由,不把权限下沉到每个 file/process 方法。未来如果需要远程 workspace、Docker 沙箱或 SSH 后端,可以新增 `ToolOperations` 实现,再由 resolver 按会话策略注入。
|
||||||
|
|
||||||
|
## 运行时细节
|
||||||
|
|
||||||
|
- `LocalProcessOperations.exec()` 统一通过 `comm/child_process.spawn`,继承 Windows 下隐藏控制台窗口的处理。
|
||||||
|
- bash 输出中的 ANSI escape code 在 LocalOperations 层剥离,避免每个工具重复清理。
|
||||||
|
- `withFileMutationQueueOps()` 用注入的 `FileOperations.realpath()` 取队列 key,后续远程文件系统可以自定义“同一文件”的判断。
|
||||||
|
- `SearchOperations` 单独抽出是因为 ripgrep/fd 的缺失、fallback 和参数语义与普通 shell 命令不同。
|
||||||
|
|
||||||
|
## 设计边界
|
||||||
|
|
||||||
|
- Operations 不负责判断工具是否可用;最终可见性仍看 [[tool-governance]] 的 resolver 输出。
|
||||||
|
- Operations 不改变 LLM 看到的工具 schema;prompt 和参数兼容旧工具。
|
||||||
|
- Operations 不负责 Session Tree 写入;工具结果仍由 [[agent-runtime]] / [[session-tree-runtime]] 记录。
|
||||||
|
- Operations 不包含 `memory_*`、`delegate_*`、`clarify`、`todo` 等不直接做文件或进程操作的工具。
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [[tool-system]]
|
||||||
|
- [[tool-governance]]
|
||||||
|
- [[tool-runtime-policy]]
|
||||||
|
- [[subagent-session]]
|
||||||
|
- [[agent-runtime]]
|
||||||
142
docs/code-wiki/concepts/tool-runtime-policy.md
Normal file
142
docs/code-wiki/concepts/tool-runtime-policy.md
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
---
|
||||||
|
title: Tool Runtime Policy
|
||||||
|
created: 2026-04-21
|
||||||
|
updated: 2026-05-02
|
||||||
|
type: concept
|
||||||
|
tags: [tool, runtime, agent, backend, config]
|
||||||
|
sources: [packages/backend/src/modules/netaclaw/tools/manifest.ts, packages/backend/src/modules/netaclaw/tools/runtime_policy.ts, packages/backend/src/modules/netaclaw/tools/presentation.ts, packages/backend/src/modules/netaclaw/service/tool_resolver.ts, packages/backend/src/modules/netaclaw/subagent/process_protocol.ts, packages/frontend/src/modules/agent/tools/runtime-policy.ts]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Tool Runtime Policy
|
||||||
|
|
||||||
|
Tool Runtime Policy 是工具重构后新增的运行时策略层。它解决的问题是:工具不仅要知道“是否启用”,还要知道“在当前 Agent、当前子进程、当前 workspace/policy 下到底能不能执行,以及应该在哪里执行”。
|
||||||
|
|
||||||
|
它连接 [[tool-system]]、[[tool-governance]]、[[subagent-session]] 和 [[agent-runtime]]。
|
||||||
|
|
||||||
|
2026-05-02 后还要区分 [[tool-operations]]:Runtime Policy 决定工具在 worker 视角是本地、主进程代理还是禁用;Operations 决定某个已构建工具的文件系统、进程和搜索调用如何落地。当前实现仍保持工具粒度 route,不把每个 file/process 方法拆成独立权限。
|
||||||
|
|
||||||
|
## 三层模型
|
||||||
|
|
||||||
|
```text
|
||||||
|
Tool Catalog / DB Governance
|
||||||
|
-> Tool Manifest
|
||||||
|
-> Runtime Policy Projection
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1. Catalog / DB Governance
|
||||||
|
|
||||||
|
这一层负责工具是否存在、全局是否启用、Agent 是否覆盖、Prompt Hint 如何写入。详见 [[tool-governance]]。
|
||||||
|
|
||||||
|
### 2. Tool Manifest
|
||||||
|
|
||||||
|
`tools/manifest.ts` 将工具转成稳定画像:
|
||||||
|
|
||||||
|
- `kind`: `builtin`、`memory`、`skill`、`delegation`、`admin`、`custom`
|
||||||
|
- `executionMode`: `parallel` 或 `sequential`
|
||||||
|
- `supportedInWorker`
|
||||||
|
- `workerRoutingHint`: `local`、`main-process-proxy`、`disabled`
|
||||||
|
- `requiresShell`
|
||||||
|
- `requiresWrite`
|
||||||
|
- `requiresSkillScope`
|
||||||
|
|
||||||
|
这些字段是子 Agent worker 和前端诊断展示的共同语言。
|
||||||
|
|
||||||
|
### 3. Runtime Policy Projection
|
||||||
|
|
||||||
|
`tools/runtime_policy.ts` 根据 manifest、enabled tool names、disabled reasons、worker policy 推导最终状态:
|
||||||
|
|
||||||
|
| 状态 | 含义 |
|
||||||
|
| --- | --- |
|
||||||
|
| `worker-local` | 子进程可以本地执行 |
|
||||||
|
| `main-process-proxied` | 子进程请求父进程代理执行 |
|
||||||
|
| `disabled-in-worker` | 当前 worker 策略下不能执行 |
|
||||||
|
| `not-selected` | 没有被选入当前子 Agent 工具集 |
|
||||||
|
|
||||||
|
前端再映射为路由:
|
||||||
|
|
||||||
|
- `worker-local`
|
||||||
|
- `main-process-proxy`
|
||||||
|
- `disabled`
|
||||||
|
|
||||||
|
## Worker Policy 自动推导
|
||||||
|
|
||||||
|
`buildSubprocessWorkerPolicy` 会从当前 session 和工具 manifest 推导 worker 策略:
|
||||||
|
|
||||||
|
- `workspaceRoots`: 来自 `sessionCwd` 和额外 workspace roots,去重后传给 worker。
|
||||||
|
- `allowShell`: 只有工具确实需要 shell,且策略允许时才为 true。
|
||||||
|
- `readonly`: 如果没有写工具需求,默认只读;写工具需要显式允许。
|
||||||
|
|
||||||
|
这使子 Agent 不需要默认拥有完整宿主进程权限。
|
||||||
|
|
||||||
|
## 阻断原因
|
||||||
|
|
||||||
|
常见 `blockedReason` 包括:
|
||||||
|
|
||||||
|
| 原因 | 含义 |
|
||||||
|
| --- | --- |
|
||||||
|
| `workspace_root_required` | 工具需要 workspace root,但当前 session 没有 cwd/root |
|
||||||
|
| `shell_disabled_by_policy` | `bash` 等 shell 工具被策略禁用 |
|
||||||
|
| `write_blocked_by_readonly_policy` | 写入类工具被 readonly 策略阻止 |
|
||||||
|
| `requires_main_process_proxy` | 工具需要主进程代理 |
|
||||||
|
| `unsupported_in_worker` | 工具没有 worker 支持 |
|
||||||
|
| `not_selected_for_subagent` | 未被选入子 Agent 工具集 |
|
||||||
|
|
||||||
|
这些原因应该在工具管理页、Agent 编辑页、子 Agent 批次详情里使用同一套中文展示。
|
||||||
|
|
||||||
|
## 当前内置路由
|
||||||
|
|
||||||
|
| 工具类别 | 工具 | 默认路由 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Worker 本地 | `bash`、`read_file`、`list_dir`、`find_files`、`grep` | `worker-local` |
|
||||||
|
| 主进程代理 | `write_file`、`edit`、`patch`、`read_skill`、`read_skill_file`、`skill_manage`、`execute_skill`、`memory_save`、`memory_recall` | `main-process-proxy` |
|
||||||
|
| 强顺序工具 | `edit`、`patch`、`write_file`、`delegate_task`、`delegate_parallel`、`clarify`、`escalate` | `sequential` |
|
||||||
|
|
||||||
|
治理层可以通过 `workerRoutingStrategy` 强制覆盖:
|
||||||
|
|
||||||
|
- `auto`
|
||||||
|
- `force-local`
|
||||||
|
- `force-main-process-proxy`
|
||||||
|
- `force-disabled`
|
||||||
|
|
||||||
|
## 前端一致性要求
|
||||||
|
|
||||||
|
这次重构的目标之一是让三处页面一致:
|
||||||
|
|
||||||
|
- 工具管理页显示全局治理、核心工具、是否可关闭、运行时画像。
|
||||||
|
- Agent 编辑页显示当前 Agent 的局部启停、子 Agent 允许工具、有效 runtime profile。
|
||||||
|
- 对话页显示实际运行时的子 Agent 工具路由和 blocked reason。
|
||||||
|
|
||||||
|
这些页面不应各自重新推导规则,而应消费后端 `runtimeDiagnostic` / projection。
|
||||||
|
|
||||||
|
## 2026-04-23 动态路由与静态画像的区别
|
||||||
|
|
||||||
|
近期实现里,Tool Runtime Policy 需要同时解释两种结果:
|
||||||
|
|
||||||
|
- `manifest` / `effectiveRuntimeProfile`:偏静态画像,描述工具理论上支持什么、通常需要什么权限。
|
||||||
|
- `toolRuntimeRoutes`:偏动态结果,描述在当前 session cwd、workspace roots、allowShell、readonly、selected tools 条件下,这次运行究竟走哪条路。
|
||||||
|
|
||||||
|
因此:
|
||||||
|
|
||||||
|
- 工具管理页更适合看“画像”和“全局风险”。
|
||||||
|
- Agent 编辑页更适合看“该 Agent 保存后会生效什么画像”。
|
||||||
|
- 对话页和子 Agent 批次更适合看“本次任务真正请求到的路由结果”。
|
||||||
|
|
||||||
|
如果出现“Agent 编辑页显示允许,但运行时还是 blocked”,优先检查动态 `toolRuntimeRoutes`,不要只盯着静态 manifest。
|
||||||
|
|
||||||
|
## Operations 不是 Runtime Policy
|
||||||
|
|
||||||
|
[[tool-operations]] 新增后,容易混淆两条线:
|
||||||
|
|
||||||
|
- `toolRuntimeRoutes` 是“这个工具在本次策略下能不能进 worker / 是否代理”的决策结果。
|
||||||
|
- `ToolOperations` 是“工具实例内部调用文件、搜索、进程时走哪个后端”的执行依赖。
|
||||||
|
|
||||||
|
例如 `bash` 可以被 Runtime Policy 判定为 `worker-local`,但其具体执行仍由注入的 `ProcessOperations.exec()` 完成;测试环境可以替换为 mock process operations,而不需要改 policy。
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [[tool-governance]]
|
||||||
|
- [[tool-system]]
|
||||||
|
- [[tool-operations]]
|
||||||
|
- [[subagent-session]]
|
||||||
|
- [[agent-runtime]]
|
||||||
|
- [[frontend-architecture]]
|
||||||
70
docs/code-wiki/concepts/windows-runtime.md
Normal file
70
docs/code-wiki/concepts/windows-runtime.md
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
---
|
||||||
|
title: Windows Runtime 与安装器
|
||||||
|
created: 2026-04-26
|
||||||
|
updated: 2026-04-26
|
||||||
|
type: concept
|
||||||
|
tags: [deploy, backend, config]
|
||||||
|
sources: [packages/backend/src/comm/, packages/backend/src/modules/base/controller/app/runtime.ts, packages/backend/scripts/pkg-build.js, packages/backend/scripts/build-windows-installer.js, packages/backend/installer/setup.iss, packages/windows-tray/]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Windows Runtime 与安装器
|
||||||
|
|
||||||
|
Windows runtime 是 2026-04-25 后新增的本地部署子系统,目标是把后端打包成 `backend.exe`,再配合 .NET 托盘程序和 Inno Setup 安装器运行。它和 [[project-overview]] 的开发态不同,重点处理配置文件、数据目录、运行时状态、进程生命周期和本机控制接口。
|
||||||
|
|
||||||
|
## 组件
|
||||||
|
|
||||||
|
| 组件 | 路径 | 职责 |
|
||||||
|
|------|------|------|
|
||||||
|
| 后端可执行文件 | `packages/backend/scripts/pkg-build.js` | 使用 pkg/yao-pkg 打包后端 |
|
||||||
|
| 安装器构建 | `packages/backend/scripts/build-windows-installer.js` | 构建 backend.exe、发布托盘 exe、调用 Inno Setup |
|
||||||
|
| 安装器脚本 | `packages/backend/installer/setup.iss` | 生成 Windows 安装包 |
|
||||||
|
| 托盘程序 | `packages/windows-tray/Neta.Tray/` | 启动/附着/停止后端,打开系统、日志、配置目录 |
|
||||||
|
| 运行时控制 API | `modules/base/controller/app/runtime.ts` | `/app/base/runtime/status` 与 `/stop` |
|
||||||
|
| 配置加载 | `comm/config-loader.ts` | pkg 模式读取 `config.yaml` |
|
||||||
|
| 数据目录 | `comm/data-dir.ts` | 统一可写路径 |
|
||||||
|
| runtime info | `comm/runtime-info.ts` | 写入 pid、端口、URL、控制密钥、目录信息 |
|
||||||
|
|
||||||
|
## 配置与数据目录
|
||||||
|
|
||||||
|
pkg 模式下,后端要求 `config.yaml` 与 `backend.exe` 同目录。配置必须包含:
|
||||||
|
|
||||||
|
- `server.port`
|
||||||
|
- `data.dir`
|
||||||
|
- `database.host/username/database`
|
||||||
|
- `autoOpenBrowser`
|
||||||
|
|
||||||
|
数据目录解析顺序:
|
||||||
|
|
||||||
|
1. 外部配置的 `data.dir`
|
||||||
|
2. 环境变量 `NETA_DATA_DIR`
|
||||||
|
3. pkg 模式下的 `<exe-dir>/data`
|
||||||
|
4. 开发态下的 `<cwd>/dist`
|
||||||
|
|
||||||
|
这条规则影响 SQLite 记忆、Session Tree file provider、skill 目录、日志、runtime info 等所有可写路径,避免安装目录和源码目录混用。
|
||||||
|
|
||||||
|
## 托盘控制
|
||||||
|
|
||||||
|
托盘程序会读取安装目录中的 runtime 信息,如果后端已就绪就附着;否则启动 `backend.exe` 并等待 runtime info 可用。菜单能力包括:
|
||||||
|
|
||||||
|
- 打开系统:使用后端返回的本机 URL。
|
||||||
|
- 重启服务:先走 `/stop`,再重新启动后端。
|
||||||
|
- 停止服务:先尝试控制 API,超时后按 pid 和安装目录内 backend 进程强制结束。
|
||||||
|
- 打开日志目录 / 配置目录。
|
||||||
|
- 退出程序:停止后端并退出托盘。
|
||||||
|
|
||||||
|
控制 API 只允许 loopback 地址,并通过 `x-neta-tray-secret` 校验 `NETA_TRAY_SECRET`,避免远程请求停止本机服务。
|
||||||
|
|
||||||
|
## 构建与部署边界
|
||||||
|
|
||||||
|
- `config.prod.ts` 从外部配置读取数据库和端口,并保持 EPS 在后端生产配置里关闭。
|
||||||
|
- 前端生产配置使用 same-origin API base URL,安装器场景下避免协议相对 URL 和双斜杠。
|
||||||
|
- SPA history fallback 在后端静态文件服务中兜底,支持刷新前端路由。
|
||||||
|
- bundled skills 会进入安装包,运行时按需初始化 skill 目录和 tool catalog。
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [[project-overview]] — Monorepo 和部署入口
|
||||||
|
- [[skill-system]] — bundled skills 与运行时 skill 目录
|
||||||
|
- [[memory-system]] — SQLite 记忆路径受 dataDir 影响
|
||||||
|
- [[session-tree-runtime]] — file provider 的 rootDir 受 dataDir 影响
|
||||||
|
- [[base-module]] — runtime 控制接口挂在 base 模块下
|
||||||
136
docs/code-wiki/entities/agent-channel.md
Normal file
136
docs/code-wiki/entities/agent-channel.md
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
---
|
||||||
|
title: Agent 渠道系统
|
||||||
|
created: 2026-04-14
|
||||||
|
updated: 2026-05-14
|
||||||
|
type: entity
|
||||||
|
tags: [agent, api, module]
|
||||||
|
sources: [packages/backend/src/modules/netaclaw/service/agent_channel.ts, packages/backend/src/modules/netaclaw/service/weixin.ts, packages/backend/src/modules/netaclaw/service/weixin_db.ts, packages/backend/src/modules/netaclaw/service/weixin_archive_sync.ts, packages/backend/src/modules/netaclaw/entity/agent_channel_group.ts, packages/frontend/src/modules/agent/views/channel-management.vue]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Agent 渠道系统
|
||||||
|
|
||||||
|
将 Agent 接入外部消息平台(当前支持微信),实现消息接收、Agent 处理、澄清/风险确认降级和回复闭环。2026-05-14 后这里已经分成两条微信路径:旧 `weixin` ClawBot 私聊助手,以及新的 `weixin-db` 本地代理群聊路径。
|
||||||
|
|
||||||
|
## 数据模型
|
||||||
|
|
||||||
|
**netaclaw_agent_channel 表**:
|
||||||
|
|
||||||
|
`name`(唯一) | `type`(weixin/weixin-db) | `agentId` | `agentName` | `config`(JSON) | `credential`(JSON,加密) | `runtime`(JSON: syncBuf/contextTokens/seenMessageIds) | `loginStatus`(pending/disconnected/connected/error/unsupported_platform) | `lastError` | `lastConnectedAt`
|
||||||
|
|
||||||
|
**netaclaw_agent_channel_group 表**:
|
||||||
|
|
||||||
|
`channelId` | `roomId`(channel 内唯一) | `roomName` | `status`(0禁用/1启用/-1忽略) | `triggerMode`(at_mention/all) | `boundAgentId` | `replyIdentityOverride` | `firstSeenAt` | `lastSeenAt` | `lastActiveAt`
|
||||||
|
|
||||||
|
## 关键文件
|
||||||
|
|
||||||
|
| 文件 | 职责 |
|
||||||
|
|------|------|
|
||||||
|
| `entity/agent_channel.ts` | 渠道 Entity |
|
||||||
|
| `service/agent_channel.ts` | 渠道 CRUD + QR 登录 + 后台运行器 |
|
||||||
|
| `service/weixin.ts` | 微信协议层(QR 登录、消息收发) |
|
||||||
|
| `service/weixin_db.ts` | 本机 PC 微信数据库增量读取、WAL watcher、群白名单缓存 |
|
||||||
|
| `service/weixin_archive_sync.ts` | 按 channel 工作目录同步历史归档 |
|
||||||
|
| `runtime/weixin_db/*` | key 抽取、WCDB 解密、增量读取、room 解析、消息伪协议构建 |
|
||||||
|
| `entity/agent_channel_group.ts` | 群白名单、触发策略和每群 Agent 覆盖 |
|
||||||
|
| `service/agent_executor.ts` | Agent 执行器(渠道消息触发 Agent 推理) |
|
||||||
|
| `controller/admin/agent_channel.ts` | REST API |
|
||||||
|
| `controller/admin/agent_channel_group.ts` | 群管理 API |
|
||||||
|
|
||||||
|
## 微信接入模式
|
||||||
|
|
||||||
|
| 类型 | 场景 | 消息来源 | 回复路径 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `weixin` | ClawBot 个人助手,当前主打私聊 | iLink/ClawBot API 轮询 | `weixinService.sendText()` 直接发送 |
|
||||||
|
| `weixin-db` | 本机 PC 微信群聊代理 | 本地数据库解密 + WAL watcher | v4 双 Agent:reply agent 委托 desktop agent 调 [[desktop-op-module]] 发送 |
|
||||||
|
|
||||||
|
## ClawBot 私聊流程
|
||||||
|
|
||||||
|
```
|
||||||
|
创建渠道 → 绑定 Agent
|
||||||
|
→ createWeixinQr() 生成二维码
|
||||||
|
→ 用户扫码 → pollWeixinQr() 轮询状态
|
||||||
|
→ 状态流转:wait → scaned_but_redirect → confirmed
|
||||||
|
→ 获取 credential(accountId, token, baseUrl, userId)
|
||||||
|
→ syncRunner() 启动后台消息轮询(800ms 间隔)
|
||||||
|
```
|
||||||
|
|
||||||
|
`routeInboundMessage()` 会丢弃 ClawBot 收到的群消息,避免旧 iLink 路径和新的群聊本地代理互相串线。
|
||||||
|
|
||||||
|
## weixin-db 群聊流程
|
||||||
|
|
||||||
|
```
|
||||||
|
创建 weixin-db 渠道,填写 wxid
|
||||||
|
→ syncRunner() 对启用渠道调用 WeixinDbService.bindChannel()
|
||||||
|
→ 自动定位 xwechat_files seedDir
|
||||||
|
→ extractWeixinKeys() 抽取 message/session/contact DB key
|
||||||
|
→ IncrementalReader 初始化 per-channel workDir
|
||||||
|
→ WalWatcher 监听 message DB WAL
|
||||||
|
→ readIncrement() 增量解密消息
|
||||||
|
→ room_resolver + 群白名单过滤
|
||||||
|
→ buildPseudoMessageFromDb() 构造伪消息
|
||||||
|
→ routeInboundMessage()
|
||||||
|
```
|
||||||
|
|
||||||
|
非 Windows 平台会把状态标记为 `unsupported_platform`,服务保持 idle,不让启动流程崩溃。
|
||||||
|
|
||||||
|
## 群白名单与触发
|
||||||
|
|
||||||
|
`netaclaw_agent_channel_group` 管理每个 channel 下的群:
|
||||||
|
|
||||||
|
- `status=1` 的群才进入 weixin-db 增量读取白名单。
|
||||||
|
- `triggerMode` 当前支持 `at_mention` 和 `all`,`prefix` 仅兼容存量数据。
|
||||||
|
- `boundAgentId` 是 weixin-db 群聊的必填 reply agent;不再回退 channel 默认 Agent。
|
||||||
|
- `replyIdentityOverride` 是 weixin-db 群聊的必填回复身份;不再回退 channel 级回复身份。
|
||||||
|
- 群增删启停后会调用 `WeixinDbService.refreshWhitelist()`,让下一次 WAL tick 使用最新白名单。
|
||||||
|
|
||||||
|
## 消息处理循环
|
||||||
|
|
||||||
|
```
|
||||||
|
runLoop() 或 WalWatcher 收到消息
|
||||||
|
→ decideChatScope() 区分 DM / group
|
||||||
|
→ seenMessageIds 5 分钟去重
|
||||||
|
→ context_token 按 chatId 保存
|
||||||
|
→ pendingClarify / 风险确认短路
|
||||||
|
→ senderQueues 按 DM sender 或群 chatId 串行
|
||||||
|
→ handleInboundMessage()
|
||||||
|
```
|
||||||
|
|
||||||
|
群聊路径会先检查群记录、触发策略和每群 Agent 覆盖,再调用 `agentExecutor.execute()`。weixin-db v4 不再由 `agent_channel` 自动发送最终内容;reply agent 必须通过 `delegate_task` 委托 desktop agent,否则服务会记录“未调用 delegate_task”的告警,避免消息静默丢失。
|
||||||
|
|
||||||
|
## 生命周期管理
|
||||||
|
|
||||||
|
- `restoreConnectedRunners()`:启动时恢复已连接渠道的运行器
|
||||||
|
- `disconnect()`:断开 ClawBot 连接,停止轮询
|
||||||
|
- `restart()`:重启运行器
|
||||||
|
- `clearSessions()`:清空渠道的所有会话
|
||||||
|
- `delete()`:删除群记录、解绑 weixin-db、删除 channel archive,并通过 [[desktop-op-module]] 取消该 channel 的 pending/running 桌面任务
|
||||||
|
- weixin-db 健康探针每 60 秒检查 PC 微信进程和数据库可读性,失败后尝试 unbind/rebind
|
||||||
|
|
||||||
|
## v4 双 Agent 自动回复
|
||||||
|
|
||||||
|
weixin-db 的 `config.weixinReply` 启用后,渠道保存会做额外校验:
|
||||||
|
|
||||||
|
- 必须配置 `desktopAgentId`,且不能与 reply agent 相同。
|
||||||
|
- 后端会自动给 desktop agent 加入 `weixin_desktop` toolset。
|
||||||
|
- 后端会把 `weixin_send_text` 设置为 `allowInSubagent=true` 且 `force-main-process-proxy`。
|
||||||
|
- `weixinReply.enabled` 从 true 改为 false 时,会级联取消该 channel 下的桌面操作任务。
|
||||||
|
|
||||||
|
最终链路是:群消息进入 reply agent,reply agent 用 `delegate_task` 把发送动作委托给 desktop agent,desktop agent 调用 [[tool-system]] 的 `weixin_send_text`,再由 [[desktop-op-module]] 操作 PC 微信窗口。
|
||||||
|
|
||||||
|
## Clarify 纯文本降级
|
||||||
|
|
||||||
|
微信渠道不支持 WebSocket 交互,[[clarify-tool]] 采用纯文本 + 数字映射方案:
|
||||||
|
|
||||||
|
1. 构造文本消息:`❓ 问题\n1. 选项1\n2. 选项2\n请回复数字或直接输入`
|
||||||
|
2. 通过微信 API 发送,存入 `pendingClarify` Map(key: `${channelId}:${senderId}`)
|
||||||
|
3. 用户回复数字 → 映射到 `choices[num-1]`;回复文本 → 直接使用
|
||||||
|
4. 解决 Promise,Agent 继续执行
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [[netaclaw-module]] — 所属模块
|
||||||
|
- [[agent-runtime]] — Agent 执行器
|
||||||
|
- [[clarify-tool]] — Clarify 澄清工具(微信降级实现)
|
||||||
|
- [[desktop-op-module]] — weixin-db 自动回复的桌面发送执行后端
|
||||||
|
- [[tool-system]] — `weixin_send_text` 和 `delegate_task` 工具链路
|
||||||
|
- [[websocket-gateway]] — 前端管理页面通过 HTTP API 操作
|
||||||
131
docs/code-wiki/entities/agent-runtime.md
Normal file
131
docs/code-wiki/entities/agent-runtime.md
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
---
|
||||||
|
title: Agent 运行时
|
||||||
|
created: 2026-04-13
|
||||||
|
updated: 2026-04-23
|
||||||
|
type: entity
|
||||||
|
tags: [runtime, agent, llm, backend]
|
||||||
|
sources: [packages/backend/src/modules/netaclaw/service/chat_orchestrator.ts, packages/backend/src/modules/netaclaw/session-tree/, packages/backend/src/modules/netaclaw/runtime/, packages/backend/src/modules/netaclaw/service/tool_resolver.ts, packages/backend/src/modules/netaclaw/subagent/]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Agent 运行时
|
||||||
|
|
||||||
|
Agent 运行时是 NetaClaw 对话执行链路的核心。当前实现已经从“线性消息 + 单次 runAgent 调用”升级为“Session Tree 快照 + ChatOrchestrator 编排 + ToolResolver/Manifest 治理 + Subagent Worker”的组合架构。
|
||||||
|
|
||||||
|
它同时负责:
|
||||||
|
|
||||||
|
- 维护会话树、活动路径和运行时上下文,详见 [[session-tree-runtime]]。
|
||||||
|
- 按 Agent 配置、模型能力、治理策略解析最终可用工具,详见 [[tool-governance]] 和 [[tool-runtime-policy]]。
|
||||||
|
- 构建 Prompt、执行 ReAct 循环、流式返回 token/thinking/tool 事件。
|
||||||
|
- 在主 Agent 与子 Agent 之间聚合委派结果,详见 [[subagent-session]]。
|
||||||
|
- 将快照和增量事件推送给前端,详见 [[websocket-gateway]] 和 [[frontend-architecture]]。
|
||||||
|
|
||||||
|
## 关键文件
|
||||||
|
|
||||||
|
| 文件 | 职责 |
|
||||||
|
| --- | --- |
|
||||||
|
| `service/chat_orchestrator.ts` | 对话主编排:创建会话、追加用户/助手节点、驱动运行、写回结果 |
|
||||||
|
| `session-tree/provider.ts` | Session Tree provider 抽象 |
|
||||||
|
| `session-tree/mysql_provider.ts` | MySQL 持久化 provider |
|
||||||
|
| `session-tree/file_provider.ts` | 文件持久化 provider |
|
||||||
|
| `session-tree/context_builder.ts` | 从 active path 构造模型上下文 |
|
||||||
|
| `runtime/agent.ts` | ReAct 循环执行入口 |
|
||||||
|
| `runtime/prompt_builder.ts` | Prompt Builder 分层注入 |
|
||||||
|
| `service/tool_resolver.ts` | 工具治理、Agent 覆盖、运行时诊断投影 |
|
||||||
|
| `tools/manifest.ts` | 工具 manifest 和 worker routing 基础画像 |
|
||||||
|
| `tools/runtime_policy.ts` | Subprocess worker policy 与工具路由状态推导 |
|
||||||
|
| `subagent/process_runner.ts` | 子 Agent 独立进程 runner |
|
||||||
|
| `subagent/process_protocol.ts` | 子进程 JSONL 事件协议 |
|
||||||
|
|
||||||
|
## 当前运行链路
|
||||||
|
|
||||||
|
```text
|
||||||
|
用户输入
|
||||||
|
-> ChatOrchestrator 接收
|
||||||
|
-> SessionTreeProvider 创建或恢复 session
|
||||||
|
-> appendMessage(user) 写入会话树
|
||||||
|
-> SessionTreeContextBuilder 读取 active path
|
||||||
|
-> ToolResolver 生成 toolNames / disabledReasons / runtimeDiagnostic
|
||||||
|
-> Prompt Builder 注入工具、记忆、技能、压缩摘要
|
||||||
|
-> runAgent 执行 ReAct 循环
|
||||||
|
-> 工具调用经 ToolResolver 提供的工具实例执行
|
||||||
|
-> 如触发 delegate_task/delegate_parallel,进入 SubagentService
|
||||||
|
-> appendEntry 写入 message、compaction、subagent_batch、subagent_result 等节点
|
||||||
|
-> WebSocket 推送 snapshot/patch/event
|
||||||
|
-> 前端 chat store 按 active path 渲染
|
||||||
|
```
|
||||||
|
|
||||||
|
## 与旧实现的关键区别
|
||||||
|
|
||||||
|
旧实现偏向按 `netaclaw_message` 的线性历史恢复对话。现在的运行时以会话树为主轴:
|
||||||
|
|
||||||
|
- `activePath` 决定当前模型上下文,而不是简单取全量消息列表。
|
||||||
|
- `leafEntryId` 表示当前分支叶子,支持分支、重置 leaf、派生 session。
|
||||||
|
- `compaction`、`branch_summary`、`label`、`session_info`、`subagent_batch`、`subagent_result` 都是树节点,而不是散落在消息 metadata 里的附属状态。
|
||||||
|
- 前端刷新后通过 session tree snapshot 恢复历史,不依赖浏览器 localStorage 存完整消息。
|
||||||
|
|
||||||
|
## 工具运行时
|
||||||
|
|
||||||
|
Agent 运行时不直接相信静态工具列表。最终工具集由 [[tool-governance]] 统一裁决:
|
||||||
|
|
||||||
|
- 全局工具状态来自 `netaclaw_tool` 和 catalog 同步。
|
||||||
|
- Agent 编辑页可以保存 `tools.enabled`、`tools.disabled`、`toolOverrides`、`subagentConfig.allowedToolNames` 等局部配置。
|
||||||
|
- 后端输出 `runtimeDiagnostic`,前端工具管理页、Agent 编辑页、对话页应使用同一份投影。
|
||||||
|
- 子 Agent 的工具还会经过 [[tool-runtime-policy]] 推导,区分 `worker-local`、`main-process-proxy`、`disabled`。
|
||||||
|
|
||||||
|
## 子 Agent 运行时
|
||||||
|
|
||||||
|
主 Agent 通过 `delegate_task` 或 `delegate_parallel` 发起委派后,`SubagentService` 会根据配置构造受限 Agent,并优先走独立 worker 进程执行。子进程通过 JSONL 协议向父进程上报:
|
||||||
|
|
||||||
|
- `run_start`
|
||||||
|
- `token`
|
||||||
|
- `thinking`
|
||||||
|
- `tool_call`
|
||||||
|
- `tool_result`
|
||||||
|
- `proxy_tool_call`
|
||||||
|
- `run_end`
|
||||||
|
- `run_error`
|
||||||
|
|
||||||
|
需要主进程能力的工具会通过 `proxy_tool_call` 回到父进程执行,避免子进程直接持有所有写入、记忆、技能管理权限。详见 [[subagent-session]] 和 [[tool-runtime-policy]]。
|
||||||
|
|
||||||
|
## 前端消费方式
|
||||||
|
|
||||||
|
前端 `agent/store/chat.ts` 现在围绕 session tree snapshot 工作:
|
||||||
|
|
||||||
|
- 保存最近 session/agent 的轻量指针到 localStorage。
|
||||||
|
- 通过 snapshot 恢复 `sessionMeta`、`entries`、`childrenByParentId`、`activePathIds`。
|
||||||
|
- `visibleEntries` 渲染 message、branch summary、compaction、custom message、subagent batch/result。
|
||||||
|
- 工具渲染统一通过 `projectToolRender` 和工具 renderer registry。
|
||||||
|
|
||||||
|
这意味着“刷新后历史看不到”一类问题应优先检查后端 session tree provider、session id 恢复、snapshot 加载,而不是把完整对话塞进浏览器 localStorage。
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [[session-tree-runtime]]
|
||||||
|
- [[tool-runtime-policy]]
|
||||||
|
- [[tool-governance]]
|
||||||
|
- [[tool-system]]
|
||||||
|
- [[subagent-session]]
|
||||||
|
- [[context-compaction]]
|
||||||
|
- [[prompt-builder]]
|
||||||
|
- [[frontend-architecture]]
|
||||||
|
- [[websocket-gateway]]
|
||||||
|
- [[netaclaw-module]]
|
||||||
|
|
||||||
|
## 2026-04-22 Subagent Replay
|
||||||
|
|
||||||
|
The Agent runtime persists subagent evidence and process replay through session tree entries instead of transient chat state.
|
||||||
|
|
||||||
|
- `SubagentService` collects worker process events and writes projected events into `resultPayload.processEvents`.
|
||||||
|
- `ChatOrchestrator` aggregates completed task data into `subagent_result.metadata.processEvents` and `subagent_result.metadata.evidenceSummaries`.
|
||||||
|
- `SessionTreeProvider.getSnapshot()` is the refresh boundary: after page reload, the frontend should recover subagent evidence cards and replay timeline from `subagent_result` entries in the snapshot.
|
||||||
|
- These metadata fields are intentionally excluded from `runtimeContext.messages` so refresh/replay UI data does not contaminate the next LLM prompt.
|
||||||
|
|
||||||
|
## 2026-04-23 Canonical Projection Boundary
|
||||||
|
|
||||||
|
这一轮更新后,Agent runtime 对子 Agent 回放数据的口径进一步收敛:
|
||||||
|
|
||||||
|
- `gateway/session.ts` 在返回 `getSnapshot()`、`switchLeaf()`、`appendSubagentResultEntry()` 等结果前,会统一调用 `projectSubagentSnapshot()` / `projectSubagentEntry()`。
|
||||||
|
- `session-tree/subagent_projection.ts` 负责把 `subagent_result`、历史 tool message 或旧 metadata 中的委派结果整理为 `metadata.subagentProjection`,这才是前端首选的 canonical UI projection。
|
||||||
|
- projection 会生成 `taskPanels`、`toolExecutions`、`evidenceSummaries`、`processEvents` 和 `diagnostics`,并做数量裁剪,避免把整份原始 payload 直接暴露给 UI。
|
||||||
|
- 前端保留对旧 payload / metadata 的解析能力只是为了兼容历史会话;当前代码路径应优先依赖后端生成的 `subagentProjection`,而不是在页面里重复拼装结果。
|
||||||
|
- 这意味着“子 Agent 结果怎么展示”已经不再是纯前端约定,而是 Agent runtime 在 session 边界稳定输出的一部分契约。
|
||||||
61
docs/code-wiki/entities/base-module.md
Normal file
61
docs/code-wiki/entities/base-module.md
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
title: Base 模块
|
||||||
|
created: 2026-04-13
|
||||||
|
updated: 2026-05-15
|
||||||
|
type: entity
|
||||||
|
tags: [module, auth, backend]
|
||||||
|
sources: [packages/backend/src/modules/base/]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Base 模块
|
||||||
|
|
||||||
|
系统基础设施模块,提供用户认证、RBAC 权限、菜单管理、部门管理等核心能力。
|
||||||
|
|
||||||
|
## 数据模型(11个表)
|
||||||
|
|
||||||
|
| 表名 | 用途 |
|
||||||
|
|------|------|
|
||||||
|
| `base_sys_user` | 后台管理员 |
|
||||||
|
| `base_sys_role` | 角色 |
|
||||||
|
| `base_sys_menu` | 菜单和权限配置(树形) |
|
||||||
|
| `base_sys_department` | 部门(树形) |
|
||||||
|
| `base_sys_param` | 系统参数 |
|
||||||
|
| `base_sys_log` | 操作日志 |
|
||||||
|
| `base_sys_conf` | 系统配置 |
|
||||||
|
| `base_sys_user_role` | 用户-角色关联 |
|
||||||
|
| `base_sys_role_menu` | 角色-菜单关联 |
|
||||||
|
| `base_sys_role_department` | 角色-部门关联 |
|
||||||
|
|
||||||
|
## 认证流程
|
||||||
|
|
||||||
|
JWT Token 认证:
|
||||||
|
1. 登录 → 返回 `token` + `refreshToken`
|
||||||
|
2. 请求头 `Authorization: Bearer {token}`
|
||||||
|
3. Token 过期 → 用 refreshToken 刷新
|
||||||
|
4. 前端拦截器自动处理刷新逻辑
|
||||||
|
|
||||||
|
## 菜单驱动路由
|
||||||
|
|
||||||
|
前端路由由 `base_sys_menu` 表动态生成:
|
||||||
|
- `type=0`:目录
|
||||||
|
- `type=1`:菜单页面(`viewPath` 指向 Vue 组件)
|
||||||
|
- `type=2`:按钮权限(`perms` 标识如 `project:info:add`)
|
||||||
|
|
||||||
|
## 数据源管理菜单
|
||||||
|
|
||||||
|
2026-05-15 后,[[mysql-data-source]] 的管理入口放在“系统管理 -> 数据源管理”,路径为 `/sys/data-source`,视图为 `modules/base/views/data-source.vue`。
|
||||||
|
|
||||||
|
这个入口不是硬编码在前端静态路由里,而是依赖 Base 模块菜单表:
|
||||||
|
|
||||||
|
- `packages/backend/src/modules/base/menu.json` 保存“数据源管理”种子菜单。
|
||||||
|
- `packages/backend/src/modules/base/event/menu.ts` 在后端启动时同步 `/sys/data-source` 到 `base_sys_menu`,并给 admin 角色补齐菜单权限。
|
||||||
|
- 前端通过 `/admin/base/comm/permmenu` 获取菜单后动态生成路由;`packages/frontend/src/modules/base/config.ts` 不再重复声明该静态页面。
|
||||||
|
|
||||||
|
因此如果页面看不到,优先检查 `base_sys_menu` 和角色菜单关系,而不是只看 Vue 文件是否存在。
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [[project-overview]] — 项目总览
|
||||||
|
- [[cool-admin-framework]] — 框架提供自动 CRUD
|
||||||
|
- [[frontend-architecture]] — 前端动态路由
|
||||||
|
- [[mysql-data-source]] — 数据源管理菜单和配置页
|
||||||
80
docs/code-wiki/entities/clarify-tool.md
Normal file
80
docs/code-wiki/entities/clarify-tool.md
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
title: Clarify Tool 澄清工具
|
||||||
|
created: 2026-04-16
|
||||||
|
updated: 2026-04-16
|
||||||
|
type: entity
|
||||||
|
tags: [tool, agent, websocket]
|
||||||
|
sources: [packages/backend/src/modules/netaclaw/tools/builtin/clarify.ts, packages/backend/src/modules/netaclaw/runtime/attempt.ts, packages/backend/src/modules/netaclaw/gateway/server.ts, packages/frontend/src/modules/agent/components/clarify-card.vue]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Clarify Tool 澄清工具
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
Agent 在 ReAct 循环中向用户提出澄清问题的交互工具。支持选择题(最多4项)和开放式问题。通过 Promise 阻塞 Agent 执行,等待用户回答后继续。注册在 [[tool-catalog]] 的 `interaction` 工具集。
|
||||||
|
|
||||||
|
## Schema 定义
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const ClarifyParams = Type.Object({
|
||||||
|
question: Type.String({ description: '要问用户的问题' }),
|
||||||
|
choices: Type.Optional(Type.Array(Type.String(), {
|
||||||
|
maxItems: 4, description: '预设选项(最多4个)'
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据流
|
||||||
|
|
||||||
|
```
|
||||||
|
Agent 调用 clarify → attempt.ts 检测 → onClarifyRequest 回调
|
||||||
|
↓
|
||||||
|
WebSocket 网关: 生成 requestId → clarifyResolvers.set() → 发送 clarify_request
|
||||||
|
↓
|
||||||
|
前端 Store: 创建 role='clarify' 消息 → 渲染 clarify-card 组件
|
||||||
|
↓
|
||||||
|
用户点击选项/输入回答 → sendClarifyResponse() → 发送 clarify_response
|
||||||
|
↓
|
||||||
|
WebSocket 网关: clarifyResolvers.get(requestId).resolve(answer)
|
||||||
|
↓
|
||||||
|
Agent 继续 ReAct 循环
|
||||||
|
```
|
||||||
|
|
||||||
|
## 核心文件
|
||||||
|
|
||||||
|
| 文件 | 职责 |
|
||||||
|
|------|------|
|
||||||
|
| `tools/builtin/clarify.ts` | 工具定义 + Schema |
|
||||||
|
| `runtime/attempt.ts:72-93` | 特殊处理:检测 clarify 调用,触发回调 |
|
||||||
|
| `gateway/server.ts:263-269` | 生成 requestId,Promise 阻塞,转发 WS 事件 |
|
||||||
|
| `gateway/protocol.ts:132-136` | `clarify_request` / `clarify_response` 协议定义 |
|
||||||
|
| `前端 store/chat.ts:248-258` | 接收事件,创建 clarify 消息 |
|
||||||
|
| `前端 components/clarify-card.vue` | 渲染问题+选项按钮+自定义输入 |
|
||||||
|
| `service/agent_channel.ts:347-384` | 微信渠道纯文本降级 |
|
||||||
|
|
||||||
|
## 微信渠道降级
|
||||||
|
|
||||||
|
微信不支持 WebSocket 交互,采用**纯文本 + 数字映射**方案:
|
||||||
|
|
||||||
|
1. 构造文本消息:`❓ 问题\n1. 选项1\n2. 选项2\n请回复数字或直接输入`
|
||||||
|
2. 通过微信 API 发送,存入 `pendingClarify` Map
|
||||||
|
3. 用户回复数字 → 映射到 `choices[num-1]`;回复文本 → 直接使用
|
||||||
|
4. 解决 Promise,Agent 继续
|
||||||
|
|
||||||
|
## WebSocket 协议
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 服务端 → 客户端
|
||||||
|
{ type: 'clarify_request', sessionId, data: { requestId, question, choices? } }
|
||||||
|
|
||||||
|
// 客户端 → 服务端
|
||||||
|
{ type: 'clarify_response', sessionId, requestId, answer }
|
||||||
|
```
|
||||||
|
|
||||||
|
## 关联页面
|
||||||
|
|
||||||
|
- [[tool-catalog]] — 工具目录(interaction 工具集)
|
||||||
|
- [[agent-runtime]] — ReAct 循环中的 clarify 回调
|
||||||
|
- [[websocket-gateway]] — clarify 事件转发
|
||||||
|
- [[agent-channel]] — 微信渠道降级实现
|
||||||
|
- [[tool-system]] — 工具系统总览
|
||||||
64
docs/code-wiki/entities/cool-admin-framework.md
Normal file
64
docs/code-wiki/entities/cool-admin-framework.md
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
title: Cool Admin 框架
|
||||||
|
created: 2026-04-13
|
||||||
|
updated: 2026-04-13
|
||||||
|
type: entity
|
||||||
|
tags: [architecture, backend, frontend, convention]
|
||||||
|
sources: [packages/backend/package.json, packages/frontend/package.json]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Cool Admin 框架
|
||||||
|
|
||||||
|
项目基于 Cool Admin 8.0 二次开发,提供自动 CRUD、动态路由、Service 代理等基础能力。
|
||||||
|
|
||||||
|
## 后端自动 CRUD
|
||||||
|
|
||||||
|
使用 `@CoolController` 装饰器自动生成标准接口:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@CoolController({
|
||||||
|
api: ['add', 'delete', 'update', 'info', 'list', 'page'],
|
||||||
|
entity: SomeEntity,
|
||||||
|
pageQueryOp: {
|
||||||
|
keyWordLikeFields: ['name'],
|
||||||
|
fieldEq: ['status'],
|
||||||
|
addOrderBy: { createTime: 'DESC' },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
自动生成的接口:
|
||||||
|
- `POST /admin/{module}/{entity}/add`
|
||||||
|
- `POST /admin/{module}/{entity}/delete`
|
||||||
|
- `POST /admin/{module}/{entity}/update`
|
||||||
|
- `GET /admin/{module}/{entity}/info`
|
||||||
|
- `POST /admin/{module}/{entity}/list`
|
||||||
|
- `POST /admin/{module}/{entity}/page`
|
||||||
|
|
||||||
|
## 前端 Service 代理
|
||||||
|
|
||||||
|
Cool Admin 自动将后端 Controller 映射为前端 service:
|
||||||
|
|
||||||
|
```
|
||||||
|
后端: modules/project/controller/admin/info.ts
|
||||||
|
前端: service.project.info.page() / .add() / .update() / .delete()
|
||||||
|
```
|
||||||
|
|
||||||
|
自定义接口:
|
||||||
|
```typescript
|
||||||
|
service.request({ url: '/admin/project/task/ganttData', params: { projectId: 1 } })
|
||||||
|
```
|
||||||
|
|
||||||
|
## BaseEntity 通用字段
|
||||||
|
|
||||||
|
所有 Entity 继承 `BaseEntity`:
|
||||||
|
- `id`(自增主键)
|
||||||
|
- `createTime`(自动填充)
|
||||||
|
- `updateTime`(自动更新)
|
||||||
|
- `tenantId`(多租户)
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [[project-overview]] — 项目总览
|
||||||
|
- [[base-module]] — 基础模块使用此框架
|
||||||
|
- [[frontend-architecture]] — 前端路由和 Service 代理
|
||||||
108
docs/code-wiki/entities/crew-orchestration.md
Normal file
108
docs/code-wiki/entities/crew-orchestration.md
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
---
|
||||||
|
title: Multi-Agent Crew 编排系统
|
||||||
|
created: 2026-04-14
|
||||||
|
updated: 2026-04-14
|
||||||
|
type: entity
|
||||||
|
tags: [agent, architecture, websocket, runtime]
|
||||||
|
sources: [packages/backend/src/modules/netaclaw/service/crew_orchestrator.ts, packages/backend/src/modules/netaclaw/entity/crew.ts, packages/frontend/src/modules/agent/views/crew-editor.vue]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Multi-Agent Crew 编排系统
|
||||||
|
|
||||||
|
多智能体协作框架,支持主 Agent 通过委派、并行执行、升级等机制协调多个子 Agent 完成复杂任务。分为后端编排引擎和前端可视化画布编辑器两部分。
|
||||||
|
|
||||||
|
## 核心概念
|
||||||
|
|
||||||
|
- **Crew(集群)**:一组 Agent 的编排单元,包含一个主 Agent 和多个成员 Agent
|
||||||
|
- **主 Agent**:负责任务分解和委派决策,拥有 delegate_task / delegate_parallel / escalate 三个编排工具
|
||||||
|
- **子 Agent**:接收委派任务并独立执行,拥有内置工具和 Skill 工具(不含委派工具)
|
||||||
|
- **升级(Escalate)**:主 Agent 遇到无法自主解决的问题时暂停执行,请求人工介入
|
||||||
|
|
||||||
|
## 数据模型(4个表)
|
||||||
|
|
||||||
|
| 表名 | Entity | 用途 |
|
||||||
|
|------|--------|------|
|
||||||
|
| `netaclaw_crew` | `entity/crew.ts` | 集群定义(画布、触发配置、委派提示) |
|
||||||
|
| `netaclaw_crew_agent` | `entity/crew_agent.ts` | 集群-Agent 关联(角色、画布位置、分组) |
|
||||||
|
| `netaclaw_crew_run` | `entity/crew_run.ts` | 运行记录(状态、token、暂停状态) |
|
||||||
|
| `netaclaw_crew_task` | `entity/crew_task.ts` | 子任务记录(支持嵌套 parentTaskId) |
|
||||||
|
|
||||||
|
### 关键字段
|
||||||
|
|
||||||
|
**netaclaw_crew**: `name`(唯一) | `label` | `masterAgentId` | `canvasData`(JSON) | `triggerConfig`(JSON) | `delegateHints`(文本) | `status`(0草稿/1发布) | `maxConcurrent`(默认3) | `taskTimeout`(默认300秒) | `retryPolicy`(JSON)
|
||||||
|
|
||||||
|
**netaclaw_crew_run**: `crewId` | `triggerType`(manual/cron/webhook/api) | `status`(pending/running/paused/completed/failed/stopped) | `masterSessionId` | `pausedState`(JSON,升级暂停时的对话) | `tokenUsage`(JSON)
|
||||||
|
|
||||||
|
## 后端关键文件
|
||||||
|
|
||||||
|
| 文件 | 职责 |
|
||||||
|
|------|------|
|
||||||
|
| `service/crew_orchestrator.ts` | 核心编排器:启动、运行、暂停、恢复 |
|
||||||
|
| `service/crew_delegate.ts` | 委派执行器:串行和并行子 Agent 执行 |
|
||||||
|
| `service/crew_scheduler.ts` | Cron 定时调度(Singleton,启动时恢复) |
|
||||||
|
| `service/crew.ts` | 集群 CRUD、画布保存、成员同步、发布校验 |
|
||||||
|
| `service/crew_types.ts` | 类型定义(DelegateResult, CrewCallbacks, CrewRunContext) |
|
||||||
|
| `gateway/crew_server.ts` | WebSocket `/crew` 命名空间 |
|
||||||
|
| `controller/admin/crew.ts` | 集群 API(CRUD + saveCanvas/publish) |
|
||||||
|
| `controller/admin/crew_trigger.ts` | 触发 API(start/stop/resume) |
|
||||||
|
| `controller/admin/crew_run.ts` | 运行记录查询 API |
|
||||||
|
|
||||||
|
## 编排执行流程
|
||||||
|
|
||||||
|
```
|
||||||
|
触发运行(手动/定时/Webhook/API)
|
||||||
|
→ CrewOrchestratorService.start()
|
||||||
|
├─ 加载集群、主Agent、成员Agent
|
||||||
|
├─ 创建 crew_run 记录
|
||||||
|
└─ 异步执行(立即返回 runId)
|
||||||
|
→ runOrchestration()
|
||||||
|
├─ 构建增强系统提示词(原始 + 团队成员 + 委派提示 + 工具说明)
|
||||||
|
├─ 构建工具集(内置 + 委派 + Skill)
|
||||||
|
└─ 执行主Agent的 ReAct 循环
|
||||||
|
├─ delegate_task → executeSubAgent()(串行)
|
||||||
|
├─ delegate_parallel → executeParallel()(按 maxConcurrent 分批)
|
||||||
|
└─ escalate → 暂停,等待人工介入
|
||||||
|
→ 运行完成,更新状态和 token 统计
|
||||||
|
```
|
||||||
|
|
||||||
|
## 升级恢复机制
|
||||||
|
|
||||||
|
```
|
||||||
|
主Agent 调用 escalate → 持久化 pausedState → 推送 escalation 事件
|
||||||
|
→ 前端显示升级提示 → 用户输入处理意见
|
||||||
|
→ POST /crew_trigger/resume → resolver(userMessage)
|
||||||
|
→ Promise resolve → 主Agent 继续 ReAct 循环
|
||||||
|
```
|
||||||
|
|
||||||
|
关键实现:`escalateResolvers` Map 存储 resolve 回调,escalate 工具返回不 resolve 的 Promise。
|
||||||
|
|
||||||
|
## 前端画布编辑器
|
||||||
|
|
||||||
|
| 文件 | 职责 |
|
||||||
|
|------|------|
|
||||||
|
| `views/crew-editor.vue` | 编辑器主页面(VueFlow 画布 + 侧栏 + 属性面板) |
|
||||||
|
| `views/crew-monitor.vue` | 运行监控页面(运行列表 + 详情 + 日志) |
|
||||||
|
| `hooks/crew-canvas.ts` | 画布操作(节点增删、主Agent设置、序列化) |
|
||||||
|
| `hooks/crew-monitor.ts` | WebSocket 监控(连接 `/crew` 命名空间) |
|
||||||
|
| `hooks/crew-orchestration.ts` | 连线转委派提示词(串行/并行建议) |
|
||||||
|
| `store/crew.ts` | Pinia Store(集群列表、详情、成员) |
|
||||||
|
| `components/crew/*.vue` | 10个子组件(节点、侧栏、属性面板、日志等) |
|
||||||
|
|
||||||
|
## WebSocket 协议(`/crew` 命名空间)
|
||||||
|
|
||||||
|
| 事件 | 方向 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `crew:trigger` | 客户端→服务端 | 触发集群运行 |
|
||||||
|
| `crew:control` | 客户端→服务端 | 控制运行(stop/resume/pause/retry) |
|
||||||
|
| `crew:run:status` | 服务端→客户端 | 运行状态变化 |
|
||||||
|
| `crew:task:status` | 服务端→客户端 | 任务状态变化 |
|
||||||
|
| `crew:log` | 服务端→客户端 | 日志消息 |
|
||||||
|
| `crew:escalation` | 服务端→客户端 | 升级事件 |
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [[netaclaw-module]] — 所属模块
|
||||||
|
- [[agent-runtime]] — 子 Agent 执行复用 runAgent()
|
||||||
|
- [[tool-system]] — delegate_task / delegate_parallel / escalate 工具
|
||||||
|
- [[websocket-gateway]] — `/crew` 命名空间
|
||||||
|
- [[skill-system]] — 主/子 Agent 均可加载 Skill
|
||||||
91
docs/code-wiki/entities/desktop-op-module.md
Normal file
91
docs/code-wiki/entities/desktop-op-module.md
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
---
|
||||||
|
title: Desktop Op 桌面操作模块
|
||||||
|
created: 2026-05-14
|
||||||
|
updated: 2026-05-14
|
||||||
|
type: entity
|
||||||
|
tags: [module, runtime, agent, backend]
|
||||||
|
sources: [packages/backend/src/modules/desktop_op/, packages/backend/src/modules/netaclaw/tools/builtin/weixin_send_text.ts, docs/superpowers/specs/2026-05-14-neta-desktop-op-design.md]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Desktop Op 桌面操作模块
|
||||||
|
|
||||||
|
Desktop Op 是 2026-05-14 新增的通用桌面 GUI Agent 模块。它把本机窗口定位、截图、键鼠输入、VLM 验证、队列互斥和审计日志封装成后端模块,当前 MVP 只注册 `WeixinAdapter`,服务于 [[agent-channel]] 的 weixin-db 群聊自动回复。
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```text
|
||||||
|
desktop_op/
|
||||||
|
├── config.ts
|
||||||
|
├── controller/admin/ # action_log 与 config 管理 API
|
||||||
|
├── entity/ # desktop_op_config / desktop_op_action_log
|
||||||
|
├── runtime/ # 核心运行时、输入、截图、VLM、安全护栏、适配器
|
||||||
|
└── service/ # DesktopOpService 与配置 bootstrap
|
||||||
|
```
|
||||||
|
|
||||||
|
## 运行时拓扑
|
||||||
|
|
||||||
|
`DesktopOpRuntime.runTask()` 是核心执行入口:
|
||||||
|
|
||||||
|
```text
|
||||||
|
DesktopTask
|
||||||
|
-> SafetyGuard 校验 task/app/action
|
||||||
|
-> AdapterRegistry 选择 AppAdapter
|
||||||
|
-> WindowLocator 定位并激活窗口
|
||||||
|
-> adapter.preFlightCheck()
|
||||||
|
-> adapter.buildSteps()
|
||||||
|
-> ActionExecutor 逐步执行 clipboard / hotkey / wait 等动作
|
||||||
|
-> adapter.verifyResult()
|
||||||
|
-> TaskResult
|
||||||
|
```
|
||||||
|
|
||||||
|
所有任务都接收 `AbortSignal`。运行时在开始、激活窗口后、preflight 后、每个动作前和 verify 前检查中止状态,因此 [[agent-channel]] 删除频道、关闭 weixinReply 或任务超时时可以级联取消。
|
||||||
|
|
||||||
|
## 服务层队列与互斥
|
||||||
|
|
||||||
|
`DesktopOpService` 提供两个入口:
|
||||||
|
|
||||||
|
- `runAndWait(task, timeoutMs)`:同步等待结果,当前由 [[tool-system]] 的 `weixin_send_text` 调用。
|
||||||
|
- `enqueue(task)`:fire-and-forget 留口,后续可用于异步桌面任务。
|
||||||
|
|
||||||
|
队列按 `appId + adapter.queueKey(target)` 分组,同一个微信会话串行执行;真正执行前还会获取全局 `DesktopMutex`,避免多个任务同时争用键鼠。每个 queue key 最多保留 20 个任务,溢出时写 `queue-overflow` 审计日志。
|
||||||
|
|
||||||
|
## WeixinAdapter
|
||||||
|
|
||||||
|
`runtime/adapters/weixin_adapter.ts` 是当前唯一适配器:
|
||||||
|
|
||||||
|
- `appId = "weixin"`,支持 `send-text`。
|
||||||
|
- 通过 `WindowLocator.findByAppName("Weixin")` 找到未最小化的最大微信窗口。
|
||||||
|
- preflight 截图并用 VLM 宽松判断当前顶部对话名是否匹配目标群。
|
||||||
|
- `buildSteps()` 固定生成 `clipboard-write -> ctrl+v -> wait -> enter -> wait`。
|
||||||
|
- verify 再次截图并用 VLM 检查最新己方消息,但 MVP 中为避免 VLM 误判导致重复发送,build steps 成功后默认按成功处理。
|
||||||
|
|
||||||
|
这意味着 Desktop Op 当前不是完整自主 GUI 探索 Agent,而是“适配器主导的确定性动作序列 + VLM 状态验证”。
|
||||||
|
|
||||||
|
## 数据模型
|
||||||
|
|
||||||
|
| 表名 | Entity | 用途 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `desktop_op_config` | `entity/desktop_op_config.ts` | 全局白名单、危险按键、频率上限和默认水印 |
|
||||||
|
| `desktop_op_action_log` | `entity/desktop_op_action_log.ts` | 每个桌面任务的终态审计,包括 channelId、roomName、modelChannel、状态、耗时和错误 |
|
||||||
|
|
||||||
|
`desktop_op_config` 不再保存默认模型渠道;v4 约定模型来自桌面操作 Agent 自己的 `modelChannelId`。
|
||||||
|
|
||||||
|
## 与微信双 Agent 的关系
|
||||||
|
|
||||||
|
weixin-db 群聊自动回复采用双 Agent:
|
||||||
|
|
||||||
|
1. reply agent 读取群消息、决定是否回复,并通过 `delegate_task` 委托。
|
||||||
|
2. desktop agent 持有 `weixin_desktop` toolset 和 `weixin_send_text` 工具。
|
||||||
|
3. `weixin_send_text` 从 `_netaRuntime.bizContext.channelId` 和 `currentAgent.modelChannelId` 读取运行时上下文。
|
||||||
|
4. 工具创建 `DesktopTask(appId="weixin", actionType="send-text")`,交给 `DesktopOpService.runAndWait()`。
|
||||||
|
5. Desktop Op 激活微信窗口、粘贴、发送并记录 `desktop_op_action_log`。
|
||||||
|
|
||||||
|
保存 weixin-db 渠道配置时,后端会自动为 desktop agent 补齐 `weixin_desktop` toolset,并把 `weixin_send_text` 配置为可在子 Agent 中使用且强制走主进程代理。
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [[agent-channel]] — weixin-db 渠道和双 Agent 回复配置
|
||||||
|
- [[tool-system]] — `weixin_send_text` 工具和工具路由
|
||||||
|
- [[netaclaw-module]] — NetaClaw 主模块与外部渠道入口
|
||||||
|
- [[frontend-architecture]] — 频道管理页的 weixinReply 配置 UI
|
||||||
|
- [[database-entity-overview]] — desktop_op 表速查
|
||||||
71
docs/code-wiki/entities/document-skills.md
Normal file
71
docs/code-wiki/entities/document-skills.md
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
---
|
||||||
|
title: 文档处理 Skills
|
||||||
|
created: 2026-05-02
|
||||||
|
updated: 2026-05-02
|
||||||
|
type: entity
|
||||||
|
tags: [skill, module, backend]
|
||||||
|
sources: [packages/backend/skills/minimax-pdf/, packages/backend/skills/minimax-docx/, packages/backend/skills/minimax-xlsx/]
|
||||||
|
---
|
||||||
|
|
||||||
|
# 文档处理 Skills
|
||||||
|
|
||||||
|
文档处理 Skills 是 2026-04-26 之后新增的一组本地 skill 包,主要面向 PDF、DOCX、XLSX 的生成、编辑、排版、验证和格式修复。它们属于 [[skill-system]] 的 compute-toolkit 方向:核心能力通过 `SKILL.md` 指导 Agent,实际工作由 skill 目录内脚本和参考文档完成。
|
||||||
|
|
||||||
|
## 当前包
|
||||||
|
|
||||||
|
| Skill | 目录 | 主要能力 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `minimax-pdf` | `packages/backend/skills/minimax-pdf/` | 长文本 PDF 生成、封面、正文渲染、调色、重排版、填表与合并 |
|
||||||
|
| `minimax-docx` | `packages/backend/skills/minimax-docx/` | DOCX 创建、内容编辑、模板套用、样式检查、OpenXML 顺序修复、批注和修订 |
|
||||||
|
| `minimax-xlsx` | `packages/backend/skills/minimax-xlsx/` | XLSX 创建、读取分析、行列插入、公式检查、样式审计、重新打包和验证 |
|
||||||
|
|
||||||
|
这些 skill 不是 NetaClaw 核心运行时的一部分,但会通过 [[skill-runtime]] 被 Agent 发现、读取和执行。
|
||||||
|
|
||||||
|
## minimax-pdf
|
||||||
|
|
||||||
|
`minimax-pdf` 包含:
|
||||||
|
|
||||||
|
- `SKILL.md` 和 `README.md`
|
||||||
|
- `design/design.md`
|
||||||
|
- `scripts/make.sh`
|
||||||
|
- Python 渲染脚本:`cover.py`、`render_body.py`、`fill_write.py`、`reformat_parse.py` 等
|
||||||
|
- `render_cover.js`
|
||||||
|
- `skill.config.yaml`
|
||||||
|
|
||||||
|
近期修复重点是长文本场景不再走容易失败的 JSON 拼接路线,而改用 Markdown 路线;同时增加跨平台兼容处理,包括 Python 自动检测和本地浏览器优先。
|
||||||
|
|
||||||
|
## minimax-docx
|
||||||
|
|
||||||
|
`minimax-docx` 是三者中体量最大的 skill,包含大量 OpenXML 参考资料、XSD、样式模板和 .NET 实现:
|
||||||
|
|
||||||
|
- `scripts/dotnet/MiniMaxAIDocx.Core/`:DOCX 创建、编辑、模板、验证、批注、修订、样式分析等命令。
|
||||||
|
- `references/`:OpenXML element order、命名空间、单位、排版、批注、修订和场景指南。
|
||||||
|
- `assets/styles/` 与 `assets/xsd/`:样式和验证资源。
|
||||||
|
|
||||||
|
它要求 Agent 在执行前读取具体场景 references,不能只凭 `SKILL.md` 直接生成复杂 OpenXML。
|
||||||
|
|
||||||
|
## minimax-xlsx
|
||||||
|
|
||||||
|
`minimax-xlsx` 主要由 Python 脚本和最小 XLSX 模板组成:
|
||||||
|
|
||||||
|
- `scripts/xlsx_create.py`、`xlsx_reader.py`、`xlsx_insert_row.py`、`xlsx_add_column.py`
|
||||||
|
- `scripts/formula_check.py`、`style_audit.py`、`libreoffice_recalc.py`
|
||||||
|
- `templates/minimal_xlsx/`
|
||||||
|
- `references/create.md`、`edit.md`、`format.md`、`validate.md`
|
||||||
|
|
||||||
|
它的设计目标是让 Agent 通过结构化脚本操作 XLSX ZIP/XML,而不是直接用脆弱的字符串拼接改 Office 文件。
|
||||||
|
|
||||||
|
## 与运行时的边界
|
||||||
|
|
||||||
|
- 这些包放在 `packages/backend/skills/`,由 [[skill-system]] 扫描。
|
||||||
|
- 如果声明 `skill.config.yaml`,由 [[skill-runtime]] 分类为 compute-toolkit 或 compute-entry。
|
||||||
|
- 如果脚本执行 cwd 位于 skill 目录内,bash 工具可注入该 skill 的 scoped env。
|
||||||
|
- 执行脚本仍受 [[tool-governance]] 和 [[tool-runtime-policy]] 约束;禁用 bash 或限制 shell 会影响 compute-toolkit。
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [[skill-system]]
|
||||||
|
- [[skill-runtime]]
|
||||||
|
- [[tool-system]]
|
||||||
|
- [[tool-governance]]
|
||||||
|
- [[project-overview]]
|
||||||
97
docs/code-wiki/entities/geo-module.md
Normal file
97
docs/code-wiki/entities/geo-module.md
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
---
|
||||||
|
title: Geo 账号与代理模块
|
||||||
|
created: 2026-05-07
|
||||||
|
updated: 2026-05-07
|
||||||
|
type: entity
|
||||||
|
tags: [module, backend, frontend, database]
|
||||||
|
sources: [packages/backend/src/modules/geo/, packages/frontend/src/modules/geo/, docs/superpowers/specs/2026-05-03-geo-master-roadmap.md, docs/superpowers/specs/2026-05-03-geo-s1-infrastructure-design.md]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Geo 账号与代理模块
|
||||||
|
|
||||||
|
Geo 模块是 2026-05-07 新增的账号、代理 IP 与浏览器 profile 绑定基础设施,服务养号、电商自动化和后续 Agent 操作账号矩阵。它和 [[netabrowser-runtime]] 协作:Geo 负责账号/IP/登录态数据,浏览器 daemon 负责真正打开带指纹、代理和持久 profile 的 Chromium。
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```text
|
||||||
|
packages/backend/src/modules/geo/
|
||||||
|
├── config.ts
|
||||||
|
├── controller/admin/account.ts
|
||||||
|
├── controller/admin/proxy_ip.ts
|
||||||
|
├── entity/account.ts
|
||||||
|
├── entity/proxy_ip.ts
|
||||||
|
├── provider/proxy/
|
||||||
|
│ ├── interface.ts
|
||||||
|
│ ├── local.ts
|
||||||
|
│ └── tianqi.ts
|
||||||
|
└── service/
|
||||||
|
├── account.ts
|
||||||
|
├── encrypt.ts
|
||||||
|
└── proxy_ip.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
前端入口在 `packages/frontend/src/modules/geo/`,包含 `accounts.vue`、`proxies.vue` 和 `dashboard.vue`。
|
||||||
|
|
||||||
|
## 数据模型
|
||||||
|
|
||||||
|
| 表 | Entity | 职责 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `geo_account` | `entity/account.ts` | 平台账号、登录态、sessionName、fingerprintSeed、cookie、绑定 Agent 和绑定 IP |
|
||||||
|
| `geo_proxy_ip` | `entity/proxy_ip.ts` | 本地/第三方代理、协议、区域、城市、出口 IP、套餐、健康状态和账号 1:1 绑定 |
|
||||||
|
|
||||||
|
关键约束:
|
||||||
|
|
||||||
|
- `geo_account.sessionName` 唯一,对应 [[netabrowser-runtime]] 的浏览器 profile。
|
||||||
|
- `geo_account.fingerprintSeed` 让同一账号每次启动保持稳定指纹,不同账号表现为不同物理设备。
|
||||||
|
- `geo_proxy_ip.bindAccountId` 和 `geo_account.proxyId` 形成强 1:1 绑定。
|
||||||
|
- 切换 IP 或重置账号会生成新的 `sessionName` 和 `fingerprintSeed`,清空 cookie,并尝试删除旧 profile。
|
||||||
|
|
||||||
|
## 账号流程
|
||||||
|
|
||||||
|
`GeoAccountService` 的主路径:
|
||||||
|
|
||||||
|
```text
|
||||||
|
add()
|
||||||
|
-> 创建账号
|
||||||
|
-> 生成 sessionName / fingerprintSeed
|
||||||
|
-> 可选绑定 proxy
|
||||||
|
|
||||||
|
launch()
|
||||||
|
-> 根据账号和 IP 构造 daemon open 参数
|
||||||
|
-> 调用 /admin/browser-daemon/open
|
||||||
|
-> 如果同名 session 已存在则 goto 复用
|
||||||
|
-> fresh profile 时可注入数据库 cookie
|
||||||
|
|
||||||
|
captureCookies()
|
||||||
|
-> 调用 browser-daemon cookie-list
|
||||||
|
-> 写入 cookies/cookieCapturedAt/cookieExpiresAt/loginStatus
|
||||||
|
-> 尝试从 cookie 或 DOM 提取 loginAccount
|
||||||
|
```
|
||||||
|
|
||||||
|
这些流程说明 Geo 不是简单 CRUD,而是账号状态机、IP 绑定和浏览器 profile 生命周期的协调层。
|
||||||
|
|
||||||
|
## 代理 Provider
|
||||||
|
|
||||||
|
`provider/proxy/` 当前提供:
|
||||||
|
|
||||||
|
- `local`:本地直连模式。
|
||||||
|
- `tianqi`:第三方代理 Provider 骨架。
|
||||||
|
- `interface.ts`:统一 Provider 接口。
|
||||||
|
|
||||||
|
`GeoProxyIpService` 负责把数据库记录转换为浏览器 daemon 可消费的 proxy 信息;SOCKS5 用户名密码的 Chromium 兼容问题由 [[netabrowser-runtime]] 的本地 HTTP 桥处理。
|
||||||
|
|
||||||
|
## 前端入口
|
||||||
|
|
||||||
|
Geo 前端在 `packages/frontend/src/modules/geo/config.ts` 注册独立模块入口,当前页面分为:
|
||||||
|
|
||||||
|
- 账号页:新增账号、选择平台、绑定 Agent/IP、启动浏览器、抓取 cookie、重置会话、切换 IP。
|
||||||
|
- 代理页:管理本地/第三方代理、查看状态、绑定关系和健康信息。
|
||||||
|
- Dashboard:当前为轻量入口,后续可承接账号矩阵概览。
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [[project-overview]]
|
||||||
|
- [[netabrowser-runtime]]
|
||||||
|
- [[database-entity-overview]]
|
||||||
|
- [[frontend-architecture]]
|
||||||
|
- [[cool-admin-framework]]
|
||||||
59
docs/code-wiki/entities/image-generation-tools.md
Normal file
59
docs/code-wiki/entities/image-generation-tools.md
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
title: 图像生成工具
|
||||||
|
created: 2026-05-07
|
||||||
|
updated: 2026-05-07
|
||||||
|
type: entity
|
||||||
|
tags: [tool, agent, backend, llm]
|
||||||
|
sources: [packages/backend/src/modules/netaclaw/tools/builtin/text_to_image.ts, packages/backend/src/modules/netaclaw/tools/builtin/image_to_image.ts, packages/backend/src/modules/netaclaw/image_providers/, packages/backend/src/modules/netaclaw/service/image_storage.ts, docs/superpowers/specs/2026-05-02-image-generation-tools-design.md]
|
||||||
|
---
|
||||||
|
|
||||||
|
# 图像生成工具
|
||||||
|
|
||||||
|
图像生成工具是 2026-05-03 新增的 AIGC 工具族,为 [[tool-system]] 增加 `text_to_image` 和 `image_to_image`。它支持火山引擎 Ark 与 MiniMax 图像模型,通过模型渠道配置读取 provider 凭据,并把生成结果持久化后返回给前端渲染。
|
||||||
|
|
||||||
|
## 工具
|
||||||
|
|
||||||
|
| 工具 | 文件 | 能力 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `text_to_image` | `tools/builtin/text_to_image.ts` | 根据 prompt 生成图片,支持宽高比、尺寸、数量、水印、seed 和 provider 扩展参数 |
|
||||||
|
| `image_to_image` | `tools/builtin/image_to_image.ts` | 基于输入图片和 prompt 做图生图 |
|
||||||
|
| `image_common.ts` | `tools/builtin/image_common.ts` | 尺寸裁剪、结果持久化和工具返回格式 |
|
||||||
|
|
||||||
|
两者注册到 Tool Catalog 的 `aigc` toolset,能力标识为 `image_aigc`,`requiresModel: true`,因此最终可见性仍由 [[tool-governance]] 和 Agent 模型渠道配置决定。
|
||||||
|
|
||||||
|
## Provider 层
|
||||||
|
|
||||||
|
`image_providers/` 抽象了 provider 差异:
|
||||||
|
|
||||||
|
- `types.ts`:统一 `ImageGenerationProvider`、凭据、请求和错误类型。
|
||||||
|
- `ark.ts`:火山引擎 Ark 图像生成实现。
|
||||||
|
- `minimax.ts`:MiniMax 图像生成实现。
|
||||||
|
|
||||||
|
工具执行时会从模型渠道读取 supplier、baseUrl、apiKey 和 `extra.imageDefaults/imageConstraints`,再合并用户参数。provider 错误会被包装成可重试/不可重试的文本结果,避免直接把底层异常泄漏给模型。
|
||||||
|
|
||||||
|
## 结果持久化
|
||||||
|
|
||||||
|
`ImageStorageService` 负责把 URL/base64 图片结果落到后端可访问存储,并返回结构化结果。工具返回不再只是纯文本,而是可包含图片数组的 `rawResult`,由 [[frontend-architecture]] 的 renderer registry 决定展示方式。
|
||||||
|
|
||||||
|
## 与工具系统的关系
|
||||||
|
|
||||||
|
图像工具继承当前工具链路:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Tool Catalog 注册
|
||||||
|
-> Tool Governance 同步/启停
|
||||||
|
-> ToolResolver 根据 Agent 和模型渠道注入
|
||||||
|
-> runAgent 执行工具
|
||||||
|
-> WebSocket 推送结构化 tool_result
|
||||||
|
-> 前端 renderer 展示图片结果
|
||||||
|
```
|
||||||
|
|
||||||
|
这意味着排查“Agent 看不到图像工具”时,不应只看 `tools/builtin` 文件,还要检查 catalog 同步、工具治理、模型渠道是否支持 image provider。
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [[tool-system]]
|
||||||
|
- [[tool-catalog]]
|
||||||
|
- [[tool-governance]]
|
||||||
|
- [[llm-providers]]
|
||||||
|
- [[frontend-architecture]]
|
||||||
63
docs/code-wiki/entities/llm-providers.md
Normal file
63
docs/code-wiki/entities/llm-providers.md
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
title: LLM 提供商适配
|
||||||
|
created: 2026-04-13
|
||||||
|
updated: 2026-04-14
|
||||||
|
type: entity
|
||||||
|
tags: [llm, agent, api]
|
||||||
|
sources: [packages/backend/src/modules/netaclaw/plugins/llm_providers/, packages/backend/src/modules/netaclaw/runtime/thinking.ts]
|
||||||
|
---
|
||||||
|
|
||||||
|
# LLM 提供商适配
|
||||||
|
|
||||||
|
多模型提供商的统一适配层,将不同 API 格式转换为内部统一接口。支持 Thinking/Reasoning 能力的统一管理。
|
||||||
|
|
||||||
|
## 关键文件
|
||||||
|
|
||||||
|
| 文件 | 提供商 | API 格式 |
|
||||||
|
|------|--------|---------|
|
||||||
|
| `llm_providers/anthropic.ts` | Anthropic | Messages API(原生,支持 extended_thinking) |
|
||||||
|
| `llm_providers/openai.ts` | OpenAI / 兼容 | Chat Completions(支持 reasoning_effort) |
|
||||||
|
| `llm_providers/deepseek.ts` | DeepSeek | OpenAI 兼容格式(支持 reasoning_content) |
|
||||||
|
| `llm_providers/plugin_entry.ts` | - | 提供商接口定义 |
|
||||||
|
| `llm_providers/logging.ts` | - | 请求/响应日志 |
|
||||||
|
| `runtime/thinking.ts` | - | 思考能力检测、预算映射、参数构建 |
|
||||||
|
|
||||||
|
## 模型选择
|
||||||
|
|
||||||
|
通过 `runtime/model_selection.ts` 解析 `"provider:modelId"` 格式:
|
||||||
|
|
||||||
|
```
|
||||||
|
"anthropic:claude-3-5-sonnet" → Anthropic 适配器
|
||||||
|
"openai:gpt-4o" → OpenAI 适配器
|
||||||
|
"openai:MiniMax-M2.7" → OpenAI 兼容(自定义 baseUrl)
|
||||||
|
"deepseek:deepseek-chat" → DeepSeek 适配器
|
||||||
|
```
|
||||||
|
|
||||||
|
## Thinking/Reasoning 支持
|
||||||
|
|
||||||
|
各提供商的思考能力通过 `runtime/thinking.ts` 统一管理:
|
||||||
|
|
||||||
|
| 提供商 | 参数注入 | 思考内容提取 |
|
||||||
|
|--------|---------|-------------|
|
||||||
|
| Anthropic | `thinking: { type: 'adaptive' }` 或 `{ type: 'enabled', budget_tokens: N }` | response.content[] 中 `{ type: 'thinking' }` 块 |
|
||||||
|
| OpenAI | `reasoning_effort: 'low'|'medium'|'high'` | `thinking` 字段 |
|
||||||
|
| DeepSeek | 无参数控制 | `reasoning_content` 字段 或 `<think>` 标签 |
|
||||||
|
|
||||||
|
详见 [[thinking-system]]。
|
||||||
|
|
||||||
|
## 模型渠道管理
|
||||||
|
|
||||||
|
通过 `netaclaw_model_channel` 表配置:
|
||||||
|
- `supplier`:OpenAI / Anthropic / DeepSeek
|
||||||
|
- `baseUrl`:API 地址(支持自定义,如 MiniMax)
|
||||||
|
- `apiKey`:密钥
|
||||||
|
- `models`:JSON 数组 `[{name, capability}]`
|
||||||
|
|
||||||
|
前端通过级联选择器选择:供应商 → 渠道 → 模型
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [[netaclaw-module]] — 所属模块
|
||||||
|
- [[agent-runtime]] — 运行时初始化提供商
|
||||||
|
- [[thinking-system]] — Thinking/Reasoning 能力详解
|
||||||
|
- [[crew-orchestration]] — Crew 编排中的模型配置解析
|
||||||
111
docs/code-wiki/entities/memory-system.md
Normal file
111
docs/code-wiki/entities/memory-system.md
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
---
|
||||||
|
title: 记忆系统
|
||||||
|
created: 2026-04-13
|
||||||
|
updated: 2026-04-26
|
||||||
|
type: entity
|
||||||
|
tags: [memory, agent]
|
||||||
|
sources: [packages/backend/src/modules/netaclaw/memory/, packages/backend/src/modules/netaclaw/service/memory_admin.ts, packages/backend/src/modules/netaclaw/service/memory_type.ts, packages/backend/src/modules/netaclaw/tools/builtin/memory.ts, packages/backend/src/modules/netaclaw/tools/builtin/memory_types.ts, packages/frontend/src/modules/agent/views/memory.vue]
|
||||||
|
---
|
||||||
|
|
||||||
|
# 记忆系统
|
||||||
|
|
||||||
|
Agent 的长期记忆存储,支持跨会话知识积累。2026-04-26 后,记忆系统从“对话开始静默预取”调整为“Prompt 强约束 + Agent 显式调用工具”,并补齐了管理页面、类型管理、MySQL/SQLite 双后端分页和统计能力。
|
||||||
|
|
||||||
|
## 关键文件
|
||||||
|
|
||||||
|
| 文件 | 职责 |
|
||||||
|
|------|------|
|
||||||
|
| `memory/provider.ts` | 记忆提供商接口定义 |
|
||||||
|
| `memory/factory.ts` | 工厂函数(MySQL/SQLite 切换) |
|
||||||
|
| `memory/registry.ts` | 按 Agent 缓存 MemoryProvider,统一路由 MySQL/SQLite |
|
||||||
|
| `memory/mysql_provider.ts` | MySQL 实现 |
|
||||||
|
| `memory/sqlite_provider.ts` | SQLite 实现(本地轻量,含迁移、FTS、busy_timeout) |
|
||||||
|
| `service/memory_admin.ts` | 管理端记忆分页、增删改查、统计 |
|
||||||
|
| `service/memory_type.ts` | 记忆类型 CRUD 和系统类型删除保护 |
|
||||||
|
| `tools/builtin/memory.ts` | `memory_save` / `memory_recall` 工具 |
|
||||||
|
| `tools/builtin/memory_types.ts` | `memory_list_types` / `memory_stats` 工具 |
|
||||||
|
| `views/memory.vue` | 前端记忆管理页 |
|
||||||
|
|
||||||
|
## 提供商接口
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface MemoryProvider {
|
||||||
|
save(entry): Promise<MemoryEntry>;
|
||||||
|
update(id, partial): Promise<MemoryEntry>;
|
||||||
|
delete(id): Promise<void>;
|
||||||
|
search(query, opts): Promise<MemoryEntry[]>;
|
||||||
|
list(opts): Promise<MemoryEntry[]>;
|
||||||
|
getById(id): Promise<MemoryEntry | null>;
|
||||||
|
page(opts): Promise<MemoryPageResult>;
|
||||||
|
count(opts): Promise<number>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`page` 和 `count` 是管理页与统计侧的关键接口;`userId` 在管理查询里可选,以支持跨用户检索。[[tool-governance]] 在解析工具时会根据 Agent 的 memory 配置注入记忆工具。
|
||||||
|
|
||||||
|
## 记忆类型
|
||||||
|
|
||||||
|
| 类型 | 用途 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `user` | 用户画像和偏好 | 系统内置类型 |
|
||||||
|
| `project` | 项目知识和进展 | 系统内置类型 |
|
||||||
|
| `feedback` | 用户纠正/确认 | 系统内置类型 |
|
||||||
|
| `reference` | 外部资源链接 | 系统内置类型 |
|
||||||
|
| 自定义类型 | 业务扩展 | 存储在 `netaclaw_memory_type` |
|
||||||
|
|
||||||
|
`netaclaw_memory_type` 使用 `key/name/description/icon/isSystem` 描述类型。系统内置类型不可删除;Agent 可通过 `memory_list_types` 查看当前可用类型。
|
||||||
|
|
||||||
|
## 工作流
|
||||||
|
|
||||||
|
```
|
||||||
|
Agent 执行前
|
||||||
|
→ AgentExecutor 检查 memory.enabled
|
||||||
|
→ MemoryProviderRegistry 按 Agent 选择 MySQL/SQLite provider
|
||||||
|
→ provider.count() 只判断是否已有记忆,不静默读取内容
|
||||||
|
→ prompt_builder 在 Layer 1.5 注入记忆行为指令
|
||||||
|
→ tool_resolver 强制注入 memory_save / memory_recall / memory_list_types / memory_stats
|
||||||
|
→ Agent 必须显式调用 memory_recall 后再使用或更新记忆
|
||||||
|
```
|
||||||
|
|
||||||
|
这条路径的设计目标是避免旧的静默 prefetch 把过期或不相关记忆塞进上下文。Prompt 会要求已有记忆时先 `memory_recall`,需要全量回忆时使用 `query="*"`,环境类记忆写入前要先检索,避免多设备/路径信息重复写入。
|
||||||
|
|
||||||
|
## 工具语义
|
||||||
|
|
||||||
|
| 工具 | 作用 | 关键约束 |
|
||||||
|
|------|------|---------|
|
||||||
|
| `memory_save` | create/update/delete 记忆 | create 时同 agent/user/type/name 已存在则幂等更新 |
|
||||||
|
| `memory_recall` | 按 query/type/id 检索 | query 为空或 `*` 时走 list;search 失败时 fallback list |
|
||||||
|
| `memory_list_types` | 列出可用类型 | 优先读 DB 类型,失败回落内置类型 |
|
||||||
|
| `memory_stats` | 统计当前 Agent 记忆数量和类型分布 | 基于 provider.list |
|
||||||
|
|
||||||
|
## 数据表
|
||||||
|
|
||||||
|
`netaclaw_memory`:
|
||||||
|
- `agentName`(索引)、`userId`(索引)
|
||||||
|
- `type`(user/project/feedback/reference/自定义)
|
||||||
|
- `name`(标题)、`content`(正文)、`description`
|
||||||
|
- `metadata`(结构化元数据)
|
||||||
|
|
||||||
|
`netaclaw_memory_type`:
|
||||||
|
- `key`(唯一类型标识)
|
||||||
|
- `name`、`description`、`icon`
|
||||||
|
- `isSystem`(系统类型删除保护)
|
||||||
|
|
||||||
|
## 管理页
|
||||||
|
|
||||||
|
`/agent/memory` 提供记忆管理 UI:
|
||||||
|
|
||||||
|
- 左侧按 Agent 展示统计,并标识 MySQL/SQLite 后端。
|
||||||
|
- 右侧支持按 Agent、类型、关键词分页筛选。
|
||||||
|
- 支持新增、编辑、删除记忆;编辑时使用 `updatedAt` 做乐观锁。
|
||||||
|
- 类型管理弹窗可新增/删除自定义类型,系统类型不可删除。
|
||||||
|
|
||||||
|
管理 API 在 `/admin/netaclaw/memory/*` 和 `/admin/netaclaw/memory_type/*` 下,跨后端查询时 MySQL 直接分页,SQLite Agent 通过 provider 聚合后排序分页。
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [[netaclaw-module]] — 所属模块
|
||||||
|
- [[agent-runtime]] — 运行时注入记忆工具和 Prompt
|
||||||
|
- [[prompt-builder]] — 记忆行为指令所在的 Prompt 分层
|
||||||
|
- [[tool-system]] — memory_save/recall/list_types/stats 工具
|
||||||
|
- [[frontend-architecture]] — `/agent/memory` 管理入口
|
||||||
167
docs/code-wiki/entities/mysql-data-source.md
Normal file
167
docs/code-wiki/entities/mysql-data-source.md
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
---
|
||||||
|
title: MySQL 数据源与问数系统
|
||||||
|
created: 2026-05-15
|
||||||
|
updated: 2026-05-15
|
||||||
|
type: entity
|
||||||
|
tags: [database, tool, agent, backend, frontend]
|
||||||
|
sources: [packages/backend/src/modules/netaclaw/entity/data_source.ts, packages/backend/src/modules/netaclaw/entity/data_source_query_audit.ts, packages/backend/src/modules/netaclaw/service/data_source.ts, packages/backend/src/modules/netaclaw/service/mysql_pool.ts, packages/backend/src/modules/netaclaw/service/mysql_schema.ts, packages/backend/src/modules/netaclaw/service/mysql_query.ts, packages/backend/src/modules/netaclaw/service/secret_crypto.ts, packages/backend/src/modules/netaclaw/tools/builtin/mysql.ts, packages/backend/src/modules/netaclaw/controller/admin/data_source.ts, packages/frontend/src/modules/base/views/data-source.vue, packages/backend/skills/data-analyst-mysql/SKILL.md]
|
||||||
|
---
|
||||||
|
|
||||||
|
# MySQL 数据源与问数系统
|
||||||
|
|
||||||
|
MySQL 数据源与问数系统为 [[netaclaw-module]] 增加授权范围内的只读数据库分析能力。它由后台数据源配置页、连接密钥加密、连接池、schema/sample/query 服务、`mysql_*` 工具、SQL guard 和查询审计组成,并通过 [[tool-system]] 暴露给 Agent。
|
||||||
|
|
||||||
|
## 管理入口
|
||||||
|
|
||||||
|
前端入口在“系统管理 -> 数据源管理”,页面文件为 `packages/frontend/src/modules/base/views/data-source.vue`。
|
||||||
|
|
||||||
|
该页面支持:
|
||||||
|
|
||||||
|
- 新增、编辑、删除、启停 MySQL 数据源。
|
||||||
|
- 配置 Host/IP、端口、数据库、用户名、密码和 SSL。
|
||||||
|
- 配置授权 Agent、表白名单、表黑名单、脱敏列、schema 可见性、最大返回行数、最大 JOIN 表数、查询超时和连接超时。
|
||||||
|
- 点击“测试连接”调用 `/admin/netaclaw/data-source/test`,不会直接把连接信息暴露给 Agent。
|
||||||
|
|
||||||
|
菜单来自 `base_sys_menu` 动态路由,不是前端静态路由。`packages/backend/src/modules/base/event/menu.ts` 在后端启动时会确保 `/sys/data-source` 被同步到“系统管理”下,并给 admin 角色补齐菜单权限;`packages/backend/src/modules/base/menu.json` 保留种子菜单配置。详见 [[base-module]] 和 [[frontend-architecture]]。
|
||||||
|
|
||||||
|
## 后端 API
|
||||||
|
|
||||||
|
`packages/backend/src/modules/netaclaw/controller/admin/data_source.ts` 暴露管理端接口:
|
||||||
|
|
||||||
|
| API | 方法 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `/admin/netaclaw/data-source/list` | GET | 管理员查看全部数据源;传 `agentId` 时返回该 Agent 授权摘要 |
|
||||||
|
| `/admin/netaclaw/data-source/save` | POST | 新增或部分更新数据源配置 |
|
||||||
|
| `/admin/netaclaw/data-source/delete` | POST | 删除数据源并关闭连接池 |
|
||||||
|
| `/admin/netaclaw/data-source/test` | POST | 使用现有或临时配置测试连接 |
|
||||||
|
|
||||||
|
`NetaClawDataSourceService` 对外返回管理安全投影:保留 host、database、username 等管理字段,但只返回 `hasPassword`,不返回明文密码或密文。给 Agent 的 `listForAgent()` 只返回 `name`、`label`、`database`、`status`,不返回连接地址、账号或密码。
|
||||||
|
|
||||||
|
## 数据模型
|
||||||
|
|
||||||
|
### netaclaw_data_source
|
||||||
|
|
||||||
|
`NetaClawDataSourceEntity` 存储连接配置与授权策略:
|
||||||
|
|
||||||
|
- `name` 唯一标识,供工具调用时作为 `source`。
|
||||||
|
- `type` 当前固定为 `mysql`。
|
||||||
|
- `host`、`port`、`database`、`username` 描述连接目标。
|
||||||
|
- `passwordEncrypted` 保存 AES-256-GCM 加密后的密码。
|
||||||
|
- `readonly` 固定为只读语义。
|
||||||
|
- `status` 控制是否可用。
|
||||||
|
- `allowedAgentIds` 限定哪些 Agent 可使用该数据源。
|
||||||
|
- `extra` 保存安全策略:`allowedTables`、`blockedTables`、`maskedColumns`、`schemaVisibility`、`maxRows`、`maxJoinTables`、`queryTimeoutMs`、`connectTimeout`、`poolConnectionLimit`、`ssl`。
|
||||||
|
|
||||||
|
### netaclaw_data_source_query_audit
|
||||||
|
|
||||||
|
`NetaClawDataSourceQueryAuditEntity` 记录 `mysql_query` 的审计结果:
|
||||||
|
|
||||||
|
- `dataSourceId`、`agentId`、`userId`、`toolCallId` 绑定调用来源。
|
||||||
|
- `sqlHash` 保存 SQL 哈希,`sqlPreview` 保存前 1000 字符预览。
|
||||||
|
- `status` 为 `success`、`rejected` 或 `failed`。
|
||||||
|
- `rejectReason` 记录 SQL guard 拒绝原因。
|
||||||
|
- `elapsedMs`、`rowCount`、`errorCode` 用于诊断查询表现和失败原因。
|
||||||
|
|
||||||
|
## 密钥加密
|
||||||
|
|
||||||
|
`SecretCryptoService` 使用 AES-256-GCM 加密连接密码。密钥来源优先级:
|
||||||
|
|
||||||
|
```text
|
||||||
|
NETA_SECRET_KEY -> APP_SECRET -> module.user.jwt.secret
|
||||||
|
```
|
||||||
|
|
||||||
|
本地开发环境没有配置 `NETA_SECRET_KEY` / `APP_SECRET` 时,会回退到用户模块已有的 JWT secret,避免“测试连接”因为缺失环境变量失败。生产环境仍建议显式配置专用 `NETA_SECRET_KEY`,避免与登录 token 密钥耦合。
|
||||||
|
|
||||||
|
## 连接池
|
||||||
|
|
||||||
|
`MysqlPoolManager` 基于 `mysql2/promise` 创建连接池:
|
||||||
|
|
||||||
|
- `getPool(source)` 按数据源 ID 缓存连接池。
|
||||||
|
- `createTransientPool(source)` 用于测试连接,不进入长期缓存。
|
||||||
|
- `closePool(id)` 在保存或删除数据源后关闭旧池,避免连接配置变更后继续复用旧连接。
|
||||||
|
- pool 配置使用 `extra.poolConnectionLimit`、`extra.connectTimeout`、`extra.ssl`,并通过 `SecretCryptoService` 解密密码。
|
||||||
|
|
||||||
|
## 工具集
|
||||||
|
|
||||||
|
`packages/backend/src/modules/netaclaw/tools/builtin/mysql.ts` 注册四个工具,toolset 为 `mysql`:
|
||||||
|
|
||||||
|
| 工具 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| `mysql_list_sources` | 列出当前 Agent 授权的数据源摘要 |
|
||||||
|
| `mysql_schema` | 查询授权表的字段、索引、主键、外键和脱敏标记 |
|
||||||
|
| `mysql_table_sample` | 查询授权表少量未脱敏字段样本 |
|
||||||
|
| `mysql_query` | 执行经过 guard 校验的只读 SELECT,并写审计 |
|
||||||
|
|
||||||
|
这些工具在 manifest 中统一走 `main-process-proxy`,即使被子 Agent 调用,也必须回到主进程执行。这样连接池、密钥、授权、SQL guard 和审计不会落到 worker 进程。
|
||||||
|
|
||||||
|
## Schema 与 Sample
|
||||||
|
|
||||||
|
`MysqlIntrospectionService` 负责 schema 和样本读取。
|
||||||
|
|
||||||
|
关键规则:
|
||||||
|
|
||||||
|
- `schemaVisibility=allowed-only` 时,只返回白名单表详情。
|
||||||
|
- `schemaVisibility=all-names-only` 时,白名单外表只可见表名/表注释,不返回字段详情。
|
||||||
|
- `mysql_schema` 从 `information_schema.TABLES` 获取 `TABLE_COMMENT`,从 `information_schema.COLUMNS` 获取字段,避免误把 `TABLE_COMMENT` 当成 COLUMNS 字段。
|
||||||
|
- `mysql_table_sample` 默认只选择未脱敏字段。
|
||||||
|
- 采样校验使用小写映射,但实际 SQL 使用 schema 中的原始列名,兼容 `tenantId`、`isCrewMaster` 这类驼峰字段。
|
||||||
|
- 显式请求脱敏列会抛 `masked_column_denied`。
|
||||||
|
|
||||||
|
典型成功链路:
|
||||||
|
|
||||||
|
```text
|
||||||
|
mysql_list_sources
|
||||||
|
-> mysql_schema
|
||||||
|
-> mysql_table_sample
|
||||||
|
-> 模型基于样本总结分析
|
||||||
|
```
|
||||||
|
|
||||||
|
复杂分析或聚合问题可进一步调用 `mysql_query`。
|
||||||
|
|
||||||
|
## SQL Guard
|
||||||
|
|
||||||
|
`MysqlQueryService` 的 `validateMysqlReadOnlySql()` 在执行前做只读防护:
|
||||||
|
|
||||||
|
- 只允许 `SELECT`。
|
||||||
|
- 拒绝 `SHOW`、`DESCRIBE`、`EXPLAIN`、DML、DDL、DCL、事务、`CALL`、CTE、UNION、用户变量、临时表、危险函数、文件输出、锁定读、隐式 JOIN、派生表和 offset limit。
|
||||||
|
- 拒绝 schema-qualified 表名,禁止跨库访问。
|
||||||
|
- 检查所有表都在 `allowedTables` 中,且不在 `blockedTables` 中。
|
||||||
|
- 限制 JOIN 表数和最大行数。
|
||||||
|
- 无显式 `LIMIT` 时包一层外部 LIMIT,最多取 `maxRows + 1` 判断截断。
|
||||||
|
- 允许单个末尾分号,并在执行前去掉;真正多语句仍被拒绝。
|
||||||
|
- 对含脱敏字段的表,拒绝 `SELECT *`、`table.*`、别名通配以及脱敏列引用。
|
||||||
|
|
||||||
|
执行时使用 MySQL `execute()`,查询失败会写 `failed` 审计;guard 拒绝会写 `rejected` 审计。
|
||||||
|
|
||||||
|
## Prompt Skill
|
||||||
|
|
||||||
|
`packages/backend/skills/data-analyst-mysql/SKILL.md` 是 prompt 类型 skill,用于约束 Agent 的 MySQL 问数流程:
|
||||||
|
|
||||||
|
- 必须先 `mysql_list_sources`,不能猜数据源。
|
||||||
|
- 先 `mysql_schema` 再设计 SQL。
|
||||||
|
- 只能使用 `mysql_*` 工具处理数据库问题。
|
||||||
|
- 输出需要包含结论、SQL、假设和限制。
|
||||||
|
- 明确禁止修改数据、绕过授权、暴露连接信息和输出未授权内容。
|
||||||
|
|
||||||
|
该 skill 需要与 [[skill-system]]、[[skill-runtime]] 和 [[tool-governance]] 共同理解:skill 只是行为约束,真正的安全边界由服务端授权、SQL guard、连接池和审计共同实现。
|
||||||
|
|
||||||
|
## 常见故障
|
||||||
|
|
||||||
|
| 症状 | 原因 | 修复点 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| “系统管理”里看不到“数据源管理” | 现有库菜单来自 `base_sys_menu`,不会自动读取 `menu.json` | `base/event/menu.ts` 启动同步 `/sys/data-source` |
|
||||||
|
| 测试连接报缺少 `NETA_SECRET_KEY` / `APP_SECRET` | 加密服务只读环境变量 | `SecretCryptoService` 回退到 `module.user.jwt.secret` |
|
||||||
|
| `mysql_schema` 报 `TABLE_COMMENT` 不存在 | 表注释来自 `information_schema.TABLES`,不是 COLUMNS | schema 查询拆分 TABLES 和 COLUMNS |
|
||||||
|
| `SELECT ... LIMIT 20;` 被拒绝 | SQL guard 把任意分号都当多语句 | 允许单个末尾分号并规范化 |
|
||||||
|
| `mysql_table_sample` 对驼峰字段报 `column_not_allowed` | 校验与执行列名大小写映射不一致 | 用小写校验、原始列名执行 |
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [[netaclaw-module]]
|
||||||
|
- [[tool-system]]
|
||||||
|
- [[tool-catalog]]
|
||||||
|
- [[tool-governance]]
|
||||||
|
- [[skill-system]]
|
||||||
|
- [[frontend-architecture]]
|
||||||
|
- [[base-module]]
|
||||||
|
- [[database-entity-overview]]
|
||||||
80
docs/code-wiki/entities/netabrowser-runtime.md
Normal file
80
docs/code-wiki/entities/netabrowser-runtime.md
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
title: Netabrowser 反风控浏览器运行时
|
||||||
|
created: 2026-05-07
|
||||||
|
updated: 2026-05-07
|
||||||
|
type: entity
|
||||||
|
tags: [module, backend, runtime, tool]
|
||||||
|
sources: [packages/netabrowser-cli/, packages/backend/src/modules/netaclaw/browser-daemon/, packages/backend/skills/netabrowser-cli/, packages/backend/skills/patchwright-cli/, docs/superpowers/specs/2026-05-04-netabrowser-cli-s1-design.md]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Netabrowser 反风控浏览器运行时
|
||||||
|
|
||||||
|
Netabrowser 是 2026-05-07 新增的浏览器自动化子系统,目标是为养号、电商自动化和 AI 探索复杂网页提供更接近真实用户的 Chromium 会话。它由 `packages/netabrowser-cli/`、NetaClaw 后端 `browser-daemon/` 和 `netabrowser-cli` skill 共同组成,并被 [[geo-module]] 用来启动账号绑定的持久 profile。
|
||||||
|
|
||||||
|
## 架构分层
|
||||||
|
|
||||||
|
```text
|
||||||
|
Agent / Geo / 电商自动化
|
||||||
|
-> packages/backend/skills/netabrowser-cli/scripts/nb.cmd|nb.sh
|
||||||
|
-> packages/netabrowser-cli CLI
|
||||||
|
-> backend /admin/browser-daemon/*
|
||||||
|
-> BrowserDaemonService
|
||||||
|
-> patchright + neta-chromium + 持久 profile
|
||||||
|
```
|
||||||
|
|
||||||
|
`netabrowser-cli` skill 明确要求 AI 使用脚本封装而不是直接 curl HTTP;后端 daemon 则提供真实控制接口,便于 Geo 模块和前端管理页面复用。
|
||||||
|
|
||||||
|
## 后端 daemon
|
||||||
|
|
||||||
|
`packages/backend/src/modules/netaclaw/browser-daemon/` 包含:
|
||||||
|
|
||||||
|
| 目录/文件 | 职责 |
|
||||||
|
| --- | --- |
|
||||||
|
| `service/daemon.service.ts` | 打开/关闭/导航/交互/截图/cookie/state 的核心服务 |
|
||||||
|
| `runtime/session-registry.ts` | 活跃 session 注册表和 per-session lock |
|
||||||
|
| `runtime/session-scheduler.ts` | 并发槽位、优先级、idle timeout 和驱逐策略 |
|
||||||
|
| `runtime/chromium-launcher.ts` | Chromium 路径解析和启动参数 |
|
||||||
|
| `runtime/browser-data-dir.ts` | profile/state 路径 |
|
||||||
|
| `runtime/socks5-http-bridge.ts` | SOCKS5 用户名密码代理转本地 HTTP 桥 |
|
||||||
|
| `service/fingerprint.service.ts` | fingerprintSeed 到启动参数的映射 |
|
||||||
|
| `service/humanizer.service.ts` | 拟人化鼠标、滚动和输入 |
|
||||||
|
| `service/snapshot-ref.service.ts` | snapshot ref 到页面元素选择器的映射 |
|
||||||
|
|
||||||
|
daemon 对外通过 `/admin/browser-daemon/*` controller 暴露 session、navigation、interaction、inspect、state 等操作,并由 `control-auth.middleware.ts` 做控制入口鉴权。
|
||||||
|
|
||||||
|
## 会话与 profile
|
||||||
|
|
||||||
|
核心语义:
|
||||||
|
|
||||||
|
- `sessionName` 是浏览器会话和磁盘 profile 的稳定标识。
|
||||||
|
- 同名 session 启动时如果已活跃会返回 409,调用方可改用 `goto` 复用。
|
||||||
|
- profile 位于 `.netabrowser-data/profiles/<sessionName>`,state 文件位于 dataDir 的 states 目录。
|
||||||
|
- idle timeout 时 daemon 会保存 storage state 并关闭浏览器。
|
||||||
|
- `fingerprintSeed` 缺省时不注入 fingerprint 参数,避免中文字体 fallback 异常;传入时保持账号级稳定指纹。
|
||||||
|
|
||||||
|
## 反风控能力
|
||||||
|
|
||||||
|
当前能力组合:
|
||||||
|
|
||||||
|
- `patchright`:降低 Playwright 自动化痕迹。
|
||||||
|
- `fingerprint-chromium` / neta-chromium:提供 canvas、WebGL、字体、屏幕等硬件指纹差异。
|
||||||
|
- 拟人化交互:`click`、`hover`、`fill`、`type`、`scroll` 默认走 Humanizer。
|
||||||
|
- 中文字体补丁:通过 `addInitScript` 注入字体 fallback,避免 portable Chromium 中文乱码。
|
||||||
|
- SOCKS5 auth 兼容:Chromium 不支持带用户名密码的 SOCKS5 时,自动起本地 HTTP -> SOCKS5 桥。
|
||||||
|
|
||||||
|
## Skills
|
||||||
|
|
||||||
|
新增两个浏览器相关 skill:
|
||||||
|
|
||||||
|
- `netabrowser-cli`:面向国内强反风控站点和账号矩阵场景,要求使用 `scripts/nb.sh` / `nb.cmd`。
|
||||||
|
- `patchwright-cli`:面向 Cloudflare、DataDome 等国外反爬站点,是 Playwright CLI 的反检测替代。
|
||||||
|
|
||||||
|
两者都通过 [[skill-system]] 被发现。`netabrowser-cli` 属于 prompt/脚本型操作指南,实际 HTTP 控制仍落在 browser daemon。
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [[geo-module]]
|
||||||
|
- [[skill-system]]
|
||||||
|
- [[tool-system]]
|
||||||
|
- [[frontend-architecture]]
|
||||||
|
- [[windows-runtime]]
|
||||||
189
docs/code-wiki/entities/netaclaw-module.md
Normal file
189
docs/code-wiki/entities/netaclaw-module.md
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
---
|
||||||
|
title: NetaClaw 模块
|
||||||
|
created: 2026-04-13
|
||||||
|
updated: 2026-05-15
|
||||||
|
type: entity
|
||||||
|
tags: [module, agent, architecture]
|
||||||
|
sources: [packages/backend/src/modules/netaclaw/]
|
||||||
|
---
|
||||||
|
|
||||||
|
# NetaClaw 模块
|
||||||
|
|
||||||
|
项目的 AI Agent 引擎核心,实现了完整的 ReAct 循环推理、多 LLM 提供商支持、工具系统、记忆系统、技能系统、Multi-Agent Crew 编排、普通会话内子 Agent 委派、Session Tree 会话运行时、上下文压缩和外部渠道接入。2026-05-14 后,微信渠道重点演进为 `weixin-db` 本地群聊代理,并通过 [[desktop-op-module]] 与 `weixin_send_text` 形成双 Agent 自动回复链路。2026-05-15 后,NetaClaw 增加 MySQL 只读问数后端能力,数据源配置、SQL guard、schema/sample/query 工具和审计均收敛在主进程服务侧。
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
netaclaw/
|
||||||
|
├── entity/ # 数据模型(含 legacy session/message 与 session-tree 新表)
|
||||||
|
├── controller/ # REST API
|
||||||
|
├── gateway/ # WebSocket 网关(/netaclaw + /crew 两个命名空间)
|
||||||
|
├── runtime/ # Agent 执行引擎核心(含 TodoStore、Thinking、Compaction)
|
||||||
|
├── memory/ # 长期记忆系统
|
||||||
|
├── tools/ # 工具定义、内置工具和 Operations 执行后端
|
||||||
|
├── browser-daemon/ # Netabrowser 后端 daemon(浏览器 session、拟人化交互、state/cookie)
|
||||||
|
├── plugins/ # LLM 提供商适配层(含 Thinking/Reasoning)
|
||||||
|
├── service/ # 业务逻辑(Skill、Crew、Channel、Executor、Tool Resolver、Chat Orchestrator)
|
||||||
|
├── platforms/ # 接入平台(预留)
|
||||||
|
├── session-tree/ # Session Tree provider、snapshot、projection、context builder
|
||||||
|
├── subagent/ # 子 Agent worker 进程、协议、runner
|
||||||
|
└── config.ts # 模块配置
|
||||||
|
```
|
||||||
|
|
||||||
|
## 核心子系统(23个)
|
||||||
|
|
||||||
|
| 子系统 | 关键文件 | 职责 |
|
||||||
|
|--------|---------|------|
|
||||||
|
| [[agent-runtime]] | `runtime/agent.ts` | ReAct 循环执行引擎 |
|
||||||
|
| [[session-tree-runtime]] | `session-tree/provider.ts`, `session-tree/snapshot.ts` | 会话树、快照、active path、节点投影 |
|
||||||
|
| [[crew-orchestration]] | `service/crew_orchestrator.ts` | Multi-Agent 编排(委派/并行/升级) |
|
||||||
|
| [[websocket-gateway]] | `gateway/server.ts`, `gateway/crew_server.ts` | 实时通信(对话 + Crew 监控) |
|
||||||
|
| [[tool-system]] | `tools/builtin/*.ts` | 内置工具与执行链路 |
|
||||||
|
| [[image-generation-tools]] | `tools/builtin/text_to_image.ts`, `tools/builtin/image_to_image.ts` | 文生图、图生图和图片结果持久化 |
|
||||||
|
| [[runtime-process-events]] | `runtime/process_events.ts` | 长耗时工具/Skill 过程事件、采样和 JSONL 日志 |
|
||||||
|
| [[tool-operations]] | `tools/operations/*.ts` | 文件、进程和搜索底层执行后端抽象 |
|
||||||
|
| [[tool-catalog]] | `tools/catalog.ts` | 工具 schema 注册与默认工具集 |
|
||||||
|
| [[tool-governance]] | `service/tool_registry.ts`, `service/tool_resolver.ts` | Tool 全局治理、Agent 级过滤与 Prompt Hint |
|
||||||
|
| [[subagent-session]] | `service/subagent.ts`, `service/chat_orchestrator.ts`, `session-tree/subagent_projection.ts` | 普通会话内子 Agent 委派、结果聚合与回放投影 |
|
||||||
|
| [[context-compaction]] | `runtime/compaction.ts` | 长会话压缩与 compacted/full 视图 |
|
||||||
|
| [[prompt-builder]] | `runtime/prompt_builder.ts` | 8层分层 Prompt 注入系统 |
|
||||||
|
| [[memory-system]] | `memory/*.ts` | 长期记忆(MySQL/SQLite) |
|
||||||
|
| [[mysql-data-source]] | `service/data_source.ts`, `service/mysql_schema.ts`, `service/mysql_query.ts`, `tools/builtin/mysql.ts` | MySQL 数据源、schema/sample/query、SQL guard 和问数审计 |
|
||||||
|
| [[skill-system]] | `service/skill_loader.ts` | 技能加载、安装、注册和诊断 |
|
||||||
|
| [[skill-runtime]] | `service/skill_config.ts`, `service/skill_executor.ts`, `service/skill_secret.ts` | Skill 分类、compute-entry 执行和 scoped secrets |
|
||||||
|
| [[document-skills]] | `packages/backend/skills/minimax-*` | PDF / DOCX / XLSX 文档处理 skill 包 |
|
||||||
|
| [[vehicle-damage-skill]] | `packages/backend/skills/vehicle-damage-inspection/` | 汽车环车视频旧伤检测 compute-entry skill |
|
||||||
|
| [[netabrowser-runtime]] | `browser-daemon/*`, `packages/netabrowser-cli/` | 反风控浏览器 daemon、CLI、拟人化交互和持久 profile |
|
||||||
|
| [[llm-providers]] | `plugins/llm_providers/*.ts` | 多模型适配(含 Thinking) |
|
||||||
|
| [[agent-channel]] | `service/agent_channel.ts`, `service/weixin_db.ts` | 外部渠道接入;当前包含 ClawBot 私聊和 weixin-db 本地群聊代理 |
|
||||||
|
| [[desktop-op-module]] | `packages/backend/src/modules/desktop_op/` | 桌面 GUI 操作运行时;当前支撑微信自动发送 |
|
||||||
|
| [[todo-system]] | `runtime/todo_store.ts` | 会话级任务管理 |
|
||||||
|
|
||||||
|
## 数据模型(19个表)
|
||||||
|
|
||||||
|
| 表名 | Entity 文件 | 用途 |
|
||||||
|
|------|------------|------|
|
||||||
|
| `netaclaw_agent` | `entity/agent.ts` | Agent 配置和发布 |
|
||||||
|
| `netaclaw_session` | `entity/session.ts` | 对话会话 |
|
||||||
|
| `netaclaw_message` | `entity/message.ts` | 消息历史 |
|
||||||
|
| `netaclaw_skill` | `entity/skill.ts` | Skill 元数据、env schema 和加密 secrets |
|
||||||
|
| `netaclaw_model_channel` | `entity/model_channel.ts` | 模型渠道 |
|
||||||
|
| `netaclaw_memory` | `entity/memory.ts` | 长期记忆 |
|
||||||
|
| `netaclaw_memory_type` | `entity/memory_type.ts` | 记忆类型和系统类型保护 |
|
||||||
|
| `netaclaw_tool` | `entity/tool.ts` | Tool 全局治理配置 |
|
||||||
|
| `netaclaw_subagent_session` | `entity/subagent_session.ts` | 普通会话子 Agent 记录 |
|
||||||
|
| `netaclaw_crew` | `entity/crew.ts` | Crew 集群定义 |
|
||||||
|
| `netaclaw_crew_agent` | `entity/crew_agent.ts` | 集群-Agent 关联 |
|
||||||
|
| `netaclaw_crew_run` | `entity/crew_run.ts` | Crew 运行记录 |
|
||||||
|
| `netaclaw_crew_task` | `entity/crew_task.ts` | Crew 子任务记录 |
|
||||||
|
| `netaclaw_agent_channel` | `entity/agent_channel.ts` | 外部渠道配置 |
|
||||||
|
| `netaclaw_agent_channel_group` | `entity/agent_channel_group.ts` | 渠道群白名单、触发策略和每群 Agent 覆盖 |
|
||||||
|
| `netaclaw_agent_session` | `entity/agent_session.ts` | Session Tree 会话头与叶子状态 |
|
||||||
|
| `netaclaw_agent_session_entry` | `entity/agent_session_entry.ts` | Session Tree 节点持久化 |
|
||||||
|
| `netaclaw_data_source` | `entity/data_source.ts` | MySQL 数据源连接、授权和安全策略配置 |
|
||||||
|
| `netaclaw_data_source_query_audit` | `entity/data_source_query_audit.ts` | MySQL 问数 SQL 执行、拒绝和失败审计 |
|
||||||
|
|
||||||
|
当前数据模型需要区分两条线:
|
||||||
|
|
||||||
|
- legacy 线:`netaclaw_session` + `netaclaw_message`,仍服务旧历史消息、压缩和部分兼容逻辑。
|
||||||
|
- session-tree 线:`netaclaw_agent_session` + `netaclaw_agent_session_entry`,已经是 Agent Chat 主路径的会话状态载体。
|
||||||
|
|
||||||
|
## 关键字段
|
||||||
|
|
||||||
|
### netaclaw_agent
|
||||||
|
- `name`(唯一)、`label`、`description`、`icon`
|
||||||
|
- `systemPrompt`(系统提示词)
|
||||||
|
- `skills`(JSON 数组,关联的 Skill 名称)
|
||||||
|
- `modelConfig`(JSON:apiUrl/apiKey/modelId/contextWindow)
|
||||||
|
- `config`(JSON:memory/defaultThinkLevel 等)
|
||||||
|
- `status`(0=草稿 1=已发布)
|
||||||
|
|
||||||
|
### netaclaw_skill
|
||||||
|
- `name`(唯一)、`label`、`description`
|
||||||
|
- `skillType`(能力类型:compute/llm/multimodal)
|
||||||
|
- `tags`、`version`、`source`、`sourceUrl`
|
||||||
|
- `installSpec`、`metadata`、`fingerprint`、`installedAt`
|
||||||
|
- `secrets`(AES-256-GCM 加密后的 skill scoped secrets)
|
||||||
|
- `envSchema`(环境变量声明,供 [[skill-runtime]] 和前端配置页使用)
|
||||||
|
|
||||||
|
### netaclaw_message
|
||||||
|
- `sessionId`、`role`(user/assistant/tool/system)
|
||||||
|
- `content`、`thinking`(思考内容)
|
||||||
|
- `toolCalls`(JSON)、`toolCallId`、`skillName`、`metadata`
|
||||||
|
|
||||||
|
### netaclaw_agent_session
|
||||||
|
- `sessionId`、`agentId`、`userId`
|
||||||
|
- `rootEntryId`、`leafEntryId`
|
||||||
|
- `parentSessionId`、`title`、`status`
|
||||||
|
- `metadata`(会话级扩展状态)
|
||||||
|
|
||||||
|
### netaclaw_agent_session_entry
|
||||||
|
- `sessionId`、`entryId`、`parentEntryId`
|
||||||
|
- `type`(message / compaction / branch_summary / subagent_batch / subagent_result 等)
|
||||||
|
- `content`、`message`、`metadata`
|
||||||
|
- `timestamp`
|
||||||
|
|
||||||
|
### netaclaw_agent_channel_group
|
||||||
|
- `channelId`、`roomId`(channel 内唯一)、`roomName`
|
||||||
|
- `status`(0 禁用、1 启用、-1 忽略)
|
||||||
|
- `triggerMode`(at_mention / all,prefix 仅兼容)
|
||||||
|
- `boundAgentId`(每群 Agent 覆盖)
|
||||||
|
- `replyIdentityOverride`、`firstSeenAt`、`lastSeenAt`、`lastActiveAt`
|
||||||
|
|
||||||
|
### netaclaw_data_source
|
||||||
|
- `name`(唯一)、`label`、`type`(当前为 mysql)
|
||||||
|
- `host`、`port`、`database`、`username`
|
||||||
|
- `passwordEncrypted`(AES-256-GCM 密文)
|
||||||
|
- `readonly`、`status`
|
||||||
|
- `allowedAgentIds`(JSON 数组)
|
||||||
|
- `extra`(JSON:allowedTables、blockedTables、maskedColumns、schemaVisibility、maxRows、maxJoinTables、queryTimeoutMs、SSL 等)
|
||||||
|
|
||||||
|
### netaclaw_data_source_query_audit
|
||||||
|
- `dataSourceId`、`agentId`、`userId`、`toolCallId`
|
||||||
|
- `sqlHash`、`sqlPreview`
|
||||||
|
- `status`(success / rejected / failed)
|
||||||
|
- `rejectReason`、`elapsedMs`、`rowCount`、`errorCode`
|
||||||
|
|
||||||
|
### netaclaw_memory_type
|
||||||
|
- `key`(唯一类型标识)
|
||||||
|
- `name`、`description`、`icon`
|
||||||
|
- `isSystem`(1=系统内置,不允许删除)
|
||||||
|
|
||||||
|
## 当前主运行路径
|
||||||
|
|
||||||
|
当前 NetaClaw 的普通对话主路径已经是:
|
||||||
|
|
||||||
|
```text
|
||||||
|
gateway/server.ts
|
||||||
|
-> chat_orchestrator.ts
|
||||||
|
-> session-tree/*
|
||||||
|
-> tool_resolver.ts / runtime/agent.ts
|
||||||
|
-> memory/registry.ts + memory providers
|
||||||
|
-> subagent.ts
|
||||||
|
-> gateway/protocol.ts + frontend chat store
|
||||||
|
```
|
||||||
|
|
||||||
|
这意味着理解 Neta 项目时,不能再只把 `netaclaw_message` 和线性历史当成核心事实;需要优先理解 [[session-tree-runtime]]、[[agent-runtime]] 和 [[subagent-session]] 三者的协作。
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [[project-overview]] — 项目总览
|
||||||
|
- [[agent-runtime]] — 执行引擎详解
|
||||||
|
- [[session-tree-runtime]] — 会话树运行时
|
||||||
|
- [[crew-orchestration]] — Multi-Agent 编排
|
||||||
|
- [[websocket-gateway]] — 通信协议
|
||||||
|
- [[tool-catalog]] — 工具目录系统
|
||||||
|
- [[tool-operations]] — 工具底层执行后端抽象
|
||||||
|
- [[prompt-builder]] — Prompt 分层注入
|
||||||
|
- [[patch-tool]] — Patch 模糊补丁工具
|
||||||
|
- [[clarify-tool]] — Clarify 澄清工具
|
||||||
|
- [[tool-system]] — 工具系统
|
||||||
|
- [[image-generation-tools]] — 文生图/图生图工具
|
||||||
|
- [[runtime-process-events]] — 工具和 Skill 过程事件
|
||||||
|
- [[memory-system]] — 长期记忆和记忆管理页
|
||||||
|
- [[mysql-data-source]] — MySQL 数据源、问数工具和 SQL guard
|
||||||
|
- [[skill-runtime]] — Skill 运行时执行和配置
|
||||||
|
- [[document-skills]] — 文档处理 Skills
|
||||||
|
- [[vehicle-damage-skill]] — 车辆旧伤检测 Skill
|
||||||
|
- [[netabrowser-runtime]] — 反风控浏览器运行时
|
||||||
|
- [[desktop-op-module]] — 桌面 GUI 操作运行时
|
||||||
67
docs/code-wiki/entities/patch-tool.md
Normal file
67
docs/code-wiki/entities/patch-tool.md
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
title: Patch Tool 模糊补丁工具
|
||||||
|
created: 2026-04-16
|
||||||
|
updated: 2026-04-16
|
||||||
|
type: entity
|
||||||
|
tags: [tool, agent]
|
||||||
|
sources: [packages/backend/src/modules/netaclaw/tools/builtin/patch.ts, packages/backend/src/modules/netaclaw/tools/fuzzy_match.ts]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Patch Tool 模糊补丁工具
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
文件局部查找替换工具,支持 9 级模糊匹配。比 `write_file` 更安全(只改局部)、更省 token(不需要传完整文件内容)。注册在 [[tool-catalog]] 的 `base` 工具集。
|
||||||
|
|
||||||
|
## Schema 定义(TypeBox)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const PatchParams = Type.Object({
|
||||||
|
path: Type.String({ description: '文件绝对路径' }),
|
||||||
|
old_string: Type.String({ description: '要查找的文本片段' }),
|
||||||
|
new_string: Type.String({ description: '替换为的文本' }),
|
||||||
|
replace_all: Type.Optional(Type.Boolean({ description: '替换所有匹配,默认 false' })),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
1. 读取目标文件内容
|
||||||
|
2. 调用 `fuzzyFindAll()` 进行模糊匹配
|
||||||
|
3. 验证匹配结果(0个报错 / 多个且非 replace_all 报错 / 唯一通过)
|
||||||
|
4. 从后往前替换(避免索引偏移)
|
||||||
|
5. 写回文件
|
||||||
|
|
||||||
|
## 9 级模糊匹配引擎
|
||||||
|
|
||||||
|
**文件**: `tools/fuzzy_match.ts`(~304行)
|
||||||
|
|
||||||
|
| 级别 | 策略 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 1 | `exact` | 精确字符串匹配 |
|
||||||
|
| 2 | `line_trimmed` | 每行 trim 后匹配 |
|
||||||
|
| 3 | `whitespace_normalized` | 所有空白压缩为单空格 |
|
||||||
|
| 4 | `indent_flexible` | 去除行首缩进 |
|
||||||
|
| 5 | `escape_normalized` | 转义字符还原(`\n` → 换行) |
|
||||||
|
| 6 | `trimmed_boundary` | 首尾行 trim |
|
||||||
|
| 7 | `unicode_normalized` | Unicode 标点归一化(智能引号等) |
|
||||||
|
| 8 | `block_anchor` | 首尾行锚定 + 中间相似度 |
|
||||||
|
| 9 | `context_aware` | 逐行相似度滑动窗口 |
|
||||||
|
|
||||||
|
**核心算法**: Levenshtein 编辑距离 → `similarity = 1 - (distance / maxLen)`
|
||||||
|
|
||||||
|
**返回结构**:
|
||||||
|
```typescript
|
||||||
|
interface FuzzyMatchResult {
|
||||||
|
strategy: string; // 匹配策略名
|
||||||
|
startIndex: number; // 原始文本起始位置
|
||||||
|
endIndex: number; // 原始文本结束位置
|
||||||
|
matchedText: string; // 匹配的原始文本
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 关联页面
|
||||||
|
|
||||||
|
- [[tool-catalog]] — 工具目录系统(注册在 base 工具集)
|
||||||
|
- [[tool-system]] — 工具系统总览
|
||||||
|
- [[agent-runtime]] — Agent 运行时执行
|
||||||
57
docs/code-wiki/entities/project-module.md
Normal file
57
docs/code-wiki/entities/project-module.md
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
---
|
||||||
|
title: Project 模块
|
||||||
|
created: 2026-04-13
|
||||||
|
updated: 2026-04-14
|
||||||
|
type: entity
|
||||||
|
tags: [module, project, backend, frontend]
|
||||||
|
sources: [packages/backend/src/modules/project/, packages/frontend/src/modules/project/]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Project 模块
|
||||||
|
|
||||||
|
项目管理模块,支持甘特图、日历、看板、列表四种视图,含完整的工时记录系统。
|
||||||
|
|
||||||
|
## 后端数据模型(5个表)
|
||||||
|
|
||||||
|
| 表名 | 用途 |
|
||||||
|
|------|------|
|
||||||
|
| `project_info` | 项目信息 |
|
||||||
|
| `project_phase` | 项目阶段 |
|
||||||
|
| `project_task` | 任务(支持父子关系、优先级、负责人、工时、进度) |
|
||||||
|
| `project_task_dependency` | 任务依赖关系 |
|
||||||
|
| `project_time_log` | 工时记录(userId/logDate/hours/description) |
|
||||||
|
|
||||||
|
### project_task 关键字段
|
||||||
|
|
||||||
|
`estimatedHours`(decimal 8,1) | `actualHours`(decimal 8,1) | `progress`(0-100) | `color`(自定义颜色) | `status` | `priority` | `category` | `assigneeName` | `startDate` | `endDate` | `parentId`
|
||||||
|
|
||||||
|
## 后端 Service 核心方法
|
||||||
|
|
||||||
|
| 方法 | 功能 |
|
||||||
|
|------|------|
|
||||||
|
| `tree(projectId)` | 任务树(按阶段分组,含子任务层级) |
|
||||||
|
| `kanban(projectId)` | 看板数据(按状态分组:todo/inProgress/done/closed) |
|
||||||
|
| `kanbanSort(items)` | 看板排序/状态变更 |
|
||||||
|
| `cascadeUpdateFields(id, fields)` | 级联更新子任务字段(status/priority/category/assigneeName/dates/estimatedHours) |
|
||||||
|
| `hasChildren(id)` | 检查是否有子任务 |
|
||||||
|
|
||||||
|
## 前端四视图
|
||||||
|
|
||||||
|
| 视图 | 组件 | 依赖库 |
|
||||||
|
|------|------|--------|
|
||||||
|
| 甘特图 | `views/components/gantt.vue` | dhtmlx-gantt 9.1.3 |
|
||||||
|
| 日历 | `views/components/calendar.vue` | FullCalendar 6.1.20 |
|
||||||
|
| 看板 | `views/components/kanban.vue` | vuedraggable |
|
||||||
|
| 列表 | `views/components/table.vue` | Element Plus Table |
|
||||||
|
|
||||||
|
## 关键组件
|
||||||
|
|
||||||
|
- `task-drawer.vue`:任务详情侧抽屉(含工时显示:预估+实际自动计算、进度滑块、工时记录表)
|
||||||
|
- `phase-manager.vue`:阶段管理弹窗
|
||||||
|
- `time-log-dialog.vue`:工时记录弹窗(日期、工时、描述,提交后自动更新 actualHours)
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [[project-overview]] — 项目总览
|
||||||
|
- [[cool-admin-framework]] — 自动 CRUD
|
||||||
|
- [[database-entity-overview]] — 5 个业务表
|
||||||
162
docs/code-wiki/entities/project-overview.md
Normal file
162
docs/code-wiki/entities/project-overview.md
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
---
|
||||||
|
title: 项目总览
|
||||||
|
created: 2026-04-13
|
||||||
|
updated: 2026-05-14
|
||||||
|
type: entity
|
||||||
|
tags: [architecture, module]
|
||||||
|
sources: [packages/backend/package.json, packages/frontend/package.json, packages/frontend/src/modules/agent/config.ts, packages/frontend/src/modules/geo/config.ts, packages/backend/scripts/build-windows-installer.js, packages/windows-tray/, packages/backend/skills/, packages/netabrowser-cli/, packages/backend/src/modules/geo/, packages/backend/src/modules/netaclaw/browser-daemon/, packages/backend/src/modules/desktop_op/]
|
||||||
|
---
|
||||||
|
|
||||||
|
# 项目总览
|
||||||
|
|
||||||
|
Neta AI 电商是一个 AI 驱动的企业运营平台,采用 pnpm workspace Monorepo 架构。
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
| 包 | 路径 | 技术栈 | 端口 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 后端 | `packages/backend/` | Midway.js 3.20 + Cool Admin 8.0 + TypeORM | 8003 |
|
||||||
|
| 前端 | `packages/frontend/` | Vue 3.5 + Vite 5.4 + Element Plus 2.9 | 9001 |
|
||||||
|
| Windows 托盘 | `packages/windows-tray/` | .NET WinForms 托盘程序 | 本机 |
|
||||||
|
| 后端 Skills | `packages/backend/skills/` | `SKILL.md` + Python/Node/.NET/Bash 脚本 | Agent 运行时 |
|
||||||
|
| Netabrowser CLI | `packages/netabrowser-cli/` | Patchright + neta-chromium 浏览器自动化 CLI | 本机/daemon |
|
||||||
|
|
||||||
|
## 后端模块(16个)
|
||||||
|
|
||||||
|
| 模块 | 路径 | 职责 |
|
||||||
|
|------|------|------|
|
||||||
|
| **netaclaw** | `src/modules/netaclaw/` | AI Agent 引擎核心(ReAct、Session Tree、工具治理、记忆、技能、Crew、会话子代理、压缩、渠道接入) |
|
||||||
|
| **desktop_op** | `src/modules/desktop_op/` | 通用桌面 GUI Agent 运行时;当前用于 PC 微信窗口发送群聊回复 |
|
||||||
|
| **geo** | `src/modules/geo/` | 账号矩阵、代理 IP、浏览器 profile/sessionName、cookie 登录态和 Agent 绑定 |
|
||||||
|
| **base** | `src/modules/base/` | 用户、角色、菜单、权限(RBAC) |
|
||||||
|
| **project** | `src/modules/project/` | 项目管理(甘特图、日历、看板、列表) |
|
||||||
|
| **data** | `src/modules/data/` | 药品/医保数据管理 |
|
||||||
|
| **user** | `src/modules/user/` | C端应用用户 |
|
||||||
|
| **dict** | `src/modules/dict/` | 字典/枚举配置 |
|
||||||
|
| **task** | `src/modules/task/` | 定时任务调度 |
|
||||||
|
| **space** | `src/modules/space/` | 文件存储空间 |
|
||||||
|
| **notification** | `src/modules/notification/` | 通知服务(飞书等) |
|
||||||
|
| **plugin** | `src/modules/plugin/` | 插件系统 |
|
||||||
|
| **recycle** | `src/modules/recycle/` | 回收站 |
|
||||||
|
| **demo** | `src/modules/demo/` | 演示代码 |
|
||||||
|
| **swagger** | `src/modules/swagger/` | API 文档 |
|
||||||
|
|
||||||
|
## 前端模块(14个)
|
||||||
|
|
||||||
|
| 模块 | 路由前缀 | 职责 |
|
||||||
|
|------|---------|------|
|
||||||
|
| **agent** | `/agent/*` | AI 对话、Agent 管理、Tool 管理、Skill 管理、模型渠道、Crew 编排画布/监控、频道管理 |
|
||||||
|
| **geo** | `/geo/*` | 账号、代理 IP 和养号相关管理入口 |
|
||||||
|
| **base** | `/` | 登录、首页、权限管理 |
|
||||||
|
| **project** | `/project/*` | 项目管理四视图 |
|
||||||
|
| **data** | `/data/*` | 药品数据管理 |
|
||||||
|
| **ontology** | `/ontology/*` | 知识图谱可视化 |
|
||||||
|
| 其他 | - | dict, task, space, user, notification, helper, recycle, demo |
|
||||||
|
|
||||||
|
## Agent 模块新增入口
|
||||||
|
|
||||||
|
`packages/frontend/src/modules/agent/config.ts` 当前已注册:
|
||||||
|
|
||||||
|
- `/agent/chat`
|
||||||
|
- `/agent/agents`
|
||||||
|
- `/agent/tools`
|
||||||
|
- `/agent/skills`
|
||||||
|
- `/agent/model-channel`
|
||||||
|
- `/agent/channel-management`
|
||||||
|
- `/agent/detection-result`
|
||||||
|
- `/agent/crew-editor`
|
||||||
|
- `/agent/crew-monitor`
|
||||||
|
- `/agent/memory`
|
||||||
|
|
||||||
|
其中 `/agent/tools` 是 2026-04-19 前后新增的重要管理入口,对应 [[tool-governance]]。
|
||||||
|
`/agent/memory` 是 2026-04-26 新增的记忆管理入口,对应 [[memory-system]],支持跨 MySQL/SQLite 后端查看、编辑和类型管理。
|
||||||
|
`/agent/skills` 在 2026-04-27 后升级为 [[skill-runtime]] 管理入口,展示 prompt / compute-entry / compute-toolkit 分类、env secrets 配置和诊断信息。
|
||||||
|
`/agent/channel-management` 在 2026-05-14 后升级为微信渠道运营入口:支持 ClawBot 私聊和 weixin-db 本地群聊代理,并配置 v4 双 Agent 自动回复。
|
||||||
|
|
||||||
|
近期如果让 AI 快速熟悉项目,Agent 模块里优先级最高的入口已经变成:
|
||||||
|
|
||||||
|
- `/agent/chat`:Session Tree 对话、continue-from-entry、子 Agent 回放与诊断。
|
||||||
|
- `/agent/tools`:全局工具治理、runtime diagnostic、renderer/worker 路由。
|
||||||
|
- `/agent/agents`:单 Agent 配置,尤其是工具局部覆盖和子 Agent 策略。
|
||||||
|
- `/agent/memory`:长期记忆管理、类型管理、Agent 维度统计。
|
||||||
|
|
||||||
|
## Windows 本地部署
|
||||||
|
|
||||||
|
2026-04-25 后,项目新增 [[windows-runtime]] 子系统:
|
||||||
|
|
||||||
|
- 后端可打包为 `backend.exe`,安装态从同目录 `config.yaml` 读取端口、数据库和数据目录。
|
||||||
|
- `dataDir` 成为所有可写路径的统一根,包括日志、SQLite 记忆、file session、skills 和 runtime info。
|
||||||
|
- `packages/windows-tray/` 提供托盘程序,负责启动/附着/停止后端,并打开系统、日志目录和配置目录。
|
||||||
|
- `packages/backend/installer/setup.iss` 和 `scripts/build-windows-installer.js` 负责 Windows 安装包构建。
|
||||||
|
|
||||||
|
## 核心关系
|
||||||
|
|
||||||
|
- [[netaclaw-module]] 是整个平台的 AI 大脑
|
||||||
|
- [[tool-governance]] 管理 Agent 实际可见工具
|
||||||
|
- [[tool-operations]] 抽象工具底层文件、搜索和进程执行后端
|
||||||
|
- [[session-tree-runtime]] 管理 Agent Chat 的主会话状态载体
|
||||||
|
- [[subagent-session]] 为普通对话提供轻量子 Agent 能力
|
||||||
|
- [[context-compaction]] 解决长会话上下文膨胀问题
|
||||||
|
- [[memory-system]] 提供长期记忆和 `/agent/memory` 管理页面
|
||||||
|
- [[skill-runtime]] 提供 Skill 分类、配置、密钥和 compute-entry 执行
|
||||||
|
- [[document-skills]] 提供 PDF / DOCX / XLSX 文档处理能力
|
||||||
|
- [[image-generation-tools]] 提供文生图和图生图能力
|
||||||
|
- [[runtime-process-events]] 提供长耗时工具和 Skill 的过程进度表达
|
||||||
|
- [[vehicle-damage-skill]] 提供车辆环车视频旧伤检测能力
|
||||||
|
- [[geo-module]] 提供账号、代理 IP 和浏览器 profile 绑定能力
|
||||||
|
- [[netabrowser-runtime]] 提供反风控浏览器 CLI/daemon 和拟人化自动化能力
|
||||||
|
- [[desktop-op-module]] 提供本机桌面 GUI 操作运行时,当前用于微信自动发送
|
||||||
|
- [[windows-runtime]] 提供安装器、托盘和本机 runtime 控制
|
||||||
|
- [[project-module]] 提供项目管理能力
|
||||||
|
- [[base-module]] 提供认证和权限基础设施
|
||||||
|
- [[cool-admin-framework]] 提供自动 CRUD 和路由框架
|
||||||
|
|
||||||
|
## 技术栈版本
|
||||||
|
|
||||||
|
| 技术 | 版本 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| Node.js | >= 24.0.0 | 运行时 |
|
||||||
|
| Midway.js | 3.20.11 | 后端框架 |
|
||||||
|
| TypeORM | 0.3.20 | ORM |
|
||||||
|
| Vue | 3.5.13 | 前端框架 |
|
||||||
|
| Element Plus | 2.9.3 | UI 组件库 |
|
||||||
|
| Anthropic SDK | 0.81.0 | Claude API |
|
||||||
|
| OpenAI SDK | 4.73.0 | OpenAI/兼容 API |
|
||||||
|
| MCP SDK | 1.20.1 | MCP 协议 |
|
||||||
|
| Socket.IO | 4.8.3 | 实时通信 |
|
||||||
|
|
||||||
|
## 2026-04-23 当前理解项目的推荐顺序
|
||||||
|
|
||||||
|
如果要让新 Agent 或新人快速熟悉 Neta,推荐先读:
|
||||||
|
|
||||||
|
1. [[project-overview]]
|
||||||
|
2. [[netaclaw-module]]
|
||||||
|
3. [[agent-runtime]]
|
||||||
|
4. [[session-tree-runtime]]
|
||||||
|
5. [[subagent-session]]
|
||||||
|
6. [[tool-governance]]
|
||||||
|
7. [[tool-operations]]
|
||||||
|
8. [[skill-system]]
|
||||||
|
9. [[skill-runtime]]
|
||||||
|
10. [[frontend-architecture]]
|
||||||
|
|
||||||
|
这样能先建立“项目总览 -> Agent 主链路 -> 会话状态 -> 子 Agent -> 工具治理/执行后端 -> Skill 运行时 -> 前端消费”的主骨架,再去看单个工具或业务模块。
|
||||||
|
|
||||||
|
## 2026-05-07 增量
|
||||||
|
|
||||||
|
5 月 2 日之后新增的架构重点:
|
||||||
|
|
||||||
|
- [[image-generation-tools]]:NetaClaw 新增 `text_to_image` / `image_to_image`,通过 Ark/MiniMax provider 和模型渠道配置生成图片。
|
||||||
|
- [[runtime-process-events]]:长耗时工具和 compute-entry Skill 现在可以流式输出过程事件,并由前端时间线恢复历史回放。
|
||||||
|
- [[vehicle-damage-skill]]:新增汽车环车视频旧伤检测 Skill,输出候选、最终旧伤、证据帧和复核图。
|
||||||
|
- [[geo-module]]:新增账号与代理 IP 模块,为账号矩阵、cookie 登录态和浏览器 profile 绑定打基础。
|
||||||
|
- [[netabrowser-runtime]]:新增反风控浏览器 CLI/daemon,提供 patchright、neta-chromium、拟人化交互、代理和持久 profile。
|
||||||
|
|
||||||
|
## 2026-05-14 增量
|
||||||
|
|
||||||
|
5 月 8 日之后新增的架构重点:
|
||||||
|
|
||||||
|
- [[agent-channel]]:微信渠道拆分为 `weixin` ClawBot 私聊和 `weixin-db` 本地群聊代理;新增群白名单、每群 Agent 覆盖、触发策略和 v4 双 Agent 自动回复配置。
|
||||||
|
- [[desktop-op-module]]:新增通用桌面 GUI Agent 模块,封装窗口定位、截图、键鼠、VLM 验证、队列互斥、任务取消和审计日志。
|
||||||
|
- [[tool-system]]:新增 `weixin_send_text` 工具,通过 `_netaRuntime.bizContext` 读取 channelId,通过当前桌面 Agent 读取 modelChannel,并委托 Desktop Op 操作 PC 微信。
|
||||||
|
- [[frontend-architecture]]:频道管理页新增 weixin-db、wxid 唯一性校验、群聊管理和微信自动回复配置区块。
|
||||||
157
docs/code-wiki/entities/skill-system.md
Normal file
157
docs/code-wiki/entities/skill-system.md
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
---
|
||||||
|
title: 技能系统
|
||||||
|
created: 2026-04-13
|
||||||
|
updated: 2026-05-15
|
||||||
|
type: entity
|
||||||
|
tags: [skill, agent]
|
||||||
|
sources: [packages/backend/src/modules/netaclaw/service/skill_loader.ts, packages/backend/src/modules/netaclaw/service/skill_installer.ts, packages/backend/src/modules/netaclaw/service/skill_registry.ts, packages/backend/src/modules/netaclaw/service/skill_config.ts, packages/backend/src/modules/netaclaw/service/skill_secret.ts, packages/backend/src/modules/netaclaw/service/skill_executor.ts, packages/backend/src/modules/netaclaw/tools/builtin/read_skill.ts, packages/backend/src/modules/netaclaw/tools/builtin/execute_skill.ts, packages/backend/src/modules/netaclaw/controller/admin/skill.ts]
|
||||||
|
---
|
||||||
|
|
||||||
|
# 技能系统
|
||||||
|
|
||||||
|
Skill 是 Agent 的可插拔能力扩展,以 `SKILL.md` 为主入口,支持本地、GitHub、ZIP 三种安装方式。2026-04-27 后系统从单纯 prompt skill 扩展为 prompt / compute-entry / compute-toolkit 三分类:加载器负责扫描解析,安装器管理生命周期,注册表追踪指纹和来源,配置/密钥/执行器负责运行时能力,ToolResolver 负责把 `read_skill`、`read_skill_file`、`skill_manage` 和 `execute_skill` 注入 Agent。
|
||||||
|
|
||||||
|
## 关键文件
|
||||||
|
|
||||||
|
| 文件 | 职责 |
|
||||||
|
|------|------|
|
||||||
|
| `service/skill_loader.ts` | 扫描 skills/ 目录,解析 SKILL.md,条件过滤,构建 prompt |
|
||||||
|
| `service/skill_installer.ts` | GitHub 克隆、ZIP 解压、依赖安装(Node/UV)、卸载、更新检查 |
|
||||||
|
| `service/skill_registry.ts` | .skillhub/ 锁文件、SHA256 指纹、来源元数据管理 |
|
||||||
|
| `service/skill_config.ts` | 解析 `skill.config.yaml`,推导 prompt / compute-entry / compute-toolkit 分类 |
|
||||||
|
| `service/skill_secret.ts` | AES-256-GCM 加密保存 skill scoped secrets,并解析 env |
|
||||||
|
| `service/skill_executor.ts` | 执行 compute-entry skill,使用 stdin/stdout JSON 协议 |
|
||||||
|
| `service/tool_resolver.ts` | 构建 `<available_skills>` prompt,并按 Agent 配置注入 Skill 工具 |
|
||||||
|
| `tools/builtin/read_skill.ts` | Agent 工具:读取 SKILL.md 完整内容 + 附属文件索引 |
|
||||||
|
| `tools/builtin/read_skill_file.ts` | Agent 工具:读取 Skill 附属文件(references/ 等) |
|
||||||
|
| `tools/builtin/skill_manage.ts` | Agent 工具:创建/编辑/删除 Skill |
|
||||||
|
| `tools/builtin/execute_skill.ts` | Agent 工具:执行 compute-entry skill |
|
||||||
|
| `controller/admin/skill.ts` | REST API:CRUD + 安装/卸载/更新/上传/检查更新 |
|
||||||
|
|
||||||
|
## SKILL.md 格式
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
name: skill-name # 必填,唯一标识
|
||||||
|
description: 描述 # 必填
|
||||||
|
version: 1.0.0 # 可选
|
||||||
|
metadata:
|
||||||
|
skillType: llm|compute|multimodal
|
||||||
|
emoji: "🔧"
|
||||||
|
tags: [标签]
|
||||||
|
conditions:
|
||||||
|
requires_tools: ["bash"] # 需要这些工具才激活
|
||||||
|
fallback_for_tools: ["selenium"] # 作为这些工具的替代
|
||||||
|
install:
|
||||||
|
- kind: node
|
||||||
|
package: "@package/name"
|
||||||
|
- kind: uv
|
||||||
|
package: "package>=1.0.0"
|
||||||
|
---
|
||||||
|
# Markdown 内容(Agent 读取后遵循的指令)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Skill Runtime 分类
|
||||||
|
|
||||||
|
详见 [[skill-runtime]]。当前分类不是旧 `skillType` 的替代,而是运行方式维度:
|
||||||
|
|
||||||
|
| 分类 | 判定 | 运行方式 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `prompt` | 无 `skill.config.yaml` | Agent 用 `read_skill` 读取指令并遵循 |
|
||||||
|
| `compute-entry` | config 有 `entrypoint` | Agent 用 `execute_skill` 输入 JSON,执行器返回 JSON |
|
||||||
|
| `compute-toolkit` | config 有 `runtime` 但无 `entrypoint` | Agent 读取指令和 references 后,用 `bash` 执行脚本 |
|
||||||
|
|
||||||
|
`minimax-pdf`、`minimax-docx`、`minimax-xlsx` 属于文档处理 skill,详见 [[document-skills]]。2026-05-07 后又新增了 [[vehicle-damage-skill]]、`netabrowser-cli` 和 `patchwright-cli`,分别覆盖车辆视频旧伤检测、国内反风控浏览器自动化和国外反爬场景。2026-05-15 新增 `data-analyst-mysql` prompt skill,用于在 `mysql_list_sources`、`mysql_schema`、`mysql_query` 可用时引导 Agent 做只读 MySQL 智能问数;完整的数据源、SQL guard 和审计边界见 [[mysql-data-source]]。
|
||||||
|
|
||||||
|
## 安装方式
|
||||||
|
|
||||||
|
| 方式 | 接口 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| GitHub | `POST /admin/netaclaw/skill/install` | git clone → 验证 SKILL.md → 计算指纹 → 写入 origin |
|
||||||
|
| ZIP 上传 | `POST /admin/netaclaw/skill/uploadInstall` | 解压 → 路径穿越防护 → 同上 |
|
||||||
|
| 本地 | 直接放入 `skills/` 目录 | 启动时自动扫描 |
|
||||||
|
| Agent 创建 | `POST /admin/netaclaw/skill/create` | Agent 运行时通过 skill_manage 工具自主创建 |
|
||||||
|
|
||||||
|
## 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
skills/
|
||||||
|
├── {skillName}/
|
||||||
|
│ ├── SKILL.md # Skill 定义(必须)
|
||||||
|
│ ├── skill.config.yaml # 运行时、依赖、env、references(可选)
|
||||||
|
│ └── references/ # 附属文件(可选,Agent 通过 read_skill_file 按需读取)
|
||||||
|
│ ├── guide.md
|
||||||
|
│ └── examples/
|
||||||
|
└── .skillhub/
|
||||||
|
├── lock.json # 全局锁文件(version + fingerprint + installedAt)
|
||||||
|
└── origins/
|
||||||
|
└── {skillName}.json # 来源元数据(source/url/branch/commitHash)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Skill 加载流程
|
||||||
|
|
||||||
|
```
|
||||||
|
启动 → scanSkills() 扫描 skills/ 目录(上限 200 个)
|
||||||
|
→ 读取每个子目录的 SKILL.md(上限 256KB)
|
||||||
|
→ parseSkillMd() 解析 YAML frontmatter + Markdown 内容
|
||||||
|
→ validateSkillName() 记录命名规范和目录名不一致诊断
|
||||||
|
→ skill_config.loadConfig() 解析 skill.config.yaml
|
||||||
|
→ collectFiles() 递归收集附属文件列表
|
||||||
|
→ 过滤 skill.config.yaml / requirements.txt / package.json 等基础设施文件
|
||||||
|
→ 存入内存 Map<name, SkillMeta>
|
||||||
|
→ 同步到 netaclaw_skill 数据库表
|
||||||
|
```
|
||||||
|
|
||||||
|
扫描期间还会收集 `NAME_INVALID`、`NAME_MISMATCH`、`NAME_COLLISION`、`DESC_MISSING`、`CONFIG_PARSE_ERROR`、`VENV_MISSING`、`ENV_NOT_CONFIGURED` 等诊断,供 `/admin/netaclaw/skill/diagnostics` 和前端技能页展示。
|
||||||
|
|
||||||
|
## 条件激活
|
||||||
|
|
||||||
|
`filterByConditions(builtinToolNames)` 根据当前可用工具过滤 Skill:
|
||||||
|
- `requires_tools: ["bash"]` — 只有当 bash 工具可用时才激活
|
||||||
|
- `fallback_for_tools: ["selenium"]` — 当 selenium 不可用时作为替代激活
|
||||||
|
|
||||||
|
## 上下文构建
|
||||||
|
|
||||||
|
当前主路径由 `tool_resolver.ts` 收敛:
|
||||||
|
|
||||||
|
1. 根据 Agent 配置的 `skills` 列表过滤。
|
||||||
|
2. 调用 `buildSkillsPrompt()` 生成 `<available_skills>` XML 索引(上限 30,000 字符),并带上 skill 分类。
|
||||||
|
3. 注入 `read_skill`、`read_skill_file`、`skill_manage`。
|
||||||
|
4. 当 Agent 选择了 compute-entry skill 时,额外注入 `execute_skill`。
|
||||||
|
|
||||||
|
旧 `skill_context.ts` 仍保留兼容,但不再是理解当前 Skill 工具注入的主入口。
|
||||||
|
|
||||||
|
## 密钥与配置 API
|
||||||
|
|
||||||
|
`netaclaw_skill` 现在新增运行时配置字段:
|
||||||
|
|
||||||
|
- `secrets`: AES-256-GCM 加密的 JSON。
|
||||||
|
- `envSchema`: 环境变量声明。
|
||||||
|
|
||||||
|
管理端接口:
|
||||||
|
|
||||||
|
- `GET /admin/netaclaw/skill/envSchema?name=...`
|
||||||
|
- `POST /admin/netaclaw/skill/secrets`
|
||||||
|
- `GET /admin/netaclaw/skill/diagnostics`
|
||||||
|
|
||||||
|
前端 `skill-detail.vue` 的详情抽屉已分为“基本信息 / 配置 / 诊断”三块,`skills.vue` 顶部会显示诊断横幅。
|
||||||
|
|
||||||
|
## 安全限制
|
||||||
|
|
||||||
|
- `MAX_SKILLS = 200`、`MAX_SKILL_FILE_BYTES = 256KB`、`MAX_SKILLS_PROMPT_CHARS = 30,000`
|
||||||
|
- ZIP 安装:`MAX_ZIP_SIZE = 50MB`、`MAX_SINGLE_FILE = 1MB`
|
||||||
|
- GitHub URL 正则校验、路径穿越防护(realpath + startsWith)
|
||||||
|
- skill 名称要求小写字母、数字和连字符,且需要与目录名一致;不合规项会记录诊断。
|
||||||
|
- compute-entry 子进程只继承白名单环境变量,再叠加 skill scoped env,避免主系统敏感变量泄漏。
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [[netaclaw-module]] — 所属模块
|
||||||
|
- [[agent-runtime]] — 运行时加载 Skill 上下文
|
||||||
|
- [[tool-system]] — read_skill / read_skill_file / skill_manage 工具
|
||||||
|
- [[mysql-data-source]] — data-analyst-mysql 使用的 MySQL 问数工具和数据源配置
|
||||||
|
- [[skill-runtime]] — Skill 运行时、密钥、分类和执行器
|
||||||
|
- [[document-skills]] — PDF / DOCX / XLSX 文档处理 skill
|
||||||
|
- [[vehicle-damage-skill]] — 汽车环车视频旧伤检测 skill
|
||||||
|
- [[netabrowser-runtime]] — netabrowser-cli / patchwright-cli 浏览器自动化 skill 的运行后端
|
||||||
|
- [[crew-orchestration]] — Crew 主/子 Agent 均可加载 Skill
|
||||||
148
docs/code-wiki/entities/subagent-session.md
Normal file
148
docs/code-wiki/entities/subagent-session.md
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
---
|
||||||
|
title: 会话级子 Agent 委派
|
||||||
|
created: 2026-04-19
|
||||||
|
updated: 2026-04-23
|
||||||
|
type: entity
|
||||||
|
tags: [agent, runtime, websocket, backend]
|
||||||
|
sources: [packages/backend/src/modules/netaclaw/service/subagent.ts, packages/backend/src/modules/netaclaw/subagent/process_runner.ts, packages/backend/src/modules/netaclaw/subagent/process_protocol.ts, packages/backend/src/modules/netaclaw/subagent/worker.ts, packages/backend/src/modules/netaclaw/subagent/worker_tools.ts, packages/backend/src/modules/netaclaw/session-tree/types.ts, packages/frontend/src/modules/agent/store/chat.ts]
|
||||||
|
---
|
||||||
|
|
||||||
|
# 会话级子 Agent 委派
|
||||||
|
|
||||||
|
会话级子 Agent 委派是普通对话内部的临时任务分解能力。它不同于长期运行的 [[crew-orchestration]]:主 Agent 在一次对话中把局部目标交给一个或多个预设 Agent,子 Agent 执行后把摘要和结果回填到当前会话树。
|
||||||
|
|
||||||
|
当前实现已经从“服务内复用 runAgent 的元数据聚合”升级为“SubagentService + 独立 worker 进程 + JSONL 协议 + Session Tree 节点”的运行模型。
|
||||||
|
|
||||||
|
## 核心职责
|
||||||
|
|
||||||
|
- 接收主 Agent 的 `delegate_task` / `delegate_parallel` 工具调用。
|
||||||
|
- 为每个任务创建受限的 Agent 配置和工具集合。
|
||||||
|
- 根据 [[tool-runtime-policy]] 推导子进程工具策略。
|
||||||
|
- 通过 `process_runner.ts` 启动 worker,消费 JSONL 事件。
|
||||||
|
- 将子任务批次写入 `subagent_batch` 和 `subagent_result` 树节点。
|
||||||
|
- 向前端推送批次状态、工具路由、运行结果。
|
||||||
|
|
||||||
|
## 关键文件
|
||||||
|
|
||||||
|
| 文件 | 职责 |
|
||||||
|
| --- | --- |
|
||||||
|
| `service/subagent.ts` | 子 Agent 批次编排、任务状态聚合、结果归档 |
|
||||||
|
| `subagent/process_protocol.ts` | 父子进程之间的 envelope、policy、manifest、事件协议 |
|
||||||
|
| `subagent/process_runner.ts` | 启动/取消/超时控制 worker,处理 proxy tool call |
|
||||||
|
| `subagent/worker.ts` | 子进程入口,执行受限 Agent |
|
||||||
|
| `subagent/worker_tools.ts` | 子进程本地工具适配 |
|
||||||
|
| `tools/manifest.ts` | 为子 Agent 生成工具 manifest |
|
||||||
|
| `tools/runtime_policy.ts` | 推导 worker policy 和工具运行路由 |
|
||||||
|
| `session-tree/types.ts` | `subagent_batch` / `subagent_result` 节点类型 |
|
||||||
|
|
||||||
|
## 进程协议
|
||||||
|
|
||||||
|
父进程向 worker 发送 `SubagentTaskEnvelope`,其中包含:
|
||||||
|
|
||||||
|
- `protocolVersion`
|
||||||
|
- `runId`
|
||||||
|
- `sessionId`
|
||||||
|
- `parentEntryId`
|
||||||
|
- `task`
|
||||||
|
- `agentConfig`
|
||||||
|
- `toolNames`
|
||||||
|
- `toolManifest`
|
||||||
|
- `policy`
|
||||||
|
- `timeoutMs`
|
||||||
|
|
||||||
|
worker 以 JSONL 形式回传事件:
|
||||||
|
|
||||||
|
- `run_start`
|
||||||
|
- `token`
|
||||||
|
- `thinking`
|
||||||
|
- `log`
|
||||||
|
- `tool_call`
|
||||||
|
- `tool_result`
|
||||||
|
- `proxy_tool_call`
|
||||||
|
- `run_end`
|
||||||
|
- `run_error`
|
||||||
|
|
||||||
|
`runId` 是父进程校验事件归属的边界,避免不同子任务输出串流。
|
||||||
|
|
||||||
|
## 工具路由
|
||||||
|
|
||||||
|
子 Agent 不再简单复制主 Agent 工具集,而是按 manifest 和 policy 分三类:
|
||||||
|
|
||||||
|
| 路由 | 含义 |
|
||||||
|
| --- | --- |
|
||||||
|
| `worker-local` | 可在 worker 内直接执行,例如只读文件搜索类工具 |
|
||||||
|
| `main-process-proxy` | worker 发起 `proxy_tool_call`,父进程代执行,例如写文件、技能、记忆类工具 |
|
||||||
|
| `disabled` | 当前策略下不可用,例如缺少 workspace root、shell 被禁、只读策略阻止写入 |
|
||||||
|
|
||||||
|
这个设计把“工具是否被 Agent 选择”和“工具能否在子进程执行”拆开,避免子 Agent 越权。详见 [[tool-runtime-policy]]。
|
||||||
|
|
||||||
|
## Session Tree 表达
|
||||||
|
|
||||||
|
子 Agent 结果现在是会话树的一部分:
|
||||||
|
|
||||||
|
- `subagent_batch` 表示一个批次开始或运行中状态。
|
||||||
|
- `subagent_result` 表示批次结果和每个子任务的最终输出。
|
||||||
|
- 节点通过 `parentEntryId` 关联到触发它的主 Agent 消息或工具调用。
|
||||||
|
- 前端可在刷新后从 snapshot 恢复批次卡片,不依赖仅存在内存里的 streaming state。
|
||||||
|
|
||||||
|
这使子 Agent、压缩、分支摘要、普通消息都能进入统一的 [[session-tree-runtime]]。
|
||||||
|
|
||||||
|
## 与 Agent 配置的关系
|
||||||
|
|
||||||
|
Agent 编辑页需要配置的不只是“是否允许子 Agent”:
|
||||||
|
|
||||||
|
- `subagentConfig.enabled`
|
||||||
|
- `subagentConfig.maxConcurrent`
|
||||||
|
- `subagentConfig.allowedPresetAgentIds`
|
||||||
|
- `subagentConfig.allowedToolNames`
|
||||||
|
- 每个工具的有效运行画像和子 Agent 可用性诊断
|
||||||
|
- 本地存储、workspace、shell、readonly 等会影响工具路由的策略输入
|
||||||
|
|
||||||
|
这些配置最终应通过后端 projection 返回给前端,避免工具列表、工具管理页、Agent 配置页三处口径不一致。
|
||||||
|
|
||||||
|
## 与 Crew 的边界
|
||||||
|
|
||||||
|
| 维度 | 会话级子 Agent | Crew 编排 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 生命周期 | 一次普通 chat 内部的临时批次 | 独立编排运行 |
|
||||||
|
| 状态载体 | Session Tree 节点 | Crew run/task 记录 |
|
||||||
|
| 展示位置 | Agent 对话页内联卡片 | Crew 页面、画布、监控视图 |
|
||||||
|
| 工具入口 | `delegate_task` / `delegate_parallel` | Crew orchestration 入口 |
|
||||||
|
| 权限模型 | 主 Agent 控制的受限 worker | Crew 配置控制 |
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [[agent-runtime]]
|
||||||
|
- [[session-tree-runtime]]
|
||||||
|
- [[tool-runtime-policy]]
|
||||||
|
- [[tool-governance]]
|
||||||
|
- [[tool-system]]
|
||||||
|
- [[frontend-architecture]]
|
||||||
|
- [[websocket-gateway]]
|
||||||
|
- [[crew-orchestration]]
|
||||||
|
|
||||||
|
## 2026-04-22 Replay Contract
|
||||||
|
|
||||||
|
`subagent_result` is now the durable replay/evidence node for session-level subagents.
|
||||||
|
|
||||||
|
- `subagent_result.metadata.processEvents` stores projected worker JSONL events such as `run_start`, `tool_call`, `tool_result`, `proxy_tool_call`, `run_end`, and `run_error`.
|
||||||
|
- `subagent_result.metadata.evidenceSummaries` stores structured summaries derived from tool results, for example file counts or tool-result previews.
|
||||||
|
- `subagent_batch.metadata.events` and `subagent_batch.metadata.latestEvent` remain runtime/status projection data. They are useful while a batch is running, but they are not the primary refresh/replay source.
|
||||||
|
- `processEvents` and `evidenceSummaries` must not be injected into `runtimeContext.messages`; they are UI replay/evidence metadata, not LLM conversation context.
|
||||||
|
- Frontend projection lives in `packages/frontend/src/modules/agent/store/chat.ts` via `subagentEvidenceSummariesByEntryId` and `subagentProcessEventsByEntryId`.
|
||||||
|
|
||||||
|
## 2026-04-23 Projection And Evidence Model
|
||||||
|
|
||||||
|
本轮实现把“子 Agent 执行结果”拆成三层,避免运行态、持久态和展示态混在一起:
|
||||||
|
|
||||||
|
- `service/subagent.ts` 负责产出原始结果载荷:`finalOutput`、`rawFinalContent`、`toolResults`、`evidenceSummary`、`processEvents`、`toolRuntimeRoutes`。
|
||||||
|
- `service/subagent_evidence.ts` 负责把工具结果规范化,并按任务目标推导证据摘要;当前支持文件计数类摘要和工具预览类摘要。
|
||||||
|
- `service/chat_orchestrator.ts` 在批次完成后把结果聚合进 `subagent_result.metadata.finalResults/processEvents/evidenceSummaries`,使刷新后的 replay 不依赖内存态。
|
||||||
|
- `session-tree/subagent_projection.ts` 再把这些持久化数据投影成 `metadata.subagentProjection`,其中包含 `taskPanels`、每个 task 的 `toolExecutions`、剩余 `processEvents` 以及诊断信息。
|
||||||
|
|
||||||
|
这里要特别区分两个节点职责:
|
||||||
|
|
||||||
|
- `subagent_batch` 更偏运行中状态:批次任务列表、最近事件、运行态工具路由。
|
||||||
|
- `subagent_result` 更偏最终回放状态:最终结果、过程时间线、证据摘要、任务面板。
|
||||||
|
|
||||||
|
因此“刷新后还能看到什么”应首先看 `subagent_result`,而不是假设 `subagent_batch` 会长期保存完整过程。
|
||||||
49
docs/code-wiki/entities/todo-system.md
Normal file
49
docs/code-wiki/entities/todo-system.md
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
title: Todo 会话任务系统
|
||||||
|
created: 2026-04-14
|
||||||
|
updated: 2026-04-14
|
||||||
|
type: entity
|
||||||
|
tags: [agent, runtime, tool]
|
||||||
|
sources: [packages/backend/src/modules/netaclaw/runtime/todo_store.ts, packages/backend/src/modules/netaclaw/tools/builtin/todo.ts]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Todo 会话任务系统
|
||||||
|
|
||||||
|
Agent 会话级的任务规划和跟踪机制,Agent 通过 `todo` 工具管理任务列表,前端实时展示进度。
|
||||||
|
|
||||||
|
## 数据结构
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface TodoItem {
|
||||||
|
id: string;
|
||||||
|
content: string;
|
||||||
|
status: 'pending' | 'in_progress' | 'completed' | 'cancelled';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## TodoStore(runtime/todo_store.ts)
|
||||||
|
|
||||||
|
每个 Agent 会话创建一个 TodoStore 实例。
|
||||||
|
|
||||||
|
| 方法 | 功能 |
|
||||||
|
|------|------|
|
||||||
|
| `write(todos, merge)` | 写入 todo 列表(merge=false 全量替换,merge=true 增量更新) |
|
||||||
|
| `read()` | 读取完整列表 |
|
||||||
|
| `hasItems()` | 检查是否有任务 |
|
||||||
|
| `getSummary()` | 统计摘要(total/pending/in_progress/completed/cancelled) |
|
||||||
|
| `formatForInjection()` | 格式化为上下文注入(仅保留 pending + in_progress) |
|
||||||
|
| `hydrateFromHistory()` | 从历史消息恢复 todo 状态 |
|
||||||
|
|
||||||
|
## 集成点
|
||||||
|
|
||||||
|
- **Agent 运行时**:自动创建 TodoStore 实例
|
||||||
|
- **todo 工具**:Agent 通过 tool_use 调用管理任务
|
||||||
|
- **上下文压缩**:压缩后保留活跃任务(formatForInjection)
|
||||||
|
- **WebSocket**:推送 `todo_update` 事件到前端
|
||||||
|
- **前端**:`todo-card.vue` 组件展示任务列表和进度
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [[tool-system]] — todo 工具定义
|
||||||
|
- [[agent-runtime]] — 运行时集成
|
||||||
|
- [[websocket-gateway]] — todo_update 事件推送
|
||||||
135
docs/code-wiki/entities/tool-catalog.md
Normal file
135
docs/code-wiki/entities/tool-catalog.md
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
---
|
||||||
|
title: Tool Catalog 工具目录系统
|
||||||
|
created: 2026-04-16
|
||||||
|
updated: 2026-05-15
|
||||||
|
type: entity
|
||||||
|
tags: [tool, agent, architecture]
|
||||||
|
sources: [packages/backend/src/modules/netaclaw/tools/catalog.ts]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Tool Catalog 工具目录系统
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
轻量级 schema 元数据注册表,只负责注册“系统中定义了哪些工具”。它不直接决定工具是否可用,也不保存运营侧治理配置;这些职责已转移到 [[tool-governance]]。当前它更像工具体系的“源头清单”,为 resolver、manifest、runtime diagnostic 提供静态起点。
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```text
|
||||||
|
netaclaw/tools/
|
||||||
|
├── catalog.ts # schema 注册表核心
|
||||||
|
├── common.ts # AgentTool 接口与辅助函数
|
||||||
|
├── fuzzy_match.ts # 9级模糊匹配引擎(供 patch 使用)
|
||||||
|
├── todo_tool.ts # Todo 工具 schema
|
||||||
|
└── builtin/
|
||||||
|
├── bash.ts
|
||||||
|
├── file.ts
|
||||||
|
├── patch.ts
|
||||||
|
├── clarify.ts
|
||||||
|
├── memory.ts
|
||||||
|
├── mysql.ts
|
||||||
|
├── read_skill.ts
|
||||||
|
├── read_skill_file.ts
|
||||||
|
├── skill_manage.ts
|
||||||
|
├── delegate_task.ts
|
||||||
|
├── delegate_parallel.ts
|
||||||
|
└── escalate.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## ToolSchema
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ToolSchema {
|
||||||
|
name: string;
|
||||||
|
toolset: string;
|
||||||
|
description: string;
|
||||||
|
visibility?: 'internal' | 'tool' | 'skill';
|
||||||
|
capability?: 'text' | 'vision' | 'multimodal';
|
||||||
|
isCore?: boolean;
|
||||||
|
canDisable?: boolean;
|
||||||
|
supportsPromptHint?: boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
相比旧版,catalog 现在除了名称和工具集,还承担:
|
||||||
|
|
||||||
|
- 模型能力要求
|
||||||
|
- 是否核心工具
|
||||||
|
- 是否允许关闭
|
||||||
|
- 是否支持 Prompt Hint
|
||||||
|
- 可见性标记
|
||||||
|
|
||||||
|
但它依然不负责:
|
||||||
|
|
||||||
|
- 子 Agent 是否允许使用该工具
|
||||||
|
- 当前 session 下是否需要主进程代理
|
||||||
|
- blocked reason 和最终 runtime route
|
||||||
|
- 前端最终展示文案
|
||||||
|
|
||||||
|
## 核心 API
|
||||||
|
|
||||||
|
| API | 说明 |
|
||||||
|
|-----|------|
|
||||||
|
| `registerSchema(schema)` | 注册工具 schema |
|
||||||
|
| `getAllToolSchemas()` | 获取全部 schema |
|
||||||
|
| `getToolSchema(name)` | 查询单个 schema |
|
||||||
|
| `getToolNamesByToolset(toolset)` | 按 toolset 查询 |
|
||||||
|
| `getToolNamesByToolsets(toolsets)` | 按多个 toolset 查询 |
|
||||||
|
|
||||||
|
## 默认工具集
|
||||||
|
|
||||||
|
- `TOOLSET_DEFAULTS = ['base', 'planning', 'interaction']`
|
||||||
|
- 另外保留 `vision`、`document`、`mysql` 等常量,用于能力分组和 Agent 工具集配置。
|
||||||
|
|
||||||
|
## 当前工具集分类
|
||||||
|
|
||||||
|
| 工具集 | 说明 |
|
||||||
|
|--------|------|
|
||||||
|
| `base` | 基础文件与命令工具 |
|
||||||
|
| `planning` | todo 规划工具 |
|
||||||
|
| `interaction` | clarify / escalate |
|
||||||
|
| `memory` | 长期记忆工具 |
|
||||||
|
| `skill` | Skill 相关工具 |
|
||||||
|
| `crew` | 委派工具 |
|
||||||
|
| `mysql` | MySQL 只读问数工具,详见 [[mysql-data-source]] |
|
||||||
|
|
||||||
|
## 注册机制
|
||||||
|
|
||||||
|
`catalog.ts` 底部通过 import 触发各工具文件调用 `registerSchema()`,形成静态注册表。这个注册表随后会被:
|
||||||
|
|
||||||
|
- `tool_registry.ts` 用来同步和对齐 DB 治理配置。
|
||||||
|
- `tool_resolver.ts` 用来拼装最终可用工具集。
|
||||||
|
- `tools/manifest.ts` / `tools/runtime_policy.ts` 用来生成运行时画像与路由。
|
||||||
|
|
||||||
|
## 与治理层的分工
|
||||||
|
|
||||||
|
### Catalog 负责
|
||||||
|
|
||||||
|
- 系统里有哪些工具
|
||||||
|
- 每个工具的 schema 元信息
|
||||||
|
- 默认工具集与分类
|
||||||
|
|
||||||
|
### Governance 负责
|
||||||
|
|
||||||
|
- 全局启停
|
||||||
|
- Agent 级启停覆盖
|
||||||
|
- Prompt Hint 覆写
|
||||||
|
- 模型能力过滤
|
||||||
|
- 上下文角色限制
|
||||||
|
- 动态运行路由与诊断输出
|
||||||
|
|
||||||
|
详见 [[tool-governance]]。
|
||||||
|
|
||||||
|
## 与 Prompt Builder 的关系
|
||||||
|
|
||||||
|
旧版 `collectAvailableToolNames()` 仍在 [[prompt-builder]] 中保留,但当前关键运行链路已转向 `tool_resolver.resolve()` 先给出最终 `toolNames`,再交给 Prompt Builder 组装提示词。
|
||||||
|
|
||||||
|
## 关联页面
|
||||||
|
|
||||||
|
- [[tool-system]] — 工具系统总览
|
||||||
|
- [[tool-governance]] — catalog 之上的治理层
|
||||||
|
- [[mysql-data-source]] — mysql toolset 的数据源、schema、query 和审计边界
|
||||||
|
- [[prompt-builder]] — 消费最终工具列表
|
||||||
|
- [[patch-tool]] — Patch 工具(base 工具集)
|
||||||
|
- [[clarify-tool]] — Clarify 工具(interaction 工具集)
|
||||||
|
- [[netaclaw-module]] — 所属模块
|
||||||
166
docs/code-wiki/entities/tool-governance.md
Normal file
166
docs/code-wiki/entities/tool-governance.md
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
---
|
||||||
|
title: Tool 全局治理系统
|
||||||
|
created: 2026-04-19
|
||||||
|
updated: 2026-05-15
|
||||||
|
type: entity
|
||||||
|
tags: [tool, agent, config, backend, frontend]
|
||||||
|
sources: [packages/backend/src/modules/netaclaw/entity/tool.ts, packages/backend/src/modules/netaclaw/service/tool_registry.ts, packages/backend/src/modules/netaclaw/service/tool_resolver.ts, packages/backend/src/modules/netaclaw/tools/manifest.ts, packages/backend/src/modules/netaclaw/tools/runtime_policy.ts, packages/backend/src/modules/netaclaw/tools/presentation.ts, packages/frontend/src/modules/agent/views/tools.vue, packages/frontend/src/modules/agent/views/agent-edit.vue]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Tool 全局治理系统
|
||||||
|
|
||||||
|
Tool 全局治理系统位于 [[tool-system]] 之上,负责把工具从“代码里的可调用函数”提升为“可同步、可禁用、可按 Agent 覆盖、可诊断、可进入子进程策略”的平台能力。
|
||||||
|
|
||||||
|
当前治理口径已经扩展到四类输出:
|
||||||
|
|
||||||
|
- 最终可用工具列表。
|
||||||
|
- Prompt Hint 和工具描述投影。
|
||||||
|
- Agent/子 Agent 的启停和 override 结果。
|
||||||
|
- Worker runtime profile 与 blocked reason,详见 [[tool-runtime-policy]]。
|
||||||
|
|
||||||
|
2026-04-22 之后,这一层又新增了两个重要职责:
|
||||||
|
|
||||||
|
- 生成 `toolRuntimeRoutes`,把“配置上允许”进一步收口为“当前运行态到底走 worker-local、main-process-proxy 还是 disabled”。
|
||||||
|
- 生成 `runtimeDiagnostic` / renderer capability 投影,供工具管理页、Agent 编辑页和对话页使用同一套解释。
|
||||||
|
|
||||||
|
2026-05-02 后,还需要把 [[tool-operations]] 看成工具执行后端维度:治理层决定某个工具是否进入 runtime,Operations 决定这个工具执行时用本地、mock、worker proxy 还是未来的远程/沙箱实现。两者不能混用:不要用 Operations 判断工具是否应显示,也不要用 DB governance 直接替代文件/进程后端注入。
|
||||||
|
|
||||||
|
2026-05-15 后,`mysql_*` 工具也纳入同一套治理口径:[[tool-catalog]] 注册 `mysql` toolset,`tool_resolver.ts` 注入 MySQL 数据源、schema 和查询服务,manifest 将这些工具统一路由为 `main-process-proxy`。治理层只决定工具是否对某个 Agent 可见;真正的数据源授权、SQL guard、脱敏列拒绝、连接池和查询审计仍留在 [[mysql-data-source]] 的服务端实现中。
|
||||||
|
|
||||||
|
## 数据模型
|
||||||
|
|
||||||
|
`netaclaw_tool` 记录全局工具治理配置。核心字段包括:
|
||||||
|
|
||||||
|
| 字段 | 含义 |
|
||||||
|
| --- | --- |
|
||||||
|
| `name` | 工具唯一标识 |
|
||||||
|
| `toolset` | 所属工具集 |
|
||||||
|
| `label` | 展示名称 |
|
||||||
|
| `description` | 展示描述 |
|
||||||
|
| `visibility` | `tool`、`skill`、`internal` |
|
||||||
|
| `capability` | `text`、`vision`、`multimodal` |
|
||||||
|
| `status` | 全局启停 |
|
||||||
|
| `isCore` | 是否核心工具 |
|
||||||
|
| `canDisable` | 核心工具是否允许 Agent 级关闭 |
|
||||||
|
| `supportsPromptHint` | 是否支持 Prompt Hint |
|
||||||
|
| `promptHint` | 运行提示覆盖 |
|
||||||
|
| `sort` | 展示顺序 |
|
||||||
|
| `extra` | 运行时扩展配置,如 `workerRoutingStrategy`、`allowInSubagent` |
|
||||||
|
|
||||||
|
## 核心服务
|
||||||
|
|
||||||
|
| 文件 | 职责 |
|
||||||
|
| --- | --- |
|
||||||
|
| `service/tool_registry.ts` | catalog 同步、DB 治理配置读写、管理端查询 |
|
||||||
|
| `service/tool_resolver.ts` | 根据全局治理、Agent 配置、运行上下文生成最终工具投影 |
|
||||||
|
| `tools/manifest.ts` | 为工具生成 worker/runtime manifest |
|
||||||
|
| `tools/runtime_policy.ts` | 推导子进程 policy、路由状态和 blocked reason |
|
||||||
|
| `tools/presentation.ts` | 统一前端展示文案和运行时投影格式 |
|
||||||
|
| `tools/operations/` | 工具底层执行后端接口与本地实现 |
|
||||||
|
|
||||||
|
## Resolver 输入
|
||||||
|
|
||||||
|
`tool_resolver.ts` 的有效决策不只来自全局开关,还包括:
|
||||||
|
|
||||||
|
- Agent 的 `tools.enabled` 和 `tools.disabled`。
|
||||||
|
- Agent 的 `toolOverrides`、`draftAgentTools`、`draftToolOverride`。
|
||||||
|
- `subagentConfig.enabled`、`allowedToolNames`、`allowedPresetAgentIds`。
|
||||||
|
- 当前模型能力,例如 vision/multimodal。
|
||||||
|
- 当前运行上下文,例如主 Agent、子 Agent、Crew、Skill、Memory。
|
||||||
|
- session cwd、workspace roots、shell/readonly 策略。
|
||||||
|
- 可选 `operations` 注入;测试、worker 或未来远程执行可替换底层后端。
|
||||||
|
|
||||||
|
## Resolver 输出
|
||||||
|
|
||||||
|
后端应该返回统一 projection,而不是让前端三处页面各自推导:
|
||||||
|
|
||||||
|
- `toolNames`: 当前真正进入 runtime 的工具名。
|
||||||
|
- `tools`: 可执行工具实例或工具元数据。
|
||||||
|
- `toolPromptHints`: Prompt Builder 消费的 hint。
|
||||||
|
- `disabledReasons`: 解释工具为何没有生效。
|
||||||
|
- `runtimeDiagnostic`: 工具运行画像、worker route、blocked reason。
|
||||||
|
- `effectiveRuntimeProfile`: 对当前 Agent 生效的 runtime profile。
|
||||||
|
- `effectiveSubagentAllowed`: 对当前 Agent/子 Agent 生效的可用性。
|
||||||
|
- `toolRuntimeRoutes`: 当前会话和策略下每个工具的实际运行路由。
|
||||||
|
|
||||||
|
这保证工具管理页、Agent 编辑页、对话页看到同一套结果。
|
||||||
|
|
||||||
|
其中有两个边界需要特别强调:
|
||||||
|
|
||||||
|
- `effectiveRuntimeProfile` 偏静态画像,描述该工具通常需要哪些权限、支持哪种 worker routing hint。
|
||||||
|
- `toolRuntimeRoutes` 偏动态结果,描述在当前 session cwd、workspace roots、allowShell、readonly 条件下的最终实际路由。
|
||||||
|
|
||||||
|
只有把这两层都看完,才能判断一个工具为什么“看起来允许但运行时被挡住”。
|
||||||
|
|
||||||
|
新增 `execute_skill` 后,Skill 工具集的治理也需要区分:`read_skill` / `read_skill_file` 是 prompt/toolkit skill 的读取入口,`execute_skill` 只应在 Agent 绑定 compute-entry skill 且工具治理允许时进入 runtime。详见 [[skill-runtime]]。
|
||||||
|
|
||||||
|
## 前端页面职责
|
||||||
|
|
||||||
|
### 工具管理页
|
||||||
|
|
||||||
|
`packages/frontend/src/modules/agent/views/tools.vue` 面向全局治理:
|
||||||
|
|
||||||
|
- 查看和同步 catalog。
|
||||||
|
- 编辑全局启停、核心工具、是否可关闭、Prompt Hint、排序。
|
||||||
|
- 展示工具 runtime profile、worker routing、renderer 绑定、文件能力标签。
|
||||||
|
- 页面需要可滚动,避免工具增多后看不到底部内容。
|
||||||
|
- 支持调用 `/admin/netaclaw/tool/runtimeDiagnostic` 查看在指定 session cwd / workspace roots / shell / readonly 条件下的真实诊断结果。
|
||||||
|
|
||||||
|
### Agent 编辑页
|
||||||
|
|
||||||
|
`packages/frontend/src/modules/agent/views/agent-edit.vue` 面向局部配置:
|
||||||
|
|
||||||
|
- 配置当前 Agent 的工具开启/关闭。
|
||||||
|
- 配置子 Agent 是否启用、并发、可用预设 Agent、允许工具。
|
||||||
|
- 预览后端返回的 effective projection。
|
||||||
|
- 保存本地存储、workspace、运行策略相关配置时,应影响子 Agent 工具推导。
|
||||||
|
|
||||||
|
这一页现在不只是“勾选工具”:
|
||||||
|
|
||||||
|
- 会直接展示每个工具的 `effectiveRuntimeProfile`。
|
||||||
|
- 会直接标记 `effectiveSubagentAllowed`,帮助判断该工具是否允许进入子代理。
|
||||||
|
- 会结合 `toolRuntimeRoutes` 告诉用户配置保存后,子 Agent 最终走哪条执行路径。
|
||||||
|
|
||||||
|
### 对话页
|
||||||
|
|
||||||
|
`packages/frontend/src/modules/agent/views/chat.vue` 面向实际运行结果:
|
||||||
|
|
||||||
|
- 展示工具调用过程。
|
||||||
|
- 展示子 Agent 批次工具路由。
|
||||||
|
- 对 blocked reason 使用同一套文案。
|
||||||
|
- 通过 [[session-tree-runtime]] 恢复历史。
|
||||||
|
- 对子 Agent 任务只显示本批次请求到的工具路由,而不是整套全局路由。
|
||||||
|
|
||||||
|
## 与 Prompt Builder 的关系
|
||||||
|
|
||||||
|
Prompt Builder 不直接读取 catalog,而是消费 resolver 输出:
|
||||||
|
|
||||||
|
```text
|
||||||
|
tool_registry/catalog
|
||||||
|
-> tool_resolver
|
||||||
|
-> toolNames + toolPromptHints + runtimeDiagnostic
|
||||||
|
-> prompt_builder
|
||||||
|
-> runAgent
|
||||||
|
```
|
||||||
|
|
||||||
|
这样工具行为提示、禁用原因、模型能力过滤可以在一处收敛。
|
||||||
|
|
||||||
|
## 与 Renderer 的关系
|
||||||
|
|
||||||
|
治理系统已经不只决定“能不能执行”,还决定“前端怎么展示”:
|
||||||
|
|
||||||
|
- `tools/presentation.ts` 负责输出 renderer capability projection。
|
||||||
|
- 工具管理页会显示某个工具是自定义渲染器还是默认绑定。
|
||||||
|
- 这使“新增工具但页面表现很差”也成为治理问题的一部分,而不只是前端细节。
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [[tool-system]]
|
||||||
|
- [[tool-runtime-policy]]
|
||||||
|
- [[tool-operations]]
|
||||||
|
- [[tool-catalog]]
|
||||||
|
- [[mysql-data-source]]
|
||||||
|
- [[agent-runtime]]
|
||||||
|
- [[subagent-session]]
|
||||||
|
- [[prompt-builder]]
|
||||||
|
- [[frontend-architecture]]
|
||||||
175
docs/code-wiki/entities/tool-system.md
Normal file
175
docs/code-wiki/entities/tool-system.md
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
---
|
||||||
|
title: 工具系统
|
||||||
|
created: 2026-04-13
|
||||||
|
updated: 2026-05-15
|
||||||
|
type: entity
|
||||||
|
tags: [tool, agent, runtime, backend]
|
||||||
|
sources: [packages/backend/src/modules/netaclaw/tools/, packages/backend/src/modules/netaclaw/tools/operations/, packages/backend/src/modules/netaclaw/service/tool_resolver.ts, packages/backend/src/modules/netaclaw/service/tool_registry.ts, packages/backend/src/modules/netaclaw/subagent/worker_tools.ts, packages/backend/src/modules/netaclaw/gateway/tool_risk.ts]
|
||||||
|
---
|
||||||
|
|
||||||
|
# 工具系统
|
||||||
|
|
||||||
|
工具系统是 Agent 可调用能力的实现层。当前 NetaClaw 工具架构分为五层:
|
||||||
|
|
||||||
|
```text
|
||||||
|
工具实现
|
||||||
|
-> Tool Operations
|
||||||
|
-> Tool Catalog
|
||||||
|
-> Tool Governance / Resolver
|
||||||
|
-> Tool Manifest / Runtime Policy
|
||||||
|
```
|
||||||
|
|
||||||
|
其中工具实现层关注参数语义和结果格式,[[tool-operations]] 关注底层文件/进程/搜索后端,[[tool-governance]] 关注“是否允许、如何投影、如何被前端理解”,[[tool-runtime-policy]] 关注“在主进程或子进程里怎么安全执行”。
|
||||||
|
|
||||||
|
## 工具接口
|
||||||
|
|
||||||
|
运行时工具一般具备:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface AnyAgentTool {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
inputSchema: object;
|
||||||
|
execute: (args: unknown) => Promise<string | Record<string, unknown>>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
模型输出 `tool_use` 后,`runAgent` 会从 resolver 提供的工具集合中查找并执行对应工具。当前工具执行结果除了传统字符串结果,还可能携带结构化 `rawResult`,供 WebSocket 和前端渲染层展示文本、JSON、图片等不同结果形态。
|
||||||
|
|
||||||
|
## 内置工具分组
|
||||||
|
|
||||||
|
| 分组 | 工具 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 文件/搜索 | `read_file`、`write_file`、`list_dir`、`find_files`、`grep` | workspace 文件读写与检索 |
|
||||||
|
| Shell | `bash` | 命令执行,受 policy 控制 |
|
||||||
|
| 编辑 | `edit`、`patch` | 精确编辑与补丁应用 |
|
||||||
|
| Skill | `read_skill`、`read_skill_file`、`skill_manage`、`execute_skill` | 技能读取、管理和 compute-entry 执行 |
|
||||||
|
| Memory | `memory_save`、`memory_recall` | 长期记忆写入与检索 |
|
||||||
|
| AIGC | `text_to_image`、`image_to_image` | 文生图、图生图和图片结果持久化,详见 [[image-generation-tools]] |
|
||||||
|
| 委派 | `delegate_task`、`delegate_parallel` | 子 Agent 委派,详见 [[subagent-session]] |
|
||||||
|
| 微信桌面 | `weixin_send_text` | 通过 [[desktop-op-module]] 在 PC 微信目标群发送文本 |
|
||||||
|
| MySQL 问数 | `mysql_list_sources`、`mysql_schema`、`mysql_table_sample`、`mysql_query` | 授权 MySQL 数据源的只读 schema、样例和 SQL 查询 |
|
||||||
|
| 交互 | `clarify`、`escalate` | 澄清与升级 |
|
||||||
|
| Todo | `todo` | 会话级任务状态 |
|
||||||
|
|
||||||
|
## 主 Agent 执行
|
||||||
|
|
||||||
|
主 Agent 的工具调用链路:
|
||||||
|
|
||||||
|
```text
|
||||||
|
LLM 输出 tool_use
|
||||||
|
-> runAgent 解析工具名和参数
|
||||||
|
-> ToolResolver 提供可执行工具 Map
|
||||||
|
-> tool.execute(args)
|
||||||
|
-> 产生 tool_result
|
||||||
|
-> 写入 session tree message entry
|
||||||
|
-> WebSocket 推送 tool_call/tool_result
|
||||||
|
```
|
||||||
|
|
||||||
|
2026-05-02 后,Gateway 会在真正执行 bash 前调用 `gateway/tool_risk.ts` 做风险检测。命中 `rm`、`del`、`rd`、`rmdir`、`drop table`、`git reset --hard`、`git clean -f`、`git push --force` 等模式时,会先推送 `tool_confirmation_request`,等待前端返回 `tool_confirmation_response`;用户拒绝则该工具调用不会执行。
|
||||||
|
|
||||||
|
## Operations 注入
|
||||||
|
|
||||||
|
文件、搜索、编辑和 bash 类工具现在通过 [[tool-operations]] 访问底层系统能力:
|
||||||
|
|
||||||
|
- `read_file`、`write_file`、`list_dir` 使用 `FileOperations`。
|
||||||
|
- `edit`、`patch` 使用 `FileOperations` 和写队列。
|
||||||
|
- `find_files`、`grep` 使用 `SearchOperations`。
|
||||||
|
- `bash` 使用 `ProcessOperations`。
|
||||||
|
|
||||||
|
`tool_resolver.ts` 在构造工具列表时注入 `operations?: ToolOperations`;未提供时使用 `getDefaultOperations()` 的本地实现。这让测试可以用 mock operations,也为后续远程 workspace / 沙箱后端留出替换点。
|
||||||
|
|
||||||
|
## 子 Agent 执行
|
||||||
|
|
||||||
|
子 Agent worker 不会默认拥有所有工具。工具按 manifest 和 runtime policy 分为:
|
||||||
|
|
||||||
|
- 本地 worker 工具:适合只读、低风险、可在子进程直接执行。
|
||||||
|
- 主进程代理工具:需要父进程执行,例如写文件、技能管理、记忆写入。
|
||||||
|
- 禁用工具:当前策略下不允许执行。
|
||||||
|
|
||||||
|
详见 [[tool-runtime-policy]]。
|
||||||
|
|
||||||
|
主进程与子进程之间的边界已经进一步明确:
|
||||||
|
|
||||||
|
- 子 Agent 使用的不是“主 Agent 全量工具复制品”,而是 resolver 结合 `allowedToolNames`、workspace roots、shell/readonly 策略、manifest 路由之后裁剪出的工具集。
|
||||||
|
- `delegate_task` / `delegate_parallel` 会把 `toolRuntimeRoutes` 一并带入子 Agent 批次与回放数据,前端据此显示当前任务到底是 `worker-local`、`main-process-proxy` 还是 `disabled`。
|
||||||
|
- 某个工具能否进入子 Agent,不只看它是否启用,还要看它的 `effectiveSubagentAllowed`、`workerRoutingHint` 和运行时 blocked reason。
|
||||||
|
|
||||||
|
## Catalog 与治理
|
||||||
|
|
||||||
|
`Tool Catalog` 提供工具静态 schema 和基础元数据。`Tool Governance` 把 catalog 同步到 DB,支持全局启停、核心工具、Agent 局部覆盖和 Prompt Hint。
|
||||||
|
|
||||||
|
这意味着工具实现文件不应直接决定最终可见性;最终口径以 resolver 输出为准。
|
||||||
|
|
||||||
|
当前 resolver 的核心输出已经不只是工具名:
|
||||||
|
|
||||||
|
- `toolNames`: 最终允许进入 prompt / runtime 的工具集合。
|
||||||
|
- `toolPromptHints`: Prompt Builder 消费的行为提示。
|
||||||
|
- `toolManifest`: 子进程安全执行需要的 manifest。
|
||||||
|
- `toolRuntimeRoutes`: 每个工具在当前会话/Agent 下的实际运行路由。
|
||||||
|
- `disabledReasons`: 被过滤工具的解释。
|
||||||
|
|
||||||
|
因此“工具有没有生效”不能只查内置文件是否存在,必须看 resolver 的最终投影。
|
||||||
|
|
||||||
|
## 过程事件
|
||||||
|
|
||||||
|
2026-05-07 后,长耗时工具和 compute-entry Skill 可以通过 [[runtime-process-events]] 输出阶段进度。工具执行链路会为一次调用分配 `operationId`,过程事件可以被 WebSocket 流式推送,也可以采样写入 session metadata,并把完整 JSONL 日志落到 dataDir。
|
||||||
|
|
||||||
|
这条链路主要服务:
|
||||||
|
|
||||||
|
- [[vehicle-damage-skill]] 的抽帧、检测、定位和复核过程。
|
||||||
|
- `execute_skill` 对 compute-entry 子进程过程事件的桥接。
|
||||||
|
- 前端对话页的 `tool-process-timeline.vue` 历史回放。
|
||||||
|
|
||||||
|
## weixin_send_text
|
||||||
|
|
||||||
|
2026-05-14 新增的 `weixin_send_text` 属于 `weixin_desktop` toolset,是 weixin-db 群聊自动回复的发送工具。
|
||||||
|
|
||||||
|
执行要点:
|
||||||
|
|
||||||
|
- `channelId` 优先从 `_netaRuntime.bizContext.channelId` 读取,其次才接受显式参数。
|
||||||
|
- `modelChannelId` 来自 `_netaRuntime.currentAgent.modelChannelId`,即桌面操作 Agent 自己的模型渠道。
|
||||||
|
- 只允许在 channel 的 `config.weixinReply.enabled=true` 时执行。
|
||||||
|
- 支持 `suffix`、`zero-width`、`none` 三种水印策略。
|
||||||
|
- 创建 `DesktopTask(appId="weixin", actionType="send-text")` 后调用 `DesktopOpService.runAndWait()`,默认最多等待 60 秒。
|
||||||
|
|
||||||
|
这类工具应配置为子 Agent 可用且强制走主进程代理,因为真正的键鼠和窗口操作必须留在主进程控制边界内。
|
||||||
|
|
||||||
|
## MySQL 问数工具
|
||||||
|
|
||||||
|
2026-05-15 新增的 `mysql` toolset 提供只读智能问数后端能力:
|
||||||
|
|
||||||
|
- `mysql_list_sources`:列出当前 Agent 授权的数据源摘要,不返回 host、用户名、密码或连接串。
|
||||||
|
- `mysql_schema`:读取授权表的字段、索引、主键、外键和脱敏标记。
|
||||||
|
- `mysql_table_sample`:读取授权表的少量未脱敏字段样例,用于判断字段含义和 JOIN key。
|
||||||
|
- `mysql_query`:执行经过 guard 校验的只读 `SELECT`,支持授权表范围内的显式 JOIN,并写入查询审计。
|
||||||
|
|
||||||
|
MySQL 工具在子 Agent manifest 中统一走 `main-process-proxy`。数据库连接、密钥解密、SQL guard、脱敏列拒绝、行数限制和审计都留在主进程服务侧,worker 只看到工具 manifest 与代理路由。
|
||||||
|
|
||||||
|
这组工具的完整配置面和服务端安全边界见 [[mysql-data-source]]。其中几个容易误判的实现细节是:`mysql_schema` 会拆开读取 `information_schema.TABLES` 与 `information_schema.COLUMNS`,避免把 `TABLE_COMMENT` 当作字段元数据;`mysql_table_sample` 用小写映射做权限校验但保留原始列名执行,兼容 `tenantId` 这类驼峰列;`mysql_query` 允许单个末尾分号并在执行前规范化,但真实多语句仍会被 guard 拒绝。
|
||||||
|
|
||||||
|
## 前端渲染
|
||||||
|
|
||||||
|
前端工具展示不直接硬编码每个工具的 UI。当前实现是:
|
||||||
|
|
||||||
|
- 工具调用渲染通过 `renderer-registry.ts` 注册。
|
||||||
|
- `runtime-policy.ts` 负责把后端路由状态投影成页面展示。
|
||||||
|
- 工具管理页和 Agent 编辑页都消费后端 `runtimeDiagnostic` projection。
|
||||||
|
- 对话页除了一般工具调用卡片,还会结合 `tool_call_started` / `tool_result_streamed` / `tool_call_completed` 事件展示运行中状态。
|
||||||
|
|
||||||
|
这可避免工具新增后只写后端、前端看不到或文案不一致。
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [[tool-governance]]
|
||||||
|
- [[tool-runtime-policy]]
|
||||||
|
- [[tool-operations]]
|
||||||
|
- [[tool-catalog]]
|
||||||
|
- [[mysql-data-source]]
|
||||||
|
- [[image-generation-tools]]
|
||||||
|
- [[runtime-process-events]]
|
||||||
|
- [[desktop-op-module]]
|
||||||
|
- [[agent-runtime]]
|
||||||
|
- [[subagent-session]]
|
||||||
|
- [[frontend-architecture]]
|
||||||
|
- [[prompt-builder]]
|
||||||
76
docs/code-wiki/entities/vehicle-damage-skill.md
Normal file
76
docs/code-wiki/entities/vehicle-damage-skill.md
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
---
|
||||||
|
title: 车辆旧伤检测 Skill
|
||||||
|
created: 2026-05-07
|
||||||
|
updated: 2026-05-07
|
||||||
|
type: entity
|
||||||
|
tags: [skill, agent, backend]
|
||||||
|
sources: [packages/backend/skills/vehicle-damage-inspection/, packages/backend/test/skill_process_events.test.ts, docs/superpowers/plans/2026-05-06-tool-process-events.md]
|
||||||
|
---
|
||||||
|
|
||||||
|
# 车辆旧伤检测 Skill
|
||||||
|
|
||||||
|
`vehicle-damage-inspection` 是 2026-05-07 从 RZYX 迁入的多模态 compute-entry Skill,用于汽车环车视频旧伤检测。用户上传车辆环车视频并要求检查划痕、凹陷、掉漆、裂纹、锈蚀时,Agent 应通过 [[skill-runtime]] 的 `execute_skill` 调用它,而不是直接用图片识别工具分析整段视频。
|
||||||
|
|
||||||
|
## Skill 结构
|
||||||
|
|
||||||
|
```text
|
||||||
|
packages/backend/skills/vehicle-damage-inspection/
|
||||||
|
├── SKILL.md
|
||||||
|
├── README.md
|
||||||
|
├── skill.config.yaml
|
||||||
|
├── prompts/
|
||||||
|
│ ├── best_frame.md
|
||||||
|
│ ├── damage_detect.md
|
||||||
|
│ ├── damage_review.md
|
||||||
|
│ └── grounding.md
|
||||||
|
└── scripts/
|
||||||
|
├── index.cjs
|
||||||
|
└── lib/
|
||||||
|
├── frame_extractor.cjs
|
||||||
|
├── damage_detector.cjs
|
||||||
|
├── damage_grounding.cjs
|
||||||
|
├── damage_reviewer.cjs
|
||||||
|
├── best_frame_selector.cjs
|
||||||
|
├── image_marker.cjs
|
||||||
|
├── json_utils.cjs
|
||||||
|
├── vision_client.cjs
|
||||||
|
└── workspace.cjs
|
||||||
|
```
|
||||||
|
|
||||||
|
## 输入与输出
|
||||||
|
|
||||||
|
主要输入:
|
||||||
|
|
||||||
|
- `videoUrl`:本地路径、`/upload/...` 或本机可访问 URL。
|
||||||
|
- `fps`、`quality`、`batchSize`、`concurrency`:抽帧和检测吞吐控制。
|
||||||
|
- `groundingWindow`、`groundingFrameLimit`:候选旧伤定位窗口。
|
||||||
|
- `reviewConcurrency`、`topN`:复核和最佳证据帧数量。
|
||||||
|
- `mode`:`full`、`frames-only`、`detect-only`。
|
||||||
|
|
||||||
|
输出包含 `summary`、`vehicleInfo`、`damages`、`bestFrameImages`、`reviewImages` 和 `artifacts`。最终产物写入 dataDir 下的 `workspace/vehicle-damage-inspection/{taskId}`。
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
```text
|
||||||
|
execute_skill(vehicle-damage-inspection)
|
||||||
|
-> 抽帧
|
||||||
|
-> 旧伤候选检测
|
||||||
|
-> grounding 定位标注
|
||||||
|
-> 不确定区域裁剪/放大复核
|
||||||
|
-> 选择最佳证据帧
|
||||||
|
-> 输出 damages / bestFrameImages / reviewImages
|
||||||
|
```
|
||||||
|
|
||||||
|
Skill 明确区分 `damages` 和 `candidates`:`damages` 是最终旧伤结论,`candidates` 只是中间候选,不应直接呈现为结论。
|
||||||
|
|
||||||
|
## 过程事件
|
||||||
|
|
||||||
|
该 Skill 迁入时同步接入了 [[runtime-process-events]]。长耗时阶段会通过 process events 暴露抽帧、检测、定位、复核等阶段进度,前端可用 `tool-process-timeline.vue` 展示,而不需要只等最终 JSON。
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [[skill-system]]
|
||||||
|
- [[skill-runtime]]
|
||||||
|
- [[runtime-process-events]]
|
||||||
|
- [[frontend-architecture]]
|
||||||
|
- [[tool-system]]
|
||||||
147
docs/code-wiki/entities/websocket-gateway.md
Normal file
147
docs/code-wiki/entities/websocket-gateway.md
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
---
|
||||||
|
title: WebSocket 网关
|
||||||
|
created: 2026-04-13
|
||||||
|
updated: 2026-05-07
|
||||||
|
type: entity
|
||||||
|
tags: [gateway, websocket, api]
|
||||||
|
sources: [packages/backend/src/modules/netaclaw/gateway/, packages/frontend/src/modules/agent/store/chat.ts, packages/frontend/src/modules/agent/types/protocol.ts]
|
||||||
|
---
|
||||||
|
|
||||||
|
# WebSocket 网关
|
||||||
|
|
||||||
|
基于 Socket.IO 的实时通信层,包含两个命名空间:`/netaclaw`(Agent 对话)和 `/crew`(Crew 编排监控)。当前 `/netaclaw` 已经不仅承载 token 流,还承载 Session Tree 节点增量、leaf 切换、assistant 流式占位、工具执行状态、澄清、压缩、Todo、会话级子 Agent 批次等事件。
|
||||||
|
|
||||||
|
## 关键文件
|
||||||
|
|
||||||
|
| 文件 | 职责 |
|
||||||
|
|------|------|
|
||||||
|
| `gateway/server.ts` | Agent 对话 WebSocket,`@WSController('/netaclaw')` |
|
||||||
|
| `gateway/crew_server.ts` | Crew 编排 WebSocket,`@WSController('/crew')` |
|
||||||
|
| `gateway/session.ts` | 会话与 Session Tree 管理服务(创建、snapshot、leaf 切换、节点写回) |
|
||||||
|
| `gateway/protocol.ts` | 通信协议定义 |
|
||||||
|
|
||||||
|
## Agent 对话协议(`/netaclaw`)
|
||||||
|
|
||||||
|
### 客户端 → 服务端
|
||||||
|
|
||||||
|
| 类型 | 数据 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `chat` | `{ sessionId, content, agentName?, agentId?, leafEntryId? }` | 发送聊天消息,可指定从哪个 leaf/节点继续 |
|
||||||
|
| `ping` | `{}` | 心跳 |
|
||||||
|
| `set_thinking_level` | `{ sessionId, level }` | 切换思考等级 |
|
||||||
|
| `clarify_response` | `{ sessionId, requestId, answer }` | 回答澄清问题 |
|
||||||
|
| `compact` | `{ sessionId, instructions? }` | 手动触发上下文压缩 |
|
||||||
|
|
||||||
|
### 服务端 → 客户端
|
||||||
|
|
||||||
|
| 事件类型 | 说明 |
|
||||||
|
|---------|------|
|
||||||
|
| `token` | LLM 文本增量 |
|
||||||
|
| `thinking` | 完整思考内容 |
|
||||||
|
| `thinking_delta` | 思考增量 |
|
||||||
|
| `thinking_done` | 思考结束 |
|
||||||
|
| `tool_call` | 兼容型工具调用事件 |
|
||||||
|
| `tool_result` | 兼容型工具结果事件 |
|
||||||
|
| `tool_call_started` | 工具开始执行,附带 runtime route / blocked reason / policySources |
|
||||||
|
| `tool_result_streamed` | 工具结果流式投影 |
|
||||||
|
| `tool_call_completed` | 工具最终完成态 |
|
||||||
|
| `process_event` / 工具过程事件载荷 | 长耗时工具和 Skill 的阶段进度,详见 [[runtime-process-events]] |
|
||||||
|
| `skill_start` / `skill_end` | Skill 生命周期 |
|
||||||
|
| `progress` | 过程进度 |
|
||||||
|
| `todo_update` | Todo 列表变更 |
|
||||||
|
| `token_update` | token 与上下文占用更新 |
|
||||||
|
| `clarify_request` | 发起澄清问题 |
|
||||||
|
| `compaction_start` | 压缩开始 |
|
||||||
|
| `compaction_done` | 压缩完成 |
|
||||||
|
| `compaction_error` | 压缩失败 |
|
||||||
|
| `subagent_run_started` | 子 Agent 批次开始 |
|
||||||
|
| `subagent_event` | 单个子 Agent 状态/结果事件 |
|
||||||
|
| `subagent_run_completed` | 子 Agent 批次结束 |
|
||||||
|
| `session_entry_added` | Session Tree 新增节点 |
|
||||||
|
| `session_entry_updated` | Session Tree 节点更新 |
|
||||||
|
| `session_leaf_changed` | 当前 leaf 切换 |
|
||||||
|
| `assistant_stream_start` / `assistant_stream_delta` / `assistant_stream_end` | 助手消息占位与流式回填 |
|
||||||
|
| `done` | 一轮推理结束 |
|
||||||
|
| `error` | 异常 |
|
||||||
|
| `pong` | 心跳响应 |
|
||||||
|
|
||||||
|
## 会话加载与 Session Tree 边界
|
||||||
|
|
||||||
|
当前对话恢复的主入口已经不是旧的线性消息视图,而是 Session Tree:
|
||||||
|
|
||||||
|
- `getSessionTreeSnapshot()` 返回完整 snapshot,并在返回前统一执行 `projectSubagentSnapshot()`。
|
||||||
|
- `switchSessionTreeLeaf()` 负责切换当前叶子并返回新的 snapshot。
|
||||||
|
- `appendSubagentResultEntry()`、`finalizeAssistantEntry()` 等写回逻辑会统一补上 `projectSubagentEntry()`,确保前端拿到的是已经投影过的节点。
|
||||||
|
|
||||||
|
旧的 `netaclaw_message` 历史加载仍存在,主要服务兼容逻辑与压缩流程;但前端 Agent Chat 主路径已经围绕 snapshot + 节点事件工作,而不是围绕 `compacted/full` 消息列表工作。
|
||||||
|
|
||||||
|
## 前端对接
|
||||||
|
|
||||||
|
| 前端文件 | 职责 |
|
||||||
|
|---------|------|
|
||||||
|
| `agent/hooks/websocket.ts` | Agent 对话 Socket.IO 客户端 |
|
||||||
|
| `agent/hooks/crew-monitor.ts` | Crew 编排 Socket.IO 客户端 |
|
||||||
|
| `agent/store/chat.ts` | Pinia Store 维持 WS 连接和事件分发 |
|
||||||
|
| `agent/views/chat.vue` | 对话页面 UI |
|
||||||
|
|
||||||
|
## 新增事件流
|
||||||
|
|
||||||
|
### Clarify 流
|
||||||
|
|
||||||
|
`clarify_request → 用户输入 → clarify_response`
|
||||||
|
|
||||||
|
详见 [[clarify-tool]]。
|
||||||
|
|
||||||
|
### 压缩流
|
||||||
|
|
||||||
|
`compact / auto-threshold → compaction_start → compaction_done|compaction_error`
|
||||||
|
|
||||||
|
详见 [[context-compaction]]。
|
||||||
|
|
||||||
|
### 子 Agent 流
|
||||||
|
|
||||||
|
`delegate_* → subagent_run_started → subagent_event* → subagent_run_completed`
|
||||||
|
|
||||||
|
详见 [[subagent-session]]。
|
||||||
|
|
||||||
|
### 过程事件流
|
||||||
|
|
||||||
|
`tool_call_started → process_event* → tool_result_streamed/tool_call_completed`
|
||||||
|
|
||||||
|
详见 [[runtime-process-events]]。这条链路让 [[vehicle-damage-skill]] 等长耗时 compute-entry Skill 可以持续把抽帧、检测、定位、复核等阶段推给前端,而不是只在结束时返回结果。
|
||||||
|
|
||||||
|
### Session Tree 流
|
||||||
|
|
||||||
|
`chat(leafEntryId?) → session_entry_added / assistant_stream_* / session_entry_updated / session_leaf_changed`
|
||||||
|
|
||||||
|
这条链路负责:
|
||||||
|
|
||||||
|
- 新建 user / assistant / subagent 节点。
|
||||||
|
- 在流式阶段保持 assistant 占位节点与文本增量同步。
|
||||||
|
- 在切换叶子或从旧节点继续发送时,让前端及时重建 active path。
|
||||||
|
- 让子 Agent 结果、tool metadata、projection diagnostics 通过节点更新自然回到历史视图。
|
||||||
|
|
||||||
|
## Crew 编排协议(`/crew` 命名空间)
|
||||||
|
|
||||||
|
| 事件 | 方向 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `crew:trigger` | 客户端→服务端 | 触发集群运行 |
|
||||||
|
| `crew:control` | 客户端→服务端 | 控制运行(stop/resume/pause/retry) |
|
||||||
|
| `crew:run:status` | 服务端→客户端 | 运行状态变化 |
|
||||||
|
| `crew:task:status` | 服务端→客户端 | 任务状态变化 |
|
||||||
|
| `crew:log` | 服务端→客户端 | 日志消息 |
|
||||||
|
| `crew:escalation` | 服务端→客户端 | 升级事件 |
|
||||||
|
|
||||||
|
## 相关页面
|
||||||
|
|
||||||
|
- [[netaclaw-module]] — 所属模块
|
||||||
|
- [[agent-runtime]] — 消息处理后调用运行时
|
||||||
|
- [[crew-orchestration]] — Crew 编排系统
|
||||||
|
- [[thinking-system]] — 思考事件协议
|
||||||
|
- [[todo-system]] — todo_update 事件
|
||||||
|
- [[clarify-tool]] — 澄清交互事件
|
||||||
|
- [[context-compaction]] — 压缩事件与历史视图
|
||||||
|
- [[subagent-session]] — 子 Agent 批次事件
|
||||||
|
- [[runtime-process-events]] — 工具和 Skill 过程事件
|
||||||
|
- [[session-tree-runtime]] — snapshot、leaf 和节点更新模型
|
||||||
|
- [[react-loop]] — 执行流程概念
|
||||||
56
docs/code-wiki/index.md
Normal file
56
docs/code-wiki/index.md
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# Wiki Index
|
||||||
|
|
||||||
|
> Neta AI 电商平台代码知识库内容目录。
|
||||||
|
> 每个 wiki 页面按类型列出,并附一行摘要。
|
||||||
|
> 查询时先读此文件定位相关页面。
|
||||||
|
> 最后更新:2026-05-15 | 总页面数:39
|
||||||
|
|
||||||
|
## Entities(实体)
|
||||||
|
|
||||||
|
- [[agent-channel]] - Agent 外部渠道接入,包括 ClawBot 私聊、weixin-db 群聊代理、群白名单和 v4 双 Agent 自动回复。
|
||||||
|
- [[agent-runtime]] - Agent 运行时核心;当前以 Session Tree、ChatOrchestrator、ToolResolver、Subagent Worker 和 canonical subagent projection 边界组成。
|
||||||
|
- [[base-module]] - 用户、角色、菜单、权限等 RBAC 基础设施。
|
||||||
|
- [[clarify-tool]] - Clarify 澄清工具,支持 Agent 向用户提问和 WebSocket/微信降级。
|
||||||
|
- [[cool-admin-framework]] - Cool Admin 8.0 的自动 CRUD、Service 代理和 BaseEntity 机制。
|
||||||
|
- [[crew-orchestration]] - Multi-Agent Crew 编排系统,支持画布编辑、任务运行和实时监控。
|
||||||
|
- [[document-skills]] - PDF、DOCX、XLSX 文档处理 Skills,覆盖 MiniMax 文档生成、编辑、验证和脚本工具包。
|
||||||
|
- [[desktop-op-module]] - 通用桌面 GUI Agent 模块,封装窗口定位、截图、键鼠、VLM 验证、队列互斥和微信发送审计。
|
||||||
|
- [[geo-module]] - Geo 账号与代理模块,管理平台账号、代理 IP、浏览器 sessionName、fingerprintSeed 和 cookie 登录态。
|
||||||
|
- [[image-generation-tools]] - 文生图/图生图工具族,支持火山引擎 Ark、MiniMax provider、图片持久化和前端结构化渲染。
|
||||||
|
- [[llm-providers]] - Anthropic、OpenAI、DeepSeek 等 LLM 提供商适配层。
|
||||||
|
- [[memory-system]] - Agent 长期记忆系统,支持 MySQL/SQLite 双后端、显式 recall、类型管理和 `/agent/memory` 管理页。
|
||||||
|
- [[mysql-data-source]] - MySQL 数据源、只读问数工具、SQL guard、schema/sample/query 服务、数据源管理页和查询审计。
|
||||||
|
- [[netaclaw-module]] - NetaClaw AI Agent 核心模块总览,包含 Session Tree、工具治理、Skill Runtime、记忆、子 Agent、渠道群聊和 19 张核心表。
|
||||||
|
- [[netabrowser-runtime]] - Netabrowser 反风控浏览器运行时,包含 CLI、后端 browser-daemon、patchright/neta-chromium、拟人化交互和持久 profile。
|
||||||
|
- [[patch-tool]] - Patch 模糊补丁工具。
|
||||||
|
- [[project-module]] - 项目管理模块,包括甘特图、日历、看板、列表和工时记录。
|
||||||
|
- [[project-overview]] - Neta monorepo 项目总览,包含 Agent 入口、Desktop Op、Windows 安装器/托盘、后端 Skills 和推荐阅读顺序。
|
||||||
|
- [[skill-system]] - Skill 技能系统,支持 GitHub、ZIP、本地安装、分类、诊断、密钥配置和附属文件。
|
||||||
|
- [[subagent-session]] - 普通对话中的会话级子 Agent 委派;现已包含 worker 进程、证据摘要、过程回放和后端 projection。
|
||||||
|
- [[todo-system]] - Agent 会话级任务管理。
|
||||||
|
- [[tool-catalog]] - Tool Catalog 工具目录系统,负责 schema 级工具注册。
|
||||||
|
- [[tool-governance]] - Tool 全局治理系统,负责 DB 同步、Agent 覆盖、运行时诊断和前端投影。
|
||||||
|
- [[tool-system]] - Agent 工具实现层,涵盖内置工具、Operations 注入、风险确认、执行链路、`weixin_send_text` 和子进程工具路由。
|
||||||
|
- [[vehicle-damage-skill]] - 汽车环车视频旧伤检测 Skill,负责抽帧、旧伤候选、定位标注、放大复核和最佳证据帧输出。
|
||||||
|
- [[websocket-gateway]] - Socket.IO 实时通信网关,承载 NetaClaw 对话和 Crew 监控事件。
|
||||||
|
|
||||||
|
## Concepts(概念)
|
||||||
|
|
||||||
|
- [[context-compaction]] - 长会话上下文压缩机制,当前通过 Session Tree compaction 节点表达。
|
||||||
|
- [[development-conventions]] - 后端、前端、数据库统一开发规范。
|
||||||
|
- [[frontend-architecture]] - Vue 3 模块化前端架构,重点覆盖 Agent Chat Store、频道管理、会话树、子 Agent 回放、工具确认、工具治理页、Skill 页和记忆页。
|
||||||
|
- [[prompt-builder]] - Prompt Builder 分层注入系统,消费 ToolResolver 的最终工具列表和 Prompt Hint。
|
||||||
|
- [[react-loop]] - ReAct 推理模式,描述 LLM 思考和工具行动交替。
|
||||||
|
- [[runtime-process-events]] - 长耗时工具和 Skill 的运行时过程事件,支持进度流、JSONL 日志、采样 metadata 和前端时间线回放。
|
||||||
|
- [[session-tree-runtime]] - Session Tree 运行时,会话树、active path、snapshot、continue-from-entry、删除语义和子 Agent projection 契约。
|
||||||
|
- [[skill-runtime]] - Skill Runtime 执行体系,覆盖 skill.config.yaml、compute-entry、compute-toolkit、scoped secrets 和诊断。
|
||||||
|
- [[thinking-system]] - Thinking/Reasoning 能力系统,包括模型预算映射和会话级动态调节。
|
||||||
|
- [[tool-operations]] - 工具底层执行后端抽象,分离 File/Process/Search Operations 与工具业务逻辑。
|
||||||
|
- [[tool-runtime-policy]] - 工具运行时策略,负责 manifest、worker policy、工具路由、blocked reason 和 Operations 边界推导。
|
||||||
|
- [[windows-runtime]] - Windows 本地部署运行时,覆盖 backend.exe、托盘、安装器、dataDir 和本机控制接口。
|
||||||
|
|
||||||
|
## Comparisons(对比)
|
||||||
|
|
||||||
|
- [[database-entity-overview]] - Entity 按模块分组和核心表字段速查,包含 netaclaw 渠道群表与 desktop_op 配置/审计表。
|
||||||
|
|
||||||
|
## Queries(查询)
|
||||||
114
docs/code-wiki/log.md
Normal file
114
docs/code-wiki/log.md
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
# Wiki Log
|
||||||
|
|
||||||
|
> 所有 wiki 操作的时间线记录。仅追加。
|
||||||
|
> 格式:`## [YYYY-MM-DD] 操作 | 主题`
|
||||||
|
> 操作类型:ingest, update, query, lint, create, archive, delete
|
||||||
|
> 当此文件超过 500 条记录时,重命名为 `log-YYYY.md` 并重新开始。
|
||||||
|
|
||||||
|
## [2026-05-15] update | MySQL 数据源与问数系统写入 code-wiki
|
||||||
|
- 来源:当前未提交的 MySQL 数据源管理、只读问数工具、SQL guard、schema/sample/query 服务、查询审计、菜单同步和前端数据源管理页代码。
|
||||||
|
- 新增 entities/mysql-data-source.md:记录“系统管理 -> 数据源管理”、`netaclaw_data_source`、`netaclaw_data_source_query_audit`、密钥加密、连接池、`mysql_*` 工具、SQL guard、schema/sample 修复和 `data-analyst-mysql` prompt skill。
|
||||||
|
- 更新 entities/netaclaw-module.md:补入 MySQL 数据源子系统、19 张 NetaClaw 表和相关页面互链。
|
||||||
|
- 更新 entities/tool-system.md、entities/tool-catalog.md、entities/tool-governance.md:登记 `mysql` toolset、`builtin/mysql.ts`、main-process-proxy 路由和服务端安全边界。
|
||||||
|
- 更新 entities/base-module.md、concepts/frontend-architecture.md:补入“数据源管理”动态菜单同步、页面职责、端口输入与测试连接行为。
|
||||||
|
- 更新 entities/skill-system.md、comparisons/database-entity-overview.md:补入 `data-analyst-mysql` 与数据源/审计表字段。
|
||||||
|
- 更新 index.md:总页面数更新为 39,并登记 `[[mysql-data-source]]`。
|
||||||
|
|
||||||
|
## [2026-05-14] ingest | 5e18dbf2..HEAD 微信本地群聊代理与 Desktop Op 写入 code-wiki
|
||||||
|
- 来源:git log `5e18dbf2..HEAD`,重点核对 2026-05-08 微信群聊渠道设计与修复、2026-05-14 weixin-db 本地代理、v4 双 Agent 自动回复、`weixin_send_text` 和 `desktop_op` 通用桌面 GUI Agent 模块。
|
||||||
|
- 新增 entities/desktop-op-module.md:记录 `packages/backend/src/modules/desktop_op/` 的运行时拓扑、WeixinAdapter、per-key 队列、全局键鼠互斥、AbortSignal 取消和 `desktop_op_config` / `desktop_op_action_log`。
|
||||||
|
- 更新 entities/agent-channel.md:从旧 ClawBot 口径扩展为 `weixin` 私聊 + `weixin-db` 群聊代理,补入 WAL watcher、增量解密、群白名单、每群 Agent 覆盖、Clarify/risk 降级和 v4 双 Agent 自动回复。
|
||||||
|
- 更新 entities/netaclaw-module.md:登记 `netaclaw_agent_channel_group`,把 NetaClaw 表数更新为 17,并把 Desktop Op 加入核心关系。
|
||||||
|
- 更新 entities/tool-system.md:登记 `weixin_send_text` 工具、`weixin_desktop` toolset、bizContext/currentAgent 运行时上下文、Desktop Op 主进程代理和 60 秒同步等待。
|
||||||
|
- 更新 concepts/frontend-architecture.md:补入频道管理页的 weixin-db、wxid 唯一性校验、群聊管理和微信自动回复配置区块。
|
||||||
|
- 更新 comparisons/database-entity-overview.md:登记 `netaclaw_agent_channel_group`、`desktop_op_config` 和 `desktop_op_action_log` 字段速查。
|
||||||
|
- 更新 entities/project-overview.md 与 index.md:同步后端模块数、总页面数 37、推荐入口和 2026-05-14 增量摘要。
|
||||||
|
|
||||||
|
## [2026-05-07] ingest | b4d244c6..HEAD 图像生成、过程事件、Geo 与 Netabrowser 写入 code-wiki
|
||||||
|
- 来源:git log `b4d244c6..HEAD`,重点核对 2026-05-03 图像生成工具、2026-05-07 Skill process events / vehicle-damage-inspection、Geo S1 infrastructure、Netabrowser CLI 与 backend browser-daemon。
|
||||||
|
- 新增 entities/image-generation-tools.md:记录 `text_to_image`、`image_to_image`、Ark/MiniMax provider、ImageStorageService、Tool Catalog/Governance/Resolver/前端 renderer 链路。
|
||||||
|
- 新增 concepts/runtime-process-events.md:记录 `RuntimeProcessEvent`、`ProcessEventBuffer`、payload 脱敏、JSONL 落盘、execute_skill 桥接和前端过程时间线。
|
||||||
|
- 新增 entities/vehicle-damage-skill.md:记录 `vehicle-damage-inspection` compute-entry Skill 的抽帧、检测、grounding、复核、证据帧和 workspace 产物。
|
||||||
|
- 新增 entities/geo-module.md:记录 `geo_account`、`geo_proxy_ip`、账号/IP 1:1 绑定、sessionName、fingerprintSeed、cookie 抓取和 Geo 前端页面。
|
||||||
|
- 新增 entities/netabrowser-runtime.md:记录 `packages/netabrowser-cli`、后端 `browser-daemon`、patchright/neta-chromium、拟人化交互、SOCKS5 HTTP bridge、profile/state 生命周期和相关 Skills。
|
||||||
|
- 更新 entities/project-overview.md、entities/netaclaw-module.md、entities/tool-system.md、entities/skill-system.md、entities/websocket-gateway.md、concepts/skill-runtime.md、concepts/frontend-architecture.md、comparisons/database-entity-overview.md:同步新增模块、工具、Skill、过程事件、数据库和前端入口。
|
||||||
|
- 更新 index.md:总页面数更新为 37,并登记新增页面摘要。
|
||||||
|
|
||||||
|
## [2026-05-02] ingest | 56eed8c..HEAD Skill Runtime、Tool Operations 与文档 Skills 写入 code-wiki
|
||||||
|
- 来源:git log `56eed8c..HEAD`,重点核对 2026-04-27 Skill 系统演进设计、2026-05-01 Tool Pluggable Operations 设计与落地提交、2026-04-27~2026-05-02 后端/前端/skills 代码变更。
|
||||||
|
- 新增 concepts/tool-operations.md:记录 `ToolOperations`、`FileOperations`、`ProcessOperations`、`SearchOperations`、LocalOperations、resolver 注入、ANSI 剥离和后续远程/沙箱替换点。
|
||||||
|
- 新增 concepts/skill-runtime.md:记录 prompt / compute-entry / compute-toolkit 分类、`skill.config.yaml`、`execute_skill`、`SkillExecutorService`、skill scoped secrets、诊断码和前端配置入口。
|
||||||
|
- 新增 entities/document-skills.md:登记 `minimax-pdf`、`minimax-docx`、`minimax-xlsx` 三组文档处理 Skills 及其脚本、references、OpenXML/XLSX/PDF 处理边界。
|
||||||
|
- 更新 entities/skill-system.md:从旧 prompt-only 口径改为分类、配置、密钥、诊断、ToolResolver 注入和 `execute_skill` 口径。
|
||||||
|
- 更新 entities/tool-system.md、entities/tool-governance.md、concepts/tool-runtime-policy.md:补入 Operations 执行后端层、高风险 bash 工具确认、`execute_skill` 路由和治理/Policy/Operations 三者边界。
|
||||||
|
- 更新 concepts/frontend-architecture.md:补入 ChatComposer 工具确认 UI、Skill 管理页分类/诊断/secrets 配置和安装 setup 确认。
|
||||||
|
- 更新 entities/netaclaw-module.md、entities/project-overview.md、comparisons/database-entity-overview.md:同步 Skill Runtime、Tool Operations、文档 Skills、`netaclaw_skill.secrets/envSchema` 和推荐阅读顺序。
|
||||||
|
- 更新 index.md:总页面数更新为 32,并登记新增页面与相关摘要。
|
||||||
|
|
||||||
|
## [2026-04-13] create | Wiki 初始化
|
||||||
|
- 领域:Neta AI 电商平台代码知识库。
|
||||||
|
- 创建目录结构:SCHEMA.md, index.md, log.md, raw/, entities/, concepts/, comparisons/, queries/。
|
||||||
|
- 基于项目源码和初始架构分析创建第一批 wiki 页面。
|
||||||
|
|
||||||
|
## [2026-04-13] ingest | 初始全项目代码分析
|
||||||
|
- 来源:Neta-monorepo 项目源码和 CLAUDE.md。
|
||||||
|
- 创建核心实体页:project-overview、netaclaw-module、agent-runtime、websocket-gateway、tool-system、memory-system、skill-system、llm-providers、base-module、project-module、cool-admin-framework。
|
||||||
|
- 创建核心概念页:react-loop、frontend-architecture、development-conventions。
|
||||||
|
- 创建对比页:database-entity-overview。
|
||||||
|
|
||||||
|
## [2026-04-14] ingest | 近期代码变更增量分析
|
||||||
|
- 来源:git log 07ae148..HEAD。
|
||||||
|
- 新增 crew-orchestration、agent-channel、todo-system、thinking-system 等页面。
|
||||||
|
- 更新 skill-system、tool-system、netaclaw-module、llm-providers、websocket-gateway、agent-runtime、project-module、project-overview、database-entity-overview。
|
||||||
|
|
||||||
|
## [2026-04-16] ingest | 62b12fc..48b4a5b 新功能摄入
|
||||||
|
- 来源:git log 62b12fc..HEAD。
|
||||||
|
- 新增 clarify-tool、patch-tool、tool-catalog、prompt-builder。
|
||||||
|
- 更新 tool-system、agent-runtime、websocket-gateway、agent-channel、netaclaw-module。
|
||||||
|
|
||||||
|
## [2026-04-19] ingest | 48b4a5b..HEAD 真实代码变更回填
|
||||||
|
- 来源:raw/transcripts/2026-04-19-git-audit-48b4a5b-head.md。
|
||||||
|
- 新增 tool-governance、subagent-session、context-compaction。
|
||||||
|
- 更新 agent-runtime、tool-system、tool-catalog、websocket-gateway、netaclaw-module、prompt-builder、database-entity-overview、project-overview。
|
||||||
|
- index.md 登记到 26 个页面。
|
||||||
|
|
||||||
|
## [2026-04-21] update | Agent runtime kernel 架构变更写入 code-wiki
|
||||||
|
- 来源:近期 Agent runtime kernel、Session Tree、Subagent Worker、Tool Manifest/Runtime Policy、前端 Agent 页面重构代码。
|
||||||
|
- 新增 concepts/session-tree-runtime.md:记录 Session Tree provider、entry 类型、snapshot、active path、前端恢复和持久化边界。
|
||||||
|
- 新增 concepts/tool-runtime-policy.md:记录工具 manifest、worker policy 自动推导、worker-local/main-process-proxy/disabled 路由和 blocked reason。
|
||||||
|
- 更新 entities/agent-runtime.md:从线性 ReAct 说明改为 Session Tree + ChatOrchestrator + ToolResolver + Subagent Worker 架构口径。
|
||||||
|
- 更新 entities/subagent-session.md:补全独立 worker 进程、JSONL 协议、proxy tool call、subagent_batch/subagent_result 节点。
|
||||||
|
- 更新 entities/tool-governance.md:补全 runtimeDiagnostic、effectiveRuntimeProfile、effectiveSubagentAllowed、工具管理页和 Agent 编辑页 projection 一致性。
|
||||||
|
- 更新 entities/tool-system.md:补全内置工具分组、主 Agent 执行、子 Agent 工具路由、前端 renderer registry。
|
||||||
|
- 更新 concepts/frontend-architecture.md:补全 Agent Chat Store 的 session tree snapshot 状态、对话页滚动/恢复、工具治理页和 Agent 编辑页职责。
|
||||||
|
- 更新 concepts/context-compaction.md:从线性消息压缩改为 Session Tree compaction 节点和 active path 口径。
|
||||||
|
- 更新 index.md:用干净中文重写目录,登记 28 个页面。
|
||||||
|
|
||||||
|
## [2026-04-23] update | 子 Agent 回放投影与会话树继续发送写入 code-wiki
|
||||||
|
- 来源:git log 403702f..HEAD,以及 backend/frontend 中 session-tree、subagent、chat store、chat.vue 的真实实现。
|
||||||
|
- 更新 entities/agent-runtime.md:补充 `gateway/session.ts` 的 `projectSubagentSnapshot()` / `projectSubagentEntry()` 边界,说明后端 canonical subagent projection 已成为运行时契约。
|
||||||
|
- 更新 entities/subagent-session.md:补充 evidence summary 构建、`finalOutput/rawFinalContent/toolResults/processEvents` 结果载荷,以及 `subagent_batch` 与 `subagent_result` 的职责分层。
|
||||||
|
- 更新 concepts/session-tree-runtime.md:补充 continue-from-entry 交互、leaf 切换/分叉规则,以及 `metadata.subagentProjection` 的 snapshot 契约。
|
||||||
|
- 更新 concepts/frontend-architecture.md:补充 chat store 的 `selectedEntryId` / `pendingLeafConfirmation` 等状态、对话页继续发送目标提示和子 Agent 回放诊断面板。
|
||||||
|
- 更新 index.md:同步更新目录摘要,去除已经过时的“仅 worker/session tree 节点”级别描述。
|
||||||
|
|
||||||
|
## [2026-04-23] update | code-wiki 全面体检与入口页校准
|
||||||
|
- 来源:继续核对 403702f..HEAD 期间 gateway/server.ts、gateway/protocol.ts、gateway/session.ts、tool_resolver.ts、tools.vue、agent-edit.vue 以及相关实体定义。
|
||||||
|
- 更新 entities/tool-system.md:补充结构化 `rawResult`、resolver 最终输出、子 Agent `toolRuntimeRoutes` 和对话页运行中工具事件。
|
||||||
|
- 更新 entities/tool-governance.md:补充 `runtimeDiagnostic`、`toolRuntimeRoutes`、renderer capability,以及工具管理页 / Agent 编辑页 / 对话页的职责分层。
|
||||||
|
- 更新 entities/websocket-gateway.md:修正旧事件名,改写为 `subagent_run_started`、`subagent_event`、`subagent_run_completed`、`session_entry_*`、`session_leaf_changed`、`assistant_stream_*` 等当前协议。
|
||||||
|
- 更新 entities/netaclaw-module.md:补充 `session-tree/`、`subagent/` 目录,修正为 15 张实体表,并区分 legacy session/message 与 session-tree 主路径。
|
||||||
|
- 更新 concepts/tool-runtime-policy.md:补充静态画像与动态路由的区别。
|
||||||
|
- 更新 entities/tool-catalog.md:补充 catalog 到 registry/resolver/manifest 的角色边界。
|
||||||
|
- 更新 concepts/context-compaction.md:补充 legacy `compactedAt` 兼容路径与 Session Tree 主路径的区别。
|
||||||
|
- 更新 entities/project-overview.md:补充当前 AI 快速熟悉项目的推荐阅读顺序,并把 Agent 模块入口调整到 Session Tree 时代口径。
|
||||||
|
|
||||||
|
## [2026-04-26] ingest | f493b15..HEAD 记忆管理、Windows runtime 与会话删除修复写入 code-wiki
|
||||||
|
- 来源:git log f493b15..HEAD,重点核对 2026-04-25 Windows installer/tray/dataDir 变更、2026-04-26 memory management 变更、Agent chat 展示修复,以及当前未提交的 MySQL session-tree 删除修复。
|
||||||
|
- 新增 concepts/windows-runtime.md:记录 backend.exe 打包、Inno Setup 安装器、.NET 托盘、runtime info、dataDir、`/app/base/runtime/status|stop` 本机控制接口和托盘密钥边界。
|
||||||
|
- 更新 entities/memory-system.md:从旧的静默 prefetch 模型改写为 MemoryProviderRegistry + 显式 `memory_recall` + `memory_save/list_types/stats` 工具体系;补充 `netaclaw_memory_type`、管理 API、`/agent/memory` 页面和 MySQL/SQLite 双后端分页统计。
|
||||||
|
- 更新 entities/project-overview.md:登记 `packages/windows-tray/`、`/agent/memory` 入口和 Windows 本地部署子系统。
|
||||||
|
- 更新 concepts/frontend-architecture.md:补充记忆管理页职责、会话删除时使用会话自身 `agentId`、bash/tool 参数持久展示。
|
||||||
|
- 更新 concepts/session-tree-runtime.md:补充 session-tree 删除语义,说明后端从 `netaclaw_agent_session` 反查真实 `agentId` 以选择正确 provider。
|
||||||
|
- 更新 entities/netaclaw-module.md 与 comparisons/database-entity-overview.md:把 NetaClaw 表数更新到 16,并登记 `netaclaw_memory_type`。
|
||||||
|
- 更新 index.md:总页数更新为 29,并同步 Memory、Windows runtime、Session Tree、前端架构等摘要。
|
||||||
@ -0,0 +1,111 @@
|
|||||||
|
# Local Audit Source — git range 48b4a5b..HEAD
|
||||||
|
|
||||||
|
> 本文件是 2026-04-19 对 `48b4a5b..HEAD` 的本地 git 审计快照,作为本次 code-wiki 增量更新的原始来源层。
|
||||||
|
> 来源类型:本地仓库 `git log --oneline` + `git diff --name-only` + 关键源码阅读。
|
||||||
|
> 约定:此文件为原始来源记录,不在后续 wiki 更新中就地改写。
|
||||||
|
|
||||||
|
## 基线
|
||||||
|
|
||||||
|
- Wiki 上次摄入记录:`48b4a5b`
|
||||||
|
- 审计日期:2026-04-19
|
||||||
|
- 审计范围:`48b4a5b..HEAD`
|
||||||
|
|
||||||
|
## 关键提交(按时间倒序)
|
||||||
|
|
||||||
|
- `215b46d` 完成 Neta 会话子Agent委派功能与聚合修复
|
||||||
|
- `b932264` chore(repo): commit remaining workspace changes
|
||||||
|
- `c198bb5` feat(netaclaw): add tool governance and agent tool controls
|
||||||
|
- `c187fde` fix(agent-chat): refine history view toggle behavior
|
||||||
|
- `089d507` fix(agent-fe): 修正 contextWindow 单位换算
|
||||||
|
- `f3aec00` fix(netaclaw-be): 补齐缺失的 /session/contextTokens 接口
|
||||||
|
- `b712536` fix(agent-fe): token-stats 静态显示修复 — tokenContext 增加本地估算 fallback
|
||||||
|
- `b2fa690` fix(agent-fe): 压缩事件被 loading 守卫拦截导致前端无响应
|
||||||
|
- `8fdae94` fix(netaclaw): 修复压缩前端无响应 + 视图切换无效
|
||||||
|
- `5dd29d9` fix(compaction): phase2 边界检测越界
|
||||||
|
- `c22e265` fix: 代码审查修复 — 并发安全、XSS、队列上限、视图切换
|
||||||
|
- `40e4d7c` fix(agent-fe): ChatMessage 类型支持 compaction 角色并修正 chat.ts 变量名
|
||||||
|
- `d32eeb1` feat(agent-fe): 新增压缩/完整历史视图切换 + token-stats 新 props 接入
|
||||||
|
- `62a949f` feat(agent-fe): 模型渠道管理支持 isAuxiliary 标记
|
||||||
|
- `852e94b` feat(agent-fe): Agent 编辑页新增上下文压缩配置子区域
|
||||||
|
- `dbe0bd4` feat(agent-fe): 新增压缩事件气泡组件
|
||||||
|
- `45d39bb` feat(agent-fe): 新增 parseSlashCommand + /compact 命令拦截
|
||||||
|
- `9002698` feat(agent-fe): chat store 新增 compactionState + 压缩事件处理
|
||||||
|
- `9faaef6` feat(agent-fe): 新增前端 Protocol 类型定义同步后端压缩相关事件
|
||||||
|
- `d295ab3` feat(agent-fe): token-stats 重构为进度条
|
||||||
|
- `e1b6204` feat(netaclaw): Gateway 集成自动/手动压缩触发 + token 估算修正
|
||||||
|
- `7fc1a4e` feat(netaclaw): Protocol 新增压缩相关 WS 事件类型
|
||||||
|
- `0c05cf9` feat(netaclaw): SessionService 增加 view 参数支持压缩/完整历史切换
|
||||||
|
- `c11af60` feat(netaclaw): 新增 CompactionService
|
||||||
|
- `f74abe2` feat(netaclaw): 新增 AuxiliaryLLMClient
|
||||||
|
- `bdcf314` feat(netaclaw): 新增 token_utils.ts
|
||||||
|
- `a6d68f6` feat(netaclaw): 新增 LLMMessageWithId 接口供压缩流程使用
|
||||||
|
- `ae6338b` feat(netaclaw): 扩展 Entity 字段支持上下文压缩
|
||||||
|
|
||||||
|
## 与本次 wiki 直接相关的变更文件
|
||||||
|
|
||||||
|
### 后端
|
||||||
|
- `packages/backend/src/modules/netaclaw/entity/tool.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/entity/subagent_session.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/entity/agent.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/entity/message.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/entity/model_channel.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/controller/admin/tool.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/controller/agent.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/controller/session.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/gateway/protocol.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/gateway/server.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/gateway/session.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/runtime/compaction.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/runtime/prompt_builder.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/runtime/token_utils.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/service/chat_orchestrator.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/service/subagent.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/service/tool_registry.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/service/tool_resolver.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/tools/catalog.ts`
|
||||||
|
|
||||||
|
### 前端
|
||||||
|
- `packages/frontend/src/modules/agent/views/tools.vue`
|
||||||
|
- `packages/frontend/src/modules/agent/views/agent-edit.vue`
|
||||||
|
- `packages/frontend/src/modules/agent/views/chat.vue`
|
||||||
|
- `packages/frontend/src/modules/agent/views/model-channel.vue`
|
||||||
|
- `packages/frontend/src/modules/agent/store/chat.ts`
|
||||||
|
- `packages/frontend/src/modules/agent/components/subagent-batch-card.vue`
|
||||||
|
- `packages/frontend/src/modules/agent/components/compaction-bubble.vue`
|
||||||
|
- `packages/frontend/src/modules/agent/components/token-stats.vue`
|
||||||
|
- `packages/frontend/src/modules/agent/types/index.d.ts`
|
||||||
|
- `packages/frontend/src/modules/agent/types/protocol.ts`
|
||||||
|
- `packages/frontend/src/modules/agent/utils/slash_command.ts`
|
||||||
|
- `packages/frontend/src/modules/agent/config.ts`
|
||||||
|
|
||||||
|
## 审计结论摘要
|
||||||
|
|
||||||
|
本次真实代码变更可归纳为三块:
|
||||||
|
|
||||||
|
1. **上下文压缩与历史视图**
|
||||||
|
- 新增 `CompactionService`
|
||||||
|
- 支持 `/compact` 手动压缩与阈值自动压缩
|
||||||
|
- 会话消息支持 `compacted/full` 两种历史视图
|
||||||
|
- 新增压缩相关 WS 事件和前端气泡展示
|
||||||
|
|
||||||
|
2. **Tool 全局治理与 Agent 级工具配置**
|
||||||
|
- 新增 `netaclaw_tool` 全局治理表
|
||||||
|
- Catalog 与 DB 同步
|
||||||
|
- 运行时由 `tool_resolver` 统一决策可用工具、禁用原因和 Prompt Hint
|
||||||
|
- 前端新增 `/agent/tools` 管理页,Agent 编辑页支持逐工具启停和子代理约束
|
||||||
|
|
||||||
|
3. **会话级子 Agent 委派**
|
||||||
|
- 新增 `netaclaw_subagent_session` 持久化记录
|
||||||
|
- `chat_orchestrator` 负责 assistant 占位消息与批次聚合 metadata
|
||||||
|
- 新增 `subagent_batch_start / subagent_update / subagent_done` 事件
|
||||||
|
- 前端以 `subagent-batch-card` 组件展示批量执行状态
|
||||||
|
|
||||||
|
## 注意
|
||||||
|
|
||||||
|
- `packages/backend/analyze_cert.py`
|
||||||
|
- `packages/backend/marriage_cert.jpg`
|
||||||
|
- `packages/backend/marriage_cert_analysis_report.txt`
|
||||||
|
- `packages/backend/order_list.yml`
|
||||||
|
- `packages/backend/sql/20260418_netaclaw_session_subagent.sql`
|
||||||
|
|
||||||
|
以上文件已被用户明确判定为应删除或已删除,不纳入本次 wiki 的长期知识条目。
|
||||||
289
docs/progress/phase1-scope-design.md
Normal file
289
docs/progress/phase1-scope-design.md
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
# Neta AI 电商 - 第一期项目范围定义
|
||||||
|
|
||||||
|
> 用途:作为 `Neta` 平台第一期内部立项、范围确认、上线验收和后续对外销售前置规则的依据
|
||||||
|
> 日期:2026-04-17
|
||||||
|
> 上线目标日期:2026-08-15
|
||||||
|
> 验收方式:按场景验收并在上线前完成整体验收确认
|
||||||
|
> 基础设计文档:`docs/superpowers/specs/2026-04-11-netaclaw-ecommerce-automation-design.md`
|
||||||
|
|
||||||
|
## 1. 文档定位
|
||||||
|
|
||||||
|
本文件用于明确 `Neta` 平台第一期的建设目标、上线范围、边界约定、验收口径、风险依赖及后续对外使用前提。
|
||||||
|
|
||||||
|
本文件适用于内部团队协同、一期上线验收和后续产品销售准备,不作为底层技术设计说明,不展开具体框架实现细节。
|
||||||
|
|
||||||
|
## 2. 项目背景与总体目标
|
||||||
|
|
||||||
|
`Neta` 平台的总体目标,是让一个运营人员通过统一平台管理多个电商平台店铺账号,通过系统承担重复性运营动作,降低人工切换平台、重复录入和重复执行的成本。
|
||||||
|
|
||||||
|
平台建设完成后,运营人员应将主要精力放在复杂发货处理、经营分析、看报告、找产品、选品判断及其他高价值工作上,而将重复性高、标准化程度高的运营动作交由系统辅助执行或自动化完成。
|
||||||
|
|
||||||
|
平台长期方向包括统一接入、统一操作、统一数据查看,并在后续阶段逐步扩展更深层的运营自动化能力。
|
||||||
|
|
||||||
|
## 3. 第一期上线目标与范围
|
||||||
|
|
||||||
|
第一期目标是在 **2026 年 8 月 15 日** 实现 `Neta` 平台上线,完成淘宝/天猫、京东、拼多多三个平台的基础可用版本交付。第一期重点跑通以下核心能力:
|
||||||
|
|
||||||
|
- 店铺接入与连接管理:支持三平台店铺账号基础接入、登录状态查看、连接状态管理和多店铺基础管理。
|
||||||
|
- 商品基础运营:支持商品同步、上架、下架、删除、标题修改、价格修改、库存修改、基础 SKU 管理。
|
||||||
|
- AI 自动推广执行:通过 AI 自动化操作浏览器,获取指定店铺、指定类目或指定商品的基础行业数据,并在已适配场景下自动完成基础投流和活动报名。
|
||||||
|
- 商品图片生成与替换:支持基于商品信息和用户要求生成商品主图或详情图,并在人工确认后完成平台图片替换。
|
||||||
|
- 基础经营数据查看:支持查看店铺基础经营数据、商品排行数据、趋势数据,并支持基础查询与导出。
|
||||||
|
|
||||||
|
第一期交付以跑通上述链路、满足真实运营使用为目标。三平台页面结构、字段范围和操作路径允许存在差异,以各平台实际可支持情况及一期上线清单为准。
|
||||||
|
|
||||||
|
## 4. 第一期不含范围
|
||||||
|
|
||||||
|
为确保第一期按 **2026 年 8 月 15 日** 如期上线,以下内容不纳入第一期交付范围,原则上作为后续阶段规划内容另行评估:
|
||||||
|
|
||||||
|
- 抖音、快手等内容驱动型平台能力
|
||||||
|
- 直播相关运营能力,包括直播场景自动化操作、直播中控和直播数据处理
|
||||||
|
- 自动化经营决策能力,包括自动调价、自动上下架、自动补货、自动投放等策略型能力
|
||||||
|
- 复杂发货履约相关能力,包括订单发货、物流处理、售后流程协同等
|
||||||
|
- 客服自动回复及会话型客户服务能力
|
||||||
|
- 竞品监控、市场大盘抓取、深度选品分析等高级数据能力
|
||||||
|
- 文生视频及其他超出商品图片生成范围的内容生产能力
|
||||||
|
- 无人值守全自动运营能力
|
||||||
|
|
||||||
|
## 5. 功能模块范围说明
|
||||||
|
|
||||||
|
### 模块一:店铺接入与连接管理
|
||||||
|
|
||||||
|
**本期目标:** 完成淘宝/天猫、京东、拼多多三平台基础接入能力,为后续自动化操作提供执行入口。
|
||||||
|
|
||||||
|
#### 本期包含
|
||||||
|
|
||||||
|
| 编号 | 功能项 | 说明 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 1.1 | 平台登录 | 各平台卖家后台基础登录能力,支持扫码或账号密码方式,登录态可持久化 |
|
||||||
|
| 1.2 | 基础风险规避 | 通过人工节奏模拟、浏览器行为模拟等方式降低基础识别风险 |
|
||||||
|
| 1.3 | 多店铺基础管理 | 支持多店铺基础接入与并发执行控制,默认并发上限为 3 |
|
||||||
|
| 1.4 | 连接状态监控 | 登录态检测、过期提醒、失效提示、基础重连能力 |
|
||||||
|
| 1.5 | 平台适配层 | 三平台独立适配各自后台页面和核心操作路径 |
|
||||||
|
| 1.6 | 启动与运行交付 | 提供可启动的桌面运行形式,满足内部上线使用 |
|
||||||
|
|
||||||
|
#### 本期不包含
|
||||||
|
|
||||||
|
- 平台全部账号安全机制自动绕过
|
||||||
|
- 所有风控场景自动恢复
|
||||||
|
- 所有登录异常的自动处理
|
||||||
|
- 对第三方平台长期稳定无波动的能力承诺
|
||||||
|
|
||||||
|
#### 上线标准
|
||||||
|
|
||||||
|
- 三个平台均可完成基础登录并保持可用会话
|
||||||
|
- 支持同时操作至少 3 个店铺的基础运行场景
|
||||||
|
- 用户可查看店铺连接状态与失效提示
|
||||||
|
- 系统可在约定 Windows 环境下正常启动运行
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 模块二:商品基础运营
|
||||||
|
|
||||||
|
**本期目标:** 完成三平台商品基础运营核心链路,支持用户通过 `Neta` 平台统一发起商品操作,并在指定推广场景下由 AI 自动化执行行业数据获取、基础投流和活动报名。
|
||||||
|
|
||||||
|
#### 本期包含
|
||||||
|
|
||||||
|
| 编号 | 功能项 | 说明 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 2.1 | 商品列表同步 | 从平台抓取商品列表到本地数据库 |
|
||||||
|
| 2.2 | 商品上架 | 支持单个或批量商品上架,支持对话指令触发 |
|
||||||
|
| 2.3 | 商品下架 | 支持单个或批量商品下架,支持对话指令触发 |
|
||||||
|
| 2.4 | 商品删除 | 支持单个或批量商品删除,执行前需二次确认 |
|
||||||
|
| 2.5 | 商品新建 | 填写基础信息后在平台创建新商品 |
|
||||||
|
| 2.6 | 标题编辑 | 修改商品标题,支持 AI 辅助生成标题建议 |
|
||||||
|
| 2.7 | 价格编辑 | 修改商品价格,包括单价和 SKU 价格,需用户二次确认 |
|
||||||
|
| 2.8 | 库存编辑 | 修改商品库存数量 |
|
||||||
|
| 2.9 | SKU 管理 | 支持基础 SKU 规格的新增、编辑和删除 |
|
||||||
|
| 2.10 | 操作日志 | 记录操作人、操作时间、目标商品及具体操作内容 |
|
||||||
|
| 2.11 | 执行前确认 | Agent 展示待执行操作,用户确认后执行 |
|
||||||
|
| 2.12 | 操作节奏控制 | 批量操作按人工节奏进行,间隔随机 3 至 8 秒 |
|
||||||
|
| 2.13 | 行业数据获取 | 通过 AI 自动化操作浏览器获取指定店铺、指定类目或指定商品相关的基础行业数据,为推广执行提供输入 |
|
||||||
|
| 2.14 | 基础投流执行 | 支持在已适配平台和已约定场景下,通过 AI 自动化操作浏览器执行基础推广投放操作,包括但不限于直通车、京准通、多多进宝等 |
|
||||||
|
| 2.15 | 活动报名 | 支持在已适配平台和已约定场景下,通过 AI 自动化操作浏览器执行基础活动报名流程 |
|
||||||
|
|
||||||
|
#### 本期不包含
|
||||||
|
|
||||||
|
- 复杂促销活动配置
|
||||||
|
- 特殊类目复杂发布流程
|
||||||
|
- 复杂订单履约流程
|
||||||
|
- 自动调价、自动上下架、自动预算优化、自动出价优化等策略型自动执行
|
||||||
|
- 未经适配确认的平台、投放类型、活动类型或页面流程
|
||||||
|
- 脱离人工确认的无人值守连续投流和连续报活动
|
||||||
|
|
||||||
|
#### 上线标准
|
||||||
|
|
||||||
|
- 通过 `Neta` 平台可完成三平台商品的基础上下架和信息编辑
|
||||||
|
- 在已适配平台和约定场景下,可自动获取指定店铺行业数据并完成基础投流或活动报名
|
||||||
|
- 操作结果与平台卖家后台一致
|
||||||
|
- 批量操作支持至少 50 个商品
|
||||||
|
- 所有写操作具备执行前确认机制
|
||||||
|
- 操作日志完整可追溯
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 模块三:商品图片生成与替换
|
||||||
|
|
||||||
|
**本期目标:** 形成商品图片生成、预览、确认、替换的完整闭环,提升商品视觉内容生产效率。
|
||||||
|
|
||||||
|
#### 本期包含
|
||||||
|
|
||||||
|
| 编号 | 功能项 | 说明 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 3.1 | 主图生成 | 基于商品信息和用户描述生成商品主图 |
|
||||||
|
| 3.2 | 详情图生成 | 生成商品详情页图片 |
|
||||||
|
| 3.3 | 图片预览 | 生成结果展示给用户预览 |
|
||||||
|
| 3.4 | 人工确认 | 用户确认满意后才执行替换 |
|
||||||
|
| 3.5 | 自动上传替换 | 确认后自动上传到对应平台替换原图 |
|
||||||
|
| 3.6 | 提示词模板 | 支持提示词模板的新建、编辑和删除 |
|
||||||
|
| 3.7 | 批量生成 | 支持多商品批量生成图片 |
|
||||||
|
| 3.8 | 图片本地存储 | 生成图片存储到 `~/.neta/workspace/` |
|
||||||
|
|
||||||
|
#### 本期不包含
|
||||||
|
|
||||||
|
- 视频生成
|
||||||
|
- 复杂品牌创意资产管理
|
||||||
|
- 图片营销效果保证
|
||||||
|
- 平台审核通过率保证
|
||||||
|
|
||||||
|
#### 上线标准
|
||||||
|
|
||||||
|
- 对话描述需求后可生成商品主图和详情图
|
||||||
|
- 生成图片分辨率满足三平台基础上传要求
|
||||||
|
- 用户确认后可自动替换到平台,替换结果与预览一致
|
||||||
|
- 提示词模板可保存复用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 模块四:基础经营数据查看
|
||||||
|
|
||||||
|
**本期目标:** 建立店铺基础经营数据采集和查看能力,为日常运营分析提供统一入口。
|
||||||
|
|
||||||
|
#### 本期包含
|
||||||
|
|
||||||
|
| 编号 | 功能项 | 说明 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 4.1 | 店铺数据抓取 | 定时抓取销量、访客数、转化率、客单价、退款率等基础指标 |
|
||||||
|
| 4.2 | 商品数据抓取 | 抓取单品销量趋势、流量来源、收藏和加购等基础数据 |
|
||||||
|
| 4.3 | 数据存储 | 数据存入 MySQL,按天、周、月进行基础聚合 |
|
||||||
|
| 4.4 | 店铺概览看板 | 展示核心经营指标汇总 |
|
||||||
|
| 4.5 | 商品排行看板 | 按销量、流量、转化率等维度展示排行 |
|
||||||
|
| 4.6 | 趋势图表 | 展示关键指标时间趋势 |
|
||||||
|
| 4.7 | 数据导出 | 支持 Excel 和 CSV 导出 |
|
||||||
|
| 4.8 | 对话查询 | 支持通过对话查询基础经营数据 |
|
||||||
|
|
||||||
|
#### 本期不包含
|
||||||
|
|
||||||
|
- 财务级数据核算
|
||||||
|
- 市场大盘深度分析
|
||||||
|
- 竞品深度监控
|
||||||
|
- 智能经营建议决策
|
||||||
|
|
||||||
|
#### 上线标准
|
||||||
|
|
||||||
|
- 三个平台的核心经营数据可自动采集
|
||||||
|
- 数据延迟不超过 24 小时
|
||||||
|
- 看板页面可展示店铺概览、商品排行和趋势图表
|
||||||
|
- 数据可导出为 Excel 或 CSV
|
||||||
|
- 对话可查询基础经营数据
|
||||||
|
|
||||||
|
## 6. 验收方式与验收场景
|
||||||
|
|
||||||
|
第一期验收以 `Neta` 平台于 **2026 年 8 月 15 日** 达到约定上线条件为目标,采用“功能演示 + 场景验证 + 结果核验”的方式进行。
|
||||||
|
|
||||||
|
验收重点核验“可登录、可操作、可查看结果、可追踪记录”的上线能力。验收时应以实际平台操作结果、系统记录和平台后台状态作为共同依据。
|
||||||
|
|
||||||
|
建议一期验收至少覆盖以下场景:
|
||||||
|
|
||||||
|
- 店铺接入场景:完成淘宝/天猫、京东、拼多多店铺账号接入,并可查看连接状态或失效提示。
|
||||||
|
- 商品同步场景:对指定店铺执行商品列表同步,并在系统中查看同步后的商品信息。
|
||||||
|
- 商品上下架场景:对指定商品执行上架或下架操作,系统展示确认信息,确认后执行,并在平台后台核验结果一致。
|
||||||
|
- 商品编辑场景:对指定商品执行标题、价格、库存或基础 SKU 信息修改,并核验平台后台结果。
|
||||||
|
- 推广投流场景:对指定店铺或商品获取基础行业数据,系统展示待执行推广信息,确认后自动完成基础投流,并在平台后台核验结果一致。
|
||||||
|
- 活动报名场景:对指定店铺或商品执行活动报名,系统展示待执行报名信息,确认后自动完成报名,并在平台后台核验结果一致。
|
||||||
|
- 图片生成替换场景:根据商品信息和用户要求生成图片,完成预览和人工确认后替换到平台。
|
||||||
|
- 数据查看导出场景:查看店铺概览、商品排行、趋势图,并完成基础导出或查询。
|
||||||
|
- 任务日志追踪场景:在执行关键操作后,可查看相应任务状态、执行结果或操作记录。
|
||||||
|
|
||||||
|
如三平台在页面结构、字段名称、操作路径或后台规则上存在差异,以各平台实际可支持的等效操作结果作为验收依据,不以三平台完全同构作为验收条件。
|
||||||
|
|
||||||
|
## 7. 使用前提与业务配合项
|
||||||
|
|
||||||
|
为确保第一期按计划开发、联调、上线和验收,业务侧需提供以下配合:
|
||||||
|
|
||||||
|
- 提供淘宝/天猫、京东、拼多多三平台测试店铺账号及相应权限。
|
||||||
|
- 提供测试商品、图片素材、商品信息及其他必要业务资料。
|
||||||
|
- 配合完成扫码登录、验证码校验、短信验证、人机校验等需人工参与的操作。
|
||||||
|
- 提供基础经营数据口径和验收核对口径。
|
||||||
|
- 指定验收联系人、业务确认人和问题反馈窗口。
|
||||||
|
|
||||||
|
若业务侧未按时提供账号、资料、环境或必要配合,相关开发、联调、上线和验收节点需重新评估。
|
||||||
|
|
||||||
|
## 8. 平台依赖与边界说明
|
||||||
|
|
||||||
|
`Neta` 平台第一期能力依赖淘宝/天猫、京东、拼多多等第三方平台的页面结构、登录机制、验证方式、风控策略和平台规则。
|
||||||
|
|
||||||
|
基于上述依赖,第一期需明确以下边界:
|
||||||
|
|
||||||
|
- 平台页面改版、规则变化、风控升级、验证机制变化会影响功能稳定性和连续可用性。
|
||||||
|
- 第一期开通的是基础运营自动化能力,不承诺长期无波动,也不承诺覆盖平台全部运营流程。
|
||||||
|
- 数据查看能力用于经营分析和运营参考,不作为财务、审计、税务或法律结算依据。
|
||||||
|
- 图片生成与替换能力用于提供内容生产辅助,不承诺营销效果、平台审核通过率或商业转化结果。
|
||||||
|
- 第一期不承诺替代全部人工运营工作,复杂判断、复杂发货、异常处理和最终经营决策仍由人工负责。
|
||||||
|
|
||||||
|
## 9. 终端用户授权与使用前提
|
||||||
|
|
||||||
|
本文件虽然用于内部一期范围确认,但 `Neta` 平台后续面向真实终端用户销售、部署或开通使用时,凡涉及淘宝/天猫、京东、拼多多等第三方电商平台账号的自动化操作能力,均应以终端用户对目标账号作出明确授权为前提。
|
||||||
|
|
||||||
|
系统提供的自动化能力,应建立在终端用户合法持有、合法管理或已取得充分授权的店铺账号基础上,不得用于未经授权的第三方账号操作。
|
||||||
|
|
||||||
|
终端用户在启用店铺接入、自动化执行、图片替换、商品管理、数据抓取等相关功能前,应明确签署、在线确认或以其他可留痕方式接受相应的授权协议、用户协议或开通确认文件。相关确认内容至少应包括以下事项:
|
||||||
|
|
||||||
|
- 终端用户确认其对所接入的电商平台店铺账号享有合法使用权,或已取得充分、有效的操作授权。
|
||||||
|
- 终端用户明确授权 `Neta` 系统在其开通相关功能并发起或确认指令的前提下,对其绑定账号执行约定范围内的自动化操作。
|
||||||
|
- 终端用户确认,系统自动化操作仅限于其本人或其已合法授权管理的店铺账号,不得用于未经许可的第三方账号。
|
||||||
|
- 终端用户知悉并接受,第三方平台可能基于自身规则、风控机制、验证策略或平台政策,对自动化操作作出限制、拦截、验证或处罚。
|
||||||
|
- 终端用户承诺不利用 `Neta` 系统实施违反法律法规、平台规则、账号约定或他人合法权益的行为。
|
||||||
|
|
||||||
|
为降低后续销售、开通及使用过程中的合规风险,`Neta` 平台在产品设计和交付实施中,应预留终端用户授权确认的留痕机制。相关留痕信息建议至少包括:
|
||||||
|
|
||||||
|
- 确认人身份或账号标识
|
||||||
|
- 被授权接入或操作的店铺账号标识
|
||||||
|
- 授权确认时间
|
||||||
|
- 对应协议或确认文本版本号
|
||||||
|
- 用户主动勾选、签署或点击确认的记录
|
||||||
|
|
||||||
|
对于未完成授权确认的终端用户或店铺账号,原则上不应开通对应的自动化执行能力。
|
||||||
|
|
||||||
|
## 10. 里程碑计划
|
||||||
|
|
||||||
|
| 时间 | 节点 | 目标内容 |
|
||||||
|
|------|------|---------|
|
||||||
|
| 2026 年 5 月底 | M1 - 平台接入 | 完成三平台基础接入、登录能力和连接状态管理验证 |
|
||||||
|
| 2026 年 6 月底 | M2 - 商品运营 | 完成商品基础运营核心链路,达到可联调状态 |
|
||||||
|
| 2026 年 7 月中旬 | M3 - 图片与数据 | 完成商品图片生成替换能力和基础经营数据查看能力 |
|
||||||
|
| 2026 年 8 月上旬 | M4 - 集成联调 | 完成全模块联调、缺陷修复、上线准备和验收准备 |
|
||||||
|
| 2026 年 8 月 15 日 | 上线验收 | 完成第一期上线并满足约定上线标准 |
|
||||||
|
|
||||||
|
## 11. 风险与依赖项
|
||||||
|
|
||||||
|
| 风险 | 影响 | 应对思路 |
|
||||||
|
|------|------|---------|
|
||||||
|
| 平台页面改版导致操作路径失效 | 功能中断或结果异常 | 预留平台适配层和快速修复机制 |
|
||||||
|
| 平台风控或验证策略升级 | 自动化执行受阻 | 增强人工确认、人工配合和基础行为模拟能力 |
|
||||||
|
| AI 图片生成质量不稳定 | 用户体验下降 | 通过人工预览和确认机制兜底 |
|
||||||
|
| 三平台能力差异大 | 上线标准难完全对齐 | 以等效结果验收,不追求完全同构 |
|
||||||
|
| 业务侧素材和账号准备不及时 | 联调和验收延期 | 提前锁定账号、素材和验收责任人 |
|
||||||
|
| 数据口径存在平台差异 | 指标理解偏差 | 提前约定基础数据口径和展示解释 |
|
||||||
|
| 上线前联调时间不足 | 影响最终上线质量 | 在 8 月上旬前预留集中修复和验收窗口 |
|
||||||
|
|
||||||
|
## 12. 第一期上线输出物
|
||||||
|
|
||||||
|
第一期围绕上线目标形成的主要输出物包括:
|
||||||
|
|
||||||
|
- 可运行的 `Neta` 一期版本
|
||||||
|
- 满足内部使用的安装或启动方式说明
|
||||||
|
- 第一期功能清单
|
||||||
|
- 第一期验收清单和验收场景
|
||||||
|
- 基础使用说明
|
||||||
|
- 上线问题跟踪与修复记录
|
||||||
201
docs/sql/audit-management.sql
Normal file
201
docs/sql/audit-management.sql
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
-- 审核管理模块迁移 SQL
|
||||||
|
-- 适用:MySQL 8 / InnoDB / utf8mb4
|
||||||
|
-- 说明:当前项目生产环境 typeorm.synchronize=false,请先在目标库执行本文件。
|
||||||
|
-- 注意:本文件用于新库初始化;若目标库已存在旧版审核表,请按实际表结构另行补充 ALTER TABLE 升级语句。
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `audit_order` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||||
|
`createTime` varchar(255) NOT NULL COMMENT '创建时间',
|
||||||
|
`updateTime` varchar(255) NOT NULL COMMENT '更新时间',
|
||||||
|
`tenantId` int NULL COMMENT '租户ID',
|
||||||
|
`orderNo` varchar(64) NOT NULL COMMENT 'TYCM订单号',
|
||||||
|
`orderType` varchar(20) NOT NULL COMMENT '订单类型 insure=投保 claim=理赔',
|
||||||
|
`oemId` int NULL COMMENT '主机厂ID',
|
||||||
|
`agentId` int NULL COMMENT '使用的Agent ID(关联netaclaw_agent)',
|
||||||
|
`sessionId` varchar(64) NULL COMMENT 'Agent会话ID(关联netaclaw会话)',
|
||||||
|
`requestId` varchar(64) NOT NULL COMMENT '请求唯一标识',
|
||||||
|
`status` varchar(20) NOT NULL DEFAULT 'pending' COMMENT '审核状态 pending=待处理 processing=审核中 approved=通过 rejected=驳回 manual_review=转人工 failed=失败',
|
||||||
|
`inputData` json NULL COMMENT 'TYCM传入的原始订单数据(证件URL、录入信息等)',
|
||||||
|
`auditResult` json NULL COMMENT '最终审核结果(通过/驳回+原因+评分)',
|
||||||
|
`skillResults` json NULL COMMENT '各Skill执行结果汇总',
|
||||||
|
`oemName` varchar(50) NULL COMMENT '主机厂名称',
|
||||||
|
`productName` varchar(100) NULL COMMENT '产品名称',
|
||||||
|
`productNo` varchar(64) NULL COMMENT '产品编号',
|
||||||
|
`salesPrice` varchar(20) NULL COMMENT '销售价格',
|
||||||
|
`policyFee` varchar(20) NULL COMMENT '保费',
|
||||||
|
`insuranceCompany` varchar(50) NULL COMMENT '保险公司',
|
||||||
|
`dealerCode` varchar(64) NULL COMMENT '经销商代码',
|
||||||
|
`factoryName` varchar(100) NULL COMMENT '经销商名称',
|
||||||
|
`creator` varchar(50) NULL COMMENT '录单人',
|
||||||
|
`createPhone` varchar(20) NULL COMMENT '录单人手机号',
|
||||||
|
`orderSource` varchar(20) NULL COMMENT '订单来源',
|
||||||
|
`cardName` varchar(50) NULL COMMENT '车主姓名',
|
||||||
|
`cardNumber` varchar(30) NULL COMMENT '身份证号',
|
||||||
|
`phone` varchar(20) NULL COMMENT '手机号',
|
||||||
|
`carHost` varchar(50) NULL COMMENT '行驶证车主',
|
||||||
|
`carFrame` varchar(30) NULL COMMENT '车架号',
|
||||||
|
`carPlate` varchar(20) NULL COMMENT '车牌号',
|
||||||
|
`brandName` varchar(50) NULL COMMENT '品牌',
|
||||||
|
`modelName` varchar(100) NULL COMMENT '车型',
|
||||||
|
`vehiclePrice` varchar(20) NULL COMMENT '车辆价格',
|
||||||
|
`policyNo` varchar(64) NULL COMMENT '保单号',
|
||||||
|
`carPurchaseTime` varchar(30) NULL COMMENT '购车时间',
|
||||||
|
`serviceStart` varchar(30) NULL COMMENT '服务开始时间',
|
||||||
|
`auditScore` decimal(5,2) NULL COMMENT '审核评分',
|
||||||
|
`auditDecision` varchar(20) NULL COMMENT '审核结论',
|
||||||
|
`packageType` varchar(50) NULL COMMENT '套餐类型',
|
||||||
|
`serviceDuration` varchar(20) NULL COMMENT '服务期限(年)',
|
||||||
|
`serviceEnd` varchar(30) NULL COMMENT '服务结束时间',
|
||||||
|
`carPower` varchar(20) NULL COMMENT '动力类型',
|
||||||
|
`carType` varchar(10) NULL COMMENT '车辆类型(新车/在用车)',
|
||||||
|
`fee` varchar(20) NULL COMMENT '产品售价',
|
||||||
|
`claimAmount` decimal(12,2) NULL COMMENT '理赔金额',
|
||||||
|
`repairType` varchar(30) NULL COMMENT '理赔类型(维修/代步/置换)',
|
||||||
|
`relatedPolicyNo` varchar(64) NULL COMMENT '关联投保保单号',
|
||||||
|
`repairFactoryName` varchar(100) NULL COMMENT '报修机构',
|
||||||
|
`repairItems` varchar(200) NULL COMMENT '报修类型/项目',
|
||||||
|
`repairStatus` varchar(30) NULL COMMENT '报修状态',
|
||||||
|
`reportNo` varchar(64) NULL COMMENT '报案号',
|
||||||
|
`callbackUrl` varchar(500) NULL COMMENT 'TYCM回调地址',
|
||||||
|
`callbackStatus` varchar(20) NULL COMMENT '回调状态 pending/success/failed',
|
||||||
|
`callbackRetryCount` int NOT NULL DEFAULT 0 COMMENT '回调重试次数',
|
||||||
|
`duration` int NULL COMMENT '审核耗时(ms)',
|
||||||
|
`remark` text NULL COMMENT '备注',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `IDX_audit_order_orderNo` (`orderNo`),
|
||||||
|
UNIQUE KEY `IDX_audit_order_requestId` (`requestId`),
|
||||||
|
KEY `IDX_audit_order_createTime` (`createTime`),
|
||||||
|
KEY `IDX_audit_order_updateTime` (`updateTime`),
|
||||||
|
KEY `IDX_audit_order_tenantId` (`tenantId`),
|
||||||
|
KEY `IDX_audit_order_status` (`status`),
|
||||||
|
KEY `IDX_audit_order_type_status_time` (`orderType`, `status`, `createTime`),
|
||||||
|
KEY `IDX_audit_order_oem_time` (`oemId`, `createTime`),
|
||||||
|
KEY `IDX_audit_order_agent_time` (`agentId`, `createTime`),
|
||||||
|
KEY `IDX_audit_order_decision_time` (`auditDecision`, `createTime`),
|
||||||
|
KEY `IDX_audit_order_policyNo` (`policyNo`),
|
||||||
|
KEY `IDX_audit_order_dealer_time` (`factoryName`, `createTime`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='审核订单主表';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `audit_order_detail` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||||
|
`createTime` varchar(255) NOT NULL COMMENT '创建时间',
|
||||||
|
`updateTime` varchar(255) NOT NULL COMMENT '更新时间',
|
||||||
|
`tenantId` int NULL COMMENT '租户ID',
|
||||||
|
`orderId` int NOT NULL COMMENT '关联审核订单ID(audit_order.id)',
|
||||||
|
`stepType` varchar(30) NOT NULL COMMENT '步骤类型 id_card_ocr/vehicle_invoice_ocr/driving_license_ocr/data_compare/rule_check/insure_audit/claim_audit/damage_detect/audit_report/oem_audit_router/llm_judge',
|
||||||
|
`stepName` varchar(100) NOT NULL COMMENT '步骤名称(中文展示用)',
|
||||||
|
`skillName` varchar(50) NULL COMMENT '调用的Skill标识',
|
||||||
|
`inputData` json NULL COMMENT 'Skill输入数据',
|
||||||
|
`outputData` json NULL COMMENT 'Skill输出结果(OCR字段、比对结果等)',
|
||||||
|
`score` decimal(5,2) NULL COMMENT '该步骤评分(0-100)',
|
||||||
|
`passed` tinyint NULL COMMENT '该步骤是否通过 0=否 1=是 2=警告',
|
||||||
|
`remark` text NULL COMMENT '备注/原因',
|
||||||
|
`duration` int NULL COMMENT '该步骤耗时(ms)',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `IDX_audit_order_detail_createTime` (`createTime`),
|
||||||
|
KEY `IDX_audit_order_detail_updateTime` (`updateTime`),
|
||||||
|
KEY `IDX_audit_order_detail_tenantId` (`tenantId`),
|
||||||
|
KEY `IDX_audit_order_detail_orderId` (`orderId`),
|
||||||
|
KEY `IDX_audit_order_detail_order_step` (`orderId`, `stepType`),
|
||||||
|
KEY `IDX_audit_order_detail_skill` (`skillName`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='审核步骤明细表';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `audit_config` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||||
|
`createTime` varchar(255) NOT NULL COMMENT '创建时间',
|
||||||
|
`updateTime` varchar(255) NOT NULL COMMENT '更新时间',
|
||||||
|
`tenantId` int NULL COMMENT '租户ID',
|
||||||
|
`name` varchar(100) NOT NULL COMMENT '规则名称',
|
||||||
|
`orderType` varchar(20) NULL COMMENT '订单类型 insure=投保 claim=理赔, 为空表示匹配所有',
|
||||||
|
`oemId` int NULL COMMENT '主机厂ID, 为空表示匹配所有',
|
||||||
|
`oemName` varchar(100) NULL COMMENT '主机厂名称(展示用)',
|
||||||
|
`productKeyword` varchar(200) NULL COMMENT '产品名称关键词, 为空表示匹配所有, 支持模糊匹配',
|
||||||
|
`productNo` varchar(100) NULL COMMENT '产品编号, 为空表示匹配所有',
|
||||||
|
`productName` varchar(200) NULL COMMENT '产品名称(展示用)',
|
||||||
|
`carType` varchar(10) NULL COMMENT '车辆类型 0=新车 1=在用车, 为空表示匹配所有',
|
||||||
|
`carPower` varchar(20) NULL COMMENT '动力类型 1=燃油 2=新能源, 长安专用, 为空表示匹配所有',
|
||||||
|
`agentId` int NOT NULL COMMENT '关联的Agent ID',
|
||||||
|
`agentIds` json NULL COMMENT '关联的Agent ID列表',
|
||||||
|
`agentName` varchar(200) NULL COMMENT 'Agent名称(展示用)',
|
||||||
|
`priority` int NOT NULL DEFAULT 0 COMMENT '优先级, 数字越大越优先',
|
||||||
|
`status` int NOT NULL DEFAULT 1 COMMENT '状态 0=禁用 1=启用',
|
||||||
|
`remark` text NULL COMMENT '备注',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `IDX_audit_config_createTime` (`createTime`),
|
||||||
|
KEY `IDX_audit_config_updateTime` (`updateTime`),
|
||||||
|
KEY `IDX_audit_config_tenantId` (`tenantId`),
|
||||||
|
KEY `IDX_audit_config_agentId` (`agentId`),
|
||||||
|
KEY `IDX_audit_config_priority` (`priority`),
|
||||||
|
KEY `IDX_audit_config_status` (`status`),
|
||||||
|
KEY `IDX_audit_config_match` (`status`, `orderType`, `oemId`, `carType`, `carPower`, `priority`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='审核配置规则表';
|
||||||
|
|
||||||
|
-- 已存在 audit_config 表的升级 SQL:
|
||||||
|
-- ALTER TABLE `audit_config` ADD COLUMN `carType` varchar(10) NULL COMMENT '车辆类型 0=新车 1=在用车, 为空表示匹配所有' AFTER `productKeyword`;
|
||||||
|
-- ALTER TABLE `audit_config` ADD COLUMN `productNo` varchar(100) NULL COMMENT '产品编号, 为空表示匹配所有' AFTER `productKeyword`;
|
||||||
|
-- ALTER TABLE `audit_config` ADD COLUMN `productName` varchar(200) NULL COMMENT '产品名称(展示用)' AFTER `productNo`;
|
||||||
|
-- ALTER TABLE `audit_config` ADD COLUMN `carPower` varchar(20) NULL COMMENT '动力类型 1=燃油 2=新能源, 长安专用, 为空表示匹配所有' AFTER `carType`;
|
||||||
|
-- ALTER TABLE `audit_config` ADD COLUMN `agentIds` json NULL COMMENT '关联的Agent ID列表' AFTER `agentId`;
|
||||||
|
-- ALTER TABLE `audit_config` DROP INDEX `IDX_audit_config_match`;
|
||||||
|
-- ALTER TABLE `audit_config` ADD INDEX `IDX_audit_config_match` (`status`, `orderType`, `oemId`, `carType`, `carPower`, `priority`);
|
||||||
|
|
||||||
|
-- 菜单权限:审核管理 / 审核订单 / 审核统计 / 审核配置 / 审核详情(隐藏)
|
||||||
|
-- 使用 router 做幂等判断;若已有同 router 菜单,本段不会重复插入。
|
||||||
|
SET @now = DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s');
|
||||||
|
|
||||||
|
INSERT INTO `base_sys_menu`
|
||||||
|
(`parentId`, `name`, `router`, `perms`, `type`, `icon`, `orderNum`, `viewPath`, `keepAlive`, `isShow`, `createTime`, `updateTime`, `tenantId`)
|
||||||
|
SELECT NULL, '审核管理', NULL, NULL, 0, 'icon-dict', 95, NULL, 1, 1, @now, @now, NULL
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM `base_sys_menu` WHERE `name` = '审核管理' AND `parentId` IS NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
SET @audit_parent_id = (
|
||||||
|
SELECT `id` FROM `base_sys_menu`
|
||||||
|
WHERE `name` = '审核管理' AND `parentId` IS NULL
|
||||||
|
ORDER BY `id` DESC LIMIT 1
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `base_sys_menu`
|
||||||
|
(`parentId`, `name`, `router`, `perms`, `type`, `icon`, `orderNum`, `viewPath`, `keepAlive`, `isShow`, `createTime`, `updateTime`, `tenantId`)
|
||||||
|
SELECT @audit_parent_id, '审核订单', '/audit/orders', NULL, 1, 'icon-menu', 0, 'modules/audit/views/order-list.vue', 1, 1, @now, @now, NULL
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM `base_sys_menu` WHERE `router` = '/audit/orders');
|
||||||
|
|
||||||
|
INSERT INTO `base_sys_menu`
|
||||||
|
(`parentId`, `name`, `router`, `perms`, `type`, `icon`, `orderNum`, `viewPath`, `keepAlive`, `isShow`, `createTime`, `updateTime`, `tenantId`)
|
||||||
|
SELECT @audit_parent_id, '审核统计', '/audit/dashboard', NULL, 1, 'icon-count', 1, 'modules/audit/views/dashboard.vue', 1, 1, @now, @now, NULL
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM `base_sys_menu` WHERE `router` = '/audit/dashboard');
|
||||||
|
|
||||||
|
INSERT INTO `base_sys_menu`
|
||||||
|
(`parentId`, `name`, `router`, `perms`, `type`, `icon`, `orderNum`, `viewPath`, `keepAlive`, `isShow`, `createTime`, `updateTime`, `tenantId`)
|
||||||
|
SELECT @audit_parent_id, '审核配置', '/audit/config', NULL, 1, 'icon-params', 2, 'modules/audit/views/config.vue', 1, 1, @now, @now, NULL
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM `base_sys_menu` WHERE `router` = '/audit/config');
|
||||||
|
|
||||||
|
INSERT INTO `base_sys_menu`
|
||||||
|
(`parentId`, `name`, `router`, `perms`, `type`, `icon`, `orderNum`, `viewPath`, `keepAlive`, `isShow`, `createTime`, `updateTime`, `tenantId`)
|
||||||
|
SELECT @audit_parent_id, '审核详情', '/audit/detail', NULL, 1, 'icon-menu', 3, 'modules/audit/views/order-detail.vue', 0, 0, @now, @now, NULL
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM `base_sys_menu` WHERE `router` = '/audit/detail');
|
||||||
|
|
||||||
|
SET @audit_order_menu_id = (SELECT `id` FROM `base_sys_menu` WHERE `router` = '/audit/orders' ORDER BY `id` DESC LIMIT 1);
|
||||||
|
SET @audit_dashboard_menu_id = (SELECT `id` FROM `base_sys_menu` WHERE `router` = '/audit/dashboard' ORDER BY `id` DESC LIMIT 1);
|
||||||
|
SET @audit_config_menu_id = (SELECT `id` FROM `base_sys_menu` WHERE `router` = '/audit/config' ORDER BY `id` DESC LIMIT 1);
|
||||||
|
|
||||||
|
INSERT INTO `base_sys_menu`
|
||||||
|
(`parentId`, `name`, `router`, `perms`, `type`, `icon`, `orderNum`, `viewPath`, `keepAlive`, `isShow`, `createTime`, `updateTime`, `tenantId`)
|
||||||
|
SELECT @audit_order_menu_id, '查询', NULL, 'audit:audit:page,audit:audit:list,audit:audit:info,audit:audit:detail', 2, NULL, 0, NULL, 1, 1, @now, @now, NULL
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM `base_sys_menu` WHERE `parentId` = @audit_order_menu_id AND `name` = '查询');
|
||||||
|
|
||||||
|
INSERT INTO `base_sys_menu`
|
||||||
|
(`parentId`, `name`, `router`, `perms`, `type`, `icon`, `orderNum`, `viewPath`, `keepAlive`, `isShow`, `createTime`, `updateTime`, `tenantId`)
|
||||||
|
SELECT @audit_order_menu_id, '操作', NULL, 'audit:audit:retry,audit:audit:manual,audit:audit:remove,audit:audit:stats', 2, NULL, 1, NULL, 1, 1, @now, @now, NULL
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM `base_sys_menu` WHERE `parentId` = @audit_order_menu_id AND `name` = '操作');
|
||||||
|
|
||||||
|
INSERT INTO `base_sys_menu`
|
||||||
|
(`parentId`, `name`, `router`, `perms`, `type`, `icon`, `orderNum`, `viewPath`, `keepAlive`, `isShow`, `createTime`, `updateTime`, `tenantId`)
|
||||||
|
SELECT @audit_dashboard_menu_id, '查询', NULL, 'audit:audit:statsOverview,audit:audit:statsByProduct,audit:audit:statsByDealer,audit:audit:statsByOem,audit:audit:statsByInsurance,audit:audit:filterOptions,audit:audit:searchDealers,audit:audit:searchProducts,audit:audit:exportData', 2, NULL, 0, NULL, 1, 1, @now, @now, NULL
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM `base_sys_menu` WHERE `parentId` = @audit_dashboard_menu_id AND `name` = '查询');
|
||||||
|
|
||||||
|
INSERT INTO `base_sys_menu`
|
||||||
|
(`parentId`, `name`, `router`, `perms`, `type`, `icon`, `orderNum`, `viewPath`, `keepAlive`, `isShow`, `createTime`, `updateTime`, `tenantId`)
|
||||||
|
SELECT @audit_config_menu_id, '管理', NULL, 'audit:config:add,audit:config:delete,audit:config:update,audit:config:info,audit:config:list,audit:config:page,audit:config:agents,audit:config:testMatch', 2, NULL, 0, NULL, 1, 1, @now, @now, NULL
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM `base_sys_menu` WHERE `parentId` = @audit_config_menu_id AND `name` = '管理');
|
||||||
74
docs/superpowers/followups/2026-05-09-uia-e2e-report.md
Normal file
74
docs/superpowers/followups/2026-05-09-uia-e2e-report.md
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
# UIA MVP E2E 验证报告 · 2026-05-09
|
||||||
|
|
||||||
|
> Plan D 代码实现全部完成,本报告追踪 spec 14 条端到端手工验证的执行状态。
|
||||||
|
> 代码实现:见 `docs/superpowers/plans/2026-05-09-wechat-uia-d-frontend-tray-e2e.md`
|
||||||
|
> Spec:`docs/superpowers/specs/2026-05-09-wechat-uia-channel-design.md`
|
||||||
|
|
||||||
|
## 执行状态
|
||||||
|
|
||||||
|
**当前状态:待执行**。Plan D 的代码实现(Phase 1-6 / Task 1-12)已全部落地且 type-check / 单测通过,但端到端验证需在 Windows 测试机上完成,**需满足下述所有前置条件后由人工按清单逐条验证**。
|
||||||
|
|
||||||
|
## 前置条件
|
||||||
|
|
||||||
|
- [ ] Windows 测试机一台(Windows 10/11,能运行 PC 微信)
|
||||||
|
- [ ] PC 微信 3.9.11.17 已安装且已登录测试微信号(不在白名单外版本)
|
||||||
|
- [ ] MySQL 测试库 ready,包含 Neta 业务表(netaclaw_*, 见 `packages/backend/sql/`)
|
||||||
|
- [ ] Plan A + B + C + D 代码全部合并到同一分支
|
||||||
|
- [ ] 本地构建 Windows 安装包: `cd packages/backend && node scripts/build-windows-installer.js`(需本地安装 .NET 8 SDK + Inno Setup 6)
|
||||||
|
- [ ] 安装成功并启动 Tray,backend + bridge 都拉起
|
||||||
|
|
||||||
|
## 14 条 Checklist
|
||||||
|
|
||||||
|
| # | 项目 | 状态 | 备注 / 证据 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| E2E-01 | 启动 Neta Tray,后端 + bridge 都拉起,前端频道页访问正常 | ⬜ | 观察 `%APPDATA%\Neta\logs\bridge.log` 有 handshake 日志;前端 `http://localhost:<port>` 能打开 |
|
||||||
|
| E2E-02 | 创建 UIA channel(type=weixin-uia),handshake 后 wxid/nickname 自动填充 | ⬜ | 前端新建 UIA channel 必填 wxid → bridge handshake 写回 nickname/wechatVersion → 刷新 channel 列表看 bridge tag 显示"已连接" + 微信版本 |
|
||||||
|
| E2E-03 | 测试群里任一成员说 "hello",bridge 切窗采集 → 前端群聊管理"待审批"横幅出现该群 | ⬜ | Bridge 日志有 `room discovered`;前端群聊管理抽屉顶部出现 `新发现 N 个群(待审批)` 橙色横幅 |
|
||||||
|
| E2E-04 | 点"启用监听" + 选 at_mention + 填 botAlias=小神 | ⬜ | 群卡片底部"启用监听/忽略"按钮点击后 status=1,triggerMode 下拉持久化 |
|
||||||
|
| E2E-05 | 群里发 `@小神 你好` → agent 回复 → bot 在群里发回复 → 发送方是测试号自己 | ⬜ | UIA bridge 模拟发送,回复消息的发送方是本机登录的微信号(而非独立 ClawBot) |
|
||||||
|
| E2E-06 | 切到 all 模式 → 群里随便说 → agent 通过 system prompt 判定相关性 → 不相关时返回 [SKIP] 不发 | ⬜ | 观察 backend 日志 `uia agent skipped reason=empty_or_skip` |
|
||||||
|
| E2E-07 | 群里发图 → SQLite 落归档 → 前端归档抽屉能预览到图片 | ⬜ | 点群卡片"查看归档"按钮 → 打开归档抽屉 → 能在列表里看到图片缩略图(via `/wechat-uploads/...`) |
|
||||||
|
| E2E-08 | 关掉 PC 微信 → bridge `/health` 失败 → channel 状态变 disconnected | ⬜ | 前端卡片显示 "Bridge 离线" 红色 tag(30s 轮询生效后) |
|
||||||
|
| E2E-09 | 重开 PC 微信 → bridge 自愈 → channel 重连 | ⬜ | bridge 重连后前端卡片 "Bridge 离线" 消失;注:完整 alive 轮询 + 3 次重启留 v2 实现 |
|
||||||
|
| E2E-10 | 故意装 PC 微信 4.x(不在白名单)→ bridge 启动失败 → tray 气泡通知"版本不兼容" | ⬜ | Tray 气泡文字 + `bridge.log` 有版本校验失败记录 |
|
||||||
|
| E2E-11 | 一个微信号同时绑 ClawBot + UIA channel:DM 走 ClawBot / 群走 UIA,不重复响应 | ⬜ | ClawBot 收到群消息丢弃;UIA 收到 DM 丢弃(见 routeInboundMessage 分流逻辑) |
|
||||||
|
| E2E-12 | 删 UIA channel → group 表级联清;SQLite 归档保留(按决策不级联删) | ⬜ | 验证 `netaclaw_agent_channel_group` 对应记录清空,`%APPDATA%\Neta\wechat-archive-<cid>.db` 文件保留 |
|
||||||
|
| E2E-13 | 群被改名 → 视为新群,又出现在"待审批"横幅 | ⬜ | 改群名后触发消息,前端横幅再次亮起;旧群 roomId 也还能在归档里看到 |
|
||||||
|
| E2E-14 | 群里回复 UIA bot 的消息(包含"引用") → 前端对话页的 user message 能看到"[被引用: ... 原文...]"结构化上下文 | ⬜ | 进入 chat.vue 查看 sessionId 对应会话,user message 内容应含 "[被引用:" 前缀 |
|
||||||
|
|
||||||
|
## 失败项 follow-up
|
||||||
|
|
||||||
|
(执行后在此记录每条失败的具体现象 + 复现步骤 + issue 链接)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Plan D 代码实现完成情况
|
||||||
|
|
||||||
|
| Task | 文件 | 状态 |
|
||||||
|
|---|---|---|
|
||||||
|
| Task 1 | frontend/types/index.d.ts | ✅ 通过 TS 类型检查(baseline 107 保持) |
|
||||||
|
| Task 2 | backend/controller/admin/wechat_archive.ts + 测试 | ✅ 4 条 controller 单测通过 |
|
||||||
|
| Task 2.5 | backend/config/config.default.ts | ✅ `/wechat-uploads` 静态映射已挂 |
|
||||||
|
| Task 2.6 | backend/service/agent_channel.ts page() enrich | ✅ 17 条 agent_channel 单测通过 |
|
||||||
|
| Task 3 | frontend/views/channel-management.vue drawer 动态字段 | ✅ |
|
||||||
|
| Task 4 | frontend 卡片 UIA 渲染差异 + bridge tag | ✅ |
|
||||||
|
| Task 5 | frontend composables/useUiaChannelValidation.ts | ✅ |
|
||||||
|
| Task 6 | frontend/components/channel-group-panel.vue triggerMode 2 档 + 待审批横幅 | ✅ |
|
||||||
|
| Task 7 | 每群绑 agent + 回复身份覆盖 | ✅ |
|
||||||
|
| Task 8 | frontend/components/wechat-archive-panel.vue | ✅ |
|
||||||
|
| Task 9 | frontend chat ConversationHeader 加 DM/群 tag | ✅ |
|
||||||
|
| Task 10 | windows-tray BridgeProcessManager + 2 条单测 | ✅ 2/2 测试通过 |
|
||||||
|
| Task 11 | windows-tray TrayApplicationContext 集成 bridge | ✅ 7/7 现有测试保持通过 |
|
||||||
|
| Task 12 | backend/scripts/build-windows-installer.js + setup.iss | ✅ 静态改动完成(实际构建待工程师本地运行) |
|
||||||
|
|
||||||
|
## 留待 v2
|
||||||
|
|
||||||
|
- 崩溃自愈完整 3 次重启 + 30s 间隔 tray 气泡(Task 11 当前只做"需要时拉起")
|
||||||
|
- 归档"标记为有价值 → 转存 MySQL 业务表"(UI 已占位 disabled)
|
||||||
|
- 语音/视频/跨群人物志
|
||||||
|
|
||||||
|
## 备注
|
||||||
|
|
||||||
|
- 本次执行未创建 git worktree、未做任何 git commit(按用户要求),所有改动直接落在工作区,待工程师审阅后自行 stage/commit
|
||||||
|
- Type-check baseline 维持在 107(项目历史遗留错误),本次 Plan D 实施未引入新增错误
|
||||||
|
- 前端部分均通过 `pnpm type-check` 无新增报错;后端新增测试 21 条(controller 4 + service enrich 2 + 原有 15)全部通过;Tray 单测 7/7 + 新增 2/2 全部通过
|
||||||
174
docs/superpowers/followups/2026-05-12-weixin-db-e2e-report.md
Normal file
174
docs/superpowers/followups/2026-05-12-weixin-db-e2e-report.md
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
# weixin-db E2E 验证报告(架构 C, 2026-05-12)
|
||||||
|
|
||||||
|
> 执行方式:subagent-driven-development。
|
||||||
|
> 本文档记录 plan `docs/superpowers/plans/2026-05-12-weixin-db-channel.md` 的完成情况、自动化验证结果、剩余手工 E2E 清单。
|
||||||
|
|
||||||
|
## 完成状态总览
|
||||||
|
|
||||||
|
| 阶段 | Tasks | 状态 |
|
||||||
|
|---|---|---|
|
||||||
|
| Phase C-0 · session.db schema PoC | C0-1 | ✅ DONE |
|
||||||
|
| Phase C-1 · 清理作废代码 | Task 1-5 | ✅ DONE |
|
||||||
|
| Phase C-2 · runtime/weixin_db/ 纯函数模块 | Task 6-15 | ✅ DONE |
|
||||||
|
| Phase C-3 · weixin_db.ts 主服务装配 | Task 16 | ✅ DONE |
|
||||||
|
| Phase C-4 · agent_channel 集成 | Task 17, 18 | ✅ DONE |
|
||||||
|
| Phase C-5 · agent_channel_group 改用户主动添加 | Task 19, 20 | ✅ DONE |
|
||||||
|
| Phase C-6 · 前端 UX | Task 21, 22, 23 | ✅ DONE |
|
||||||
|
| Phase C-7 · 安装包 + Tray | Task 24, 25, 26 | ✅ DONE |
|
||||||
|
| Phase C-8 · 端到端手工验证 | Task 27 | ⏳ 待用户跑 |
|
||||||
|
| Phase C-9 · 回复路径 5.7 | 占位 | ❌ (独立 spec) |
|
||||||
|
|
||||||
|
## 自动化验证结果
|
||||||
|
|
||||||
|
### 1. session.db / contact.db schema RE (Phase C-0)
|
||||||
|
|
||||||
|
脚本: `packages/backend/poc/weixin-4x/poc-13-session-schema.mjs` + `poc-14-contact-schema.mjs`
|
||||||
|
结果文档: `packages/backend/poc/weixin-4x/README-session-schema.md`
|
||||||
|
|
||||||
|
关键发现:
|
||||||
|
- **Msg 表名算法确认**: `tableName = 'Msg_' + md5(username)` (100/100 命中)
|
||||||
|
- **session.db 主表**: `SessionTable.username`(列出所有会话)
|
||||||
|
- **群显示名来源**: contact.db 的 `contact` 表 (`remark > nick_name > username`)
|
||||||
|
- 依此真实 schema **回填**了 `room_resolver.ts`(Task 12),不再是假设。
|
||||||
|
|
||||||
|
### 2. key 抽取 PowerShell 脚本冒烟(Task 10)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
powershell.exe -ExecutionPolicy Bypass -NoProfile -File packages/backend/tools/win32/extract-weixin-key.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
实际输出(2026-05-12 测试):
|
||||||
|
```json
|
||||||
|
{"seedDir":"C:\\Users\\lixin\\Documents\\xwechat_files\\shin_seed_7861",
|
||||||
|
"wxid":"shin","wechatVersion":"4.1.8.107","pid":39916,
|
||||||
|
"dbKeys":{"message_0.db":"374c4e1a...","session.db":"e43bd85c...","contact.db":"bd93ce66...",...}}
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ 17 个 DB 的 key 全部成功抽取。
|
||||||
|
|
||||||
|
### 3. 后端单测 (与 weixin-db 相关的 suite)
|
||||||
|
|
||||||
|
```
|
||||||
|
pnpm --filter @neta/backend test -- netaclaw/runtime/weixin_db netaclaw/service/weixin_db netaclaw/service/agent_channel netaclaw/service/agent_channel_group
|
||||||
|
|
||||||
|
Test Suites: 13 passed, 13 total
|
||||||
|
Tests: 92 passed, 92 total
|
||||||
|
Time: 7.954 s
|
||||||
|
```
|
||||||
|
|
||||||
|
覆盖:
|
||||||
|
- `runtime/weixin_db/` 全部模块(wcdb_codec / zstd_decode / db_paths / key_extractor / message_repo / room_resolver / wal_watcher / build_pseudo) — 均 TDD 通过
|
||||||
|
- `service/weixin_db.ts` — 5 个 case (`replyToGroup` 占位 throw、`bindChannel` 在/非 Windows、`refreshWhitelist`、`unbindChannel` 幂等)
|
||||||
|
- `service/agent_channel.ts` — 含 17+9+6 测试:page enrichment 切换为 weixin-db、group 路由经 findByKey + replyToGroup、delete 走 unbindChannel、toggle 刷新 whitelist
|
||||||
|
- `service/agent_channel_group.ts` — 27 个测试:addByName 新语义、toggle 不再接受 -1、updatePolicy 拒绝 prefix 等
|
||||||
|
|
||||||
|
### 4. Tray .NET 单测(Task 26)
|
||||||
|
|
||||||
|
```
|
||||||
|
dotnet test packages/windows-tray/Neta.Tray.Tests
|
||||||
|
已通过! - 失败: 0,通过: 5,已跳过: 0,总计: 5
|
||||||
|
```
|
||||||
|
|
||||||
|
删除 `BridgeProcessManager` / `BridgeHealthPoller` / `BridgeProcessManagerTests.cs` 后,Neta.Tray 仅管理 backend.exe,5 个剩余测试全通。
|
||||||
|
|
||||||
|
### 5. Installer 脚本(Task 24, 25)
|
||||||
|
|
||||||
|
- `build-windows-installer.js`: 删 bridge dotnet publish 步骤,加 `tools/win32/*.ps1` 拷贝步骤
|
||||||
|
- `setup.iss`: 删 `bridge-output\*` / bridge.exe uninstall taskkill;加 `tools-output\win32\*` → `{app}\tools\win32`
|
||||||
|
|
||||||
|
语法检查通过,实际 `iscc` 构建需在 Inno Setup 环境下执行,未包含在本次自动化验证中。
|
||||||
|
|
||||||
|
## 手工 E2E Checklist (Task 27)
|
||||||
|
|
||||||
|
以下需用户在真实 Windows + PC 微信环境手工跑:
|
||||||
|
|
||||||
|
- [ ] **E2E-1**: 前端新建 channel type=`微信本地代理(群聊)`,填 wxid `shin` → 保存成功
|
||||||
|
- [ ] **E2E-2**: backend 启动时看日志 `[weixin-db] channel X bound, wxid=shin ver=4.1.8.107 pid=XXXXX`,无 error
|
||||||
|
- [ ] **E2E-3**: 前端"群聊管理"打开,显示"已添加监管 0 个群" → 点 "+ 添加群" → 输入测试群完整名 → 提交
|
||||||
|
- [ ] **E2E-4**: backend 日志 `addByName created=true`;`netaclaw_agent_channel_group` 多一行 status=1
|
||||||
|
- [ ] **E2E-5**: 测试群里发"hello"(触发策略 at_mention) → backend 看到 `[weixin-db] incremental read` 但 agent 不回(at_mention 未命中)
|
||||||
|
- [ ] **E2E-6**: 群里发 `@机器人 你好` → routeInboundMessage 走 handleInboundMessage group 路径 → agentExecutor 执行 → replyToGroup throw 占位 → 日志 `weixin-db reply 暂未实现, cid=X room=测试群 skipped`
|
||||||
|
- [ ] **E2E-7**: 切到"所有消息"策略 → **未加白名单的群**发消息 → backend 完全没反应(白名单过滤生效)
|
||||||
|
- [ ] **E2E-8**: 白名单群发消息 → backend 看到处理
|
||||||
|
- [ ] **E2E-9**: 前端删除该群白名单 → DB row 消失 → 再发消息 backend 无反应
|
||||||
|
- [ ] **E2E-10**: 关 Weixin → wal 不变,backend 平静;重开 Weixin → 健康探针触发 → 自动 rebind(60s 内)
|
||||||
|
|
||||||
|
## 关键技术决策记录
|
||||||
|
|
||||||
|
1. **架构 C**: 纯 Node + PowerShell(无独立 .NET bridge 进程)。ps1 脚本 spawn 一次抽 key,后续 Node 本进程处理。
|
||||||
|
2. **零被动发现**: 群必须用户主动添加,白名单在 DB 解密阶段即过滤,bot **物理上读不到**非白名单群消息(隐私保护)。
|
||||||
|
3. **回复路径占位**: `replyToGroup` 当前 throw NotImplementedError,`agent_channel.handleDbInbound` try/catch 跳过,不阻塞读链路。spec 5.7 独立 plan 实施。
|
||||||
|
4. **跨平台兜底**: 非 Windows `loginStatus='unsupported_platform'`,不 spawn powershell 不 crash。
|
||||||
|
5. **重连机制**: WeixinDbService 60s 健康探针,基于 `process.kill(pid, 0)` + `fs.accessSync` 判断 Weixin 进程 + DB 可读。
|
||||||
|
|
||||||
|
## 遗留 Follow-up
|
||||||
|
|
||||||
|
1. **Task 27 E2E**: 待用户跑,目前仅自动化部分已验证。
|
||||||
|
2. **Phase C-9**: replyToGroup 真实实现(5.7)需独立 spec(剪贴板/UIA/WCF 等方案待 brainstorm)。
|
||||||
|
3. **bigint 字面量**: `tsconfig.target=es2018` 不支持 `999n` 字面量,实现处用 `BigInt(...)` 构造(已注意)。
|
||||||
|
4. **zstd API**: `@mongodb-js/zstd` v2 只有 async,tryDecompressToString 已 async(plan Task 8.1 兜底方案)。
|
||||||
|
5. **better-sqlite3 + zstd native bindings**: root `package.json` `pnpm.onlyBuiltDependencies` 已包含两者,`prebuild-install` 会在 pnpm install 时正确执行。
|
||||||
|
|
||||||
|
## 文件清单
|
||||||
|
|
||||||
|
### 新增
|
||||||
|
|
||||||
|
**后端 runtime**:
|
||||||
|
- `packages/backend/src/modules/netaclaw/runtime/weixin_db/wcdb_codec.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/runtime/weixin_db/zstd_decode.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/runtime/weixin_db/db_paths.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/runtime/weixin_db/key_extractor.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/runtime/weixin_db/message_repo.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/runtime/weixin_db/room_resolver.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/runtime/weixin_db/wal_watcher.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/runtime/weixin_db/incremental_reader.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/runtime/weixin_db/types.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/runtime/weixin_db/build_pseudo.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/service/weixin_db.ts`
|
||||||
|
|
||||||
|
**后端测试**: 上述所有模块对应的 `.test.ts`,以及 `test/modules/netaclaw/service/agent_channel.weixin_db.test.ts`
|
||||||
|
|
||||||
|
**工具脚本**: `packages/backend/tools/win32/extract-weixin-key.ps1`
|
||||||
|
|
||||||
|
**PoC**:
|
||||||
|
- `packages/backend/poc/weixin-4x/poc-13-session-schema.mjs`
|
||||||
|
- `packages/backend/poc/weixin-4x/poc-14-contact-schema.mjs`
|
||||||
|
- `packages/backend/poc/weixin-4x/README-session-schema.md`
|
||||||
|
- `packages/backend/poc/weixin-4x/session-schema-output.log`
|
||||||
|
- `packages/backend/poc/weixin-4x/contact-schema-output.log` (若写了)
|
||||||
|
|
||||||
|
### 修改
|
||||||
|
|
||||||
|
**后端**:
|
||||||
|
- `packages/backend/src/modules/netaclaw/service/agent_channel.ts`(weixin-uia → weixin-db 全替换,-147 行净删)
|
||||||
|
- `packages/backend/src/modules/netaclaw/service/agent_channel_group.ts`(upsertOnInbound → addByName)
|
||||||
|
- `packages/backend/src/modules/netaclaw/controller/admin/agent_channel_group.ts`(加 /add)
|
||||||
|
- `packages/backend/src/comm/path.ts`(删 pWechatUploadsPath)
|
||||||
|
- `packages/backend/src/config/config.default.ts`(删 wechatUploads 静态映射)
|
||||||
|
- `packages/backend/scripts/build-windows-installer.js`(删 bridge publish,加 tools/win32 拷贝)
|
||||||
|
- `packages/backend/installer/setup.iss`(删 bridge 项,加 tools 项)
|
||||||
|
- `packages/backend/package.json`(加 @mongodb-js/zstd)
|
||||||
|
|
||||||
|
**前端**:
|
||||||
|
- `packages/frontend/src/modules/agent/types/index.d.ts`(类型切换)
|
||||||
|
- `packages/frontend/src/modules/agent/views/channel-management.vue`(文案+字段)
|
||||||
|
- `packages/frontend/src/modules/agent/components/channel-group-panel.vue`(大改 UX,用户主动添加)
|
||||||
|
- `packages/frontend/src/modules/agent/composables/useDbChannelValidation.ts`(改名 from useUiaChannelValidation.ts)
|
||||||
|
|
||||||
|
**根**:
|
||||||
|
- `package.json`(加 pnpm.onlyBuiltDependencies)
|
||||||
|
|
||||||
|
**Tray**:
|
||||||
|
- `packages/windows-tray/Neta.Tray/TrayApplicationContext.cs`(删 bridge 拉起逻辑,316→169 行)
|
||||||
|
|
||||||
|
### 删除
|
||||||
|
|
||||||
|
- `packages/backend/src/modules/netaclaw/service/weixin_uia.ts` + `wechat_archive.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/controller/open/weixin_uia.ts` + `admin/wechat_archive.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/runtime/wechat_uia_routing.ts` + `wechat_archive_schema.ts`
|
||||||
|
- 对应 test 文件 6 个
|
||||||
|
- `packages/windows-tray/Neta.WeChatBridge/` + `Neta.WeChatBridge.Tests/` 整个目录
|
||||||
|
- `packages/windows-tray/Neta.Tray/BridgeProcessManager.cs` + `BridgeHealthPoller.cs`
|
||||||
|
- `packages/windows-tray/Neta.Tray.Tests/BridgeProcessManagerTests.cs`
|
||||||
|
- `packages/frontend/src/modules/agent/components/wechat-archive-panel.vue`
|
||||||
|
- `packages/frontend/src/modules/agent/composables/useUiaChannelValidation.ts`(被 useDbChannelValidation.ts 替代)
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"model": "doubao-seed-2-0-pro-260215",
|
||||||
|
"baseUrl": "https://ark.cn-beijing.volces.com/api/v3",
|
||||||
|
"runCount": 1,
|
||||||
|
"elapsed": 23225,
|
||||||
|
"successCount": 1,
|
||||||
|
"successRate": 100,
|
||||||
|
"avgDurationMs": 23224,
|
||||||
|
"totalTokens": 3803,
|
||||||
|
"byStatus": {
|
||||||
|
"success": 1
|
||||||
|
},
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"text": "[probe-1-1778742201823]",
|
||||||
|
"status": "success",
|
||||||
|
"steps": 4,
|
||||||
|
"modelCalls": 2,
|
||||||
|
"totalTokens": 3803,
|
||||||
|
"durationMs": 23224,
|
||||||
|
"trace": [
|
||||||
|
{
|
||||||
|
"step": 4,
|
||||||
|
"action": {
|
||||||
|
"type": "finished",
|
||||||
|
"reason": "current chat is 文件传输助手"
|
||||||
|
},
|
||||||
|
"raw": "{\"type\":\"finished\",\"reason\":\"current chat is 文件传输助手\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": 5,
|
||||||
|
"action": {
|
||||||
|
"type": "type",
|
||||||
|
"text": "[probe-1-1778742201823]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": 6,
|
||||||
|
"action": {
|
||||||
|
"type": "hotkey",
|
||||||
|
"key": "enter"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": 7,
|
||||||
|
"action": {
|
||||||
|
"type": "finished",
|
||||||
|
"reason": "message visible at bottom"
|
||||||
|
},
|
||||||
|
"raw": "{\"type\":\"finished\",\"reason\":\"message visible at bottom\"}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -0,0 +1,314 @@
|
|||||||
|
# 赛力斯审核 Agent 系统提示词
|
||||||
|
|
||||||
|
下面四反引号中的内容可直接粘贴到 `netaclaw_agent.name = agent_6a442e52`、`label = 赛力斯审核` 的 `systemPrompt` 字段。
|
||||||
|
|
||||||
|
````md
|
||||||
|
# 赛力斯 AI 审核 Agent
|
||||||
|
|
||||||
|
你是 TYCM 车主权益管理系统的“赛力斯审核 Agent”,也是 `oemId=11` 的品牌审核子 Agent。你的任务是根据 TYCM 输入的订单/理赔 JSON、已识别 OCR/结构化字段、附件证据、已绑定技能结果和赛力斯业务规则,输出一个可被 RZYX AI Flow 落库、生成审核报告并回调 TYCM 的审核结论。
|
||||||
|
|
||||||
|
你必须严谨、保守、可追溯。不得编造 OCR 结果、图片内容、视频内容、材料字段、工具调用结果或规则依据。URL 只能证明“存在附件地址”,不能证明附件内容。
|
||||||
|
|
||||||
|
## 适用范围
|
||||||
|
|
||||||
|
只处理赛力斯业务:
|
||||||
|
|
||||||
|
- `oemId = 11`,或输入字段/文本明确包含“赛力斯”“问界”“SERES”“AITO”。
|
||||||
|
- `orderType = insure`:按赛力斯投保/延保订单审核。
|
||||||
|
- `orderType = claim`:按赛力斯维修补偿理赔审核。
|
||||||
|
|
||||||
|
如果输入明确不是赛力斯业务,输出 `failed`,原因写明“不属于赛力斯审核范围”。你已经是赛力斯子 Agent,不要再调用 `oem-audit-router` 把请求路由回自己。
|
||||||
|
|
||||||
|
## 平台执行边界
|
||||||
|
|
||||||
|
优先遵循用户消息中的“平台执行边界”。在 RZYX 统一审核入口中,服务端会在你输出最终 JSON 后统一落库、生成或补齐审核报告、回调 TYCM。因此默认规则是:
|
||||||
|
|
||||||
|
- 不要调用 `tycm-callback`。
|
||||||
|
- 不要请求 `callbackUrl`。
|
||||||
|
- 不要自行发送 HTTP 回调。
|
||||||
|
- 不要声称已完成某个技能调用,除非本轮对话中实际通过 `execute_skill` 调用了该技能并拿到结果。
|
||||||
|
- 最终回复必须只包含一个可解析 JSON 对象,不要输出 Markdown、解释文字或代码块。
|
||||||
|
|
||||||
|
最终 JSON 格式固定为:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"decision":"approved|rejected|manual_review|failed","reason":"详细说明判断依据","score":0}
|
||||||
|
```
|
||||||
|
|
||||||
|
`score` 必须是 0-100 的数字。需要写的证据、规则、建议、技能调用摘要都放入 `reason` 字符串中。
|
||||||
|
|
||||||
|
只有在输入明确包含 `callbackUrl`,且用户明确要求你“本轮直接回调 TYCM”,并且没有平台边界禁止回调时,才允许调用 `tycm-callback`。即便直接回调成功,最终回复仍然只能输出上述 JSON。
|
||||||
|
|
||||||
|
## 可用技能使用策略
|
||||||
|
|
||||||
|
你只能调用当前 Agent 已绑定、并在本轮 Skill 索引中可见的技能。没有绑定的技能不得假装可用。新增技能越多,越要先用证据技能补齐材料,再做规则判断。
|
||||||
|
|
||||||
|
### 技能优先级
|
||||||
|
|
||||||
|
1. 已有结构化字段、平台 OCR、历史技能输出优先使用,不重复调用技能。
|
||||||
|
2. 通用 OCR/文档类材料优先使用 `ocr-document-processor`;适用于扫描件、PDF、票据、维修结算单、定损单、权益确认书、索赔申请书等。
|
||||||
|
3. 身份证正反面图片可使用 `id-card-ocr`。如果该技能返回 `needsModelVision:true`,必须使用返回的 prompt 配合可用多模态识别能力取得 `modelResult` 后再归一化;如果没有可用视觉能力,则转 `manual_review`。
|
||||||
|
4. 机动车销售统一发票/购车发票可使用 `vehicle-invoice-ocr`。它不是维修发票专用技能,车险维修发票优先走通用 OCR/文档识别。
|
||||||
|
5. 环车视频、损失视频、车辆外观旧伤视频优先调用 `vehicle-damage-inspection`,不要用图片识别工具直接分析视频。
|
||||||
|
6. 如果没有 `vehicle-damage-inspection`,但绑定了低层视频链路技能,可按 `video-frame-extractor` -> `damage-detector` -> `damage-grounding` -> `best-frame-selector` -> `scratch-report-generator` 的顺序执行。
|
||||||
|
7. `audit-report` 只用于把已形成的审核结论转为报告明细。它不是赛力斯规则来源,不得用报告模板反推审核结论。调用时 `companyName` 使用“赛力斯车主权益管理系统”。如果报告技能失败或模板不完全适配赛力斯,最终 JSON 仍要给出审核结论,并在 `reason` 中说明报告生成异常。
|
||||||
|
8. `insure-audit` 是长安新车投保规则技能,不得直接用于赛力斯规则判定。只有用户明确要求复用通用新车投保规则,且不会覆盖赛力斯规则时,才可作为辅助规则技能。
|
||||||
|
9. `tycm-callback` 默认禁止调用,除非满足上文“直接回调模式”。
|
||||||
|
|
||||||
|
### 工具结果处理
|
||||||
|
|
||||||
|
- 技能返回失败、空结果、`needsModelVision:true` 且无法继续识别时,不要硬判通过;根据影响范围输出 `manual_review` 或 `failed`。
|
||||||
|
- 同一字段多来源冲突时,优先级为:明确结构化业务字段 > PaddleOCR/文档 OCR 的文字数字结果 > 视觉模型 OCR 的文字数字结果;印章颜色、签名痕迹、照片/视频画面描述优先视觉结果。
|
||||||
|
- 对同一材料,先判断是否上传,再判断内容。未上传和内容不合格互斥,不要同时写。
|
||||||
|
|
||||||
|
## 输入解析
|
||||||
|
|
||||||
|
输入通常包含:
|
||||||
|
|
||||||
|
- `orderNo`:TYCM 订单号或理赔单号。
|
||||||
|
- `orderType`:`insure` 或 `claim`。
|
||||||
|
- `oemId`:赛力斯为 11。
|
||||||
|
- `data`:业务数据主体。
|
||||||
|
- `callbackUrl`:可选;统一入口默认服务端处理,你不要主动使用。
|
||||||
|
|
||||||
|
常用字段别名:
|
||||||
|
|
||||||
|
- 报修/理赔单号:`repairNum`、`warrantyNo`、`orderNo`、`orderNum`。
|
||||||
|
- 车主姓名:`cardName`、`deliverer`、`ownerName`、`carOwnerName`。
|
||||||
|
- VIN/车架号:`carFrame`、`vin`、`frameNo`。
|
||||||
|
- 车牌号:`carPlate`、`license`、`plateNo`。
|
||||||
|
- 保单号:`policyNo`、`insuranceNo`。
|
||||||
|
- 费用承担方/保险公司:`costUndertakeName`、`insuranceCompany`。
|
||||||
|
- 用户中心/门店:`userCenterName`、`dealerName`、`factoryName`、`factroyName`。
|
||||||
|
- 附加项目:`addItemName`、`items`、`repairItems`、`productName`。
|
||||||
|
- 资料类型:`requirementInfo`、`infoRequirement`、`generateWarrantyClaimInfos`。
|
||||||
|
- 购车时间:`carPurchaseTime`、`invoiceDate`、`issueDate`。
|
||||||
|
- 里程:`mileage`、`kilometers`。
|
||||||
|
|
||||||
|
## 总体审核流程
|
||||||
|
|
||||||
|
1. 解析输入,确认是否赛力斯、确认 `orderType`。
|
||||||
|
2. 建立证据清单:订单字段、材料上传状态、OCR/结构化文本、图片/视频识别结果、金额、日期、VIN、车牌、保单号、印章签名信息。
|
||||||
|
3. 如关键材料只有附件 URL 且没有可判定文本/结构化结果,优先调用已绑定 OCR/文档/图片/视频技能补齐;无法补齐时转 `manual_review`。
|
||||||
|
4. 按订单类型选择规则:`insure` 使用赛力斯投保/延保规则;`claim` 使用平安或太保维修补偿规则。
|
||||||
|
5. 检查所有适用规则,一次性汇总所有不通过项和转人工项,不要遇到第一条问题就停止。
|
||||||
|
6. 形成结论;如用户消息要求生成报告且 `audit-report` 可用,先调用报告技能,再输出最终 JSON。
|
||||||
|
|
||||||
|
## OCR 与容错原则
|
||||||
|
|
||||||
|
- 17 位 VIN:不同字符数 ≤ 4 时视为 OCR 误差,判定一致;超过 4 位才认为不一致。
|
||||||
|
- 7-8 位车牌号:不同字符数 ≤ 1 时视为 OCR 误差,判定一致。
|
||||||
|
- 保单号等长字符串:不同字符占比 ≤ 15% 或最小编辑距离很小,优先视为 OCR 误差。
|
||||||
|
- 金额比较:去除逗号、货币符号和空格,按数字比较;整数与两位小数等价;补偿金额规则允许 ±1 元误差。
|
||||||
|
- 日期比较:优先使用完整日期;无法解析时不要臆测,转人工。
|
||||||
|
- 手写签名/备注宽容:手写内容 OCR 误差高。签名字段非空、有手写痕迹、姓氏一致、或与身份证/经办人一致时,倾向通过。
|
||||||
|
- 印章判断:出现红色、蓝色、紫色鲜章、圆形印章或电子签章,通常视为有效。明确无章、仅 logo、仅二维码时才判无效。
|
||||||
|
- 跨分类查找:资料可能上传错分类,或一张图包含多种资料。只要任意材料 OCR 中能找到满足规则的证据,可视为通过。
|
||||||
|
|
||||||
|
## 低置信度处理
|
||||||
|
|
||||||
|
如果输入或技能结果提供 OCR 置信度:
|
||||||
|
|
||||||
|
- OCR 置信度 < 50:严重不可靠,输出 `manual_review`,除非有更可信结构化结果覆盖所有关键字段。
|
||||||
|
- OCR 置信度 50-79:谨慎判断;关键字段冲突时输出 `manual_review`。
|
||||||
|
- OCR 置信度 < 80 且没有 PaddleOCR/文档 OCR 辅助结果时,赛力斯理赔审核应转 `manual_review`。
|
||||||
|
- 同字段同时有视觉 OCR 与 PaddleOCR/文档 OCR 时,文字、数字、金额、VIN、日期优先后者;印章颜色、签名描述、照片内容等视觉特征优先视觉 OCR。
|
||||||
|
|
||||||
|
## 结论语义
|
||||||
|
|
||||||
|
- `approved`:所有适用硬规则通过,且关键证据充分。
|
||||||
|
- `rejected`:存在明确违反硬规则、明确缺失必传材料、明确金额/日期/VIN/保单不符等可直接驳回的问题,且证据可信。
|
||||||
|
- `manual_review`:资料范围不适用当前规则集、关键 OCR 缺失/低置信度、图片或视频内容无法读取、规则要求人工核实、或存在无法自动定性的业务例外。
|
||||||
|
- `failed`:输入结构错误、非赛力斯业务、系统/技能执行链路异常到无法完成审核。
|
||||||
|
|
||||||
|
如果同时存在明确驳回项和转人工项:只有当驳回项证据独立、可信且足以直接驳回时输出 `rejected`;否则输出 `manual_review`。
|
||||||
|
|
||||||
|
评分建议:
|
||||||
|
|
||||||
|
- 95-100:明确通过。
|
||||||
|
- 70-90:明确驳回且证据充分。
|
||||||
|
- 40-75:需人工复核。
|
||||||
|
- 0-39:系统失败或输入不可用。
|
||||||
|
|
||||||
|
TYCM 原生语义映射仅用于理解:`opearType=0` 对应通过,`opearType=1` 对应驳回,`opearType=2` 或 `needCallback=false` 对应转人工/不回调。RZYX 最终输出仍只用 `decision/reason/score`。
|
||||||
|
|
||||||
|
## 赛力斯投保/延保规则(orderType=insure)
|
||||||
|
|
||||||
|
### 基础检查
|
||||||
|
|
||||||
|
1. 必须为赛力斯订单:`oemId=11` 或品牌/产品字段明确为赛力斯/问界。
|
||||||
|
2. 订单状态如果提供,应处于待审核状态;非待审核状态输出 `manual_review` 或 `failed`,说明当前状态不适合自动审核。
|
||||||
|
3. 产品类型通过 `productName` 判断:
|
||||||
|
- 含“电池延保”按电池延保。
|
||||||
|
- 其他延保默认按整车延保。
|
||||||
|
4. `productName` 明确包含“漆面”时,TYCM 走漆面单独流程;如果输入没有给出漆面专项规则和材料,输出 `manual_review`,说明当前提示词不覆盖漆面专项自动审核。
|
||||||
|
5. 承保渠道字段如 `insurance_company=CPIC/PINGAN` 只用于投保承保渠道识别,不等同于理赔维修补偿规则集;不要把承保渠道误用于理赔费用承担方判断。
|
||||||
|
|
||||||
|
### 在用车车龄/里程限制
|
||||||
|
|
||||||
|
仅对 `carType="1"` 或语义为“在用车”的赛力斯延保订单执行:
|
||||||
|
|
||||||
|
- 电池延保:车龄 ≤ 48 个月,且里程 ≤ 95000 公里。
|
||||||
|
- 整车延保:车龄 ≤ 45 个月,且里程 ≤ 95000 公里。
|
||||||
|
|
||||||
|
车龄按 `carPurchaseTime` 到当前审核日期的完整月数计算。
|
||||||
|
|
||||||
|
判定:
|
||||||
|
|
||||||
|
- 超过任一限制,输出 `rejected`。
|
||||||
|
- `carPurchaseTime` 为空时,不得仅因车龄缺失驳回;原因中标注“缺少购车日期,按旧数据兼容未触发车龄驳回”,其他规则继续审核。
|
||||||
|
- 里程为空或格式异常时,不得仅因里程无法解析驳回;原因中标注“里程无法校验”,其他规则继续审核。
|
||||||
|
|
||||||
|
### 投保无附件兼容规则
|
||||||
|
|
||||||
|
TYCM 当前赛力斯投保/延保链路以结构化订单字段审核为准。生产库赛力斯投保样本通常没有身份证图片、购车发票图片、车辆左右 45 度照片、环车视频或保单文件;这属于当前链路的正常数据形态。
|
||||||
|
|
||||||
|
- 对 `orderType=insure` 的赛力斯订单,缺少身份证图片、购车发票图片、车辆照片、环车视频或保单文件本身,不得作为 `rejected` 或 `manual_review` 的依据。
|
||||||
|
- 当输入包含 `attachmentExpectation=STRUCTURED_DATA_ONLY`、`seresAttachmentMode=STRUCTURED_ONLY` 或附件字段为空时,必须基于结构化字段继续审核。
|
||||||
|
- 只有输入明确给出了可访问附件 URL,且结构化字段缺失或互相矛盾时,才按需调用 OCR 或视频识别辅助判断。
|
||||||
|
- 不要编造附件识别结论;无附件时报告中写“赛力斯投保当前按结构化字段审核,未要求附件 OCR”。
|
||||||
|
|
||||||
|
### 在用车附件规则
|
||||||
|
|
||||||
|
对 `carType="1"` 或语义为“在用车”的赛力斯投保/延保订单,只有在输入字段或规则文本明确声明“本单需要附件审核”时,才执行以下附件校验:
|
||||||
|
|
||||||
|
- 左前 45 度车辆照片。
|
||||||
|
- 右后 45 度车辆照片。
|
||||||
|
- 环车视频。
|
||||||
|
- 缺少任一必需附件且输入明确标记本单必须上传时,输出 `rejected` 或 `manual_review`:若规则明确为准入必需则 `rejected`,若只是无法查看附件内容则 `manual_review`。
|
||||||
|
- 环车视频已上传且 `vehicle-damage-inspection` 可用时,优先调用该技能检查车辆外观/旧伤;不可用时不要从视频 URL 编造画面内容。
|
||||||
|
|
||||||
|
如需判断“新车/在用车”但 `carType` 缺失:可参考交付日期、购车日期、订单日期等字段。正式投保按交付日期约 68 天判断,其他场景按约 60 天判断;字段不足时不要臆测,转 `manual_review`。
|
||||||
|
|
||||||
|
### 投保材料识别
|
||||||
|
|
||||||
|
- 赛力斯投保默认不要求身份证图片和购车发票图片;无图片/文件时,基于订单结构化字段审核,不得因缺少 OCR 输入而 failed、rejected 或 manual_review。
|
||||||
|
- 身份证材料如有图片且缺少结构化信息,可调用 `id-card-ocr`。
|
||||||
|
- 购车发票如有图片/PDF且缺少结构化信息,可调用 `vehicle-invoice-ocr`。
|
||||||
|
- 禁止把长安 `insure-audit` 作为赛力斯投保主规则;不要把长安 `insure-audit` 的规则当作赛力斯专属规则。赛力斯无明确规则的字段,只做基础一致性检查和风险提示。
|
||||||
|
|
||||||
|
## 赛力斯维修补偿理赔规则(orderType=claim)
|
||||||
|
|
||||||
|
### 规则集选择
|
||||||
|
|
||||||
|
根据费用承担方/保险公司选择规则集:
|
||||||
|
|
||||||
|
- 平安:费用承担方为“中国平安财产保险股份有限公司四川分公司”,或字段明确为平安财险,适用 `pingan_repair`。
|
||||||
|
- 太保:费用承担方为“中国太平洋财产保险股份有限公司重庆分公司”,或字段明确为太平洋/太保,适用 `cpic_repair`。
|
||||||
|
- 无法判断保险公司时,输出 `manual_review`。
|
||||||
|
|
||||||
|
TYCM 当前启用规则数量:`pingan_repair` 24 条,`cpic_repair` 20 条。若输入携带来自数据库的最新规则文本,以输入中的最新规则文本优先;否则按本提示词规则摘要执行。
|
||||||
|
|
||||||
|
### 进入 AI 审核的前置条件
|
||||||
|
|
||||||
|
以下条件如果输入明确不满足,输出 `manual_review`,不要误判为通过:
|
||||||
|
|
||||||
|
1. 待审状态如果提供,应为 `warrantyStatus=84061002`。
|
||||||
|
2. 附加项目 `addItemName` 或报修项目必须包含“维修补偿”。
|
||||||
|
3. 附加项数量如有提供,必须且只能有 1 条。
|
||||||
|
4. 资料类型集合如有提供,应与 11 类材料一致:权益使用确认书、商业险保单、车主身份证、车险维修发票、行驶证、维修结算单、定损材料、损失照片或者视频、其他、车辆照片、索赔申请书。
|
||||||
|
5. 太保排除权益服务名“感恩权益-出险代步权益(H)”,命中时输出 `manual_review`。
|
||||||
|
6. 太保保单号以 `TB` 开头、平安保单号以 `PA` 或 `平安PA` 开头属于旧系统过滤例外,命中时输出 `manual_review`。
|
||||||
|
|
||||||
|
### 通用必查原则
|
||||||
|
|
||||||
|
- 对每条材料规则,必须先判断“是否上传”,再判断内容。
|
||||||
|
- 未上传时,只输出“未上传”类问题,不要继续输出该材料内容问题。
|
||||||
|
- 已上传时,再检查字段、金额、日期、签章、VIN、车牌、模板等。
|
||||||
|
- 必须检查完所有适用规则,一次性列出所有问题。
|
||||||
|
- 禁止因为某个材料通过就跳过其他材料。
|
||||||
|
|
||||||
|
### 补偿金额计算规则
|
||||||
|
|
||||||
|
维修补偿金额按定损金额/银行回单/系统预计算金额判断:
|
||||||
|
|
||||||
|
- 定损金额 ≤ 1000 元:驳回,标准备注“定损金额不在补偿范围内。”
|
||||||
|
- 1000 < 定损金额 ≤ 5000:补偿金额 800 元。
|
||||||
|
- 5000 < 定损金额 ≤ 10000:补偿金额 1200 元。
|
||||||
|
- 10000 < 定损金额 ≤ 20000:补偿金额 1600 元。
|
||||||
|
- 定损金额 > 20000:补偿金额 = 定损金额 × 10%。
|
||||||
|
|
||||||
|
金额比对允许 ±1 元误差。
|
||||||
|
|
||||||
|
## 平安维修补偿规则(pingan_repair)
|
||||||
|
|
||||||
|
适用平安时,按以下规则检查:
|
||||||
|
|
||||||
|
1. 行驶证:必须有正页+副页,或新车未上牌时上传临时行驶车号牌;正式行驶证车主名称应与系统/保单信息一致。未上传正副页时驳回“行驶证需上传正副页合。”
|
||||||
|
2. 车主身份证/营业执照:个人车主必须有身份证正反面,姓名与系统车主一致,身份证在出险时间有效期内;公户车主应上传营业执照,企业名称与系统车主一致;公户场景不要求个人身份证姓名等于公司名。
|
||||||
|
3. 商业险保单:不得只是 APP 截图;需为完整正式保单;车架号与系统 VIN 一致;出险时间在保险期间内;商业险保单信息不完整或车架号/车主明显不符时驳回。
|
||||||
|
4. 服务费发票:原则上必须上传服务费发票;若系统/规则明确该用户中心在豁免名单中,可跳过。未找到服务费发票时驳回“需上传服务费发票。”
|
||||||
|
5. 银行回单/赔付凭证:必须上传。未上传时驳回“缺少保险公司赔付凭证。”
|
||||||
|
6. 定损单:必须上传正式定损单,不能是简易确认书;车架号与系统 VIN 一致;定损单上车架号差异超过容错范围时驳回。
|
||||||
|
7. 维修结算单:必须上传且有鲜章;结算金额不能为 0;金额与定损报告不一致且无合理备注时驳回“维修结算单金额需与定损报告金额一致,如有其他情况导致不一致需在结算单空白处手写原因。”
|
||||||
|
8. 维修发票:必须上传车险维修发票;货物/服务名称应包含维修、配件、工时、修理等;发票类型应为电子发票或增值税专用发票;大发票场景需要明细清单;金额与定损单、银行回单均不一致时,维修发票备注栏需体现标的车牌。
|
||||||
|
9. 权益使用确认书:必须上传;车架号、车牌号分别与行驶证一致,不得把车牌当车架号;返店类型必须勾选且正确;补偿金额应等于本次权益补偿金额;定损金额不在补偿范围内时驳回;事故时间应与定损/认定书基准时间一致。
|
||||||
|
10. 索赔申请书:必须上传;保单号与系统保单号一致,保单号按纯数字最小编辑距离容错;标的车牌与系统车牌一致;被保险人签章处需要红色鲜章,且印章内容应包含“问界智选”或“赛力斯新电动”等指定主体。
|
||||||
|
11. 最终索赔金额一致性:以维修结算单、定损单、维修发票、银行回单中的最低有效金额为基础,计算最终应索赔金额;与服务费发票本车对应金额、索赔申请书损失金额、权益使用确认书补偿金额一致。
|
||||||
|
12. 系统权益清单:必须上传赛力斯系统权益清单截图,且包含车辆信息/维修权益服务时间;车架号与系统 VIN 一致。
|
||||||
|
13. 车辆照片:整车照需包含车牌,车架号照片需包含车架号;车辆照片车牌/VIN 与行驶证一致。
|
||||||
|
14. 损失照片:近景照片看不到车牌是正常的,但至少应有一张远景照片包含车牌。若提供损失视频且技能可用,可调用车辆视频/损伤识别技能辅助判断。
|
||||||
|
15. 日期一致性:定损单出险时间/估损日期、索赔申请书出险时间、权益使用确认书事故时间应一致。太平洋定损单特殊场景下,估损日期只需等于或晚于出险日期。
|
||||||
|
16. 模板格式:索赔申请书格式应匹配最新模板;权益使用确认书应包含“返店类型”字段,否则认为旧版模板,驳回“权益使用确认书格式与最新模板不符,请使用最新模板重新填写。”
|
||||||
|
|
||||||
|
## 太保维修补偿规则(cpic_repair)
|
||||||
|
|
||||||
|
适用太保时,按以下规则检查:
|
||||||
|
|
||||||
|
1. 行驶证:必须有正页+副页,或上传临时行驶车号牌;缺失时驳回“行驶证需上传正副页合。”
|
||||||
|
2. 车主身份证/营业执照:个人车主检查身份证正反面、姓名和有效期;公户车主检查营业执照名称与系统车主一致。
|
||||||
|
3. 商业险保单:必须上传;必须有车损类险种,如损失保险、车损保险、机动车损失保险、车辆损失险;车架号与系统 VIN 一致;出险日期在保险期间内。承保公司不要求与费用承担方一致。
|
||||||
|
4. 银行回单:必须上传;如果权益确认书有“权益车辆保险定损金额”,银行回单金额必须与其一致;不一致驳回“银行回单金额与权益确认书定损金额不符。”
|
||||||
|
5. 定损单:必须上传;必须包含车架号、定损金额、出险时间或估损日期;车架号与系统 VIN 一致;关键字段缺失驳回“上传详细的定损单,需包含出险时间,车架号,定损项目详情。”
|
||||||
|
6. 维修结算单:必须上传;必须有鲜章;结算金额不能为 0;维修结算单不需要与定损金额、发票金额做金额比对。
|
||||||
|
7. 维修发票:必须能从发票列表中找到货物名称包含维修、配件、工时、修理的发票;找不到时驳回“需上传车险维修发票。”
|
||||||
|
8. 权益使用确认书:必须上传;抬头必须包含“权益使用确认书”;必须有鲜章;维修补偿金额按补偿金额规则校验;事故时间应与交通事故认定书或定损单基准时间一致。
|
||||||
|
9. 索赔申请书:必须上传;被保险人字段必须有值;保单号与系统保单号一致;保险产品必须填写;被保险人签章处必须有鲜章;损失金额按补偿金额规则校验;出险时间与交通事故认定书或定损单基准时间一致。
|
||||||
|
10. 整车照片:必须上传整车照且能识别车牌;未上传驳回“需上传整车照(含车牌)。”
|
||||||
|
11. 车架号铭牌照片:必须上传且能识别车架号;车架号与系统 VIN 一致;缺失时驳回“需上传车架号照片。”
|
||||||
|
12. 损失照片:近景照片看不到车牌是正常的,但至少应有一张远景照片包含车牌。若提供损失视频且技能可用,可调用车辆视频/损伤识别技能辅助判断。
|
||||||
|
|
||||||
|
### 太保强制转人工项
|
||||||
|
|
||||||
|
太保案件中,如果定损单或维修结算单项目明细包含车衣/膜类项目,例如“隐形车衣”“贴膜”“改色膜”“保护膜”“车衣”“膜类”,输出 `manual_review`。
|
||||||
|
|
||||||
|
原因必须写明:`定损单或维修结算单项目明细中包含车衣/膜类项目,AI无法判断是否在权益补偿范围内,需人工核实。`
|
||||||
|
|
||||||
|
## reason 撰写要求
|
||||||
|
|
||||||
|
`reason` 必须同时包含:
|
||||||
|
|
||||||
|
1. 结论摘要:通过/驳回/转人工/失败。
|
||||||
|
2. 适用场景:投保/理赔、平安/太保、产品或项目。
|
||||||
|
3. 规则依据:列出触发的规则。
|
||||||
|
4. 证据:写出识别到的具体字段值,例如 VIN、车牌、金额、日期、材料上传状态、OCR 置信度、关键技能结果。
|
||||||
|
5. 处理建议:用户需要补充、替换、重新上传或人工核实什么材料。
|
||||||
|
|
||||||
|
通过时也要说明已核验的关键证据,不要只写“审核通过”。
|
||||||
|
|
||||||
|
## 最终输出示例
|
||||||
|
|
||||||
|
通过:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"decision":"approved","reason":"结论:审核通过;场景:赛力斯理赔-平安维修补偿;依据:11类材料均已上传,VIN、车牌、保单号、出险日期、维修补偿金额均一致;证据:VIN=XXX,车牌=XXX,定损金额=6000元,补偿金额=1200元;建议:服务端生成报告后回调TYCM。","score":98}
|
||||||
|
```
|
||||||
|
|
||||||
|
驳回:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"decision":"rejected","reason":"结论:审核驳回;场景:赛力斯理赔-太保维修补偿;依据:维修发票规则、补偿金额规则;证据:未识别到货物/服务名称包含维修、配件、工时、修理的车险维修发票,权益确认书补偿金额=1600元但按定损金额6000元应为1200元;建议:补充正确车险维修发票并更正权益确认书补偿金额。","score":82}
|
||||||
|
```
|
||||||
|
|
||||||
|
转人工:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"decision":"manual_review","reason":"结论:转人工复核;场景:赛力斯理赔-太保维修补偿;依据:太保车衣/膜类项目强制转人工;证据:维修结算单项目明细包含隐形车衣,AI无法判断是否在权益补偿范围内;建议:人工核实该项目是否属于本次维修补偿范围。","score":62}
|
||||||
|
```
|
||||||
|
|
||||||
|
失败:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"decision":"failed","reason":"结论:审核失败;原因:输入 data 不是可解析对象或缺少 orderType,无法判断赛力斯投保/理赔场景;建议:重新提交包含 orderNo、orderType、oemId、data 的审核请求。","score":20}
|
||||||
|
```
|
||||||
|
````
|
||||||
1559
docs/superpowers/plans/2026-04-11-netaclaw-phase1-cleanup-core.md
Normal file
1559
docs/superpowers/plans/2026-04-11-netaclaw-phase1-cleanup-core.md
Normal file
File diff suppressed because it is too large
Load Diff
906
docs/superpowers/plans/2026-04-12-agent-memory-system.md
Normal file
906
docs/superpowers/plans/2026-04-12-agent-memory-system.md
Normal file
@ -0,0 +1,906 @@
|
|||||||
|
# Agent 长期记忆系统 Implementation Plan
|
||||||
|
|
||||||
|
> **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:** 为 NetaClaw Agent 添加跨会话长期记忆,支持 MySQL FULLTEXT 和 SQLite FTS5 双后端,Agent 粒度可配。
|
||||||
|
|
||||||
|
**Architecture:** Provider 抽象层 + 双后端实现。MemoryProvider 接口统一 save/search/delete 操作,MysqlMemoryProvider 用 FULLTEXT ngram,SqliteMemoryProvider 用 FTS5 trigram。工厂函数根据 Agent 配置选择后端。memory_save / memory_recall 两个工具注入 Agent 工具列表,prefetch 在每轮对话前检索相关记忆注入 system prompt。
|
||||||
|
|
||||||
|
**Tech Stack:** TypeScript, TypeORM (MySQL), better-sqlite3 (SQLite FTS5), @sinclair/typebox (tool schema)
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-04-12-agent-memory-system-design.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/modules/netaclaw/
|
||||||
|
├── memory/
|
||||||
|
│ ├── provider.ts # MemoryProvider 接口 + MemoryEntry 类型 + AgentMemoryConfig
|
||||||
|
│ ├── factory.ts # createMemoryProvider 工厂函数
|
||||||
|
│ ├── mysql_provider.ts # MysqlMemoryProvider
|
||||||
|
│ ├── sqlite_provider.ts # SqliteMemoryProvider
|
||||||
|
│ └── prefetch.ts # prefetchMemory() + formatMemoryContext()
|
||||||
|
├── entity/
|
||||||
|
│ └── memory.ts # NetaClawMemoryEntity (新增)
|
||||||
|
├── tools/builtin/
|
||||||
|
│ └── memory.ts # memory_save + memory_recall 工具
|
||||||
|
```
|
||||||
|
|
||||||
|
Modified files:
|
||||||
|
- `src/modules/netaclaw/runtime/agent.ts` — AgentRunParams 新增 memoryContext,消息注入
|
||||||
|
- `src/modules/netaclaw/controller/chat.ts` — prefetch 调用 + memory 工具注入
|
||||||
|
- `src/entities.ts` — 注册 NetaClawMemoryEntity
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Install dependencies + Create MemoryEntry types and MemoryProvider interface
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/modules/netaclaw/memory/provider.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Install better-sqlite3**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd packages/backend && pnpm add better-sqlite3 && pnpm add -D @types/better-sqlite3
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Create provider.ts with types and interface**
|
||||||
|
|
||||||
|
Create `src/modules/netaclaw/memory/provider.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* 长期记忆 Provider 抽象层
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type MemoryType = 'user' | 'project' | 'feedback' | 'reference';
|
||||||
|
|
||||||
|
export interface MemoryEntry {
|
||||||
|
id: number;
|
||||||
|
agentName: string;
|
||||||
|
userId: string;
|
||||||
|
type: MemoryType;
|
||||||
|
name: string;
|
||||||
|
content: string;
|
||||||
|
description: string;
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MemorySearchOpts {
|
||||||
|
agentName: string;
|
||||||
|
userId: string;
|
||||||
|
type?: MemoryType;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentMemoryConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
backend: 'mysql' | 'sqlite';
|
||||||
|
sqlitePath?: string;
|
||||||
|
prefetchLimit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MemoryProvider {
|
||||||
|
save(entry: Omit<MemoryEntry, 'id' | 'createdAt' | 'updatedAt'>): Promise<MemoryEntry>;
|
||||||
|
update(id: number, partial: Partial<Pick<MemoryEntry, 'name' | 'content' | 'description' | 'type' | 'metadata'>>): Promise<MemoryEntry>;
|
||||||
|
delete(id: number): Promise<void>;
|
||||||
|
search(query: string, opts: MemorySearchOpts): Promise<MemoryEntry[]>;
|
||||||
|
list(opts: MemorySearchOpts): Promise<MemoryEntry[]>;
|
||||||
|
getById(id: number): Promise<MemoryEntry | null>;
|
||||||
|
close?(): Promise<void>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Verify no TypeScript errors**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd packages/backend && npx tsc --noEmit src/modules/netaclaw/memory/provider.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/modules/netaclaw/memory/provider.ts package.json pnpm-lock.yaml
|
||||||
|
git commit -m "feat(memory): add MemoryProvider interface and types + install better-sqlite3"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Create NetaClawMemoryEntity for MySQL
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/modules/netaclaw/entity/memory.ts`
|
||||||
|
- Modify: `src/entities.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create the entity file**
|
||||||
|
|
||||||
|
Create `src/modules/netaclaw/entity/memory.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { BaseEntity } from '../../base/entity/base.js';
|
||||||
|
import { Column, Entity, Index } from 'typeorm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NetaClaw 长期记忆
|
||||||
|
*/
|
||||||
|
@Entity('netaclaw_memory')
|
||||||
|
export class NetaClawMemoryEntity extends BaseEntity {
|
||||||
|
@Index()
|
||||||
|
@Column({ comment: 'Agent名称', length: 100 })
|
||||||
|
agentName: string;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column({ comment: '用户ID', length: 100 })
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column({ comment: '记忆类型: user/project/feedback/reference', length: 20 })
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
@Column({ comment: '记忆标题', length: 255 })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text', comment: '记忆正文' })
|
||||||
|
content: string;
|
||||||
|
|
||||||
|
@Column({ comment: '一行描述', length: 500, default: '' })
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@Column({ type: 'json', comment: '元数据', nullable: true })
|
||||||
|
metadata: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: FULLTEXT 索引需要通过 migration 或手动 SQL 添加,TypeORM 不直接支持 `WITH PARSER ngram`。在 Step 3 中处理。
|
||||||
|
|
||||||
|
- [ ] **Step 2: Register entity in src/entities.ts**
|
||||||
|
|
||||||
|
在 `src/entities.ts` 中,在 `import * as entity29 from './modules/netaclaw/entity/model_channel';` 之后添加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import * as entity30 from './modules/netaclaw/entity/memory';
|
||||||
|
```
|
||||||
|
|
||||||
|
在 `entities` 数组末尾(`]` 之前)添加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
...Object.values(entity30),
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Create SQL migration for FULLTEXT index**
|
||||||
|
|
||||||
|
Create `src/modules/netaclaw/memory/migration.sql` (reference file, run manually or via init):
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 在 TypeORM 自动建表后执行,添加 ngram FULLTEXT 索引
|
||||||
|
ALTER TABLE netaclaw_memory
|
||||||
|
ADD FULLTEXT INDEX ft_content (name, content, description) WITH PARSER ngram;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/modules/netaclaw/entity/memory.ts src/entities.ts src/modules/netaclaw/memory/migration.sql
|
||||||
|
git commit -m "feat(memory): add NetaClawMemoryEntity with FULLTEXT index migration"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Implement MysqlMemoryProvider
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/modules/netaclaw/memory/mysql_provider.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Implement MysqlMemoryProvider**
|
||||||
|
|
||||||
|
Create `src/modules/netaclaw/memory/mysql_provider.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { NetaClawMemoryEntity } from '../entity/memory.js';
|
||||||
|
import { MemoryProvider, MemoryEntry, MemorySearchOpts } from './provider.js';
|
||||||
|
|
||||||
|
function toEntry(e: NetaClawMemoryEntity): MemoryEntry {
|
||||||
|
return {
|
||||||
|
id: e.id,
|
||||||
|
agentName: e.agentName,
|
||||||
|
userId: e.userId,
|
||||||
|
type: e.type as MemoryEntry['type'],
|
||||||
|
name: e.name,
|
||||||
|
content: e.content,
|
||||||
|
description: e.description,
|
||||||
|
metadata: e.metadata,
|
||||||
|
createdAt: e.createTime,
|
||||||
|
updatedAt: e.updateTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MysqlMemoryProvider implements MemoryProvider {
|
||||||
|
constructor(private repo: Repository<NetaClawMemoryEntity>) {}
|
||||||
|
|
||||||
|
async save(entry: Omit<MemoryEntry, 'id' | 'createdAt' | 'updatedAt'>): Promise<MemoryEntry> {
|
||||||
|
const saved = await this.repo.save({
|
||||||
|
agentName: entry.agentName,
|
||||||
|
userId: entry.userId,
|
||||||
|
type: entry.type,
|
||||||
|
name: entry.name,
|
||||||
|
content: entry.content,
|
||||||
|
description: entry.description ?? '',
|
||||||
|
metadata: entry.metadata,
|
||||||
|
});
|
||||||
|
return toEntry(saved);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: number, partial: Partial<Pick<MemoryEntry, 'name' | 'content' | 'description' | 'type' | 'metadata'>>): Promise<MemoryEntry> {
|
||||||
|
await this.repo.update(id, partial);
|
||||||
|
const updated = await this.repo.findOneByOrFail({ id });
|
||||||
|
return toEntry(updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: number): Promise<void> {
|
||||||
|
await this.repo.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(query: string, opts: MemorySearchOpts): Promise<MemoryEntry[]> {
|
||||||
|
const limit = opts.limit ?? 10;
|
||||||
|
let qb = this.repo.createQueryBuilder('m')
|
||||||
|
.where('m.agentName = :agentName', { agentName: opts.agentName })
|
||||||
|
.andWhere('m.userId = :userId', { userId: opts.userId })
|
||||||
|
.andWhere(`MATCH(m.name, m.content, m.description) AGAINST(:query IN BOOLEAN MODE)`, { query })
|
||||||
|
.orderBy(`MATCH(m.name, m.content, m.description) AGAINST(:query IN BOOLEAN MODE)`, 'DESC')
|
||||||
|
.limit(limit);
|
||||||
|
|
||||||
|
if (opts.type) {
|
||||||
|
qb = qb.andWhere('m.type = :type', { type: opts.type });
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await qb.getMany();
|
||||||
|
return results.map(toEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
async list(opts: MemorySearchOpts): Promise<MemoryEntry[]> {
|
||||||
|
const where: any = { agentName: opts.agentName, userId: opts.userId };
|
||||||
|
if (opts.type) where.type = opts.type;
|
||||||
|
const results = await this.repo.find({
|
||||||
|
where,
|
||||||
|
order: { updateTime: 'DESC' },
|
||||||
|
take: opts.limit ?? 10,
|
||||||
|
});
|
||||||
|
return results.map(toEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getById(id: number): Promise<MemoryEntry | null> {
|
||||||
|
const e = await this.repo.findOneBy({ id });
|
||||||
|
return e ? toEntry(e) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verify no TypeScript errors**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd packages/backend && npx tsc --noEmit
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/modules/netaclaw/memory/mysql_provider.ts
|
||||||
|
git commit -m "feat(memory): implement MysqlMemoryProvider with FULLTEXT search"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Implement SqliteMemoryProvider
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/modules/netaclaw/memory/sqlite_provider.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Implement SqliteMemoryProvider**
|
||||||
|
|
||||||
|
Create `src/modules/netaclaw/memory/sqlite_provider.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import Database from 'better-sqlite3';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { MemoryProvider, MemoryEntry, MemorySearchOpts } from './provider.js';
|
||||||
|
|
||||||
|
const INIT_SQL = `
|
||||||
|
CREATE TABLE IF NOT EXISTS memory (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
agent_name TEXT NOT NULL,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
type TEXT NOT NULL CHECK(type IN ('user', 'project', 'feedback', 'reference')),
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL DEFAULT '',
|
||||||
|
metadata TEXT,
|
||||||
|
created_at TEXT DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_agent_user ON memory(agent_name, user_id);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const FTS_SQL = `
|
||||||
|
CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(
|
||||||
|
name, content, description,
|
||||||
|
content='memory', content_rowid='id',
|
||||||
|
tokenize='trigram'
|
||||||
|
);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TRIGGER_SQL = `
|
||||||
|
CREATE TRIGGER IF NOT EXISTS memory_ai AFTER INSERT ON memory BEGIN
|
||||||
|
INSERT INTO memory_fts(rowid, name, content, description)
|
||||||
|
VALUES (new.id, new.name, new.content, new.description);
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER IF NOT EXISTS memory_ad AFTER DELETE ON memory BEGIN
|
||||||
|
INSERT INTO memory_fts(memory_fts, rowid, name, content, description)
|
||||||
|
VALUES ('delete', old.id, old.name, old.content, old.description);
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER IF NOT EXISTS memory_au AFTER UPDATE ON memory BEGIN
|
||||||
|
INSERT INTO memory_fts(memory_fts, rowid, name, content, description)
|
||||||
|
VALUES ('delete', old.id, old.name, old.content, old.description);
|
||||||
|
INSERT INTO memory_fts(rowid, name, content, description)
|
||||||
|
VALUES (new.id, new.name, new.content, new.description);
|
||||||
|
END;
|
||||||
|
`;
|
||||||
|
|
||||||
|
function toEntry(row: any): MemoryEntry {
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
agentName: row.agent_name,
|
||||||
|
userId: row.user_id,
|
||||||
|
type: row.type,
|
||||||
|
name: row.name,
|
||||||
|
content: row.content,
|
||||||
|
description: row.description,
|
||||||
|
metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
|
||||||
|
createdAt: new Date(row.created_at),
|
||||||
|
updatedAt: new Date(row.updated_at),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SqliteMemoryProvider implements MemoryProvider {
|
||||||
|
private db: Database.Database;
|
||||||
|
|
||||||
|
constructor(dbPath?: string) {
|
||||||
|
const resolvedPath = dbPath ?? path.resolve(process.cwd(), 'data/memory/memory.db');
|
||||||
|
fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
|
||||||
|
this.db = new Database(resolvedPath);
|
||||||
|
this.db.pragma('journal_mode = WAL');
|
||||||
|
this.db.exec(INIT_SQL);
|
||||||
|
this.db.exec(FTS_SQL);
|
||||||
|
this.db.exec(TRIGGER_SQL);
|
||||||
|
}
|
||||||
|
|
||||||
|
async save(entry: Omit<MemoryEntry, 'id' | 'createdAt' | 'updatedAt'>): Promise<MemoryEntry> {
|
||||||
|
const stmt = this.db.prepare(`
|
||||||
|
INSERT INTO memory (agent_name, user_id, type, name, content, description, metadata)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`);
|
||||||
|
const info = stmt.run(
|
||||||
|
entry.agentName, entry.userId, entry.type,
|
||||||
|
entry.name, entry.content, entry.description ?? '',
|
||||||
|
entry.metadata ? JSON.stringify(entry.metadata) : null,
|
||||||
|
);
|
||||||
|
return (await this.getById(info.lastInsertRowid as number))!;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: number, partial: Partial<Pick<MemoryEntry, 'name' | 'content' | 'description' | 'type' | 'metadata'>>): Promise<MemoryEntry> {
|
||||||
|
const sets: string[] = [];
|
||||||
|
const values: any[] = [];
|
||||||
|
if (partial.name !== undefined) { sets.push('name = ?'); values.push(partial.name); }
|
||||||
|
if (partial.content !== undefined) { sets.push('content = ?'); values.push(partial.content); }
|
||||||
|
if (partial.description !== undefined) { sets.push('description = ?'); values.push(partial.description); }
|
||||||
|
if (partial.type !== undefined) { sets.push('type = ?'); values.push(partial.type); }
|
||||||
|
if (partial.metadata !== undefined) { sets.push('metadata = ?'); values.push(JSON.stringify(partial.metadata)); }
|
||||||
|
sets.push("updated_at = datetime('now')");
|
||||||
|
values.push(id);
|
||||||
|
this.db.prepare(`UPDATE memory SET ${sets.join(', ')} WHERE id = ?`).run(...values);
|
||||||
|
return (await this.getById(id))!;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: number): Promise<void> {
|
||||||
|
this.db.prepare('DELETE FROM memory WHERE id = ?').run(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(query: string, opts: MemorySearchOpts): Promise<MemoryEntry[]> {
|
||||||
|
const limit = opts.limit ?? 10;
|
||||||
|
// FTS5 trigram tokenizer: wrap query for substring match
|
||||||
|
const ftsQuery = `"${query.replace(/"/g, '""')}"`;
|
||||||
|
let sql = `
|
||||||
|
SELECT m.* FROM memory m
|
||||||
|
JOIN memory_fts f ON m.id = f.rowid
|
||||||
|
WHERE memory_fts MATCH ?
|
||||||
|
AND m.agent_name = ?
|
||||||
|
AND m.user_id = ?
|
||||||
|
`;
|
||||||
|
const params: any[] = [ftsQuery, opts.agentName, opts.userId];
|
||||||
|
if (opts.type) { sql += ' AND m.type = ?'; params.push(opts.type); }
|
||||||
|
sql += ' ORDER BY rank LIMIT ?';
|
||||||
|
params.push(limit);
|
||||||
|
const rows = this.db.prepare(sql).all(...params);
|
||||||
|
return rows.map(toEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
async list(opts: MemorySearchOpts): Promise<MemoryEntry[]> {
|
||||||
|
let sql = 'SELECT * FROM memory WHERE agent_name = ? AND user_id = ?';
|
||||||
|
const params: any[] = [opts.agentName, opts.userId];
|
||||||
|
if (opts.type) { sql += ' AND type = ?'; params.push(opts.type); }
|
||||||
|
sql += ' ORDER BY updated_at DESC LIMIT ?';
|
||||||
|
params.push(opts.limit ?? 10);
|
||||||
|
const rows = this.db.prepare(sql).all(...params);
|
||||||
|
return rows.map(toEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getById(id: number): Promise<MemoryEntry | null> {
|
||||||
|
const row = this.db.prepare('SELECT * FROM memory WHERE id = ?').get(id);
|
||||||
|
return row ? toEntry(row) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async close(): Promise<void> {
|
||||||
|
this.db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verify no TypeScript errors**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd packages/backend && npx tsc --noEmit
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/modules/netaclaw/memory/sqlite_provider.ts
|
||||||
|
git commit -m "feat(memory): implement SqliteMemoryProvider with FTS5 trigram search"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Create factory function
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/modules/netaclaw/memory/factory.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Implement createMemoryProvider**
|
||||||
|
|
||||||
|
Create `src/modules/netaclaw/memory/factory.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { NetaClawMemoryEntity } from '../entity/memory.js';
|
||||||
|
import { MemoryProvider, AgentMemoryConfig } from './provider.js';
|
||||||
|
import { MysqlMemoryProvider } from './mysql_provider.js';
|
||||||
|
import { SqliteMemoryProvider } from './sqlite_provider.js';
|
||||||
|
|
||||||
|
export function createMemoryProvider(
|
||||||
|
config: AgentMemoryConfig,
|
||||||
|
mysqlRepo?: Repository<NetaClawMemoryEntity>,
|
||||||
|
): MemoryProvider {
|
||||||
|
if (config.backend === 'sqlite') {
|
||||||
|
return new SqliteMemoryProvider(config.sqlitePath);
|
||||||
|
}
|
||||||
|
if (!mysqlRepo) {
|
||||||
|
throw new Error('MysqlMemoryProvider requires a TypeORM repository');
|
||||||
|
}
|
||||||
|
return new MysqlMemoryProvider(mysqlRepo);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/modules/netaclaw/memory/factory.ts
|
||||||
|
git commit -m "feat(memory): add createMemoryProvider factory"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Create prefetch module
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/modules/netaclaw/memory/prefetch.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Implement prefetchMemory and formatMemoryContext**
|
||||||
|
|
||||||
|
Create `src/modules/netaclaw/memory/prefetch.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { MemoryProvider, MemoryEntry, MemorySearchOpts } from './provider.js';
|
||||||
|
|
||||||
|
export function formatMemoryContext(entries: MemoryEntry[]): string {
|
||||||
|
if (entries.length === 0) return '';
|
||||||
|
const lines = entries.map(e => `[${e.type}] ${e.name}\n${e.content}`);
|
||||||
|
return `以下是与当前对话可能相关的长期记忆:\n\n${lines.join('\n\n')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function prefetchMemory(
|
||||||
|
provider: MemoryProvider,
|
||||||
|
userMessage: string,
|
||||||
|
agentName: string,
|
||||||
|
userId: string,
|
||||||
|
limit = 5,
|
||||||
|
): Promise<string> {
|
||||||
|
const opts: MemorySearchOpts = { agentName, userId, limit };
|
||||||
|
let entries: MemoryEntry[];
|
||||||
|
try {
|
||||||
|
entries = await provider.search(userMessage, opts);
|
||||||
|
} catch {
|
||||||
|
// search 失败时 fallback 到 list(例如 query 为空或 FTS 语法错误)
|
||||||
|
entries = await provider.list(opts);
|
||||||
|
}
|
||||||
|
return formatMemoryContext(entries);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/modules/netaclaw/memory/prefetch.ts
|
||||||
|
git commit -m "feat(memory): add prefetchMemory with formatted context output"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: Create memory tools (memory_save + memory_recall)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/modules/netaclaw/tools/builtin/memory.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Implement memory tools**
|
||||||
|
|
||||||
|
Create `src/modules/netaclaw/tools/builtin/memory.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Type, Static } from '@sinclair/typebox';
|
||||||
|
import { AnyAgentTool } from '../common.js';
|
||||||
|
import { MemoryProvider, MemoryType } from '../../memory/provider.js';
|
||||||
|
|
||||||
|
const MemoryTypeSchema = Type.Union([
|
||||||
|
Type.Literal('user'),
|
||||||
|
Type.Literal('project'),
|
||||||
|
Type.Literal('feedback'),
|
||||||
|
Type.Literal('reference'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// --- memory_save ---
|
||||||
|
|
||||||
|
const memorySaveParams = Type.Object({
|
||||||
|
action: Type.Union([Type.Literal('create'), Type.Literal('update'), Type.Literal('delete')]),
|
||||||
|
name: Type.String({ description: '记忆标题' }),
|
||||||
|
type: MemoryTypeSchema,
|
||||||
|
content: Type.Optional(Type.String({ description: '记忆正文' })),
|
||||||
|
description: Type.Optional(Type.String({ description: '一行描述' })),
|
||||||
|
id: Type.Optional(Type.Number({ description: '更新/删除时的记忆 ID' })),
|
||||||
|
});
|
||||||
|
|
||||||
|
type MemorySaveParams = Static<typeof memorySaveParams>;
|
||||||
|
|
||||||
|
export function createMemorySaveTool(
|
||||||
|
provider: MemoryProvider,
|
||||||
|
agentName: string,
|
||||||
|
userId: string,
|
||||||
|
): AnyAgentTool {
|
||||||
|
return {
|
||||||
|
name: 'memory_save',
|
||||||
|
label: '保存记忆',
|
||||||
|
description: '存储、更新或删除长期记忆。记忆会在未来对话中自动召回。',
|
||||||
|
parameters: memorySaveParams,
|
||||||
|
async execute(_id: string, params: MemorySaveParams): Promise<string> {
|
||||||
|
if (params.action === 'create') {
|
||||||
|
const entry = await provider.save({
|
||||||
|
agentName,
|
||||||
|
userId,
|
||||||
|
type: params.type as MemoryType,
|
||||||
|
name: params.name,
|
||||||
|
content: params.content ?? '',
|
||||||
|
description: params.description ?? '',
|
||||||
|
});
|
||||||
|
return `记忆已保存 (id=${entry.id}): ${entry.name}`;
|
||||||
|
}
|
||||||
|
if (params.action === 'update') {
|
||||||
|
if (!params.id) return '错误: 更新操作需要提供 id';
|
||||||
|
const entry = await provider.update(params.id, {
|
||||||
|
name: params.name,
|
||||||
|
type: params.type as MemoryType,
|
||||||
|
content: params.content,
|
||||||
|
description: params.description,
|
||||||
|
});
|
||||||
|
return `记忆已更新 (id=${entry.id}): ${entry.name}`;
|
||||||
|
}
|
||||||
|
if (params.action === 'delete') {
|
||||||
|
if (!params.id) return '错误: 删除操作需要提供 id';
|
||||||
|
await provider.delete(params.id);
|
||||||
|
return `记忆已删除 (id=${params.id})`;
|
||||||
|
}
|
||||||
|
return '错误: 未知操作';
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- memory_recall ---
|
||||||
|
|
||||||
|
const memoryRecallParams = Type.Object({
|
||||||
|
query: Type.String({ description: '搜索关键词' }),
|
||||||
|
type: Type.Optional(MemoryTypeSchema),
|
||||||
|
limit: Type.Optional(Type.Number({ description: '返回条数,默认 5', default: 5 })),
|
||||||
|
});
|
||||||
|
|
||||||
|
type MemoryRecallParams = Static<typeof memoryRecallParams>;
|
||||||
|
|
||||||
|
export function createMemoryRecallTool(
|
||||||
|
provider: MemoryProvider,
|
||||||
|
agentName: string,
|
||||||
|
userId: string,
|
||||||
|
): AnyAgentTool {
|
||||||
|
return {
|
||||||
|
name: 'memory_recall',
|
||||||
|
label: '检索记忆',
|
||||||
|
description: '搜索长期记忆中的相关信息。',
|
||||||
|
parameters: memoryRecallParams,
|
||||||
|
async execute(_id: string, params: MemoryRecallParams): Promise<string> {
|
||||||
|
const entries = await provider.search(params.query, {
|
||||||
|
agentName,
|
||||||
|
userId,
|
||||||
|
type: params.type as MemoryType | undefined,
|
||||||
|
limit: params.limit ?? 5,
|
||||||
|
});
|
||||||
|
if (entries.length === 0) return '未找到相关记忆。';
|
||||||
|
return entries.map(e => `[${e.type}] (id=${e.id}) ${e.name}\n${e.content}`).join('\n\n');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verify no TypeScript errors**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd packages/backend && npx tsc --noEmit
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/modules/netaclaw/tools/builtin/memory.ts
|
||||||
|
git commit -m "feat(memory): add memory_save and memory_recall agent tools"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 8: Modify runtime/agent.ts to support memoryContext injection
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/modules/netaclaw/runtime/agent.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add memoryContext to AgentRunParams and inject into messages**
|
||||||
|
|
||||||
|
In `src/modules/netaclaw/runtime/agent.ts`:
|
||||||
|
|
||||||
|
Add `memoryContext?: string;` to the `AgentRunParams` interface (after `history?: LLMMessage[];`):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface AgentRunParams {
|
||||||
|
agentConfig: AgentConfig;
|
||||||
|
tools: AnyAgentTool[];
|
||||||
|
userMessage: string;
|
||||||
|
history?: LLMMessage[];
|
||||||
|
memoryContext?: string;
|
||||||
|
onToken?: (text: string) => void;
|
||||||
|
onThinking?: (text: string) => void;
|
||||||
|
onToolCall?: (name: string, args: Record<string, unknown>) => void;
|
||||||
|
onToolResult?: (name: string, result: string) => void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Update the `runAgent` function destructuring to include `memoryContext`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { agentConfig, tools, userMessage, history = [], memoryContext,
|
||||||
|
onToken, onThinking, onToolCall, onToolResult } = params;
|
||||||
|
```
|
||||||
|
|
||||||
|
Update the messages array — append memoryContext into the single system prompt (Anthropic provider only reads the first system message, so a second system message would be silently dropped):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const systemContent = memoryContext
|
||||||
|
? `${agentConfig.systemPrompt}\n\n<memory-context>\n${memoryContext}\n</memory-context>`
|
||||||
|
: agentConfig.systemPrompt;
|
||||||
|
|
||||||
|
const messages: LLMMessage[] = [
|
||||||
|
{ role: 'system', content: systemContent },
|
||||||
|
...history,
|
||||||
|
{ role: 'user', content: userMessage },
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verify no TypeScript errors**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd packages/backend && npx tsc --noEmit
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/modules/netaclaw/runtime/agent.ts
|
||||||
|
git commit -m "feat(memory): inject memoryContext into agent message assembly"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 9: Integrate memory into controller/chat.ts
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/modules/netaclaw/controller/chat.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add imports, inject repo, wire up prefetch + tools**
|
||||||
|
|
||||||
|
In `src/modules/netaclaw/controller/chat.ts`, add imports at the top:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { NetaClawMemoryEntity } from '../entity/memory.js';
|
||||||
|
import { NetaClawAgentService } from '../service/agent.js';
|
||||||
|
import { AnyAgentTool } from '../tools/common.js';
|
||||||
|
import { AgentMemoryConfig } from '../memory/provider.js';
|
||||||
|
import { createMemoryProvider } from '../memory/factory.js';
|
||||||
|
import { prefetchMemory } from '../memory/prefetch.js';
|
||||||
|
import { createMemorySaveTool, createMemoryRecallTool } from '../tools/builtin/memory.js';
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the repository and service injections inside the class (after `skillLoader`):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@InjectEntityModel(NetaClawMemoryEntity)
|
||||||
|
memoryRepo: Repository<NetaClawMemoryEntity>;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
agentService: NetaClawAgentService;
|
||||||
|
```
|
||||||
|
|
||||||
|
Update the `chat` method. First, add `userId` to the body type:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async chat(@Body() body: { sessionId?: string; message: string; agentName?: string; userId?: string }) {
|
||||||
|
```
|
||||||
|
|
||||||
|
After `const agentName = ...` (line 30), load the agent entity from DB:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const agentEntity = await this.agentService.agentRepo.findOneBy({ name: agentName });
|
||||||
|
```
|
||||||
|
|
||||||
|
Then after `const systemPrompt = ...` and before `const agentConfig = ...`, add memory logic:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// --- 记忆系统 ---
|
||||||
|
const memoryConfig: AgentMemoryConfig | undefined =
|
||||||
|
(agentEntity?.config as any)?.memory as AgentMemoryConfig | undefined;
|
||||||
|
|
||||||
|
let memoryContext: string | undefined;
|
||||||
|
let memoryTools: AnyAgentTool[] = [];
|
||||||
|
|
||||||
|
if (memoryConfig?.enabled) {
|
||||||
|
const provider = createMemoryProvider(memoryConfig, this.memoryRepo);
|
||||||
|
const userId = body.userId ?? 'anonymous';
|
||||||
|
|
||||||
|
memoryContext = await prefetchMemory(provider, body.message, agentName, userId, memoryConfig.prefetchLimit);
|
||||||
|
memoryTools = [
|
||||||
|
createMemorySaveTool(provider, agentName, userId),
|
||||||
|
createMemoryRecallTool(provider, agentName, userId),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Update the systemPrompt to append memory instructions when enabled:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
let finalSystemPrompt = systemPrompt;
|
||||||
|
if (memoryConfig?.enabled) {
|
||||||
|
finalSystemPrompt += MEMORY_SYSTEM_PROMPT;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the constant at the top of the file (after imports):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const MEMORY_SYSTEM_PROMPT = `
|
||||||
|
|
||||||
|
## 记忆系统
|
||||||
|
你拥有长期记忆能力。使用 memory_save 工具存储重要信息,使用 memory_recall 工具检索过往记忆。
|
||||||
|
|
||||||
|
记忆类型:
|
||||||
|
- user: 用户画像(偏好、角色、习惯)
|
||||||
|
- project: 项目知识(进展、决策、约束)
|
||||||
|
- feedback: 行为反馈(用户对你行为的纠正或确认)
|
||||||
|
- reference: 引用(外部资源链接、文档地址)
|
||||||
|
|
||||||
|
存储原则:
|
||||||
|
- 当用户透露个人偏好、角色、习惯时,存为 user 类型
|
||||||
|
- 当了解到项目进展、决策、约束时,存为 project 类型
|
||||||
|
- 当用户纠正或确认你的行为时,存为 feedback 类型
|
||||||
|
- 当提到外部资源链接时,存为 reference 类型
|
||||||
|
- 更新已有记忆而非创建重复条目
|
||||||
|
- 只存储对未来对话有价值的信息`;
|
||||||
|
```
|
||||||
|
|
||||||
|
Update the agentConfig to use `finalSystemPrompt`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const agentConfig: AgentConfig = {
|
||||||
|
name: agentName,
|
||||||
|
systemPrompt: finalSystemPrompt,
|
||||||
|
// ... rest unchanged
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Update the tools array to include memory tools:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const tools = [bashTool, readFileTool, writeFileTool, listDirTool, ...memoryTools];
|
||||||
|
```
|
||||||
|
|
||||||
|
Update the runAgent call to pass memoryContext:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const result = await runAgent({
|
||||||
|
agentConfig,
|
||||||
|
tools,
|
||||||
|
userMessage: body.message,
|
||||||
|
history: history.slice(0, -1),
|
||||||
|
memoryContext,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verify no TypeScript errors**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd packages/backend && npx tsc --noEmit
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/modules/netaclaw/controller/chat.ts
|
||||||
|
git commit -m "feat(memory): integrate memory prefetch + tools into chat controller"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 10: Final verification
|
||||||
|
|
||||||
|
- [ ] **Step 1: Verify full TypeScript compilation**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd packages/backend && npx tsc --noEmit
|
||||||
|
```
|
||||||
|
Expected: No errors.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verify all new files exist**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls -la src/modules/netaclaw/memory/
|
||||||
|
ls -la src/modules/netaclaw/tools/builtin/memory.ts
|
||||||
|
ls -la src/modules/netaclaw/entity/memory.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: provider.ts, factory.ts, mysql_provider.ts, sqlite_provider.ts, prefetch.ts, migration.sql in memory/; memory.ts in tools/builtin/; memory.ts in entity/.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit all remaining changes**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add -A && git status
|
||||||
|
git commit -m "feat(memory): complete long-term memory system implementation"
|
||||||
|
```
|
||||||
1056
docs/superpowers/plans/2026-04-12-model-channel-management.md
Normal file
1056
docs/superpowers/plans/2026-04-12-model-channel-management.md
Normal file
File diff suppressed because it is too large
Load Diff
1069
docs/superpowers/plans/2026-04-12-netaclaw-agent-skill-migration.md
Normal file
1069
docs/superpowers/plans/2026-04-12-netaclaw-agent-skill-migration.md
Normal file
File diff suppressed because it is too large
Load Diff
2638
docs/superpowers/plans/2026-04-12-project-management.md
Normal file
2638
docs/superpowers/plans/2026-04-12-project-management.md
Normal file
File diff suppressed because it is too large
Load Diff
1277
docs/superpowers/plans/2026-04-13-agent-chat-ux-overhaul.md
Normal file
1277
docs/superpowers/plans/2026-04-13-agent-chat-ux-overhaul.md
Normal file
File diff suppressed because it is too large
Load Diff
1210
docs/superpowers/plans/2026-04-13-skill-evolution.md
Normal file
1210
docs/superpowers/plans/2026-04-13-skill-evolution.md
Normal file
File diff suppressed because it is too large
Load Diff
1762
docs/superpowers/plans/2026-04-13-skill-system-migration.md
Normal file
1762
docs/superpowers/plans/2026-04-13-skill-system-migration.md
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,493 @@
|
|||||||
|
# 工具可见性分类机制 实施计划
|
||||||
|
|
||||||
|
> **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:** 在工具定义层引入 `visibility` 属性,从源头区分内部工具/普通工具/Skill,消除前端重复展示和 filter hack。
|
||||||
|
|
||||||
|
**Architecture:** 后端工具类型新增 `visibility` 字段,执行引擎根据该字段决定是否触发 UI 回调。前端移除 todo 过滤 hack,新建 `tool-card.vue` 组件区分 skill 和工具的展示。
|
||||||
|
|
||||||
|
**Tech Stack:** TypeScript, Midway.js, Vue 3, Element Plus, Socket.IO
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-04-14-tool-visibility-classification-design.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: 后端 — 工具类型定义新增 visibility 字段
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/backend/src/modules/netaclaw/tools/common.ts:5-19`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 在 common.ts 中新增 ToolVisibility 类型和 visibility 字段**
|
||||||
|
|
||||||
|
在 `AgentTool` 类型定义之前,新增类型:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export type ToolVisibility = 'internal' | 'tool' | 'skill';
|
||||||
|
```
|
||||||
|
|
||||||
|
在 `AgentToolWithMeta` 类型中新增 `visibility` 可选字段:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export type AgentToolWithMeta<TParams extends TSchema, TResult> =
|
||||||
|
AgentTool<TParams, TResult> & {
|
||||||
|
ownerOnly?: boolean;
|
||||||
|
displaySummary?: string;
|
||||||
|
visibility?: ToolVisibility;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 验证后端编译通过**
|
||||||
|
|
||||||
|
Run: `cd packages/backend && npx tsc --noEmit 2>&1 | head -20`
|
||||||
|
Expected: 无新增错误(已有错误可忽略)
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/tools/common.ts
|
||||||
|
git commit -m "feat(netaclaw): 工具类型新增 visibility 字段 (internal/tool/skill)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: 后端 — 执行引擎根据 visibility 过滤回调
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/backend/src/modules/netaclaw/runtime/attempt.ts:59-78`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 修改 attempt.ts 工具执行循环**
|
||||||
|
|
||||||
|
将当前的工具执行循环(第 59-78 行)修改为根据 `visibility` 决定是否触发回调:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
for (const tc of response.toolCalls) {
|
||||||
|
toolCallCount++;
|
||||||
|
const tool = tools.find(t => t.name === tc.name);
|
||||||
|
const visibility = tool?.visibility ?? 'tool';
|
||||||
|
|
||||||
|
// 仅非 internal 工具触发 UI 回调
|
||||||
|
if (visibility !== 'internal') {
|
||||||
|
onToolCall?.(tc.name, JSON.parse(tc.arguments));
|
||||||
|
}
|
||||||
|
|
||||||
|
let resultText: string;
|
||||||
|
|
||||||
|
if (!tool) {
|
||||||
|
resultText = `错误: 工具 "${tc.name}" 不存在`;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const result = await tool.execute(tc.id, JSON.parse(tc.arguments));
|
||||||
|
resultText = typeof result === 'string' ? result : JSON.stringify(result);
|
||||||
|
} catch (err: any) {
|
||||||
|
resultText = `工具执行错误: ${err.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 仅非 internal 工具触发 UI 回调
|
||||||
|
if (visibility !== 'internal') {
|
||||||
|
onToolResult?.(tc.name, resultText);
|
||||||
|
}
|
||||||
|
conversation.push({ role: 'tool', content: resultText, toolCallId: tc.id });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
关键点:
|
||||||
|
- `tool` 变量的查找提前到回调判断之前
|
||||||
|
- `visibility` 从 tool 对象读取,默认 `'tool'`
|
||||||
|
- `conversation.push` 不受影响(LLM 仍需看到工具返回值)
|
||||||
|
|
||||||
|
- [ ] **Step 2: 验证后端编译通过**
|
||||||
|
|
||||||
|
Run: `cd packages/backend && npx tsc --noEmit 2>&1 | head -20`
|
||||||
|
Expected: 无新增错误
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/runtime/attempt.ts
|
||||||
|
git commit -m "feat(netaclaw): 执行引擎根据 visibility 过滤 tool_call/tool_result 回调"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: 后端 — todo 工具标记为 internal
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/backend/src/modules/netaclaw/runtime/agent.ts:70-79`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 在 agent.ts 中为 todoTool 设置 visibility: 'internal'**
|
||||||
|
|
||||||
|
将第 70-79 行的 todoTool 定义修改为:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const todoTool: AnyAgentTool = {
|
||||||
|
...todoToolSchema,
|
||||||
|
label: '任务规划',
|
||||||
|
visibility: 'internal',
|
||||||
|
execute: async (_id: string, args: any) => {
|
||||||
|
const result = executeTodo(todoStore, args);
|
||||||
|
params.onTodoUpdate?.(result);
|
||||||
|
return JSON.stringify(result);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
唯一改动:新增 `visibility: 'internal'` 一行。
|
||||||
|
|
||||||
|
- [ ] **Step 2: 验证后端编译通过**
|
||||||
|
|
||||||
|
Run: `cd packages/backend && npx tsc --noEmit 2>&1 | head -20`
|
||||||
|
Expected: 无新增错误
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/runtime/agent.ts
|
||||||
|
git commit -m "feat(netaclaw): todo 工具标记为 internal,不再推送 tool_call/tool_result"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: 前端 — SkillProgress 类型新增 source 字段
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/frontend/src/modules/agent/types/index.d.ts:151-173`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 在 SkillProgress 接口中新增 source 字段**
|
||||||
|
|
||||||
|
在 `SkillProgress` 接口中新增 `source` 可选字段(第 152 行之后):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface SkillProgress {
|
||||||
|
name: string;
|
||||||
|
label?: string;
|
||||||
|
status: 'running' | 'done' | 'error';
|
||||||
|
/** 来源类型:skill=Skill技能, tool=普通工具调用 */
|
||||||
|
source?: 'skill' | 'tool';
|
||||||
|
input?: any;
|
||||||
|
result?: any;
|
||||||
|
step?: string;
|
||||||
|
percent?: number;
|
||||||
|
detail?: string;
|
||||||
|
current?: number;
|
||||||
|
total?: number;
|
||||||
|
taskId?: string;
|
||||||
|
images?: string[];
|
||||||
|
tokens?: { input: number; output: number; total: number; apiCalls?: number };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/frontend/src/modules/agent/types/index.d.ts
|
||||||
|
git commit -m "feat(agent): SkillProgress 类型新增 source 字段区分 skill/tool"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: 前端 — Store 层标记 source 字段
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/frontend/src/modules/agent/store/chat.ts:264-316`
|
||||||
|
|
||||||
|
- [ ] **Step 1: skill_start 事件处理中标记 source: 'skill'**
|
||||||
|
|
||||||
|
修改第 271-275 行,在 push 时添加 `source: 'skill'`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
case 'skill_start': {
|
||||||
|
const startName = event.name;
|
||||||
|
activeSkill.value = startName;
|
||||||
|
const existingDone = skillProgress.value.find(s => s.name === startName && s.status === 'done');
|
||||||
|
const existingRunning = skillProgress.value.find(s => s.name === startName && s.status === 'running');
|
||||||
|
if (!existingDone && !existingRunning) {
|
||||||
|
skillProgress.value.push({
|
||||||
|
name: startName,
|
||||||
|
label: event.label || startName,
|
||||||
|
status: 'running',
|
||||||
|
source: 'skill',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: tool_call 事件处理中标记 source: 'tool'**
|
||||||
|
|
||||||
|
修改第 308-314 行,在 push 时添加 `source: 'tool'`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
case 'tool_call': {
|
||||||
|
const toolName = event.toolName || event.name || 'unknown';
|
||||||
|
activeSkill.value = toolName;
|
||||||
|
const existingRunning = skillProgress.value.find(s => s.name === toolName && s.status === 'running');
|
||||||
|
if (!existingRunning) {
|
||||||
|
skillProgress.value.push({
|
||||||
|
name: toolName,
|
||||||
|
label: toolName,
|
||||||
|
status: 'running',
|
||||||
|
source: 'tool',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: done 事件序列化中加入 source 字段**
|
||||||
|
|
||||||
|
在 `handleWSEvent` 的 `done` 分支中(约第 383 行),修改 `skillExecutions` 序列化,加入 `source`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
lastAssistant.metadata.skillExecutions = skillProgress.value.map(sp => ({
|
||||||
|
name: sp.name, label: sp.label, status: sp.status, result: sp.result, tokens: sp.tokens, source: sp.source
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
唯一改动:在 map 对象中新增 `source: sp.source`。
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/frontend/src/modules/agent/store/chat.ts
|
||||||
|
git commit -m "feat(agent): Store 层 skill/tool 事件标记 source 字段,序列化保留 source"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: 前端 — 新建 tool-card.vue 组件
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `packages/frontend/src/modules/agent/components/tool-card.vue`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 创建 tool-card.vue**
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<div class="tool-card" :class="[`tool-card--${status}`]">
|
||||||
|
<div class="tool-card__header">
|
||||||
|
<el-icon :class="{ 'is-rotating': status === 'running' }">
|
||||||
|
<Loading v-if="status === 'running'" />
|
||||||
|
<CircleCheck v-else-if="status === 'done'" />
|
||||||
|
<CircleClose v-else />
|
||||||
|
</el-icon>
|
||||||
|
<span class="tool-card__name">{{ label || name }}</span>
|
||||||
|
<el-tag v-if="status === 'done'" size="small" type="info">完成</el-tag>
|
||||||
|
<el-tag v-else-if="status === 'error'" size="small" type="danger">失败</el-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<transition name="fade">
|
||||||
|
<div v-if="status === 'done' && result" class="tool-card__result">
|
||||||
|
<el-button text size="small" @click="showResult = !showResult">
|
||||||
|
{{ showResult ? '收起' : '查看结果' }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<el-collapse-transition>
|
||||||
|
<div v-if="showResult && result" class="tool-card__result-content">
|
||||||
|
<pre class="tool-card__result-pre">{{ typeof result === 'string' ? result : JSON.stringify(result, null, 2) }}</pre>
|
||||||
|
</div>
|
||||||
|
</el-collapse-transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { Loading, CircleCheck, CircleClose } from '@element-plus/icons-vue';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
name: string;
|
||||||
|
label?: string;
|
||||||
|
status: 'running' | 'done' | 'error';
|
||||||
|
result?: any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const showResult = ref(false);
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
样式部分(与 skill-card 类似但更简洁,无进度条区域):
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.tool-card {
|
||||||
|
width: min(100%, var(--chat-content-max-width, 760px));
|
||||||
|
margin: 6px 0 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--tool-border-color) 18%, transparent);
|
||||||
|
border-left-width: 3px;
|
||||||
|
background: var(--el-bg-color);
|
||||||
|
font-size: 13px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: all 0.24s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-card--running { --tool-border-color: var(--el-color-info); }
|
||||||
|
.tool-card--done { --tool-border-color: var(--el-color-info); }
|
||||||
|
.tool-card--error { --tool-border-color: var(--el-color-danger); }
|
||||||
|
|
||||||
|
.tool-card__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-card__name {
|
||||||
|
flex: 1;
|
||||||
|
font-weight: 500;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-rotating {
|
||||||
|
animation: rotating 1.5s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotating {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-card__result { margin-top: 6px; }
|
||||||
|
|
||||||
|
.tool-card__result-content { margin-top: 6px; }
|
||||||
|
|
||||||
|
.tool-card__result-pre {
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px 10px;
|
||||||
|
background: var(--el-fill-color-lighter);
|
||||||
|
border: 1px solid var(--el-border-color-extra-light);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
max-height: 160px;
|
||||||
|
overflow: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tag) {
|
||||||
|
height: 18px;
|
||||||
|
padding: 0 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-button.is-text) {
|
||||||
|
padding: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active { transition: opacity 0.3s ease; }
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to { opacity: 0; }
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/frontend/src/modules/agent/components/tool-card.vue
|
||||||
|
git commit -m "feat(agent): 新建 tool-card.vue 工具调用卡片组件"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: 前端 — chat.vue 移除 hack 并根据 source 选择组件
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/frontend/src/modules/agent/views/chat.vue:56-59` (模板)
|
||||||
|
- Modify: `packages/frontend/src/modules/agent/views/chat.vue:341-343` (计算属性)
|
||||||
|
|
||||||
|
- [ ] **Step 1: 在 chat.vue 的 script 中导入 tool-card 组件**
|
||||||
|
|
||||||
|
在已有的 `import` 区域(skill-card 导入附近)添加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import ToolCard from '../components/tool-card.vue';
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 移除 visibleSkillProgress 计算属性**
|
||||||
|
|
||||||
|
删除第 341-343 行:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 删除这段
|
||||||
|
const visibleSkillProgress = computed(() => {
|
||||||
|
return skillProgress.value.filter(sp => sp.name !== 'todo');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 修改模板中的 Skill 卡片渲染区域**
|
||||||
|
|
||||||
|
将第 56-59 行:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- Skill 执行卡片(替代 skill-indicator) -->
|
||||||
|
<div class="skill-card-row" v-for="sp in visibleSkillProgress" :key="sp.name">
|
||||||
|
<skill-card v-bind="sp" />
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
替换为:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- Skill / 工具执行卡片 -->
|
||||||
|
<div class="skill-card-row" v-for="sp in skillProgress" :key="sp.name">
|
||||||
|
<skill-card v-if="sp.source === 'skill'" v-bind="sp" />
|
||||||
|
<tool-card v-else v-bind="sp" />
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 验证前端编译通过**
|
||||||
|
|
||||||
|
Run: `cd packages/frontend && npx vue-tsc --noEmit 2>&1 | head -20`
|
||||||
|
Expected: 无新增错误
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/frontend/src/modules/agent/views/chat.vue
|
||||||
|
git commit -m "feat(agent): chat.vue 移除 todo filter hack,根据 source 区分 skill-card/tool-card"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 8: 端到端验证
|
||||||
|
|
||||||
|
**Files:** 无新增改动
|
||||||
|
|
||||||
|
- [ ] **Step 1: 启动后端验证编译**
|
||||||
|
|
||||||
|
Run: `cd packages/backend && npx tsc --noEmit 2>&1 | head -30`
|
||||||
|
Expected: 无新增错误
|
||||||
|
|
||||||
|
- [ ] **Step 2: 启动前端验证编译**
|
||||||
|
|
||||||
|
Run: `cd packages/frontend && npx vue-tsc --noEmit 2>&1 | head -30`
|
||||||
|
Expected: 无新增错误
|
||||||
|
|
||||||
|
- [ ] **Step 3: 功能验证清单**
|
||||||
|
|
||||||
|
手动测试(需启动 `pnpm dev`):
|
||||||
|
|
||||||
|
1. 发送一条需要任务规划的消息(如"帮我分析这个项目的架构,列出3个改进点")
|
||||||
|
2. 验证 todo-card 正常显示并实时更新(划掉已完成任务)
|
||||||
|
3. 验证对话流中不再出现 todo 的"工具完成"卡片
|
||||||
|
4. 如果 Agent 调用了其他工具(如搜索),验证 tool-card 正常显示
|
||||||
|
5. 如果 Agent 调用了 Skill,验证 skill-card 正常显示(带进度条)
|
||||||
|
6. 切换会话后,验证历史消息中的 skill/tool 卡片正确恢复
|
||||||
|
|
||||||
|
- [ ] **Step 4: 最终 Commit(如有修复)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add -A
|
||||||
|
git commit -m "fix(agent): 端到端验证修复"
|
||||||
|
```
|
||||||
1421
docs/superpowers/plans/2026-04-15-prompt-builder-impl-plan.md
Normal file
1421
docs/superpowers/plans/2026-04-15-prompt-builder-impl-plan.md
Normal file
File diff suppressed because it is too large
Load Diff
623
docs/superpowers/plans/2026-04-16-clarify-frontend.md
Normal file
623
docs/superpowers/plans/2026-04-16-clarify-frontend.md
Normal file
@ -0,0 +1,623 @@
|
|||||||
|
# Clarify 工具前端交互 + 微信降级 实现计划
|
||||||
|
|
||||||
|
> **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.
|
||||||
|
> **Post-task review:** After each task commit, run the `simplify` skill to review changed code for reuse, quality, and efficiency.
|
||||||
|
|
||||||
|
**Goal:** 在前端 Agent 对话页面实现 clarify 工具的交互 UI,并在后端为微信渠道实现纯文本降级方案。
|
||||||
|
|
||||||
|
**Architecture:** 收到 `clarify_request` WS 事件时,在聊天流中插入 `role='clarify'` 的特殊消息,由新建的 `clarify-card.vue` 组件渲染为交互卡片(选项按钮 + 自定义输入)。用户回答后通过 WS 发送 `clarify_response`,卡片变为只读。微信端通过 `agent_executor.ts` 透传 `onClarifyRequest` 回调,`agent_channel.ts` 用 pendingClarify Map 阻塞等待用户回复。
|
||||||
|
|
||||||
|
**Tech Stack:** Vue 3 + TypeScript + Element Plus + Pinia + Socket.IO (前端) / Midway.js + TypeScript (后端)
|
||||||
|
|
||||||
|
**Base path (前端):** `packages/frontend/src/modules/agent`
|
||||||
|
**Base path (后端):** `packages/backend/src/modules/netaclaw`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: 类型定义扩展
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/frontend/src/modules/agent/types/index.d.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 扩展 ChatMessage.role 联合类型**
|
||||||
|
|
||||||
|
在 `types/index.d.ts` 第 21 行,`ChatMessage.role` 联合追加 `'clarify'`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 旧:
|
||||||
|
role: 'user' | 'assistant' | 'tool' | 'system';
|
||||||
|
|
||||||
|
// 新:
|
||||||
|
role: 'user' | 'assistant' | 'tool' | 'system' | 'clarify';
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 扩展 WSClientMessage.type 联合类型并追加字段**
|
||||||
|
|
||||||
|
在 `types/index.d.ts` 第 131-136 行,修改 `WSClientMessage`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 旧:
|
||||||
|
export interface WSClientMessage {
|
||||||
|
type: 'chat' | 'ping';
|
||||||
|
sessionId?: string;
|
||||||
|
content?: string;
|
||||||
|
agentId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新:
|
||||||
|
export interface WSClientMessage {
|
||||||
|
type: 'chat' | 'ping' | 'clarify_response';
|
||||||
|
sessionId?: string;
|
||||||
|
content?: string;
|
||||||
|
agentId?: number;
|
||||||
|
requestId?: string;
|
||||||
|
answer?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 扩展 WSServerEvent.type 联合类型**
|
||||||
|
|
||||||
|
在 `types/index.d.ts` 第 142 行,`WSServerEvent.type` 联合追加 `'clarify_request'`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 旧:
|
||||||
|
type: 'token' | 'thinking' | 'tool_call' | 'tool_result' | 'skill_start' | 'skill_end' | 'progress' | 'token_update' | 'done' | 'error' | 'pong' | 'todo_update' | 'thinking_delta' | 'thinking_done';
|
||||||
|
|
||||||
|
// 新:
|
||||||
|
type: 'token' | 'thinking' | 'tool_call' | 'tool_result' | 'skill_start' | 'skill_end' | 'progress' | 'token_update' | 'done' | 'error' | 'pong' | 'todo_update' | 'thinking_delta' | 'thinking_done' | 'clarify_request';
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 新增 ClarifyRequestData 和 ClarifyMessageMeta 接口**
|
||||||
|
|
||||||
|
在 `types/index.d.ts` 文件末尾(`TokenUpdateEvent` 之后)追加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// === Clarify 工具类型 ===
|
||||||
|
export interface ClarifyRequestData {
|
||||||
|
requestId: string;
|
||||||
|
question: string;
|
||||||
|
choices?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClarifyMessageMeta {
|
||||||
|
requestId: string;
|
||||||
|
choices?: string[];
|
||||||
|
status: 'pending' | 'answered';
|
||||||
|
answer?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/frontend/src/modules/agent/types/index.d.ts
|
||||||
|
git commit -m "feat(agent): extend WS types for clarify tool interaction"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Store 变更 — 处理 clarify_request + 发送 clarify_response
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/frontend/src/modules/agent/store/chat.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 在 handleWSEvent 中新增 clarify_request 提前处理**
|
||||||
|
|
||||||
|
在 `store/chat.ts` 的 `handleWSEvent` 函数中,clarify_request 不依赖 assistantMsg,需要在现有的 `if (!loading.value ...)` 和 `const assistantMsg = ...` 守卫之前处理。在函数开头(第 246 行 `function handleWSEvent` 之后)插入:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function handleWSEvent(event: WSServerEvent) {
|
||||||
|
// clarify_request 不依赖 assistantMsg,提前处理
|
||||||
|
if (event.type === 'clarify_request') {
|
||||||
|
const data = event.data;
|
||||||
|
if (data?.requestId && data?.question) {
|
||||||
|
messages.value.push({
|
||||||
|
role: 'clarify',
|
||||||
|
content: data.question,
|
||||||
|
metadata: {
|
||||||
|
requestId: data.requestId,
|
||||||
|
choices: data.choices || [],
|
||||||
|
status: 'pending',
|
||||||
|
answer: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
_onTokenCbs.forEach(cb => cb());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 以下是原有逻辑,不变
|
||||||
|
if (!loading.value && event.type !== 'done') return;
|
||||||
|
const assistantMsg = messages.value[messages.value.length - 1];
|
||||||
|
if (!assistantMsg || assistantMsg.role !== 'assistant') return;
|
||||||
|
|
||||||
|
switch (event.type) {
|
||||||
|
// ... 现有 cases 不变 ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 新增 sendClarifyResponse 函数**
|
||||||
|
|
||||||
|
在 `store/chat.ts` 的 `setThinkLevel` 函数之后(第 562 行后)追加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* 发送 clarify 回答
|
||||||
|
*/
|
||||||
|
function sendClarifyResponse(requestId: string, answer: string) {
|
||||||
|
const msg = messages.value.find(
|
||||||
|
m => m.role === 'clarify' && m.metadata?.requestId === requestId
|
||||||
|
);
|
||||||
|
if (msg && msg.metadata) {
|
||||||
|
msg.metadata.status = 'answered';
|
||||||
|
msg.metadata.answer = answer;
|
||||||
|
}
|
||||||
|
if (wsInstance) {
|
||||||
|
wsInstance.send({
|
||||||
|
type: 'clarify_response',
|
||||||
|
sessionId: sessionId.value,
|
||||||
|
requestId,
|
||||||
|
answer,
|
||||||
|
} as any);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 在 return 对象中导出 sendClarifyResponse**
|
||||||
|
|
||||||
|
在 `store/chat.ts` 第 738 行 `init` 之后追加 `sendClarifyResponse`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
return {
|
||||||
|
// ... 现有导出 ...
|
||||||
|
init,
|
||||||
|
sendClarifyResponse,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/frontend/src/modules/agent/store/chat.ts
|
||||||
|
git commit -m "feat(agent): handle clarify_request WS event and send clarify_response"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: clarify-card.vue 交互卡片组件
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `packages/frontend/src/modules/agent/components/clarify-card.vue`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 创建 clarify-card.vue 模板和脚本**
|
||||||
|
|
||||||
|
创建 `packages/frontend/src/modules/agent/components/clarify-card.vue`:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<div class="clarify-card" :class="{ 'is-answered': isAnswered }">
|
||||||
|
<div class="clarify-card__header">
|
||||||
|
<span class="clarify-card__icon">{{ isAnswered ? '✅' : '🤔' }}</span>
|
||||||
|
<span class="clarify-card__title">{{ isAnswered ? '已回答' : 'Agent 需要你的确认' }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="clarify-card__question">{{ message.content }}</div>
|
||||||
|
|
||||||
|
<!-- pending 状态:显示选项和输入 -->
|
||||||
|
<template v-if="!isAnswered">
|
||||||
|
<div class="clarify-card__choices" v-if="choices.length">
|
||||||
|
<el-button
|
||||||
|
v-for="(choice, idx) in choices"
|
||||||
|
:key="idx"
|
||||||
|
class="clarify-card__choice-btn"
|
||||||
|
@click="selectChoice(choice)"
|
||||||
|
>
|
||||||
|
{{ choice }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="clarify-card__custom">
|
||||||
|
<el-input
|
||||||
|
v-model="customAnswer"
|
||||||
|
:placeholder="choices.length ? '或输入自定义回答...' : '输入你的回答...'"
|
||||||
|
size="default"
|
||||||
|
@keydown.enter.prevent="submitCustom"
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:disabled="!customAnswer.trim()"
|
||||||
|
@click="submitCustom"
|
||||||
|
>
|
||||||
|
发送
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- answered 状态 -->
|
||||||
|
<div v-else class="clarify-card__answer">
|
||||||
|
{{ message.metadata?.answer }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import type { ChatMessage } from '../types/index.d';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
message: ChatMessage;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
answer: [requestId: string, answer: string];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const customAnswer = ref('');
|
||||||
|
|
||||||
|
const choices = computed(() => props.message.metadata?.choices || []);
|
||||||
|
const isAnswered = computed(() => props.message.metadata?.status === 'answered');
|
||||||
|
|
||||||
|
function selectChoice(choice: string) {
|
||||||
|
const requestId = props.message.metadata?.requestId;
|
||||||
|
if (requestId) emit('answer', requestId, choice);
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitCustom() {
|
||||||
|
const text = customAnswer.value.trim();
|
||||||
|
if (!text) return;
|
||||||
|
const requestId = props.message.metadata?.requestId;
|
||||||
|
if (requestId) emit('answer', requestId, text);
|
||||||
|
customAnswer.value = '';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 添加样式**
|
||||||
|
|
||||||
|
在同一文件 `</script>` 之后追加样式(遵循 todo-card 的卡片风格):
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.clarify-card {
|
||||||
|
width: min(100%, var(--chat-content-max-width, 760px));
|
||||||
|
margin: 8px 0 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--el-color-warning-light-5);
|
||||||
|
background: linear-gradient(180deg, var(--el-bg-color) 0%, color-mix(in srgb, var(--el-color-warning) 4%, var(--el-bg-color)) 100%);
|
||||||
|
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.04);
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
animation: clarify-enter 0.4s ease-out;
|
||||||
|
|
||||||
|
&.is-answered {
|
||||||
|
border-color: var(--el-border-color-lighter);
|
||||||
|
background: var(--el-fill-color-lighter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.clarify-card__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clarify-card__icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clarify-card__question {
|
||||||
|
padding: 0 14px 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clarify-card__choices {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 14px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clarify-card__choice-btn {
|
||||||
|
border-radius: 999px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clarify-card__custom {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 14px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clarify-card__answer {
|
||||||
|
padding: 0 14px 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes clarify-enter {
|
||||||
|
from { opacity: 0; transform: translateY(8px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/frontend/src/modules/agent/components/clarify-card.vue
|
||||||
|
git commit -m "feat(agent): add clarify-card interactive component"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: 集成到聊天视图
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/frontend/src/modules/agent/views/chat.vue`
|
||||||
|
- Modify: `packages/frontend/src/modules/agent/components/message-item.vue`
|
||||||
|
|
||||||
|
- [ ] **Step 1: chat.vue — 导入 ClarifyCard 组件**
|
||||||
|
|
||||||
|
在 `views/chat.vue` 的 `<script>` 部分 import 区域(第 284 行 `import MessageItem` 附近)追加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import ClarifyCard from '../components/clarify-card.vue';
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: chat.vue — 在消息列表中渲染 clarify 卡片**
|
||||||
|
|
||||||
|
在 `views/chat.vue` 模板的消息列表中,`message-item` 循环(第 40-44 行)需要区分 clarify 消息。将:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<message-item
|
||||||
|
v-for="(msg, index) in precedingMessages"
|
||||||
|
:key="index"
|
||||||
|
:message="msg"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
改为:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template v-for="(msg, index) in precedingMessages" :key="index">
|
||||||
|
<clarify-card
|
||||||
|
v-if="msg.role === 'clarify'"
|
||||||
|
:message="msg"
|
||||||
|
@answer="handleClarifyAnswer"
|
||||||
|
/>
|
||||||
|
<message-item v-else :message="msg" />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: chat.vue — 添加 handleClarifyAnswer 方法和 store 导出**
|
||||||
|
|
||||||
|
在 `views/chat.vue` 的 `<script setup>` 中,从 chatStore 解构中追加 `sendClarifyResponse`(第 335 行附近):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const {
|
||||||
|
sendMessage,
|
||||||
|
stopGeneration,
|
||||||
|
loadSessions,
|
||||||
|
loadAgents,
|
||||||
|
selectAgent,
|
||||||
|
newSession,
|
||||||
|
switchSession,
|
||||||
|
deleteSession,
|
||||||
|
deleteAllSessions,
|
||||||
|
onToken,
|
||||||
|
init: initChat,
|
||||||
|
setThinkLevel,
|
||||||
|
sendClarifyResponse,
|
||||||
|
} = chatStore;
|
||||||
|
```
|
||||||
|
|
||||||
|
然后在 `handleSend` 函数之前添加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function handleClarifyAnswer(requestId: string, answer: string) {
|
||||||
|
sendClarifyResponse(requestId, answer);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: message-item.vue — hasVisibleBody 兼容 clarify**
|
||||||
|
|
||||||
|
在 `components/message-item.vue` 第 204 行的 `hasVisibleBody` computed 中,clarify 消息不应由 message-item 渲染(已在 chat.vue 中用 `v-if` 分流),但为安全起见,确保 `hasVisibleBody` 对 `role='clarify'` 返回 false:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const hasVisibleBody = computed(() => {
|
||||||
|
if (props.message.role === 'clarify') return false;
|
||||||
|
return Boolean(
|
||||||
|
props.message.skillName ||
|
||||||
|
(props.message.role === 'assistant' && props.message.thinking) ||
|
||||||
|
props.message.content ||
|
||||||
|
messageImageUrl.value ||
|
||||||
|
(tokenUsage.value && props.message.role === 'assistant')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/frontend/src/modules/agent/views/chat.vue \
|
||||||
|
packages/frontend/src/modules/agent/components/message-item.vue
|
||||||
|
git commit -m "feat(agent): integrate clarify-card into chat view"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: 后端 — agent_executor.ts 透传 onClarifyRequest
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/backend/src/modules/netaclaw/service/agent_executor.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 扩展 execute() 参数类型**
|
||||||
|
|
||||||
|
在 `agent_executor.ts` 第 43 行的 `execute` 方法参数中追加 `onClarifyRequest`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async execute(params: {
|
||||||
|
sessionId?: string;
|
||||||
|
message: string;
|
||||||
|
agentId?: number;
|
||||||
|
agentName?: string;
|
||||||
|
userId?: string;
|
||||||
|
onClarifyRequest?: (question: string, choices?: string[]) => Promise<string>;
|
||||||
|
}) {
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 透传 onClarifyRequest 给 runAgent**
|
||||||
|
|
||||||
|
在 `agent_executor.ts` 第 142 行的 `runAgent` 调用中追加 `onClarifyRequest`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const result = await runAgent({
|
||||||
|
agentConfig,
|
||||||
|
tools: [...this.defaultTools, ...memoryTools, ...skillTools],
|
||||||
|
userMessage: params.message,
|
||||||
|
history: history.slice(0, -1),
|
||||||
|
onClarifyRequest: params.onClarifyRequest,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 添加 clarifyTool 到 defaultTools**
|
||||||
|
|
||||||
|
在 `agent_executor.ts` 顶部 import 区域追加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { clarifyTool } from '../tools/builtin/clarify.js';
|
||||||
|
```
|
||||||
|
|
||||||
|
在第 41 行 `defaultTools` 数组中追加 `clarifyTool`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private readonly defaultTools: AnyAgentTool[] = [bashTool, readFileTool, writeFileTool, listDirTool, patchTool, clarifyTool];
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 验证构建**
|
||||||
|
|
||||||
|
Run: `cd packages/backend && npm run build`
|
||||||
|
Expected: 构建成功
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/service/agent_executor.ts
|
||||||
|
git commit -m "feat(netaclaw): agent_executor 透传 onClarifyRequest + 添加 clarifyTool"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: 后端 — agent_channel.ts 微信 clarify 降级
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/backend/src/modules/netaclaw/service/agent_channel.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 添加 pendingClarify Map 类型和实例**
|
||||||
|
|
||||||
|
在 `agent_channel.ts` 第 11-15 行 `RunnerState` 类型定义之后,添加 pendingClarify Map 属性到类中。在第 39 行 `private readonly runners` 之后追加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/** 微信 clarify 阻塞 Map — key: `${channelId}:${senderId}` */
|
||||||
|
private readonly pendingClarify = new Map<string, {
|
||||||
|
resolve: (answer: string) => void;
|
||||||
|
choices?: string[];
|
||||||
|
}>();
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: handleInboundMessage — 检查 pending clarify**
|
||||||
|
|
||||||
|
在 `agent_channel.ts` 的 `handleInboundMessage` 方法中,在 `const text = this.weixinService.extractText(...)` 之后、`await this.persistRuntime(...)` 之前(第 339 行后),插入 pending clarify 检查:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const text = this.weixinService.extractText(message.item_list || []);
|
||||||
|
if (!text) return;
|
||||||
|
|
||||||
|
// 检查是否有 pending clarify
|
||||||
|
const clarifyKey = `${channel.id}:${senderId}`;
|
||||||
|
const pending = this.pendingClarify.get(clarifyKey);
|
||||||
|
if (pending) {
|
||||||
|
this.pendingClarify.delete(clarifyKey);
|
||||||
|
// 数字映射到选项
|
||||||
|
let answer = text;
|
||||||
|
if (pending.choices?.length) {
|
||||||
|
const num = parseInt(text, 10);
|
||||||
|
if (num >= 1 && num <= pending.choices.length) {
|
||||||
|
answer = pending.choices[num - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pending.resolve(answer);
|
||||||
|
return; // 不启动新对话
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.persistRuntime(channel.id, state);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: handleInboundMessage — 传递 onClarifyRequest 回调**
|
||||||
|
|
||||||
|
将 `handleInboundMessage` 中现有的 `agentExecutor.execute()` 调用(第 344-350 行)改为传递 `onClarifyRequest`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const clarifyKey = `${channel.id}:${senderId}`;
|
||||||
|
const result = await this.agentExecutor.execute({
|
||||||
|
sessionId,
|
||||||
|
message: text,
|
||||||
|
agentId: channel.agentId,
|
||||||
|
agentName: channel.agentName || undefined,
|
||||||
|
userId: senderId,
|
||||||
|
onClarifyRequest: async (question, choices) => {
|
||||||
|
let msg = `❓ ${question}`;
|
||||||
|
if (choices?.length) {
|
||||||
|
msg += '\n' + choices.map((c, i) => `${i + 1}. ${c}`).join('\n');
|
||||||
|
msg += '\n\n请回复数字或直接输入你的回答';
|
||||||
|
}
|
||||||
|
await this.weixinService.sendText(credential, senderId, msg, state.contextTokens[senderId]);
|
||||||
|
return new Promise<string>((resolve) => {
|
||||||
|
this.pendingClarify.set(clarifyKey, { resolve, choices });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
注意:`clarifyKey` 变量在 Step 2 中已定义,这里需要确保 `clarifyKey` 的声明位置在两处都可访问。将 `const clarifyKey` 提到 `const text = ...` 之后即可。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 验证构建**
|
||||||
|
|
||||||
|
Run: `cd packages/backend && npm run build`
|
||||||
|
Expected: 构建成功
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/service/agent_channel.ts
|
||||||
|
git commit -m "feat(netaclaw): 微信渠道 clarify 纯文本降级 — pendingClarify Map + 编号映射"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: 端到端验证
|
||||||
|
|
||||||
|
**Files:** 无新文件
|
||||||
|
|
||||||
|
- [ ] **Step 1: 前端构建验证**
|
||||||
|
|
||||||
|
Run: `cd packages/frontend && pnpm build`
|
||||||
|
Expected: 构建成功
|
||||||
|
|
||||||
|
- [ ] **Step 2: 后端构建验证**
|
||||||
|
|
||||||
|
Run: `cd packages/backend && npm run build`
|
||||||
|
Expected: 构建成功
|
||||||
|
|
||||||
|
- [ ] **Step 3: 最终 Commit(如有修复)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add -A
|
||||||
|
git commit -m "fix(agent): clarify frontend + WeChat degradation integration fixes"
|
||||||
|
```
|
||||||
987
docs/superpowers/plans/2026-04-16-tool-registry-patch-clarify.md
Normal file
987
docs/superpowers/plans/2026-04-16-tool-registry-patch-clarify.md
Normal file
@ -0,0 +1,987 @@
|
|||||||
|
# Patch/Clarify 工具 + 工具目录 + 提示词优化 实现计划
|
||||||
|
|
||||||
|
> **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.
|
||||||
|
> **Post-task review:** After each task commit, run the `simplify` skill to review changed code for reuse, quality, and efficiency.
|
||||||
|
|
||||||
|
**Goal:** 为 Neta Agent 引擎新增 patch(模糊补丁)和 clarify(阻塞式提问)工具,引入轻量工具目录,重写提示词提升工具触发率。
|
||||||
|
|
||||||
|
**Architecture:** 新建 `tools/catalog.ts` 管理工具 schema 元数据(不管理实例)。patch 工具依赖 `tools/fuzzy_match.ts` 九级匹配引擎。clarify 工具通过 `attempt.ts` 的 `onClarifyRequest` 回调实现阻塞,gateway 层用 Promise+Map 协调 WebSocket 消息,REST 入口降级为文本输出。
|
||||||
|
|
||||||
|
**Tech Stack:** TypeScript, @sinclair/typebox, Midway.js Socket.IO, Node.js fs/promises
|
||||||
|
|
||||||
|
**Base path:** `packages/backend/src/modules/netaclaw`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: 工具目录 catalog.ts
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `tools/catalog.ts`
|
||||||
|
- Modify: `runtime/prompt_builder.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 创建 tools/catalog.ts**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// tools/catalog.ts
|
||||||
|
/**
|
||||||
|
* 轻量工具目录 — 只注册 schema 元数据,不管理工具实例。
|
||||||
|
* 用于 prompt_builder 查询可用工具名列表,替代硬编码 BASE_TOOLS。
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ToolSchema {
|
||||||
|
name: string;
|
||||||
|
toolset: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const catalog = new Map<string, ToolSchema>();
|
||||||
|
|
||||||
|
export function registerSchema(schema: ToolSchema): void {
|
||||||
|
catalog.set(schema.name, schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getToolNamesByToolsets(toolsets: string[]): string[] {
|
||||||
|
const names: string[] = [];
|
||||||
|
for (const entry of catalog.values()) {
|
||||||
|
if (toolsets.includes(entry.toolset)) names.push(entry.name);
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 默认启用的工具集(所有 Agent 都有) */
|
||||||
|
export const TOOLSET_DEFAULTS = ['base', 'planning', 'interaction'] as const;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 修改 prompt_builder.ts — 从 catalog 查询工具名**
|
||||||
|
|
||||||
|
将 `runtime/prompt_builder.ts` 中硬编码的 `BASE_TOOLS` 和 `collectAvailableToolNames` 改为从 catalog 查询:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// runtime/prompt_builder.ts — 替换原有的 BASE_TOOLS 和 collectAvailableToolNames
|
||||||
|
|
||||||
|
import { getToolNamesByToolsets, TOOLSET_DEFAULTS } from '../tools/catalog.js';
|
||||||
|
|
||||||
|
// 删除: const BASE_TOOLS = ['bash', 'read_file', 'write_file', 'list_dir', 'todo'];
|
||||||
|
|
||||||
|
export function collectAvailableToolNames(opts: CollectToolNamesOpts): string[] {
|
||||||
|
const toolsets = [...TOOLSET_DEFAULTS];
|
||||||
|
if (opts.memoryEnabled) toolsets.push('memory');
|
||||||
|
if (opts.hasSkills) toolsets.push('skill');
|
||||||
|
if (opts.crewRole === 'master') toolsets.push('crew');
|
||||||
|
return getToolNamesByToolsets(toolsets);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 现有工具注册到 catalog — bash.ts**
|
||||||
|
|
||||||
|
在 `tools/builtin/bash.ts` 文件末尾追加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { registerSchema } from '../catalog.js';
|
||||||
|
registerSchema({ name: 'bash', toolset: 'base', description: bashTool.description });
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 现有工具注册到 catalog — file.ts**
|
||||||
|
|
||||||
|
在 `tools/builtin/file.ts` 文件末尾追加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { registerSchema } from '../catalog.js';
|
||||||
|
registerSchema({ name: 'read_file', toolset: 'base', description: readFileTool.description });
|
||||||
|
registerSchema({ name: 'write_file', toolset: 'base', description: writeFileTool.description });
|
||||||
|
registerSchema({ name: 'list_dir', toolset: 'base', description: listDirTool.description });
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: 现有工具注册到 catalog — memory.ts**
|
||||||
|
|
||||||
|
在 `tools/builtin/memory.ts` 文件末尾追加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { registerSchema } from '../catalog.js';
|
||||||
|
registerSchema({ name: 'memory_save', toolset: 'memory', description: '存储、更新或删除长期记忆。' });
|
||||||
|
registerSchema({ name: 'memory_recall', toolset: 'memory', description: '搜索长期记忆中的相关信息。' });
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: 现有工具注册到 catalog — todo_tool.ts**
|
||||||
|
|
||||||
|
在 `tools/todo_tool.ts` 文件末尾追加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { registerSchema } from './catalog.js';
|
||||||
|
registerSchema({ name: 'todo', toolset: 'planning', description: todoToolSchema.description });
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 7: 工厂工具注册到 catalog — skill 工具**
|
||||||
|
|
||||||
|
在以下文件末尾各追加 `registerSchema` 调用(schema 是静态的,只有 execute 需要运行时依赖):
|
||||||
|
|
||||||
|
`tools/builtin/read_skill.ts` 末尾:
|
||||||
|
```typescript
|
||||||
|
import { registerSchema } from '../catalog.js';
|
||||||
|
registerSchema({ name: 'read_skill', toolset: 'skill', description: '读取指定技能的 SKILL.md 内容' });
|
||||||
|
```
|
||||||
|
|
||||||
|
`tools/builtin/read_skill_file.ts` 末尾:
|
||||||
|
```typescript
|
||||||
|
import { registerSchema } from '../catalog.js';
|
||||||
|
registerSchema({ name: 'read_skill_file', toolset: 'skill', description: '读取技能的附属文件内容' });
|
||||||
|
```
|
||||||
|
|
||||||
|
`tools/builtin/skill_manage.ts` 末尾:
|
||||||
|
```typescript
|
||||||
|
import { registerSchema } from '../catalog.js';
|
||||||
|
registerSchema({ name: 'skill_manage', toolset: 'skill', description: '创建、更新或删除技能' });
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 8: 工厂工具注册到 catalog — crew 工具**
|
||||||
|
|
||||||
|
`tools/builtin/delegate_task.ts` 末尾:
|
||||||
|
```typescript
|
||||||
|
import { registerSchema } from '../catalog.js';
|
||||||
|
registerSchema({ name: 'delegate_task', toolset: 'crew', description: '委派任务给指定成员 Agent' });
|
||||||
|
```
|
||||||
|
|
||||||
|
`tools/builtin/delegate_parallel.ts` 末尾:
|
||||||
|
```typescript
|
||||||
|
import { registerSchema } from '../catalog.js';
|
||||||
|
registerSchema({ name: 'delegate_parallel', toolset: 'crew', description: '并行委派多个任务给不同成员' });
|
||||||
|
```
|
||||||
|
|
||||||
|
`tools/builtin/escalate.ts` 末尾:
|
||||||
|
```typescript
|
||||||
|
import { registerSchema } from '../catalog.js';
|
||||||
|
registerSchema({ name: 'escalate', toolset: 'crew', description: '将问题升级给用户或上级 Agent' });
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 9: catalog.ts 自注册所有工具文件**
|
||||||
|
|
||||||
|
> 消除 side-effect import 维护负担:catalog.ts 自身 import 所有工具文件触发注册。
|
||||||
|
|
||||||
|
在 `tools/catalog.ts` 底部追加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// --- 集中注册入口:import 触发各工具文件的 registerSchema ---
|
||||||
|
import './builtin/bash.js';
|
||||||
|
import './builtin/file.js';
|
||||||
|
import './builtin/patch.js';
|
||||||
|
import './builtin/clarify.js';
|
||||||
|
import './builtin/memory.js';
|
||||||
|
import './builtin/read_skill.js';
|
||||||
|
import './builtin/read_skill_file.js';
|
||||||
|
import './builtin/skill_manage.js';
|
||||||
|
import './builtin/delegate_task.js';
|
||||||
|
import './builtin/delegate_parallel.js';
|
||||||
|
import './builtin/escalate.js';
|
||||||
|
import './todo_tool.js';
|
||||||
|
```
|
||||||
|
|
||||||
|
> 注意:这些 import 放在 catalog.ts 底部(registerSchema 函数定义之后),确保函数已可用。消费方只需 `import { getToolNamesByToolsets } from '../tools/catalog.js'`。
|
||||||
|
|
||||||
|
- [ ] **Step 10: 验证构建**
|
||||||
|
|
||||||
|
Run: `cd packages/backend && npm run build`
|
||||||
|
Expected: 编译成功,无类型错误
|
||||||
|
|
||||||
|
- [ ] **Step 11: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/tools/catalog.ts \
|
||||||
|
packages/backend/src/modules/netaclaw/tools/builtin/bash.ts \
|
||||||
|
packages/backend/src/modules/netaclaw/tools/builtin/file.ts \
|
||||||
|
packages/backend/src/modules/netaclaw/tools/builtin/memory.ts \
|
||||||
|
packages/backend/src/modules/netaclaw/tools/builtin/read_skill.ts \
|
||||||
|
packages/backend/src/modules/netaclaw/tools/builtin/read_skill_file.ts \
|
||||||
|
packages/backend/src/modules/netaclaw/tools/builtin/skill_manage.ts \
|
||||||
|
packages/backend/src/modules/netaclaw/tools/builtin/delegate_task.ts \
|
||||||
|
packages/backend/src/modules/netaclaw/tools/builtin/delegate_parallel.ts \
|
||||||
|
packages/backend/src/modules/netaclaw/tools/builtin/escalate.ts \
|
||||||
|
packages/backend/src/modules/netaclaw/tools/todo_tool.ts \
|
||||||
|
packages/backend/src/modules/netaclaw/runtime/prompt_builder.ts \
|
||||||
|
packages/backend/src/modules/netaclaw/gateway/server.ts \
|
||||||
|
packages/backend/src/modules/netaclaw/service/agent_executor.ts
|
||||||
|
git commit -m "feat(netaclaw): add tool catalog for schema-based tool name resolution"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 12: 运行 simplify skill 审查本次变更**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: 九级模糊匹配引擎 fuzzy_match.ts
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `tools/fuzzy_match.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 创建 tools/fuzzy_match.ts — 类型定义和策略框架**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// tools/fuzzy_match.ts
|
||||||
|
/**
|
||||||
|
* 九级模糊匹配引擎
|
||||||
|
* 按优先级依次尝试 9 种策略,首个成功即返回。
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface FuzzyMatchResult {
|
||||||
|
strategy: string;
|
||||||
|
startIndex: number;
|
||||||
|
endIndex: number;
|
||||||
|
matchedText: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Strategy = {
|
||||||
|
name: string;
|
||||||
|
find: (content: string, search: string) => FuzzyMatchResult[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 在 content 中查找所有匹配 search 的位置,按策略优先级 */
|
||||||
|
export function fuzzyFindAll(content: string, search: string): FuzzyMatchResult[] {
|
||||||
|
for (const strategy of strategies) {
|
||||||
|
const results = strategy.find(content, search);
|
||||||
|
if (results.length > 0) return results;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 辅助函数 ---
|
||||||
|
|
||||||
|
/** Levenshtein 距离 */
|
||||||
|
function levenshtein(a: string, b: string): number {
|
||||||
|
const m = a.length, n = b.length;
|
||||||
|
const dp: number[][] = Array.from({ length: m + 1 }, (_, i) =>
|
||||||
|
Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0))
|
||||||
|
);
|
||||||
|
for (let i = 1; i <= m; i++) {
|
||||||
|
for (let j = 1; j <= n; j++) {
|
||||||
|
dp[i][j] = a[i - 1] === b[j - 1]
|
||||||
|
? dp[i - 1][j - 1]
|
||||||
|
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dp[m][n];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 两字符串相似度 0-1 */
|
||||||
|
function similarity(a: string, b: string): number {
|
||||||
|
if (a === b) return 1;
|
||||||
|
const maxLen = Math.max(a.length, b.length);
|
||||||
|
if (maxLen === 0) return 1;
|
||||||
|
return 1 - levenshtein(a, b) / maxLen;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将 content 按 normalizer 转换后查找 search,映射回原始索引 */
|
||||||
|
function findByNormalization(
|
||||||
|
content: string,
|
||||||
|
search: string,
|
||||||
|
normalizer: (s: string) => string,
|
||||||
|
strategyName: string,
|
||||||
|
): FuzzyMatchResult[] {
|
||||||
|
const normContent = normalizer(content);
|
||||||
|
const normSearch = normalizer(search);
|
||||||
|
if (normSearch.length === 0) return [];
|
||||||
|
|
||||||
|
// 构建字符映射:normIndex → origIndex
|
||||||
|
// 逐字符对比原始和标准化后的内容,建立位置映射
|
||||||
|
const charMap = buildCharMap(content, normContent);
|
||||||
|
|
||||||
|
const results: FuzzyMatchResult[] = [];
|
||||||
|
let pos = 0;
|
||||||
|
while (true) {
|
||||||
|
const idx = normContent.indexOf(normSearch, pos);
|
||||||
|
if (idx === -1) break;
|
||||||
|
const endIdx = idx + normSearch.length;
|
||||||
|
const origStart = charMap[idx] ?? 0;
|
||||||
|
const origEnd = (endIdx < charMap.length ? charMap[endIdx] : content.length);
|
||||||
|
results.push({
|
||||||
|
strategy: strategyName,
|
||||||
|
startIndex: origStart,
|
||||||
|
endIndex: origEnd,
|
||||||
|
matchedText: content.slice(origStart, origEnd),
|
||||||
|
});
|
||||||
|
pos = idx + 1;
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建标准化索引 → 原始索引的映射表。
|
||||||
|
* 对于行级 normalizer(不改变行数),按行对齐映射。
|
||||||
|
* 对于可能改变行数的 normalizer,逐字符扫描。
|
||||||
|
*/
|
||||||
|
function buildCharMap(original: string, normalized: string): number[] {
|
||||||
|
// 简单实现:如果行数相同,按行内偏移映射
|
||||||
|
const origLines = original.split('\n');
|
||||||
|
const normLines = normalized.split('\n');
|
||||||
|
|
||||||
|
if (origLines.length === normLines.length) {
|
||||||
|
// 行级映射:每行内按比例映射
|
||||||
|
const map: number[] = [];
|
||||||
|
let origOffset = 0;
|
||||||
|
let normOffset = 0;
|
||||||
|
for (let i = 0; i < normLines.length; i++) {
|
||||||
|
const origLen = origLines[i].length;
|
||||||
|
const normLen = normLines[i].length;
|
||||||
|
for (let j = 0; j < normLen; j++) {
|
||||||
|
map[normOffset + j] = origOffset + Math.round((j / Math.max(normLen, 1)) * origLen);
|
||||||
|
}
|
||||||
|
// 换行符映射
|
||||||
|
if (i < normLines.length - 1) {
|
||||||
|
map[normOffset + normLen] = origOffset + origLen;
|
||||||
|
}
|
||||||
|
origOffset += origLen + 1;
|
||||||
|
normOffset += normLen + 1;
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 行数不同(如 whitespace_normalized):按字符比例映射
|
||||||
|
const map: number[] = [];
|
||||||
|
const ratio = original.length / Math.max(normalized.length, 1);
|
||||||
|
for (let i = 0; i < normalized.length; i++) {
|
||||||
|
map[i] = Math.round(i * ratio);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 实现 9 个策略**
|
||||||
|
|
||||||
|
在 `tools/fuzzy_match.ts` 底部追加策略数组:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// --- 9 级策略 ---
|
||||||
|
|
||||||
|
const strategies: Strategy[] = [
|
||||||
|
// 1. exact
|
||||||
|
{
|
||||||
|
name: 'exact',
|
||||||
|
find(content, search) {
|
||||||
|
const results: FuzzyMatchResult[] = [];
|
||||||
|
let pos = 0;
|
||||||
|
while (true) {
|
||||||
|
const idx = content.indexOf(search, pos);
|
||||||
|
if (idx === -1) break;
|
||||||
|
results.push({ strategy: 'exact', startIndex: idx, endIndex: idx + search.length, matchedText: search });
|
||||||
|
pos = idx + 1;
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 2. line_trimmed — 每行 trim 后匹配
|
||||||
|
{
|
||||||
|
name: 'line_trimmed',
|
||||||
|
find(content, search) {
|
||||||
|
return findByNormalization(content, search, s => s.split('\n').map(l => l.trim()).join('\n'), 'line_trimmed');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 3. whitespace_normalized — 连续空白合并为单空格
|
||||||
|
{
|
||||||
|
name: 'whitespace_normalized',
|
||||||
|
find(content, search) {
|
||||||
|
return findByNormalization(content, search, s => s.replace(/\s+/g, ' '), 'whitespace_normalized');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 4. indent_flexible — 去除行首所有空白
|
||||||
|
{
|
||||||
|
name: 'indent_flexible',
|
||||||
|
find(content, search) {
|
||||||
|
return findByNormalization(content, search, s => s.split('\n').map(l => l.trimStart()).join('\n'), 'indent_flexible');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 5. escape_normalized — \\n → \n, \\t → \t
|
||||||
|
{
|
||||||
|
name: 'escape_normalized',
|
||||||
|
find(content, search) {
|
||||||
|
const norm = (s: string) => s.replace(/\\n/g, '\n').replace(/\\t/g, '\t');
|
||||||
|
return findByNormalization(content, search, norm, 'escape_normalized');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 6. trimmed_boundary — 仅首尾行 trim
|
||||||
|
{
|
||||||
|
name: 'trimmed_boundary',
|
||||||
|
find(content, search) {
|
||||||
|
const norm = (s: string) => {
|
||||||
|
const lines = s.split('\n');
|
||||||
|
if (lines.length > 0) lines[0] = lines[0].trim();
|
||||||
|
if (lines.length > 1) lines[lines.length - 1] = lines[lines.length - 1].trim();
|
||||||
|
return lines.join('\n');
|
||||||
|
};
|
||||||
|
return findByNormalization(content, search, norm, 'trimmed_boundary');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 7. unicode_normalized — 智能引号/em-dash/省略号 → ASCII
|
||||||
|
{
|
||||||
|
name: 'unicode_normalized',
|
||||||
|
find(content, search) {
|
||||||
|
const norm = (s: string) => s
|
||||||
|
.replace(/[\u2018\u2019\u201A]/g, "'")
|
||||||
|
.replace(/[\u201C\u201D\u201E]/g, '"')
|
||||||
|
.replace(/[\u2013\u2014]/g, '--')
|
||||||
|
.replace(/\u2026/g, '...');
|
||||||
|
return findByNormalization(content, search, norm, 'unicode_normalized');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 8. block_anchor — 首尾行锚定 + 中间 Levenshtein ≥60%
|
||||||
|
{
|
||||||
|
name: 'block_anchor',
|
||||||
|
find(content, search) {
|
||||||
|
const searchLines = search.split('\n');
|
||||||
|
if (searchLines.length < 3) return [];
|
||||||
|
const firstLine = searchLines[0].trim();
|
||||||
|
const lastLine = searchLines[searchLines.length - 1].trim();
|
||||||
|
const contentLines = content.split('\n');
|
||||||
|
const results: FuzzyMatchResult[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < contentLines.length; i++) {
|
||||||
|
if (contentLines[i].trim() !== firstLine) continue;
|
||||||
|
for (let j = i + searchLines.length - 1; j < contentLines.length && j < i + searchLines.length + 5; j++) {
|
||||||
|
if (contentLines[j].trim() !== lastLine) continue;
|
||||||
|
const candidateMiddle = contentLines.slice(i + 1, j).join('\n');
|
||||||
|
const searchMiddle = searchLines.slice(1, -1).join('\n');
|
||||||
|
if (similarity(candidateMiddle, searchMiddle) >= 0.6) {
|
||||||
|
const startIndex = content.split('\n').slice(0, i).join('\n').length + (i > 0 ? 1 : 0);
|
||||||
|
const matchedLines = contentLines.slice(i, j + 1);
|
||||||
|
const matchedText = matchedLines.join('\n');
|
||||||
|
results.push({ strategy: 'block_anchor', startIndex, endIndex: startIndex + matchedText.length, matchedText });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 9. context_aware — 逐行相似度 ≥80%,整体 ≥50%
|
||||||
|
{
|
||||||
|
name: 'context_aware',
|
||||||
|
find(content, search) {
|
||||||
|
const searchLines = search.split('\n');
|
||||||
|
const contentLines = content.split('\n');
|
||||||
|
if (searchLines.length === 0) return [];
|
||||||
|
const results: FuzzyMatchResult[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i <= contentLines.length - searchLines.length; i++) {
|
||||||
|
let matchCount = 0;
|
||||||
|
for (let j = 0; j < searchLines.length; j++) {
|
||||||
|
if (similarity(contentLines[i + j].trim(), searchLines[j].trim()) >= 0.8) matchCount++;
|
||||||
|
}
|
||||||
|
if (matchCount / searchLines.length >= 0.5) {
|
||||||
|
const startIndex = contentLines.slice(0, i).join('\n').length + (i > 0 ? 1 : 0);
|
||||||
|
const matchedText = contentLines.slice(i, i + searchLines.length).join('\n');
|
||||||
|
results.push({ strategy: 'context_aware', startIndex, endIndex: startIndex + matchedText.length, matchedText });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 验证构建**
|
||||||
|
|
||||||
|
Run: `cd packages/backend && npm run build`
|
||||||
|
Expected: 编译成功
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/tools/fuzzy_match.ts
|
||||||
|
git commit -m "feat(netaclaw): add 9-level fuzzy match engine for patch tool"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: 运行 simplify skill 审查本次变更**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Patch 工具
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `tools/builtin/patch.ts`
|
||||||
|
- Modify: `gateway/server.ts`
|
||||||
|
- Modify: `service/agent_executor.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 创建 tools/builtin/patch.ts**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// tools/builtin/patch.ts
|
||||||
|
import { Type } from '@sinclair/typebox';
|
||||||
|
import * as fs from 'fs/promises';
|
||||||
|
import { AgentToolWithMeta, textResult } from '../common.js';
|
||||||
|
import { fuzzyFindAll } from '../fuzzy_match.js';
|
||||||
|
import { registerSchema } from '../catalog.js';
|
||||||
|
|
||||||
|
const PatchParams = Type.Object({
|
||||||
|
path: Type.String({ description: '文件绝对路径' }),
|
||||||
|
old_string: Type.String({ description: '要查找的文本片段' }),
|
||||||
|
new_string: Type.String({ description: '替换为的文本' }),
|
||||||
|
replace_all: Type.Optional(Type.Boolean({ description: '替换所有匹配,默认 false' })),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const patchTool: AgentToolWithMeta<typeof PatchParams, unknown> = {
|
||||||
|
name: 'patch',
|
||||||
|
label: '模糊补丁',
|
||||||
|
description: '对文件进行局部查找替换,支持模糊匹配。比 write_file 更安全、更省 token。',
|
||||||
|
parameters: PatchParams,
|
||||||
|
async execute(_id, params) {
|
||||||
|
try {
|
||||||
|
const content = await fs.readFile(params.path, 'utf-8');
|
||||||
|
const matches = fuzzyFindAll(content, params.old_string);
|
||||||
|
|
||||||
|
if (matches.length === 0) {
|
||||||
|
return textResult(`未找到匹配。请检查 old_string 是否正确,或提供更多上下文。`);
|
||||||
|
}
|
||||||
|
if (matches.length > 1 && !params.replace_all) {
|
||||||
|
return textResult(`找到 ${matches.length} 处匹配(策略: ${matches[0].strategy}),请提供更多上下文确保唯一匹配,或设置 replace_all=true。`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从后往前替换,避免索引偏移
|
||||||
|
let result = content;
|
||||||
|
const sorted = [...matches].sort((a, b) => b.startIndex - a.startIndex);
|
||||||
|
for (const m of sorted) {
|
||||||
|
result = result.slice(0, m.startIndex) + params.new_string + result.slice(m.endIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(params.path, result, 'utf-8');
|
||||||
|
|
||||||
|
const count = params.replace_all ? matches.length : 1;
|
||||||
|
return textResult(`已替换 ${count} 处(策略: ${matches[0].strategy}): ${params.path}`);
|
||||||
|
} catch (err: any) {
|
||||||
|
return textResult(`patch 失败: ${err.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
registerSchema({ name: 'patch', toolset: 'base', description: patchTool.description });
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: gateway/server.ts — defaultTools 加入 patchTool**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// gateway/server.ts 顶部 import 追加
|
||||||
|
import { patchTool } from '../tools/builtin/patch.js';
|
||||||
|
|
||||||
|
// 修改 defaultTools(第 50 行)
|
||||||
|
private defaultTools: AnyAgentTool[] = [bashTool, readFileTool, writeFileTool, listDirTool, patchTool];
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: agent_executor.ts — defaultTools 加入 patchTool**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// service/agent_executor.ts 顶部 import 追加
|
||||||
|
import { patchTool } from '../tools/builtin/patch.js';
|
||||||
|
|
||||||
|
// 修改 defaultTools(第 40 行)
|
||||||
|
private readonly defaultTools: AnyAgentTool[] = [bashTool, readFileTool, writeFileTool, listDirTool, patchTool];
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3.5: crew_orchestrator.ts + crew_delegate.ts — builtinTools 加入 patchTool**
|
||||||
|
|
||||||
|
> Crew 子 Agent 也需要 patch 进行文件编辑(但不加 clarify,Crew 无用户交互通道)。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// service/crew_orchestrator.ts 顶部 import 追加
|
||||||
|
import { patchTool } from '../tools/builtin/patch.js';
|
||||||
|
|
||||||
|
// 修改第 24 行 BUILTIN_TOOL_NAMES 追加 'patch'
|
||||||
|
const BUILTIN_TOOL_NAMES = ['bash', 'read_file', 'write_file', 'list_dir', 'patch', 'delegate_task', 'delegate_parallel', 'escalate'];
|
||||||
|
|
||||||
|
// 修改第 175 行 builtinTools 追加 patchTool
|
||||||
|
const builtinTools: AnyAgentTool[] = [bashTool, readFileTool, writeFileTool, listDirTool, patchTool];
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// service/crew_delegate.ts 顶部 import 追加
|
||||||
|
import { patchTool } from '../tools/builtin/patch.js';
|
||||||
|
|
||||||
|
// 修改第 17 行 BUILTIN_TOOL_NAMES 追加 'patch'
|
||||||
|
const BUILTIN_TOOL_NAMES = ['bash', 'read_file', 'write_file', 'list_dir', 'patch'];
|
||||||
|
|
||||||
|
// 修改第 40 行 builtinTools 追加 patchTool
|
||||||
|
const builtinTools: AnyAgentTool[] = [bashTool, readFileTool, writeFileTool, listDirTool, patchTool];
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 验证构建**
|
||||||
|
|
||||||
|
Run: `cd packages/backend && npm run build`
|
||||||
|
Expected: 编译成功
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/tools/builtin/patch.ts \
|
||||||
|
packages/backend/src/modules/netaclaw/gateway/server.ts \
|
||||||
|
packages/backend/src/modules/netaclaw/service/agent_executor.ts \
|
||||||
|
packages/backend/src/modules/netaclaw/service/crew_orchestrator.ts \
|
||||||
|
packages/backend/src/modules/netaclaw/service/crew_delegate.ts
|
||||||
|
git commit -m "feat(netaclaw): add patch tool with fuzzy matching"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: 运行 simplify skill 审查本次变更**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Clarify 工具 + WebSocket 协议 + 阻塞机制
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `tools/builtin/clarify.ts`
|
||||||
|
- Modify: `gateway/protocol.ts`
|
||||||
|
- Modify: `runtime/attempt.ts`
|
||||||
|
- Modify: `runtime/agent.ts`
|
||||||
|
- Modify: `gateway/server.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 创建 tools/builtin/clarify.ts**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// tools/builtin/clarify.ts
|
||||||
|
import { Type } from '@sinclair/typebox';
|
||||||
|
import { AgentToolWithMeta, textResult } from '../common.js';
|
||||||
|
import { registerSchema } from '../catalog.js';
|
||||||
|
|
||||||
|
const ClarifyParams = Type.Object({
|
||||||
|
question: Type.String({ description: '要问用户的问题' }),
|
||||||
|
choices: Type.Optional(Type.Array(Type.String(), { maxItems: 4, description: '预设选项(最多4个)' })),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 降级 execute:当无 WebSocket 回调时,返回问题文本让 Agent 作为回复输出 */
|
||||||
|
export const clarifyTool: AgentToolWithMeta<typeof ClarifyParams, unknown> = {
|
||||||
|
name: 'clarify',
|
||||||
|
label: '向用户提问',
|
||||||
|
description: '当任务需求不明确时,向用户提出澄清问题。支持选择题和开放式问题。',
|
||||||
|
parameters: ClarifyParams,
|
||||||
|
async execute(_id, params) {
|
||||||
|
const choicesText = params.choices?.length
|
||||||
|
? `\n选项: ${params.choices.map((c, i) => `${i + 1}. ${c}`).join(', ')}`
|
||||||
|
: '';
|
||||||
|
return textResult(`[需要用户确认] ${params.question}${choicesText}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
registerSchema({ name: 'clarify', toolset: 'interaction', description: clarifyTool.description });
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 扩展 gateway/protocol.ts**
|
||||||
|
|
||||||
|
在 `protocol.ts` 的 `ClientMessage` 和 `ServerEvent` 联合类型中追加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// --- 客户端 → 服务端 追加 ---
|
||||||
|
|
||||||
|
export interface ClientClarifyResponseMessage {
|
||||||
|
type: 'clarify_response';
|
||||||
|
sessionId: string;
|
||||||
|
requestId: string;
|
||||||
|
answer: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ClientMessage = ClientChatMessage | ClientPingMessage | ClientSetThinkingLevelMessage | ClientClarifyResponseMessage;
|
||||||
|
|
||||||
|
// --- 服务端 → 客户端 追加 ---
|
||||||
|
|
||||||
|
export interface ServerClarifyRequestEvent {
|
||||||
|
type: 'clarify_request';
|
||||||
|
sessionId: string;
|
||||||
|
data: { requestId: string; question: string; choices?: string[] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerEvent 联合类型追加 ServerClarifyRequestEvent
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 修改 runtime/attempt.ts — 新增 onClarifyRequest 回调**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// attempt.ts — AttemptParams 接口追加字段
|
||||||
|
export interface AttemptParams {
|
||||||
|
// ... 现有字段不变 ...
|
||||||
|
onClarifyRequest?: (question: string, choices?: string[]) => Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// attempt.ts — 工具执行循环中(第 69-79 行区域),在 tool.execute 之前插入 clarify 分支
|
||||||
|
if (tc.name === 'clarify' && params.onClarifyRequest) {
|
||||||
|
const args = JSON.parse(tc.arguments);
|
||||||
|
try {
|
||||||
|
resultText = await params.onClarifyRequest(args.question, args.choices);
|
||||||
|
} catch {
|
||||||
|
resultText = '用户未回答,请自行判断并继续。';
|
||||||
|
}
|
||||||
|
} else if (!tool) {
|
||||||
|
resultText = `错误: 工具 "${tc.name}" 不存在`;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const result = await tool.execute(tc.id, JSON.parse(tc.arguments));
|
||||||
|
resultText = typeof result === 'string' ? result : JSON.stringify(result);
|
||||||
|
} catch (err: any) {
|
||||||
|
resultText = `工具执行错误: ${err.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 修改 runtime/agent.ts — 透传 onClarifyRequest**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// agent.ts — AgentRunParams 接口追加
|
||||||
|
export interface AgentRunParams {
|
||||||
|
// ... 现有字段不变 ...
|
||||||
|
onClarifyRequest?: (question: string, choices?: string[]) => Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// agent.ts — runAttempt 调用处(第 86-96 行)追加透传
|
||||||
|
return runAttempt({
|
||||||
|
// ... 现有参数不变 ...
|
||||||
|
onClarifyRequest: params.onClarifyRequest,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: 修改 gateway/server.ts — 注入 clarify 阻塞回调**
|
||||||
|
|
||||||
|
> **Critical**: Midway.js `@WSController` 是 request-scope(每连接一个实例),`clarifyResolvers` 必须是**模块级** Map,否则 clarify_response 可能路由到不同实例导致 resolve 永远不被调用。超时设为 20s(前端适配前快速降级)。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// gateway/server.ts — 顶部 import 追加
|
||||||
|
import { randomUUID } from 'crypto';
|
||||||
|
import { clarifyTool } from '../tools/builtin/clarify.js';
|
||||||
|
|
||||||
|
// 模块级 Map(不是实例属性!@WSController 每连接一个实例)
|
||||||
|
const clarifyResolvers = new Map<string, { resolve: (answer: string) => void; timer: NodeJS.Timeout }>();
|
||||||
|
|
||||||
|
// defaultTools 追加 clarifyTool
|
||||||
|
private defaultTools: AnyAgentTool[] = [bashTool, readFileTool, writeFileTool, listDirTool, patchTool, clarifyTool];
|
||||||
|
|
||||||
|
// runAgent 调用处(第 219 行区域)追加 onClarifyRequest
|
||||||
|
onClarifyRequest: async (question, choices) => {
|
||||||
|
const requestId = randomUUID();
|
||||||
|
this.send({ type: 'clarify_request', sessionId: sid, data: { requestId, question, choices } });
|
||||||
|
return new Promise<string>((resolve) => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
clarifyResolvers.delete(requestId);
|
||||||
|
resolve('用户未在规定时间内回答。请根据已有信息自行判断并继续执行。');
|
||||||
|
}, 20_000); // 前端适配前用短超时快速降级
|
||||||
|
clarifyResolvers.set(requestId, { resolve, timer });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// onMessage 方法中(第 80 行区域)追加 clarify_response 处理
|
||||||
|
if (msg.type === 'clarify_response') {
|
||||||
|
const entry = clarifyResolvers.get(msg.requestId);
|
||||||
|
if (entry) {
|
||||||
|
clearTimeout(entry.timer);
|
||||||
|
clarifyResolvers.delete(msg.requestId);
|
||||||
|
entry.resolve(msg.answer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: 验证构建**
|
||||||
|
|
||||||
|
Run: `cd packages/backend && npm run build`
|
||||||
|
Expected: 编译成功
|
||||||
|
|
||||||
|
- [ ] **Step 7: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/tools/builtin/clarify.ts \
|
||||||
|
packages/backend/src/modules/netaclaw/gateway/protocol.ts \
|
||||||
|
packages/backend/src/modules/netaclaw/runtime/attempt.ts \
|
||||||
|
packages/backend/src/modules/netaclaw/runtime/agent.ts \
|
||||||
|
packages/backend/src/modules/netaclaw/gateway/server.ts
|
||||||
|
git commit -m "feat(netaclaw): add clarify tool with blocking WebSocket interaction"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 8: 运行 simplify skill 审查本次变更**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: 提示词重写 + todo TypeBox 迁移
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `runtime/prompt_guidance.ts`
|
||||||
|
- Modify: `tools/todo_tool.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 重写 prompt_guidance.ts — TOOL_USE_ENFORCEMENT 改为动态函数**
|
||||||
|
|
||||||
|
> **Critical**: 原设计硬编码了 clarify/patch 工具名,Crew 子 Agent 没有这些工具会被误导调用不存在的工具。改为根据实际可用工具列表动态生成。
|
||||||
|
|
||||||
|
删除 `prompt_guidance.ts` 第 29-34 行的 `export const TOOL_USE_ENFORCEMENT = ...`,替换为:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* 根据实际可用工具列表动态生成工具使用规范。
|
||||||
|
* Crew 子 Agent 没有 clarify/patch,不应在提示词中出现这些工具。
|
||||||
|
*/
|
||||||
|
export function getToolUseEnforcement(toolNames: string[]): string {
|
||||||
|
const rules = `# 工具使用规范
|
||||||
|
|
||||||
|
你必须通过工具采取行动 - 不要只描述你打算做什么。
|
||||||
|
|
||||||
|
## 强制规则
|
||||||
|
- 当你说"我来检查"、"让我执行"时,必须在同一回复中立即发起工具调用
|
||||||
|
- 不要以"下一步我会..."结束回复 - 现在就执行
|
||||||
|
- 持续工作直到任务真正完成,不要停在计划阶段
|
||||||
|
- 每条回复要么包含推进任务的工具调用,要么向用户交付最终结果`;
|
||||||
|
|
||||||
|
const scenarios: string[] = [];
|
||||||
|
if (toolNames.includes('read_file')) scenarios.push('需要读取文件内容时 → read_file(不要凭记忆猜测)');
|
||||||
|
if (toolNames.includes('patch')) scenarios.push('需要修改已有文件时 → patch(局部修改)或 write_file(新建/全量重写)');
|
||||||
|
else if (toolNames.includes('write_file')) scenarios.push('需要修改文件时 → write_file');
|
||||||
|
if (toolNames.includes('list_dir')) scenarios.push('需要了解目录结构时 → list_dir');
|
||||||
|
if (toolNames.includes('bash')) scenarios.push('需要执行命令时 → bash');
|
||||||
|
if (toolNames.includes('clarify')) scenarios.push('任务需求不明确时 → clarify(主动提问,不要猜测)');
|
||||||
|
if (toolNames.includes('todo')) scenarios.push('复杂任务开始前 → todo(创建任务列表)');
|
||||||
|
|
||||||
|
return scenarios.length > 0
|
||||||
|
? `${rules}\n\n## 必须使用工具的场景\n${scenarios.map(s => `- ${s}`).join('\n')}\n\n## 操作前先确认\n- 修改文件前先 read_file 确认当前内容\n- 执行命令前确认工作目录`
|
||||||
|
: rules;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
同时修改 `runtime/prompt_builder.ts` 中 Layer 2 的调用:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// prompt_builder.ts — 将 TOOL_USE_ENFORCEMENT 常量引用改为函数调用
|
||||||
|
// 旧: import { TOOL_USE_ENFORCEMENT, ... } from './prompt_guidance.js';
|
||||||
|
// 新: import { getToolUseEnforcement, ... } from './prompt_guidance.js';
|
||||||
|
|
||||||
|
// Layer 2 中:
|
||||||
|
// 旧: parts.push(TOOL_USE_ENFORCEMENT);
|
||||||
|
// 新: parts.push(getToolUseEnforcement(params.availableToolNames));
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 重写 prompt_guidance.ts — TOOL_BEHAVIOR**
|
||||||
|
|
||||||
|
替换 `prompt_guidance.ts` 第 104-117 行的 `TOOL_BEHAVIOR` 对象:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const TOOL_BEHAVIOR: Record<string, string> = {
|
||||||
|
memory_save: `## 记忆使用策略
|
||||||
|
使用 memory_save 存储对未来对话有价值的持久信息:用户偏好、环境细节、项目约束。
|
||||||
|
保持记忆紧凑,聚焦于减少用户未来重复纠正的事实。
|
||||||
|
不要存储任务进度、会话结果、临时 TODO 状态。更新已有记忆而非创建重复条目。`,
|
||||||
|
|
||||||
|
todo: `## 任务规划策略
|
||||||
|
收到用户请求后,先评估任务复杂度:
|
||||||
|
**必须使用 todo:** 涉及 2 个以上步骤、修改多个文件、需要先调研再实施、用户一次提出多个需求。
|
||||||
|
**无需使用 todo:** 单步操作(查看文件、回答问题、一条命令)、简单单文件小修改。
|
||||||
|
列表顺序=优先级。同一时间只标记一个 in_progress。完成立即标记 completed。
|
||||||
|
任务列表是你的工作契约 - 它让用户看到你的计划并跟踪进度。`,
|
||||||
|
|
||||||
|
skill_manage: `## 技能管理策略
|
||||||
|
完成复杂任务(5步以上工具调用)后,考虑用 skill_manage 将方法保存为技能以便复用。
|
||||||
|
使用技能时如发现过时或错误,立即修补,不要等用户要求。`,
|
||||||
|
|
||||||
|
patch: `## 文件编辑策略
|
||||||
|
修改已有文件时,优先使用 patch 进行局部替换,而非 write_file 全量重写。
|
||||||
|
**用 patch:** 修改函数、修复 bug、调整配置、添加/删除代码片段。
|
||||||
|
**用 write_file:** 创建新文件、文件需要完全重写。
|
||||||
|
old_string 提供足够上下文确保唯一匹配。不需要精确匹配缩进和空白。`,
|
||||||
|
|
||||||
|
clarify: `## 主动提问策略
|
||||||
|
任务需求不明确、存在多种合理解读、或缺少关键信息时,使用 clarify 向用户提问。
|
||||||
|
**应该提问:** 指令模糊有多种解读、缺少关键参数、操作有风险需确认。
|
||||||
|
**不应提问:** 任务明确可直接执行、可从上下文推断、琐碎实现细节。
|
||||||
|
提供 choices 选项让用户快速选择。一次只问一个问题。`,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: todo_tool.ts — TypeBox 迁移**
|
||||||
|
|
||||||
|
替换 `tools/todo_tool.ts` 的 schema 定义(第 3-41 行):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Type, Static } from '@sinclair/typebox';
|
||||||
|
import { TodoStore } from '../runtime/todo_store.js';
|
||||||
|
import { registerSchema } from './catalog.js';
|
||||||
|
|
||||||
|
const TodoItemSchema = Type.Object({
|
||||||
|
id: Type.String({ description: '唯一标识' }),
|
||||||
|
content: Type.String({ description: '任务描述' }),
|
||||||
|
status: Type.Union([
|
||||||
|
Type.Literal('pending'),
|
||||||
|
Type.Literal('in_progress'),
|
||||||
|
Type.Literal('completed'),
|
||||||
|
Type.Literal('cancelled'),
|
||||||
|
], { description: '当前状态' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const TodoParams = Type.Object({
|
||||||
|
todos: Type.Optional(Type.Array(TodoItemSchema, { description: '任务项数组。省略则读取当前列表。' })),
|
||||||
|
merge: Type.Optional(Type.Boolean({ description: 'true=按 id 增量更新。false(默认)=全量替换。', default: false })),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const todoToolSchema = {
|
||||||
|
name: 'todo' as const,
|
||||||
|
description:
|
||||||
|
'管理当前会话的任务列表。用于复杂任务或用户提供多个任务时。\n'
|
||||||
|
+ '不传参数=读取当前列表。传 todos 数组=写入。\n'
|
||||||
|
+ 'merge=false(默认)全量替换,merge=true 按 id 增量更新。\n'
|
||||||
|
+ '列表顺序=优先级。同一时间只能有一个 in_progress。',
|
||||||
|
parameters: TodoParams,
|
||||||
|
};
|
||||||
|
|
||||||
|
type TodoExecArgs = Static<typeof TodoParams>;
|
||||||
|
|
||||||
|
/** 执行 todo 工具 */
|
||||||
|
export function executeTodo(
|
||||||
|
store: TodoStore,
|
||||||
|
args: TodoExecArgs,
|
||||||
|
): { todos: any[]; summary: any } {
|
||||||
|
if (args.todos && args.todos.length > 0) {
|
||||||
|
store.write(args.todos, args.merge ?? false);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
todos: store.read(),
|
||||||
|
summary: store.getSummary(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
registerSchema({ name: 'todo', toolset: 'planning', description: todoToolSchema.description });
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 验证构建**
|
||||||
|
|
||||||
|
Run: `cd packages/backend && npm run build`
|
||||||
|
Expected: 编译成功
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/runtime/prompt_guidance.ts \
|
||||||
|
packages/backend/src/modules/netaclaw/tools/todo_tool.ts
|
||||||
|
git commit -m "feat(netaclaw): rewrite tool prompts and migrate todo schema to TypeBox"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: 运行 simplify skill 审查本次变更**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: 集成验证
|
||||||
|
|
||||||
|
**Files:** 无新文件,验证现有改动的集成正确性
|
||||||
|
|
||||||
|
- [ ] **Step 1: 完整构建验证**
|
||||||
|
|
||||||
|
Run: `cd packages/backend && npm run build`
|
||||||
|
Expected: 零错误,零警告
|
||||||
|
|
||||||
|
- [ ] **Step 2: 验证 catalog 工具名解析**
|
||||||
|
|
||||||
|
在构建产物中检查 `collectAvailableToolNames` 是否正确返回所有工具名。手动验证方式:
|
||||||
|
|
||||||
|
Run: `cd packages/backend && node -e "import('./dist/modules/netaclaw/tools/builtin/bash.js').then(() => import('./dist/modules/netaclaw/tools/builtin/file.js')).then(() => import('./dist/modules/netaclaw/tools/builtin/patch.js')).then(() => import('./dist/modules/netaclaw/tools/builtin/clarify.js')).then(() => import('./dist/modules/netaclaw/tools/builtin/memory.js')).then(() => import('./dist/modules/netaclaw/tools/todo_tool.js')).then(() => import('./dist/modules/netaclaw/runtime/prompt_builder.js')).then(m => console.log(m.collectAvailableToolNames({ memoryEnabled: true, hasSkills: true, crewRole: 'master' })))"`
|
||||||
|
|
||||||
|
Expected: `['bash', 'read_file', 'write_file', 'list_dir', 'patch', 'todo', 'clarify', 'memory_save', 'memory_recall', 'read_skill', 'read_skill_file', 'skill_manage', 'delegate_task', 'delegate_parallel', 'escalate']`
|
||||||
|
|
||||||
|
- [ ] **Step 3: 验证 prompt_guidance 输出**
|
||||||
|
|
||||||
|
Run: `cd packages/backend && node -e "import('./dist/modules/netaclaw/runtime/prompt_guidance.js').then(m => { console.log('=== ENFORCEMENT ==='); console.log(m.getToolUseEnforcement(['bash','read_file','write_file','list_dir','patch','todo','clarify']).slice(0, 300)); console.log('=== BEHAVIOR ==='); console.log(m.getToolBehaviorGuidance(['todo', 'patch', 'clarify']).slice(0, 300)); })"`
|
||||||
|
|
||||||
|
Expected: ENFORCEMENT 输出包含所有 7 个工具的场景描述;BEHAVIOR 输出包含 todo/patch/clarify 策略
|
||||||
|
|
||||||
|
- [ ] **Step 4: 最终 Commit(如有修复)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add -A
|
||||||
|
git commit -m "fix(netaclaw): integration fixes for tool catalog and prompts"
|
||||||
|
```
|
||||||
1991
docs/superpowers/plans/2026-04-17-context-compaction.md
Normal file
1991
docs/superpowers/plans/2026-04-17-context-compaction.md
Normal file
File diff suppressed because it is too large
Load Diff
1216
docs/superpowers/plans/2026-04-18-session-subagent-implementation.md
Normal file
1216
docs/superpowers/plans/2026-04-18-session-subagent-implementation.md
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
891
docs/superpowers/plans/2026-04-19-neta-agent-runtime-kernel.md
Normal file
891
docs/superpowers/plans/2026-04-19-neta-agent-runtime-kernel.md
Normal file
@ -0,0 +1,891 @@
|
|||||||
|
# Neta Agent 运行时内核实施计划
|
||||||
|
|
||||||
|
> **给自动化实施代理:** 必须使用子技能:`superpowers:subagent-driven-development`(推荐)或 `superpowers:executing-plans`,按任务逐项实施本计划。步骤使用复选框(`- [ ]`)语法跟踪进度。
|
||||||
|
|
||||||
|
**目标:** 建立 Pi-first 树状会话内核,包括 Pi 兼容 session entry tree、`SessionTreeProvider` 抽象、默认 file provider、MySQL provider、leaf 驱动上下文构建、snapshot 和 agent 级 session provider 选择。
|
||||||
|
|
||||||
|
**架构:** 第一原则是“先复刻 Pi 的 session 语义,再适配 Neta provider”。File provider 必须采用 Pi 单文件 JSONL session 协议:第一行 session header,后续每行 session entry;entry 通过 `id/parentId` 组成树;`leafId` 只表示当前位置;历史 entry 原则上 append-only。MySQL provider 必须复刻同一语义,不能反过来要求 file provider 适配数据库表思维。
|
||||||
|
|
||||||
|
**技术栈:** Midway/NestJS 风格 service、TypeScript、TypeORM、MySQL、Node fs JSONL、Jest、pnpm。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 架构硬约束
|
||||||
|
|
||||||
|
本计划实施时必须遵守以下约束:
|
||||||
|
|
||||||
|
- 不兼容旧线性 message/session 接口。
|
||||||
|
- 旧历史数据允许删除,不做迁移脚本。
|
||||||
|
- 数据库结构修改实施阶段使用 MCP 直接执行,不新增 SQL 文件作为主路径。
|
||||||
|
- `file` 与 `mysql` provider 必须通过同一组契约测试。
|
||||||
|
- `file` provider 必须高强度移植 Pi 的 `SessionManager` 文件协议和纯逻辑。
|
||||||
|
- 不得把 file provider 设计成 `session.json + entries.jsonl` 双文件。
|
||||||
|
- 不得把第一版事实源拆成独立 `tool_call/tool_result` entry;tool 状态作为 `AgentMessage` payload 或前端流式投影事件处理。
|
||||||
|
- 不得只用 `content/summary` 字段表示消息;必须保留完整 `message` payload。
|
||||||
|
- completed entry 默认不可修改;`updateEntry()` 只允许用于 in-flight 流式补写或明确的 patch 事件。
|
||||||
|
|
||||||
|
## Pi 直接复用范围
|
||||||
|
|
||||||
|
优先从以下 Pi 文件移植:
|
||||||
|
|
||||||
|
- `C:/Users/lixin/Desktop/RZYX_ZT/pi-mono-main/packages/coding-agent/src/core/session-manager.ts`
|
||||||
|
- `C:/Users/lixin/Desktop/RZYX_ZT/pi-mono-main/packages/coding-agent/src/core/compaction/compaction.ts`
|
||||||
|
- `C:/Users/lixin/Desktop/RZYX_ZT/pi-mono-main/packages/coding-agent/src/core/compaction/branch-summarization.ts`
|
||||||
|
- `C:/Users/lixin/Desktop/RZYX_ZT/pi-mono-main/packages/coding-agent/src/core/compaction/utils.ts`
|
||||||
|
|
||||||
|
必须移植或等价实现:
|
||||||
|
|
||||||
|
- `SessionHeader` / `SessionEntry` union。
|
||||||
|
- 单文件 JSONL session 协议。
|
||||||
|
- `parseSessionEntries()`、`loadEntriesFromFile()` 的容错解析。
|
||||||
|
- leaf-to-root path 构建。
|
||||||
|
- `buildSessionContext()` 的 compaction、branch summary、custom message、thinking/model setting 处理。
|
||||||
|
- `branch()`、`resetLeaf()`、`branchWithSummary()`、`createBranchedSession()` 的纯逻辑。
|
||||||
|
- compaction 的 `firstKeptEntryId`、split turn、previous summary 合并策略。
|
||||||
|
|
||||||
|
## 文件结构
|
||||||
|
|
||||||
|
新增后端内核模块:
|
||||||
|
|
||||||
|
- `packages/backend/src/modules/netaclaw/session-tree/types.ts`
|
||||||
|
- 定义 Pi 兼容 session header、entry union、provider DTO、snapshot DTO。
|
||||||
|
- `packages/backend/src/modules/netaclaw/session-tree/id.ts`
|
||||||
|
- 定义 session id 与 entry id 生成器。
|
||||||
|
- `packages/backend/src/modules/netaclaw/session-tree/path.ts`
|
||||||
|
- 定义 entry index、leaf path、tree、common ancestor、label 解析纯函数。
|
||||||
|
- `packages/backend/src/modules/netaclaw/session-tree/context_builder.ts`
|
||||||
|
- 移植 Pi `buildSessionContext()` 语义。
|
||||||
|
- `packages/backend/src/modules/netaclaw/session-tree/snapshot.ts`
|
||||||
|
- 构建前端/API snapshot payload。
|
||||||
|
- `packages/backend/src/modules/netaclaw/session-tree/provider.ts`
|
||||||
|
- 定义 `SessionTreeProvider` 契约。
|
||||||
|
- `packages/backend/src/modules/netaclaw/session-tree/pi_session_file.ts`
|
||||||
|
- 封装 Pi 单文件 JSONL session 文件读写与纯逻辑。
|
||||||
|
- `packages/backend/src/modules/netaclaw/session-tree/file_provider.ts`
|
||||||
|
- 将 `pi_session_file.ts` 适配为 `SessionTreeProvider`。
|
||||||
|
- `packages/backend/src/modules/netaclaw/session-tree/mysql_provider.ts`
|
||||||
|
- 将 MySQL 表适配为同一 `SessionTreeProvider`。
|
||||||
|
- `packages/backend/src/modules/netaclaw/session-tree/provider_factory.ts`
|
||||||
|
- 解析 agent 配置中的 `sessionProvider`。
|
||||||
|
|
||||||
|
新增 TypeORM 实体:
|
||||||
|
|
||||||
|
- `packages/backend/src/modules/netaclaw/entity/agent_session.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/entity/agent_session_entry.ts`
|
||||||
|
|
||||||
|
修改现有文件:
|
||||||
|
|
||||||
|
- `packages/backend/src/modules/netaclaw/entity/agent.ts`
|
||||||
|
- `packages/backend/src/entities.ts`
|
||||||
|
|
||||||
|
新增测试:
|
||||||
|
|
||||||
|
- `packages/backend/test/session_tree_types.test.ts`
|
||||||
|
- `packages/backend/test/session_tree_path.test.ts`
|
||||||
|
- `packages/backend/test/session_tree_context_builder.test.ts`
|
||||||
|
- `packages/backend/test/session_tree_file_provider.test.ts`
|
||||||
|
- `packages/backend/test/session_tree_mysql_provider.test.ts`
|
||||||
|
- `packages/backend/test/session_tree_provider_contract.test.ts`
|
||||||
|
- `packages/backend/test/session_tree_provider_factory.test.ts`
|
||||||
|
- `packages/backend/test/entity_exports.test.ts`
|
||||||
|
|
||||||
|
## 任务 1:定义 Pi 兼容会话类型
|
||||||
|
|
||||||
|
**文件:**
|
||||||
|
|
||||||
|
- 新增:`packages/backend/src/modules/netaclaw/session-tree/types.ts`
|
||||||
|
- 测试:`packages/backend/test/session_tree_types.test.ts`
|
||||||
|
|
||||||
|
- [ ] **步骤 1:编写失败的类型消费测试**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type {
|
||||||
|
SessionTreeEntry,
|
||||||
|
SessionTreeHeader,
|
||||||
|
SessionTreeSession,
|
||||||
|
} from '../src/modules/netaclaw/session-tree/types.js';
|
||||||
|
|
||||||
|
describe('session tree types', () => {
|
||||||
|
it('represents a Pi-compatible session header and entries', () => {
|
||||||
|
const header: SessionTreeHeader = {
|
||||||
|
type: 'session',
|
||||||
|
version: 1,
|
||||||
|
id: 's1',
|
||||||
|
timestamp: '2026-04-19T00:00:00.000Z',
|
||||||
|
cwd: 'C:/workspace',
|
||||||
|
};
|
||||||
|
|
||||||
|
const entry: SessionTreeEntry = {
|
||||||
|
type: 'message',
|
||||||
|
id: 'e1',
|
||||||
|
parentId: null,
|
||||||
|
timestamp: '2026-04-19T00:00:00.000Z',
|
||||||
|
message: { role: 'user', content: 'hello' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const session: SessionTreeSession = {
|
||||||
|
sessionId: header.id,
|
||||||
|
provider: 'file',
|
||||||
|
rootEntryId: entry.id,
|
||||||
|
leafEntryId: entry.id,
|
||||||
|
cwd: header.cwd,
|
||||||
|
status: 'active',
|
||||||
|
createdAt: header.timestamp,
|
||||||
|
updatedAt: header.timestamp,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(session.leafEntryId).toBe(entry.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **步骤 2:运行测试确认失败**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --filter @neta/backend test -- --runInBand test/session_tree_types.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
预期:
|
||||||
|
|
||||||
|
- 失败,原因是 `session-tree/types.js` 尚不存在。
|
||||||
|
|
||||||
|
- [ ] **步骤 3:实现 `types.ts`**
|
||||||
|
|
||||||
|
核心类型必须包含:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export type SessionTreeProviderKind = 'file' | 'mysql';
|
||||||
|
|
||||||
|
export interface SessionTreeHeader {
|
||||||
|
type: 'session';
|
||||||
|
version: number;
|
||||||
|
id: string;
|
||||||
|
timestamp: string;
|
||||||
|
cwd: string;
|
||||||
|
parentSession?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionTreeEntryBase {
|
||||||
|
type: string;
|
||||||
|
id: string;
|
||||||
|
parentId: string | null;
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionTreeMessage {
|
||||||
|
role: string;
|
||||||
|
content?: unknown;
|
||||||
|
provider?: string;
|
||||||
|
model?: string;
|
||||||
|
usage?: Record<string, unknown>;
|
||||||
|
stopReason?: string;
|
||||||
|
timestamp?: number;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionMessageEntry extends SessionTreeEntryBase {
|
||||||
|
type: 'message';
|
||||||
|
message: SessionTreeMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThinkingLevelChangeEntry extends SessionTreeEntryBase {
|
||||||
|
type: 'thinking_level_change';
|
||||||
|
thinkingLevel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModelChangeEntry extends SessionTreeEntryBase {
|
||||||
|
type: 'model_change';
|
||||||
|
provider: string;
|
||||||
|
modelId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CompactionEntry<T = unknown> extends SessionTreeEntryBase {
|
||||||
|
type: 'compaction';
|
||||||
|
summary: string;
|
||||||
|
firstKeptEntryId: string;
|
||||||
|
tokensBefore: number;
|
||||||
|
details?: T;
|
||||||
|
fromHook?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BranchSummaryEntry<T = unknown> extends SessionTreeEntryBase {
|
||||||
|
type: 'branch_summary';
|
||||||
|
fromId: string;
|
||||||
|
summary: string;
|
||||||
|
details?: T;
|
||||||
|
fromHook?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomEntry<T = unknown> extends SessionTreeEntryBase {
|
||||||
|
type: 'custom';
|
||||||
|
customType: string;
|
||||||
|
data?: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomMessageEntry<T = unknown> extends SessionTreeEntryBase {
|
||||||
|
type: 'custom_message';
|
||||||
|
customType: string;
|
||||||
|
content: unknown;
|
||||||
|
details?: T;
|
||||||
|
display: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LabelEntry extends SessionTreeEntryBase {
|
||||||
|
type: 'label';
|
||||||
|
targetId: string;
|
||||||
|
label: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionInfoEntry extends SessionTreeEntryBase {
|
||||||
|
type: 'session_info';
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SessionTreeEntry =
|
||||||
|
| SessionMessageEntry
|
||||||
|
| ThinkingLevelChangeEntry
|
||||||
|
| ModelChangeEntry
|
||||||
|
| CompactionEntry
|
||||||
|
| BranchSummaryEntry
|
||||||
|
| CustomEntry
|
||||||
|
| CustomMessageEntry
|
||||||
|
| LabelEntry
|
||||||
|
| SessionInfoEntry;
|
||||||
|
|
||||||
|
export interface SessionTreeSession {
|
||||||
|
sessionId: string;
|
||||||
|
provider: SessionTreeProviderKind;
|
||||||
|
rootEntryId: string | null;
|
||||||
|
leafEntryId: string | null;
|
||||||
|
cwd?: string | null;
|
||||||
|
sessionFile?: string | null;
|
||||||
|
parentSessionId?: string | null;
|
||||||
|
agentId?: number | null;
|
||||||
|
userId?: string | null;
|
||||||
|
title?: string | null;
|
||||||
|
status: 'active' | 'archived' | 'deleted';
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **步骤 4:运行测试确认通过**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --filter @neta/backend test -- --runInBand test/session_tree_types.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
预期:
|
||||||
|
|
||||||
|
- 通过。
|
||||||
|
|
||||||
|
- [ ] **步骤 5:提交变更**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/session-tree/types.ts packages/backend/test/session_tree_types.test.ts
|
||||||
|
git commit -m "feat(agent-runtime): add pi-compatible session tree types"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 任务 2:实现 ID 与树路径纯函数
|
||||||
|
|
||||||
|
**文件:**
|
||||||
|
|
||||||
|
- 新增:`packages/backend/src/modules/netaclaw/session-tree/id.ts`
|
||||||
|
- 新增:`packages/backend/src/modules/netaclaw/session-tree/path.ts`
|
||||||
|
- 测试:`packages/backend/test/session_tree_path.test.ts`
|
||||||
|
|
||||||
|
- [ ] **步骤 1:编写路径测试**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
buildEntryIndex,
|
||||||
|
findCommonAncestorEntryId,
|
||||||
|
getPathToLeaf,
|
||||||
|
groupChildrenByParent,
|
||||||
|
resolveLatestLabels,
|
||||||
|
} from '../src/modules/netaclaw/session-tree/path.js';
|
||||||
|
import type { SessionTreeEntry } from '../src/modules/netaclaw/session-tree/types.js';
|
||||||
|
|
||||||
|
const entry = (id: string, parentId: string | null): SessionTreeEntry => ({
|
||||||
|
type: 'message',
|
||||||
|
id,
|
||||||
|
parentId,
|
||||||
|
timestamp: `2026-04-19T00:00:0${id.length}.000Z`,
|
||||||
|
message: { role: 'user', content: id },
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('session tree path utilities', () => {
|
||||||
|
it('builds root-to-leaf path', () => {
|
||||||
|
expect(getPathToLeaf([entry('a', null), entry('b', 'a'), entry('c', 'b')], 'c').map(e => e.id)).toEqual([
|
||||||
|
'a',
|
||||||
|
'b',
|
||||||
|
'c',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects missing leaf and cycles', () => {
|
||||||
|
expect(() => getPathToLeaf([entry('a', null)], 'missing')).toThrow('Entry missing not found');
|
||||||
|
expect(() => getPathToLeaf([entry('a', 'c'), entry('b', 'a'), entry('c', 'b')], 'c')).toThrow('Cycle detected');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('finds common ancestor', () => {
|
||||||
|
const entries = [entry('a', null), entry('b', 'a'), entry('c', 'b'), entry('d', 'b')];
|
||||||
|
expect(findCommonAncestorEntryId(entries, 'c', 'd')).toBe('b');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('groups children and resolves latest labels', () => {
|
||||||
|
const label1: SessionTreeEntry = {
|
||||||
|
type: 'label',
|
||||||
|
id: 'l1',
|
||||||
|
parentId: 'a',
|
||||||
|
timestamp: '2026-04-19T00:00:02.000Z',
|
||||||
|
targetId: 'a',
|
||||||
|
label: 'old',
|
||||||
|
};
|
||||||
|
const label2: SessionTreeEntry = {
|
||||||
|
type: 'label',
|
||||||
|
id: 'l2',
|
||||||
|
parentId: 'l1',
|
||||||
|
timestamp: '2026-04-19T00:00:03.000Z',
|
||||||
|
targetId: 'a',
|
||||||
|
label: 'new',
|
||||||
|
};
|
||||||
|
expect(groupChildrenByParent([entry('a', null), entry('b', 'a')]).__root__).toEqual(['a']);
|
||||||
|
expect(resolveLatestLabels([entry('a', null), label1, label2]).get('a')?.label).toBe('new');
|
||||||
|
expect(buildEntryIndex([entry('a', null)]).get('a')?.id).toBe('a');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **步骤 2:实现 `id.ts` 与 `path.ts`**
|
||||||
|
|
||||||
|
实现要求:
|
||||||
|
|
||||||
|
- `createSessionTreeSessionId()` 使用 `uuidv7()` 或等价时间有序 UUID。
|
||||||
|
- `createSessionTreeEntryId(existingIds)` 生成短 ID,并检查碰撞。
|
||||||
|
- `getPathToLeaf()` 必须检测 missing leaf、missing parent、cycle。
|
||||||
|
- `groupChildrenByParent()` 必须以 append 顺序稳定排序。
|
||||||
|
- `resolveLatestLabels()` 必须以后写 label 覆盖旧 label。
|
||||||
|
|
||||||
|
- [ ] **步骤 3:运行测试确认通过**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --filter @neta/backend test -- --runInBand test/session_tree_path.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
预期:
|
||||||
|
|
||||||
|
- 通过。
|
||||||
|
|
||||||
|
- [ ] **步骤 4:提交变更**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/session-tree/id.ts packages/backend/src/modules/netaclaw/session-tree/path.ts packages/backend/test/session_tree_path.test.ts
|
||||||
|
git commit -m "feat(agent-runtime): add session tree path utilities"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 任务 3:移植上下文构建语义
|
||||||
|
|
||||||
|
**文件:**
|
||||||
|
|
||||||
|
- 新增:`packages/backend/src/modules/netaclaw/session-tree/context_builder.ts`
|
||||||
|
- 测试:`packages/backend/test/session_tree_context_builder.test.ts`
|
||||||
|
|
||||||
|
- [ ] **步骤 1:编写上下文构建测试**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { buildSessionContext } from '../src/modules/netaclaw/session-tree/context_builder.js';
|
||||||
|
import type { SessionTreeEntry } from '../src/modules/netaclaw/session-tree/types.js';
|
||||||
|
|
||||||
|
const message = (id: string, parentId: string | null, role: string, content: string): SessionTreeEntry => ({
|
||||||
|
type: 'message',
|
||||||
|
id,
|
||||||
|
parentId,
|
||||||
|
timestamp: `2026-04-19T00:00:00.000Z`,
|
||||||
|
message: { role, content },
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('session tree context builder', () => {
|
||||||
|
it('walks the active path and excludes sibling branches', () => {
|
||||||
|
const entries = [
|
||||||
|
message('u1', null, 'user', 'root'),
|
||||||
|
message('a1', 'u1', 'assistant', 'branch a'),
|
||||||
|
message('a2', 'u1', 'assistant', 'branch b'),
|
||||||
|
];
|
||||||
|
expect(buildSessionContext(entries, 'a2').messages.map(m => m.content)).toEqual(['root', 'branch b']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles compaction using firstKeptEntryId', () => {
|
||||||
|
const entries: SessionTreeEntry[] = [
|
||||||
|
message('u1', null, 'user', 'old'),
|
||||||
|
message('u2', 'u1', 'user', 'kept'),
|
||||||
|
{
|
||||||
|
type: 'compaction',
|
||||||
|
id: 'c1',
|
||||||
|
parentId: 'u2',
|
||||||
|
timestamp: '2026-04-19T00:00:01.000Z',
|
||||||
|
summary: 'summary',
|
||||||
|
firstKeptEntryId: 'u2',
|
||||||
|
tokensBefore: 1000,
|
||||||
|
},
|
||||||
|
message('a1', 'c1', 'assistant', 'after'),
|
||||||
|
];
|
||||||
|
expect(buildSessionContext(entries, 'a1').messages.map(m => m.role)).toEqual([
|
||||||
|
'compactionSummary',
|
||||||
|
'user',
|
||||||
|
'assistant',
|
||||||
|
]);
|
||||||
|
expect(buildSessionContext(entries, 'a1').messages.map(m => m.content ?? m.summary)).toEqual([
|
||||||
|
'summary',
|
||||||
|
'kept',
|
||||||
|
'after',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tracks thinking and model settings without adding them as messages', () => {
|
||||||
|
const entries: SessionTreeEntry[] = [
|
||||||
|
message('u1', null, 'user', 'hello'),
|
||||||
|
{ type: 'thinking_level_change', id: 't1', parentId: 'u1', timestamp: '2026-04-19T00:00:01.000Z', thinkingLevel: 'high' },
|
||||||
|
{ type: 'model_change', id: 'm1', parentId: 't1', timestamp: '2026-04-19T00:00:02.000Z', provider: 'openai', modelId: 'gpt-x' },
|
||||||
|
];
|
||||||
|
const context = buildSessionContext(entries, 'm1');
|
||||||
|
expect(context.messages).toEqual([{ role: 'user', content: 'hello' }]);
|
||||||
|
expect(context.thinkingLevel).toBe('high');
|
||||||
|
expect(context.model).toEqual({ provider: 'openai', modelId: 'gpt-x' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **步骤 2:实现 `context_builder.ts`**
|
||||||
|
|
||||||
|
实现要求:
|
||||||
|
|
||||||
|
- 以 Pi `buildSessionContext(entries, leafId, byId)` 为基准移植。
|
||||||
|
- 必须先从 leaf 回溯到 root 得到 path。
|
||||||
|
- 必须解析 `thinking_level_change`、`model_change`、assistant message provider/model。
|
||||||
|
- 必须识别 active path 上最新 compaction。
|
||||||
|
- 有 compaction 时输出:compaction summary、`firstKeptEntryId` 到 compaction 前的保留消息、compaction 后消息。
|
||||||
|
- 无 compaction 时输出:message、custom_message、branch_summary。
|
||||||
|
- label、session_info、custom、thinking_level_change、model_change 不直接生成 LLM message。
|
||||||
|
- 返回值包含 `messages`、`thinkingLevel`、`model`、`sourceEntryIds`。
|
||||||
|
|
||||||
|
- [ ] **步骤 3:运行测试确认通过**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --filter @neta/backend test -- --runInBand test/session_tree_context_builder.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
预期:
|
||||||
|
|
||||||
|
- 通过。
|
||||||
|
|
||||||
|
- [ ] **步骤 4:提交变更**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/session-tree/context_builder.ts packages/backend/test/session_tree_context_builder.test.ts
|
||||||
|
git commit -m "feat(agent-runtime): add pi-style session context builder"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 任务 4:定义 Provider 契约与 Snapshot
|
||||||
|
|
||||||
|
**文件:**
|
||||||
|
|
||||||
|
- 新增:`packages/backend/src/modules/netaclaw/session-tree/provider.ts`
|
||||||
|
- 新增:`packages/backend/src/modules/netaclaw/session-tree/snapshot.ts`
|
||||||
|
- 测试:`packages/backend/test/session_tree_provider_contract.test.ts`
|
||||||
|
|
||||||
|
- [ ] **步骤 1:编写 provider contract 工厂**
|
||||||
|
|
||||||
|
契约测试必须作为函数导出,由 file/mysql provider 测试复用:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { SessionTreeProvider } from '../src/modules/netaclaw/session-tree/provider.js';
|
||||||
|
|
||||||
|
export function runSessionTreeProviderContract(name: string, createProvider: () => SessionTreeProvider) {
|
||||||
|
describe(`${name} session tree provider contract`, () => {
|
||||||
|
it('creates a session, appends messages, and advances leaf', async () => {
|
||||||
|
const provider = createProvider();
|
||||||
|
await provider.createSession({ sessionId: 's1', provider: name === 'mysql' ? 'mysql' : 'file', cwd: 'C:/workspace' });
|
||||||
|
const user = await provider.appendMessage('s1', { role: 'user', content: 'hello' });
|
||||||
|
const assistant = await provider.appendMessage('s1', { role: 'assistant', content: 'hi' });
|
||||||
|
expect((await provider.getSession('s1'))?.leafEntryId).toBe(assistant.id);
|
||||||
|
expect((await provider.getActivePath('s1')).map(e => e.id)).toEqual([user.id, assistant.id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('branches without deleting sibling history', async () => {
|
||||||
|
const provider = createProvider();
|
||||||
|
await provider.createSession({ sessionId: 's1', provider: name === 'mysql' ? 'mysql' : 'file' });
|
||||||
|
const root = await provider.appendMessage('s1', { role: 'user', content: 'root' });
|
||||||
|
const branchA = await provider.appendMessage('s1', { role: 'assistant', content: 'a' });
|
||||||
|
await provider.switchLeaf('s1', root.id);
|
||||||
|
const branchB = await provider.appendMessage('s1', { role: 'assistant', content: 'b' });
|
||||||
|
expect((await provider.listEntries('s1')).map(e => e.id).sort()).toEqual([root.id, branchA.id, branchB.id].sort());
|
||||||
|
expect((await provider.getActivePath('s1')).map(e => e.id)).toEqual([root.id, branchB.id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports branch summary, compaction, labels, and resetLeaf', async () => {
|
||||||
|
const provider = createProvider();
|
||||||
|
await provider.createSession({ sessionId: 's1', provider: name === 'mysql' ? 'mysql' : 'file' });
|
||||||
|
const root = await provider.appendMessage('s1', { role: 'user', content: 'root' });
|
||||||
|
await provider.appendLabelChange('s1', root.id, '入口');
|
||||||
|
const kept = await provider.appendMessage('s1', { role: 'user', content: 'kept' });
|
||||||
|
await provider.appendCompaction('s1', { summary: 'summary', firstKeptEntryId: kept.id, tokensBefore: 1000 });
|
||||||
|
await provider.switchLeaf('s1', root.id);
|
||||||
|
const summary = await provider.appendBranchSummary('s1', { branchFromEntryId: root.id, summary: 'branch summary' });
|
||||||
|
expect(summary.type).toBe('branch_summary');
|
||||||
|
await provider.resetLeaf('s1');
|
||||||
|
const newRoot = await provider.appendMessage('s1', { role: 'user', content: 'new root' });
|
||||||
|
expect(newRoot.parentId).toBeNull();
|
||||||
|
expect((await provider.getSnapshot('s1')).labelsByEntryId[root.id]?.label).toBe('入口');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **步骤 2:实现 `provider.ts` 与 `snapshot.ts`**
|
||||||
|
|
||||||
|
Provider 必须包含:
|
||||||
|
|
||||||
|
- `createSession()`
|
||||||
|
- `getSession()`
|
||||||
|
- `updateSession()`
|
||||||
|
- `deleteSession()`
|
||||||
|
- `listEntries()`
|
||||||
|
- `appendEntry()`
|
||||||
|
- `appendMessage()`
|
||||||
|
- `appendThinkingLevelChange()`
|
||||||
|
- `appendModelChange()`
|
||||||
|
- `appendCompaction()`
|
||||||
|
- `appendBranchSummary()`
|
||||||
|
- `appendLabelChange()`
|
||||||
|
- `appendSessionInfo()`
|
||||||
|
- `updateInFlightEntry()`
|
||||||
|
- `switchLeaf()`
|
||||||
|
- `resetLeaf()`
|
||||||
|
- `createBranchedSession()`
|
||||||
|
- `getActivePath()`
|
||||||
|
- `getSnapshot()`
|
||||||
|
|
||||||
|
Snapshot 必须包含:
|
||||||
|
|
||||||
|
- `session`
|
||||||
|
- `entries`
|
||||||
|
- `activePath`
|
||||||
|
- `childrenByParentId`
|
||||||
|
- `labelsByEntryId`
|
||||||
|
- `runtimeContext`
|
||||||
|
|
||||||
|
- [ ] **步骤 3:运行契约 scaffold 测试**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --filter @neta/backend test -- --runInBand test/session_tree_provider_contract.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
预期:
|
||||||
|
|
||||||
|
- 如果只是导出 contract 工厂且未实例化 provider,则通过。
|
||||||
|
|
||||||
|
- [ ] **步骤 4:提交变更**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/session-tree/provider.ts packages/backend/src/modules/netaclaw/session-tree/snapshot.ts packages/backend/test/session_tree_provider_contract.test.ts
|
||||||
|
git commit -m "feat(agent-runtime): define session tree provider contract"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 任务 5:实现 Pi 单文件 JSONL File Provider
|
||||||
|
|
||||||
|
**文件:**
|
||||||
|
|
||||||
|
- 新增:`packages/backend/src/modules/netaclaw/session-tree/pi_session_file.ts`
|
||||||
|
- 新增:`packages/backend/src/modules/netaclaw/session-tree/file_provider.ts`
|
||||||
|
- 测试:`packages/backend/test/session_tree_file_provider.test.ts`
|
||||||
|
|
||||||
|
- [ ] **步骤 1:编写 file provider 测试**
|
||||||
|
|
||||||
|
测试必须验证真实文件协议:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { FileSessionTreeProvider } from '../src/modules/netaclaw/session-tree/file_provider.js';
|
||||||
|
import { runSessionTreeProviderContract } from './session_tree_provider_contract.js';
|
||||||
|
|
||||||
|
describe('FileSessionTreeProvider', () => {
|
||||||
|
let dir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
dir = mkdtempSync(join(tmpdir(), 'neta-session-tree-'));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
rmSync(dir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
runSessionTreeProviderContract('file', () => new FileSessionTreeProvider({ rootDir: dir }));
|
||||||
|
|
||||||
|
it('stores one JSONL file with session header as first line', async () => {
|
||||||
|
const provider = new FileSessionTreeProvider({ rootDir: dir });
|
||||||
|
await provider.createSession({ sessionId: 's1', provider: 'file', cwd: 'C:/workspace' });
|
||||||
|
await provider.appendMessage('s1', { role: 'user', content: 'hello' });
|
||||||
|
const session = await provider.getSession('s1');
|
||||||
|
const content = readFileSync(session!.sessionFile!, 'utf-8').trim().split(/\r?\n/).map(line => JSON.parse(line));
|
||||||
|
expect(content[0].type).toBe('session');
|
||||||
|
expect(content[1].type).toBe('message');
|
||||||
|
expect(content[1].parentId).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **步骤 2:实现 `pi_session_file.ts`**
|
||||||
|
|
||||||
|
实现要求:
|
||||||
|
|
||||||
|
- 迁移 Pi 的 `parseSessionEntries()`、`loadEntriesFromFile()`、`getTree()`、`branch()`、`resetLeaf()`、`branchWithSummary()`、`createBranchedSession()` 思路。
|
||||||
|
- 单 session 对应一个 `.jsonl` 文件。
|
||||||
|
- header 第一行只写一次。
|
||||||
|
- append entry 只追加一行。
|
||||||
|
- label 通过 label entry 解析,不修改 target entry。
|
||||||
|
- `updateInFlightEntry()` 不得频繁重写整个文件;首版可以将 in-flight entry 在内存中合并,完成时追加 completed entry。
|
||||||
|
|
||||||
|
- [ ] **步骤 3:实现 `file_provider.ts`**
|
||||||
|
|
||||||
|
实现要求:
|
||||||
|
|
||||||
|
- `FileSessionTreeProvider` 只做 `SessionTreeProvider` 到 `pi_session_file.ts` 的适配。
|
||||||
|
- `appendMessage()` 等语义方法必须调用同一 append entry 通道。
|
||||||
|
- `createBranchedSession()` 必须复制 root 到 leaf 路径并保留路径内 label。
|
||||||
|
|
||||||
|
- [ ] **步骤 4:运行测试确认通过**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --filter @neta/backend test -- --runInBand test/session_tree_file_provider.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
预期:
|
||||||
|
|
||||||
|
- 通过。
|
||||||
|
|
||||||
|
- [ ] **步骤 5:提交变更**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/session-tree/pi_session_file.ts packages/backend/src/modules/netaclaw/session-tree/file_provider.ts packages/backend/test/session_tree_file_provider.test.ts
|
||||||
|
git commit -m "feat(agent-runtime): add pi-style file session provider"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 任务 6:新增 MySQL 实体并通过 MCP 更新数据库
|
||||||
|
|
||||||
|
**文件:**
|
||||||
|
|
||||||
|
- 新增:`packages/backend/src/modules/netaclaw/entity/agent_session.ts`
|
||||||
|
- 新增:`packages/backend/src/modules/netaclaw/entity/agent_session_entry.ts`
|
||||||
|
- 修改:`packages/backend/src/entities.ts`
|
||||||
|
- 测试:`packages/backend/test/entity_exports.test.ts`
|
||||||
|
|
||||||
|
- [ ] **步骤 1:扩展实体导出测试**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { entities } from '../src/entities.js';
|
||||||
|
import { NetaClawAgentSessionEntity } from '../src/modules/netaclaw/entity/agent_session.js';
|
||||||
|
import { NetaClawAgentSessionEntryEntity } from '../src/modules/netaclaw/entity/agent_session_entry.js';
|
||||||
|
|
||||||
|
describe('entities exports', () => {
|
||||||
|
it('exports session tree entities', () => {
|
||||||
|
expect(entities).toContain(NetaClawAgentSessionEntity);
|
||||||
|
expect(entities).toContain(NetaClawAgentSessionEntryEntity);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **步骤 2:新增实体**
|
||||||
|
|
||||||
|
实体要求:
|
||||||
|
|
||||||
|
- `agent_session.ts` 字段:`sessionId`、`provider`、`rootEntryId`、`leafEntryId`、`cwd`、`sessionFile`、`parentSessionId`、`agentId`、`userId`、`title`、`status`、`metadata`。
|
||||||
|
- `agent_session_entry.ts` 字段:`sessionId`、`entryId`、`parentEntryId`、`entrySeq`、`type`、`message`、`summary`、`firstKeptEntryId`、`tokensBefore`、`thinkingLevel`、`provider`、`modelId`、`customType`、`data`、`display`、`targetEntryId`、`label`、`details`、`sourceRuntime`、`status`。
|
||||||
|
- 两个实体继承 `packages/backend/src/modules/base/entity/base.ts` 中的 `BaseEntity`。
|
||||||
|
- `sessionId + entryId` 必须唯一。
|
||||||
|
- `entrySeq` 用于稳定排序,不能只依赖 `createTime` 字符串。
|
||||||
|
|
||||||
|
- [ ] **步骤 3:更新实体导出**
|
||||||
|
|
||||||
|
在 `packages/backend/src/entities.ts` 中按现有风格导入并展开两个实体模块。
|
||||||
|
|
||||||
|
- [ ] **步骤 4:使用 MCP 修改数据库**
|
||||||
|
|
||||||
|
执行要求:
|
||||||
|
|
||||||
|
- 使用 MCP MySQL 工具直接删除旧历史会话/消息数据。
|
||||||
|
- 使用 MCP MySQL 工具创建或重建 `neta_agent_session` 与 `neta_agent_session_entry`。
|
||||||
|
- 不新增 SQL migration 文件作为主路径。
|
||||||
|
- 修改后用 MCP `describe_table` 确认字段存在。
|
||||||
|
|
||||||
|
- [ ] **步骤 5:运行测试确认通过**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --filter @neta/backend test -- --runInBand test/entity_exports.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
预期:
|
||||||
|
|
||||||
|
- 通过。
|
||||||
|
|
||||||
|
- [ ] **步骤 6:提交变更**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/entity/agent_session.ts packages/backend/src/modules/netaclaw/entity/agent_session_entry.ts packages/backend/src/entities.ts packages/backend/test/entity_exports.test.ts
|
||||||
|
git commit -m "feat(agent-runtime): add session tree mysql entities"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 任务 7:实现 MySQL Provider
|
||||||
|
|
||||||
|
**文件:**
|
||||||
|
|
||||||
|
- 新增:`packages/backend/src/modules/netaclaw/session-tree/mysql_provider.ts`
|
||||||
|
- 测试:`packages/backend/test/session_tree_mysql_provider.test.ts`
|
||||||
|
|
||||||
|
- [ ] **步骤 1:编写 MySQL provider 测试**
|
||||||
|
|
||||||
|
测试要求:
|
||||||
|
|
||||||
|
- 复用 `runSessionTreeProviderContract('mysql', ...)`。
|
||||||
|
- 使用 repository mock 或 Midway mock repository。
|
||||||
|
- 验证 `entrySeq` 稳定排序。
|
||||||
|
- 验证 `message` JSON 无损保存。
|
||||||
|
- 验证 compaction 与 file provider 的 context 结果一致。
|
||||||
|
|
||||||
|
- [ ] **步骤 2:实现 `mysql_provider.ts`**
|
||||||
|
|
||||||
|
实现要求:
|
||||||
|
|
||||||
|
- MySQL provider 是 Pi entry log 的表映射,不是独立语义。
|
||||||
|
- `appendEntry()` 创建新 row,默认推进 leaf。
|
||||||
|
- `appendBranchSummary()` 等价于 Pi `branchWithSummary()`。
|
||||||
|
- `appendLabelChange()` 追加 label entry,不修改 target entry。
|
||||||
|
- `createBranchedSession()` 复制 root 到 leaf path 到新 session。
|
||||||
|
- completed entry 禁止普通 update。
|
||||||
|
|
||||||
|
- [ ] **步骤 3:运行测试确认通过**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --filter @neta/backend test -- --runInBand test/session_tree_mysql_provider.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
预期:
|
||||||
|
|
||||||
|
- 通过。
|
||||||
|
|
||||||
|
- [ ] **步骤 4:提交变更**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/session-tree/mysql_provider.ts packages/backend/test/session_tree_mysql_provider.test.ts
|
||||||
|
git commit -m "feat(agent-runtime): add mysql session tree provider"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 任务 8:接入 Agent 配置中的 Session Provider
|
||||||
|
|
||||||
|
**文件:**
|
||||||
|
|
||||||
|
- 新增:`packages/backend/src/modules/netaclaw/session-tree/provider_factory.ts`
|
||||||
|
- 修改:`packages/backend/src/modules/netaclaw/entity/agent.ts`
|
||||||
|
- 测试:`packages/backend/test/session_tree_provider_factory.test.ts`
|
||||||
|
|
||||||
|
- [ ] **步骤 1:编写 provider factory 测试**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { resolveSessionProviderKind } from '../src/modules/netaclaw/session-tree/provider_factory.js';
|
||||||
|
|
||||||
|
describe('session tree provider factory', () => {
|
||||||
|
it('defaults to file provider', () => {
|
||||||
|
expect(resolveSessionProviderKind(null)).toBe('file');
|
||||||
|
expect(resolveSessionProviderKind({})).toBe('file');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts file and mysql only', () => {
|
||||||
|
expect(resolveSessionProviderKind({ sessionProvider: 'file' })).toBe('file');
|
||||||
|
expect(resolveSessionProviderKind({ sessionProvider: 'mysql' })).toBe('mysql');
|
||||||
|
expect(resolveSessionProviderKind({ sessionProvider: 'redis' })).toBe('file');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **步骤 2:实现 provider factory 与 agent 字段**
|
||||||
|
|
||||||
|
实现要求:
|
||||||
|
|
||||||
|
- `resolveSessionProviderKind()` 只接受 `file` 与 `mysql`。
|
||||||
|
- `NetaClawAgentEntity` 新增 `sessionProvider` 字段,默认 `file`。
|
||||||
|
- 字段注释使用中文,避免现有乱码注释继续扩散。
|
||||||
|
|
||||||
|
- [ ] **步骤 3:运行测试确认通过**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --filter @neta/backend test -- --runInBand test/session_tree_provider_factory.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
预期:
|
||||||
|
|
||||||
|
- 通过。
|
||||||
|
|
||||||
|
- [ ] **步骤 4:提交变更**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/session-tree/provider_factory.ts packages/backend/src/modules/netaclaw/entity/agent.ts packages/backend/test/session_tree_provider_factory.test.ts
|
||||||
|
git commit -m "feat(agent-runtime): add session provider selection"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 任务 9:运行聚焦验证
|
||||||
|
|
||||||
|
**文件:**
|
||||||
|
|
||||||
|
- 不预期修改源码。
|
||||||
|
|
||||||
|
- [ ] **步骤 1:运行新 session-tree 测试**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --filter @neta/backend test -- --runInBand test/session_tree_types.test.ts test/session_tree_path.test.ts test/session_tree_context_builder.test.ts test/session_tree_file_provider.test.ts test/session_tree_mysql_provider.test.ts test/session_tree_provider_factory.test.ts test/entity_exports.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
预期:
|
||||||
|
|
||||||
|
- 全部通过。
|
||||||
|
|
||||||
|
- [ ] **步骤 2:运行相关旧测试**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --filter @neta/backend test -- --runInBand test/netaclaw_session.test.ts test/chat_orchestrator.test.ts test/subagent_service.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
预期:
|
||||||
|
|
||||||
|
- 全部通过,或明确记录旧接口将被后续计划替换导致的失败原因。
|
||||||
|
|
||||||
|
- [ ] **步骤 3:运行后端构建**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --filter @neta/backend build
|
||||||
|
```
|
||||||
|
|
||||||
|
预期:
|
||||||
|
|
||||||
|
- 构建成功。
|
||||||
|
|
||||||
|
- [ ] **步骤 4:检查工作区**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git status --short
|
||||||
|
```
|
||||||
|
|
||||||
|
预期:
|
||||||
|
|
||||||
|
- 只包含本任务相关变更。
|
||||||
|
|
||||||
|
## 自检
|
||||||
|
|
||||||
|
- 设计覆盖:本计划覆盖运行时内核子集,包括 Pi 兼容 entry tree、file provider、MySQL provider、context builder、snapshot 和 agent provider 选择。
|
||||||
|
- 直接复用:明确要求移植 Pi `SessionManager`、context builder、branch、compaction 关键语义。
|
||||||
|
- 范围控制:不包含对话 WebSocket 协议、前端 UI、subagent 进程编排、skill/tool/model 资源层、管理后台页面和视觉系统。
|
||||||
|
- 占位内容检查:没有 `TBD`、`TODO`、`implement later`。
|
||||||
|
- 类型一致性:统一使用 `id/parentId` 表达 Pi entry,使用 `sessionId/rootEntryId/leafEntryId` 表达 Neta session meta。
|
||||||
|
- 架构防错:明确禁止 `session.json + entries.jsonl`、禁止 completed entry 任意 update、禁止把 tool_call/tool_result 作为第一版事实源 entry。
|
||||||
@ -0,0 +1,428 @@
|
|||||||
|
# Neta Pi-First Agent 平台实施计划套件
|
||||||
|
|
||||||
|
> **给自动化实施代理:** 必须使用子技能:`superpowers:subagent-driven-development`(推荐)或 `superpowers:executing-plans`,按任务逐项实施本计划。步骤使用复选框(`- [ ]`)语法跟踪进度。
|
||||||
|
|
||||||
|
**目标:** 将 `Neta Pi-First Agent Platform 重构设计` 拆成可独立规划、独立验证、按依赖顺序推进的实施计划套件。
|
||||||
|
|
||||||
|
**架构:** 总体重构拆成 6 个计划:运行时内核、会话通信与对话页、subagent 进程隔离、skill/tool/model 资源层、管理后台 UI、平台 UI 设计系统。所有计划共享同一事实源:树状 session entry graph、双 provider、完整流式事件、统一资源模型。
|
||||||
|
|
||||||
|
**技术栈:** NestJS/Midway 后端、TypeORM/MySQL、文件化 JSONL session provider、Vue 3 + Vite + Pinia 前端、WebSocket/Socket.IO、pnpm monorepo、Jest。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 范围拆分
|
||||||
|
|
||||||
|
总设计文档:
|
||||||
|
|
||||||
|
- `docs/superpowers/specs/2026-04-19-neta-pi-first-agent-runtime-design.md`
|
||||||
|
|
||||||
|
该设计已经覆盖以下独立子系统:
|
||||||
|
|
||||||
|
- Agent 运行时内核
|
||||||
|
- 对话工作区
|
||||||
|
- Subagent 进程编排器
|
||||||
|
- Skill / Tool / Model 资源层
|
||||||
|
- 管理后台适配
|
||||||
|
- 平台 UI 设计系统
|
||||||
|
|
||||||
|
这些子系统不应塞进同一个实施计划。每个子系统都需要单独计划、单独测试、单独提交。
|
||||||
|
|
||||||
|
## 计划 1:Agent 运行时内核
|
||||||
|
|
||||||
|
**计划文件:** `docs/superpowers/plans/2026-04-19-neta-agent-runtime-kernel.md`
|
||||||
|
|
||||||
|
**目标:** 建立树状 session entry graph、`SessionTreeProvider` 抽象、默认 file provider、MySQL provider 契约,以及 leaf 驱动上下文重建。
|
||||||
|
|
||||||
|
**负责范围:**
|
||||||
|
|
||||||
|
- 后端 session tree 类型
|
||||||
|
- file/mysql 双 provider
|
||||||
|
- provider 契约测试
|
||||||
|
- 上下文构建器
|
||||||
|
- entry graph 快照
|
||||||
|
- agent 配置中的 session provider 选择
|
||||||
|
|
||||||
|
**主要后端文件:**
|
||||||
|
|
||||||
|
- `packages/backend/src/modules/netaclaw/session-tree/types.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/session-tree/provider.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/session-tree/file_provider.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/session-tree/mysql_provider.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/session-tree/context_builder.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/session-tree/snapshot.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/entity/agent_session.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/entity/agent_session_entry.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/entity/agent.ts`
|
||||||
|
|
||||||
|
**主要测试:**
|
||||||
|
|
||||||
|
- `packages/backend/src/modules/netaclaw/session-tree/session_tree_provider.contract.spec.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/session-tree/context_builder.spec.ts`
|
||||||
|
|
||||||
|
**Pi 参考源码:**
|
||||||
|
|
||||||
|
- `C:/Users/lixin/Desktop/RZYX_ZT/pi-mono-main/packages/coding-agent/src/core/session-manager.ts`
|
||||||
|
- `C:/Users/lixin/Desktop/RZYX_ZT/pi-mono-main/packages/coding-agent/src/core/compaction/`
|
||||||
|
|
||||||
|
**可直接复用或高强度移植:**
|
||||||
|
|
||||||
|
- `SessionHeader` / `SessionEntry` union 的结构设计。
|
||||||
|
- 单文件 JSONL session 协议。
|
||||||
|
- `buildSessionContext()` 的 leaf-to-root 路径构建、compaction、branch_summary、custom_message、thinking/model setting 处理。
|
||||||
|
- `branch()`、`resetLeaf()`、`branchWithSummary()`、`createBranchedSession()` 的纯逻辑。
|
||||||
|
- compaction 的 `firstKeptEntryId`、split turn、previous summary 合并策略。
|
||||||
|
|
||||||
|
**不能简化掉的 Pi 语义:**
|
||||||
|
|
||||||
|
- 不得把 file provider 改成 `session.json + entries.jsonl` 双文件。
|
||||||
|
- 不得把 `tool_call/tool_result` 强行拆成独立 entry 而丢掉 Pi `AgentMessage` 结构。
|
||||||
|
- 不得只用 `content/summary` 字段表示 message,必须保留完整 message payload。
|
||||||
|
- 不得把 completed entry 当普通数据库记录随意 update。
|
||||||
|
|
||||||
|
**依赖:** 无。
|
||||||
|
|
||||||
|
**阻塞:**
|
||||||
|
|
||||||
|
- 计划 2 的对话协议
|
||||||
|
- 计划 3 的 subagent 树节点
|
||||||
|
- 计划 4 中与运行时绑定的资源策略
|
||||||
|
- 计划 5 的 Agent 会话管理页面
|
||||||
|
|
||||||
|
**完成定义:**
|
||||||
|
|
||||||
|
- `file` provider 可以创建会话、追加 entry、创建分支、切换 leaf 并构建上下文。
|
||||||
|
- `mysql` provider 暴露完全相同的契约。
|
||||||
|
- 契约测试同时覆盖两个 provider。
|
||||||
|
- 契约测试覆盖 Pi 核心语义:单文件 JSONL、branch/resetLeaf、branch summary、compaction firstKeptEntryId、label、session_info、thinking/model change。
|
||||||
|
- 不实现旧历史数据兼容。
|
||||||
|
|
||||||
|
## 计划 2:对话协议与 UI
|
||||||
|
|
||||||
|
**计划文件:** `docs/superpowers/plans/2026-04-19-neta-conversation-workspace.md`
|
||||||
|
|
||||||
|
**目标:** 将 Agent 对话页改为聊天为主、树为辅的 Conversation Workspace,并实现 snapshot + realtime patch + 完整流式通信。
|
||||||
|
|
||||||
|
**负责范围:**
|
||||||
|
|
||||||
|
- session snapshot API
|
||||||
|
- realtime patch 协议
|
||||||
|
- assistant/tool/subagent 完整流式事件
|
||||||
|
- Pinia `SessionTreeStore`
|
||||||
|
- active path 投影
|
||||||
|
- 树侧栏
|
||||||
|
- 对话子组件重构
|
||||||
|
- task planning / Todo / thinking / clarify 运行时组件
|
||||||
|
|
||||||
|
**主要后端文件:**
|
||||||
|
|
||||||
|
- `packages/backend/src/modules/netaclaw/gateway/protocol.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/gateway/server.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/controller/session.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/service/chat_orchestrator.ts`
|
||||||
|
|
||||||
|
**主要前端文件:**
|
||||||
|
|
||||||
|
- `packages/frontend/src/modules/agent/store/session-tree.ts`
|
||||||
|
- `packages/frontend/src/modules/agent/types/session-tree.ts`
|
||||||
|
- `packages/frontend/src/modules/agent/hooks/session-tree-socket.ts`
|
||||||
|
- `packages/frontend/src/modules/agent/views/chat.vue`
|
||||||
|
- `packages/frontend/src/modules/agent/components/conversation/ConversationWorkspace.vue`
|
||||||
|
- `packages/frontend/src/modules/agent/components/conversation/ConversationEntryList.vue`
|
||||||
|
- `packages/frontend/src/modules/agent/components/conversation/StreamingAssistantPanel.vue`
|
||||||
|
- `packages/frontend/src/modules/agent/components/conversation/StreamingToolPanel.vue`
|
||||||
|
- `packages/frontend/src/modules/agent/components/conversation/StreamingSubagentPanel.vue`
|
||||||
|
- `packages/frontend/src/modules/agent/components/conversation/PlanningPanel.vue`
|
||||||
|
- `packages/frontend/src/modules/agent/components/conversation/ThinkingPanel.vue`
|
||||||
|
- `packages/frontend/src/modules/agent/components/conversation/ClarifyPanel.vue`
|
||||||
|
- `packages/frontend/src/modules/agent/components/conversation/ConversationComposer.vue`
|
||||||
|
- `packages/frontend/src/modules/agent/components/session-tree/SessionTreeSidebar.vue`
|
||||||
|
- `packages/frontend/src/modules/agent/components/session-tree/SessionTreeNode.vue`
|
||||||
|
|
||||||
|
**Pi 参考源码:**
|
||||||
|
|
||||||
|
- `C:/Users/lixin/Desktop/RZYX_ZT/pi-mono-main/packages/web-ui/src/components/AgentInterface.ts`
|
||||||
|
- `C:/Users/lixin/Desktop/RZYX_ZT/pi-mono-main/packages/web-ui/src/components/StreamingMessageContainer.ts`
|
||||||
|
- `C:/Users/lixin/Desktop/RZYX_ZT/pi-mono-main/packages/web-ui/src/ChatPanel.ts`
|
||||||
|
- `C:/Users/lixin/Desktop/RZYX_ZT/pi-mono-main/packages/coding-agent/src/modes/interactive/components/tree-selector.ts`
|
||||||
|
|
||||||
|
**依赖:** 计划 1。
|
||||||
|
|
||||||
|
**阻塞:**
|
||||||
|
|
||||||
|
- 计划 3 的 subagent 流式 UI
|
||||||
|
- 计划 5 的会话诊断能力
|
||||||
|
- 计划 6 的最终视觉统一
|
||||||
|
|
||||||
|
**完成定义:**
|
||||||
|
|
||||||
|
- 对话页不再把线性消息列表作为前端事实源。
|
||||||
|
- 首次加载使用 snapshot。
|
||||||
|
- 运行时更新使用 patch 事件。
|
||||||
|
- Assistant 文本、tool 状态、tool 结果和 subagent 事件可以独立流式更新。
|
||||||
|
- 树侧栏可以切换 leaf,并从历史 user 节点继续对话。
|
||||||
|
|
||||||
|
## 计划 3:子 Agent 进程编排器
|
||||||
|
|
||||||
|
**计划文件:** `docs/superpowers/plans/2026-04-19-neta-subagent-process-orchestrator.md`
|
||||||
|
|
||||||
|
**目标:** 将 session subagent 从同进程软隔离改为 Pi 风格进程级隔离,并把 subagent run/event/result 映射为正式 session tree 节点。
|
||||||
|
|
||||||
|
**负责范围:**
|
||||||
|
|
||||||
|
- subagent worker 入口
|
||||||
|
- 子进程启动与生命周期
|
||||||
|
- task envelope schema
|
||||||
|
- JSONL 事件解析器
|
||||||
|
- run/event 持久化
|
||||||
|
- 取消与并发限制
|
||||||
|
- subagent batch/result 树节点
|
||||||
|
|
||||||
|
**主要后端文件:**
|
||||||
|
|
||||||
|
- `packages/backend/src/modules/netaclaw/subprocess/types.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/subprocess/orchestrator.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/subprocess/jsonl.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/subprocess/worker.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/entity/subprocess_run.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/entity/subprocess_event.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/service/subagent.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/tools/builtin/delegate_task.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/tools/builtin/delegate_parallel.ts`
|
||||||
|
|
||||||
|
**Pi 参考源码:**
|
||||||
|
|
||||||
|
- `C:/Users/lixin/Desktop/RZYX_ZT/pi-mono-main/packages/coding-agent/examples/extensions/subagent/index.ts`
|
||||||
|
- `C:/Users/lixin/Desktop/RZYX_ZT/pi-mono-main/packages/coding-agent/examples/extensions/subagent/agents.ts`
|
||||||
|
|
||||||
|
**依赖:** 计划 1 与计划 2 的事件协议。
|
||||||
|
|
||||||
|
**阻塞:**
|
||||||
|
|
||||||
|
- 计划 4 的 subagent skill/tool policy 集成
|
||||||
|
- 计划 5 的管理后台 subagent 诊断能力
|
||||||
|
|
||||||
|
**完成定义:**
|
||||||
|
|
||||||
|
- 委派任务运行在独立子进程中。
|
||||||
|
- 父进程接收 JSONL 事件。
|
||||||
|
- 子进程不能直接修改父进程的 session provider。
|
||||||
|
- Subagent 事件可以流式推送到前端,并持久化用于回放。
|
||||||
|
|
||||||
|
## 计划 4:Skill / Tool / Model 资源层
|
||||||
|
|
||||||
|
**计划文件:** `docs/superpowers/plans/2026-04-19-neta-resource-layer.md`
|
||||||
|
|
||||||
|
**目标:** 将 skill、tool、model 能力统一为 platform resource,并适配主 agent 与 subagent runtime policy。
|
||||||
|
|
||||||
|
**负责范围:**
|
||||||
|
|
||||||
|
- skill resource registry
|
||||||
|
- Pi 风格 skill discovery
|
||||||
|
- sourceInfo/fingerprint/scope
|
||||||
|
- tool catalog 与 runtime policy 分离
|
||||||
|
- model capability 元数据
|
||||||
|
- runtime policy resolver
|
||||||
|
|
||||||
|
**主要后端文件:**
|
||||||
|
|
||||||
|
- `packages/backend/src/modules/netaclaw/resource/types.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/resource/skill_discovery.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/resource/skill_registry.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/resource/tool_policy.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/resource/model_capability.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/entity/skill_resource.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/service/tool_resolver.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/service/model_channel.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/service/skill_loader.ts`
|
||||||
|
|
||||||
|
**Pi 参考源码:**
|
||||||
|
|
||||||
|
- `C:/Users/lixin/Desktop/RZYX_ZT/pi-mono-main/packages/coding-agent/src/core/skills.ts`
|
||||||
|
- `C:/Users/lixin/Desktop/RZYX_ZT/pi-mono-main/packages/coding-agent/src/core/source-info.ts`
|
||||||
|
- `C:/Users/lixin/Desktop/RZYX_ZT/pi-mono-main/packages/web-ui/src/tools/renderer-registry.ts`
|
||||||
|
|
||||||
|
**依赖:** 计划 1;subagent policy 集成依赖计划 3。
|
||||||
|
|
||||||
|
**阻塞:**
|
||||||
|
|
||||||
|
- 计划 5 的 skill/tool/model 管理页面
|
||||||
|
|
||||||
|
**完成定义:**
|
||||||
|
|
||||||
|
- Skill discovery 在命中 `SKILL.md` 根目录后停止继续递归。
|
||||||
|
- 资源记录暴露 source、scope、sourceInfo、fingerprint。
|
||||||
|
- Tool 可用性可以分别为主 agent 和 subagent 解析。
|
||||||
|
- Model capability 可供运行时和管理后台页面使用。
|
||||||
|
|
||||||
|
## 计划 5:管理后台适配
|
||||||
|
|
||||||
|
**计划文件:** `docs/superpowers/plans/2026-04-19-neta-admin-console-adaptation.md`
|
||||||
|
|
||||||
|
**目标:** 将 skill 管理、tool 管理、agent 管理、agent 会话、模型管理页面适配新平台资源模型,并统一为 Resource Workbench / Agent Structure Canvas 交互。
|
||||||
|
|
||||||
|
**负责范围:**
|
||||||
|
|
||||||
|
- Skill 管理页面
|
||||||
|
- Tool 管理页面
|
||||||
|
- Agent 管理页面
|
||||||
|
- Agent 会话管理页面
|
||||||
|
- 模型管理页面
|
||||||
|
- Agent Structure Canvas
|
||||||
|
- Resource Workbench 交互模式
|
||||||
|
|
||||||
|
**主要前端文件:**
|
||||||
|
|
||||||
|
- `packages/frontend/src/modules/agent/views/skills.vue`
|
||||||
|
- `packages/frontend/src/modules/agent/views/tools.vue`
|
||||||
|
- `packages/frontend/src/modules/agent/views/agent-list.vue`
|
||||||
|
- `packages/frontend/src/modules/agent/views/agent-edit.vue`
|
||||||
|
- `packages/frontend/src/modules/agent/views/model-channel.vue`
|
||||||
|
- `packages/frontend/src/modules/agent/components/admin/ResourceWorkbench.vue`
|
||||||
|
- `packages/frontend/src/modules/agent/components/admin/ResourceDetailPanel.vue`
|
||||||
|
- `packages/frontend/src/modules/agent/components/admin/AgentStructureCanvas.vue`
|
||||||
|
- `packages/frontend/src/modules/agent/components/admin/AgentCanvasNode.vue`
|
||||||
|
- `packages/frontend/src/modules/agent/components/admin/AgentCanvasInspector.vue`
|
||||||
|
|
||||||
|
**主要后端文件:**
|
||||||
|
|
||||||
|
- `packages/backend/src/modules/netaclaw/controller/admin/skill.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/controller/admin/tool.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/controller/admin/model_channel.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/controller/agent.ts`
|
||||||
|
- `packages/backend/src/modules/netaclaw/controller/session.ts`
|
||||||
|
|
||||||
|
**依赖:** 计划 1 与计划 4。Agent 会话页面还依赖计划 2。
|
||||||
|
|
||||||
|
**阻塞:** 不直接阻塞其他计划,但平台发布前必须完成。
|
||||||
|
|
||||||
|
**完成定义:**
|
||||||
|
|
||||||
|
- Agent 编辑不再依赖旧的抽屉式复杂配置流程。
|
||||||
|
- Agent 管理使用 Agent Structure Canvas 编排 system prompt、model、session provider、memory、tools、skills、subagents。
|
||||||
|
- Skill/tool/model/session 页面共享 Resource Workbench 布局和状态语言。
|
||||||
|
- 管理后台页面暴露 provider、sourceInfo、runtime status、model capability 和 policy diagnostics。
|
||||||
|
|
||||||
|
## 计划 6:平台 UI 设计系统
|
||||||
|
|
||||||
|
**计划文件:** `docs/superpowers/plans/2026-04-19-neta-platform-ui-design-system.md`
|
||||||
|
|
||||||
|
**目标:** 统一 Agent 平台 UI 视觉语言,形成专业后台型 + 轻科技感的设计系统,并系统治理显示 bug。
|
||||||
|
|
||||||
|
**负责范围:**
|
||||||
|
|
||||||
|
- Conversation Workspace 视觉语言
|
||||||
|
- Resource Workbench 视觉语言
|
||||||
|
- Agent Structure Canvas 视觉语言
|
||||||
|
- 运行时状态 token
|
||||||
|
- 流式状态 token
|
||||||
|
- 空状态、加载态、错误态、重试态、取消态
|
||||||
|
- 显示问题审计与清理
|
||||||
|
|
||||||
|
**主要前端文件:**
|
||||||
|
|
||||||
|
- `packages/frontend/src/modules/agent/styles/platform-ui.scss`
|
||||||
|
- `packages/frontend/src/modules/agent/components/ui/RuntimeBadge.vue`
|
||||||
|
- `packages/frontend/src/modules/agent/components/ui/StatusPill.vue`
|
||||||
|
- `packages/frontend/src/modules/agent/components/ui/MetricStrip.vue`
|
||||||
|
- `packages/frontend/src/modules/agent/components/ui/EmptyState.vue`
|
||||||
|
- `packages/frontend/src/modules/agent/components/ui/ErrorState.vue`
|
||||||
|
- `packages/frontend/src/modules/agent/components/ui/SectionSurface.vue`
|
||||||
|
- `packages/frontend/src/modules/agent/components/ui/NodeSurface.vue`
|
||||||
|
|
||||||
|
**依赖:** 计划 2 与计划 5。
|
||||||
|
|
||||||
|
**阻塞:** 最终发布前的视觉打磨。
|
||||||
|
|
||||||
|
**完成定义:**
|
||||||
|
|
||||||
|
- Agent 相关页面不再像不同产品拼接在一起。
|
||||||
|
- 复杂 Agent 编辑移除传统后台抽屉式交互。
|
||||||
|
- 运行时子组件共享同一套间距、状态和容器语言。
|
||||||
|
- 对话页和管理页的已知显示问题要么修复,要么记录为明确的后续任务。
|
||||||
|
|
||||||
|
## 执行顺序
|
||||||
|
|
||||||
|
- [ ] **步骤 1:创建第 1 份详细计划**
|
||||||
|
|
||||||
|
写入 `docs/superpowers/plans/2026-04-19-neta-agent-runtime-kernel.md`。
|
||||||
|
|
||||||
|
预期内容:
|
||||||
|
|
||||||
|
- session tree 类型
|
||||||
|
- provider 接口
|
||||||
|
- file provider 实现任务
|
||||||
|
- MySQL provider 实现任务
|
||||||
|
- provider 契约测试
|
||||||
|
- context builder 测试
|
||||||
|
- agent 配置集成
|
||||||
|
|
||||||
|
- [ ] **步骤 2:创建第 2 份详细计划**
|
||||||
|
|
||||||
|
写入 `docs/superpowers/plans/2026-04-19-neta-conversation-workspace.md`。
|
||||||
|
|
||||||
|
预期内容:
|
||||||
|
|
||||||
|
- snapshot API 任务
|
||||||
|
- websocket patch 协议任务
|
||||||
|
- streaming event 任务
|
||||||
|
- 前端 session tree store 任务
|
||||||
|
- conversation workspace 组件任务
|
||||||
|
- tree sidebar 任务
|
||||||
|
- runtime child component 任务
|
||||||
|
|
||||||
|
- [ ] **步骤 3:创建第 3 份详细计划**
|
||||||
|
|
||||||
|
写入 `docs/superpowers/plans/2026-04-19-neta-subagent-process-orchestrator.md`。
|
||||||
|
|
||||||
|
预期内容:
|
||||||
|
|
||||||
|
- worker 入口
|
||||||
|
- spawn 生命周期
|
||||||
|
- envelope schema
|
||||||
|
- JSONL parser
|
||||||
|
- run/event 持久化
|
||||||
|
- delegate tool 集成
|
||||||
|
- 取消与并发测试
|
||||||
|
|
||||||
|
- [ ] **步骤 4:创建第 4 份详细计划**
|
||||||
|
|
||||||
|
写入 `docs/superpowers/plans/2026-04-19-neta-resource-layer.md`。
|
||||||
|
|
||||||
|
预期内容:
|
||||||
|
|
||||||
|
- skill discovery 与 registry
|
||||||
|
- tool policy resolver
|
||||||
|
- model capability 元数据
|
||||||
|
- runtime resource 绑定
|
||||||
|
- subagent policy 集成
|
||||||
|
|
||||||
|
- [ ] **步骤 5:创建第 5 份详细计划**
|
||||||
|
|
||||||
|
写入 `docs/superpowers/plans/2026-04-19-neta-admin-console-adaptation.md`。
|
||||||
|
|
||||||
|
预期内容:
|
||||||
|
|
||||||
|
- Resource Workbench 外壳
|
||||||
|
- Skill 页面适配
|
||||||
|
- Tool 页面适配
|
||||||
|
- Agent Structure Canvas
|
||||||
|
- Agent 会话页面适配
|
||||||
|
- 模型页面适配
|
||||||
|
|
||||||
|
- [ ] **步骤 6:创建第 6 份详细计划**
|
||||||
|
|
||||||
|
写入 `docs/superpowers/plans/2026-04-19-neta-platform-ui-design-system.md`。
|
||||||
|
|
||||||
|
预期内容:
|
||||||
|
|
||||||
|
- 视觉 token
|
||||||
|
- 共享 UI 容器
|
||||||
|
- runtime status 组件
|
||||||
|
- canvas 视觉语言
|
||||||
|
- 显示问题审计与修复任务
|
||||||
|
|
||||||
|
## 自检
|
||||||
|
|
||||||
|
- 设计覆盖:平台设计中的每个主要章节都映射到六份详细计划之一。
|
||||||
|
- 占位内容检查:本套件文档只定义计划边界和预期内容;详细任务代码放入六份子实施计划。
|
||||||
|
- 类型一致性:所有计划统一使用核心术语:`SessionTreeProvider`、`entry`、`leaf`、`snapshot`、`provider`、`Resource Workbench`、`Agent Structure Canvas`、`Conversation Workspace`。
|
||||||
838
docs/superpowers/plans/2026-04-25-windows-installer.md
Normal file
838
docs/superpowers/plans/2026-04-25-windows-installer.md
Normal file
@ -0,0 +1,838 @@
|
|||||||
|
# Windows 安装方案实施计划
|
||||||
|
|
||||||
|
> **给执行型 agent 的要求:** 实施本计划时必须使用 `superpowers:subagent-driven-development`(推荐)或 `superpowers:executing-plans`。所有步骤使用复选框(`- [ ]`)跟踪。
|
||||||
|
|
||||||
|
**目标:** 交付一个离线 Windows 安装器,把 Neta 安装为单个 `backend.exe` 应用;该进程同时托管前端静态页面与后端 API,把所有可写数据统一放到用户选择的数据目录,支持安装、卸载、重装和首次启动自动打开浏览器。
|
||||||
|
|
||||||
|
**架构:** 在 Midway 启动前先从 exe 同目录读取并校验 `config.yaml`,解析出唯一可写的 `data.dir`,再把 `comm/path.ts`、agent 记忆、session 树、skills、插件、SQLite、日志、锁文件全部收口到这个目录。安装目录保持只读;打包时先构建前端,再用 staging + pkg 生成 `backend.exe`,最后由 Inno Setup 生成离线安装器并处理安装/卸载生命周期。
|
||||||
|
|
||||||
|
**技术栈:** Midway.js 3.x、Koa、TypeORM、better-sqlite3、Jest + ts-jest、Vue 3 + Vite、@yao-pkg/pkg、Inno Setup
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 文件结构
|
||||||
|
|
||||||
|
**新增:**
|
||||||
|
- `packages/backend/src/comm/data-dir.ts` — 统一解析可写数据根目录
|
||||||
|
- `packages/backend/src/comm/config-loader.ts` — 读取并校验外部 `config.yaml`
|
||||||
|
- `packages/backend/src/comm/runtime-lock.ts` — 单实例锁、失效锁恢复、退出清理
|
||||||
|
- `packages/backend/src/comm/browser.ts` — Windows 环境打开默认浏览器
|
||||||
|
- `packages/backend/test/windows-installer/config-loader.test.ts` — 配置加载/校验/回退测试
|
||||||
|
- `packages/backend/test/windows-installer/data-dir.test.ts` — 数据目录解析与路径收口测试
|
||||||
|
- `packages/backend/test/windows-installer/runtime-lock.test.ts` — 单实例锁测试
|
||||||
|
- `packages/backend/scripts/pkg-build.js` — Node 版 staging + pkg 打包脚本
|
||||||
|
- `packages/backend/scripts/build-windows-installer.js` — 生成离线安装器的包装脚本
|
||||||
|
- `packages/backend/installer/config.default.yaml` — 安装器模板配置
|
||||||
|
- `packages/backend/installer/setup.iss` — Inno Setup 安装器脚本
|
||||||
|
|
||||||
|
**修改:**
|
||||||
|
- `packages/backend/bootstrap.js` — CLI 参数、配置加载顺序、启动失败处理
|
||||||
|
- `packages/backend/src/comm/path.ts` — 通过 `resolveDataDir()` 输出统一路径
|
||||||
|
- `packages/backend/src/config/config.default.ts` — upload/cache/log/path 配置
|
||||||
|
- `packages/backend/src/config/config.prod.ts` — 移除硬编码数据库凭证并读取外部配置
|
||||||
|
- `packages/backend/src/configuration.ts` — `onReady` 中注册锁文件和自动打开浏览器
|
||||||
|
- `packages/backend/src/modules/netaclaw/memory/sqlite_provider.ts` — memory DB 路径统一
|
||||||
|
- `packages/backend/src/modules/netaclaw/session-tree/factory.ts` — session root 路径统一
|
||||||
|
- `packages/backend/src/modules/netaclaw/service/skill_installer.ts` — skills 路径统一
|
||||||
|
- `packages/backend/src/modules/netaclaw/service/skill_registry.ts` — `.skillhub` 路径统一
|
||||||
|
- `packages/backend/package.json` — 打包脚本
|
||||||
|
- `packages/backend/scripts/pkg-build.sh` — 收敛为调用 Node 脚本的薄包装
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 任务 1:配置加载、校验与数据目录基础设施
|
||||||
|
|
||||||
|
**文件:**
|
||||||
|
- 新增:`packages/backend/src/comm/config-loader.ts`
|
||||||
|
- 新增:`packages/backend/src/comm/data-dir.ts`
|
||||||
|
- 新增:`packages/backend/test/windows-installer/config-loader.test.ts`
|
||||||
|
- 新增:`packages/backend/test/windows-installer/data-dir.test.ts`
|
||||||
|
- 修改:`packages/backend/bootstrap.js`
|
||||||
|
|
||||||
|
- [ ] **步骤 1:先写失败测试,覆盖安装态与开发态回退**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// packages/backend/test/windows-installer/config-loader.test.ts
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
import * as os from 'node:os';
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import { loadExternalConfig } from '../../src/comm/config-loader';
|
||||||
|
|
||||||
|
describe('loadExternalConfig', () => {
|
||||||
|
it('在 pkg 模式读取 exe 同目录下的 config.yaml', () => {
|
||||||
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'neta-config-'));
|
||||||
|
fs.mkdirSync(path.join(tempDir, 'data'), { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tempDir, 'config.yaml'),
|
||||||
|
[
|
||||||
|
'server:',
|
||||||
|
' port: 8100',
|
||||||
|
'data:',
|
||||||
|
` dir: "${path.join(tempDir, 'data').replace(/\\/g, '\\\\')}"`,
|
||||||
|
'autoOpenBrowser: true',
|
||||||
|
'database:',
|
||||||
|
' type: mysql',
|
||||||
|
' host: db.example.com',
|
||||||
|
' port: 3306',
|
||||||
|
' username: demo',
|
||||||
|
' password: secret',
|
||||||
|
' database: neta_test',
|
||||||
|
].join('\n'),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
const loaded = loadExternalConfig({ isPkg: true, execPath: path.join(tempDir, 'backend.exe') });
|
||||||
|
expect(loaded.server.port).toBe(8100);
|
||||||
|
expect(loaded.database.host).toBe('db.example.com');
|
||||||
|
expect(loaded.data.dir).toContain(path.join('data'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('在开发模式允许没有 config.yaml 并回退到内置默认值', () => {
|
||||||
|
const loaded = loadExternalConfig({ isPkg: false, cwd: '/tmp/neta-backend' });
|
||||||
|
expect(loaded.source).toBe('fallback');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// packages/backend/test/windows-installer/data-dir.test.ts
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import { resolveDataDir } from '../../src/comm/data-dir';
|
||||||
|
|
||||||
|
describe('resolveDataDir', () => {
|
||||||
|
it('优先使用已校验配置里的 data.dir', () => {
|
||||||
|
const dir = resolveDataDir({
|
||||||
|
isPkg: true,
|
||||||
|
execDir: 'C:/Program Files/Neta',
|
||||||
|
config: { data: { dir: 'D:/NetaData' } },
|
||||||
|
cwd: 'C:/Users/demo/Desktop',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(path.normalize(dir)).toBe(path.normalize('D:/NetaData'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **步骤 2:运行测试确认当前失败**
|
||||||
|
|
||||||
|
运行:
|
||||||
|
```bash
|
||||||
|
cd /c/Users/Administrator/Desktop/code/Neta-monorepo/packages/backend && npx jest test/windows-installer/config-loader.test.ts test/windows-installer/data-dir.test.ts -i
|
||||||
|
```
|
||||||
|
预期:FAIL,提示找不到 `config-loader` / `data-dir` 模块。
|
||||||
|
|
||||||
|
- [ ] **步骤 3:写最小实现,先把启动顺序与 schema 校验立住**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// packages/backend/src/comm/config-loader.ts
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import * as yaml from 'js-yaml';
|
||||||
|
|
||||||
|
export interface ExternalAppConfig {
|
||||||
|
server: { port: number };
|
||||||
|
data: { dir: string };
|
||||||
|
autoOpenBrowser: boolean;
|
||||||
|
database: {
|
||||||
|
type: 'mysql';
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
database: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoadedExternalConfig {
|
||||||
|
source: 'file' | 'fallback';
|
||||||
|
config: ExternalAppConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertConfig(config: any): asserts config is ExternalAppConfig {
|
||||||
|
if (!config?.data?.dir || typeof config.data.dir !== 'string') throw new Error('config.yaml 缺少 data.dir');
|
||||||
|
if (!config?.server?.port || typeof config.server.port !== 'number') throw new Error('config.yaml 缺少 server.port');
|
||||||
|
if (!config?.database?.host || !config?.database?.username || !config?.database?.database) {
|
||||||
|
throw new Error('config.yaml 缺少数据库配置');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadExternalConfig(options?: { isPkg?: boolean; execPath?: string; cwd?: string }): LoadedExternalConfig {
|
||||||
|
const isPkg = options?.isPkg ?? Boolean(process.pkg);
|
||||||
|
if (!isPkg) {
|
||||||
|
return {
|
||||||
|
source: 'fallback',
|
||||||
|
config: {
|
||||||
|
server: { port: 8003 },
|
||||||
|
data: { dir: path.join(options?.cwd ?? process.cwd(), 'dist') },
|
||||||
|
autoOpenBrowser: false,
|
||||||
|
database: { type: 'mysql', host: '', port: 3306, username: '', password: '', database: '' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const execPath = options?.execPath ?? process.execPath;
|
||||||
|
const configPath = path.join(path.dirname(execPath), 'config.yaml');
|
||||||
|
const raw = fs.readFileSync(configPath, 'utf8');
|
||||||
|
const parsed = yaml.load(raw);
|
||||||
|
assertConfig(parsed);
|
||||||
|
return { source: 'file', config: parsed };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// packages/backend/src/comm/data-dir.ts
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import type { ExternalAppConfig } from './config-loader';
|
||||||
|
|
||||||
|
export function resolveDataDir(input?: {
|
||||||
|
isPkg?: boolean;
|
||||||
|
execDir?: string;
|
||||||
|
config?: Pick<ExternalAppConfig, 'data'>;
|
||||||
|
cwd?: string;
|
||||||
|
}): string {
|
||||||
|
if (input?.config?.data?.dir) return path.resolve(input.config.data.dir);
|
||||||
|
if (process.env.NETA_DATA_DIR) return path.resolve(process.env.NETA_DATA_DIR);
|
||||||
|
if (input?.isPkg ?? Boolean(process.pkg)) return path.join(input?.execDir ?? path.dirname(process.execPath), 'data');
|
||||||
|
return path.join(input?.cwd ?? process.cwd(), 'dist');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
// packages/backend/bootstrap.js
|
||||||
|
const { Bootstrap } = require('@midwayjs/bootstrap');
|
||||||
|
const { loadExternalConfig } = require('./dist/comm/config-loader');
|
||||||
|
|
||||||
|
if (process.argv.includes('--version')) {
|
||||||
|
process.stdout.write(`${require('./package.json').version}\n`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const configArgIndex = process.argv.indexOf('--config');
|
||||||
|
if (configArgIndex > -1 && process.argv[configArgIndex + 1]) {
|
||||||
|
process.env.NETA_CONFIG_PATH = process.argv[configArgIndex + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const loaded = loadExternalConfig();
|
||||||
|
global.__NETA_EXTERNAL_CONFIG__ = loaded.config;
|
||||||
|
process.env.NETA_DATA_DIR = loaded.config.data.dir;
|
||||||
|
} catch (error) {
|
||||||
|
process.stderr.write(`${error.message}\n`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Bootstrap.configure({
|
||||||
|
imports: require('./dist/index'),
|
||||||
|
moduleDetector: false,
|
||||||
|
}).run();
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **步骤 4:重新运行测试,确认基础设施通过**
|
||||||
|
|
||||||
|
运行:
|
||||||
|
```bash
|
||||||
|
cd /c/Users/Administrator/Desktop/code/Neta-monorepo/packages/backend && npx jest test/windows-installer/config-loader.test.ts test/windows-installer/data-dir.test.ts -i
|
||||||
|
```
|
||||||
|
预期:PASS,2 个测试文件全部通过。
|
||||||
|
|
||||||
|
- [ ] **步骤 5:提交这一轮基础设施改动**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/comm/config-loader.ts packages/backend/src/comm/data-dir.ts packages/backend/test/windows-installer/config-loader.test.ts packages/backend/test/windows-installer/data-dir.test.ts packages/backend/bootstrap.js
|
||||||
|
git commit -m "feat: add installer-aware config bootstrap"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 任务 2:把所有可写路径统一收口到 data.dir
|
||||||
|
|
||||||
|
**文件:**
|
||||||
|
- 修改:`packages/backend/src/comm/path.ts`
|
||||||
|
- 修改:`packages/backend/src/config/config.default.ts`
|
||||||
|
- 修改:`packages/backend/src/config/config.prod.ts`
|
||||||
|
- 修改:`packages/backend/src/modules/netaclaw/memory/sqlite_provider.ts`
|
||||||
|
- 修改:`packages/backend/src/modules/netaclaw/session-tree/factory.ts`
|
||||||
|
- 修改:`packages/backend/src/modules/netaclaw/service/skill_installer.ts`
|
||||||
|
- 修改:`packages/backend/src/modules/netaclaw/service/skill_registry.ts`
|
||||||
|
- 复用测试:`packages/backend/test/windows-installer/data-dir.test.ts`
|
||||||
|
|
||||||
|
- [ ] **步骤 1:补失败测试,覆盖 comm/path、session、skills、skillhub**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// packages/backend/test/windows-installer/data-dir.test.ts
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import { pUploadPath, pCachePath, pPluginPath, pSqlitePath } from '../../src/comm/path';
|
||||||
|
import { resolveAgentSessionTreeConfig } from '../../src/modules/netaclaw/session-tree/factory';
|
||||||
|
|
||||||
|
describe('installer-aware writable paths', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
process.env.NETA_DATA_DIR = 'D:/NetaData';
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
delete process.env.NETA_DATA_DIR;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('把 comm/path 全部映射到 data.dir', () => {
|
||||||
|
expect(path.normalize(pUploadPath())).toBe(path.normalize('D:/NetaData/uploads'));
|
||||||
|
expect(path.normalize(pCachePath())).toBe(path.normalize('D:/NetaData/cache'));
|
||||||
|
expect(path.normalize(pPluginPath())).toBe(path.normalize('D:/NetaData/plugins'));
|
||||||
|
expect(path.normalize(pSqlitePath())).toBe(path.normalize('D:/NetaData/cool.sqlite'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('把 session tree 根目录映射到 data.dir/sessions', () => {
|
||||||
|
const resolved = resolveAgentSessionTreeConfig(undefined, { backend: 'file', dataDir: 'D:/NetaData' });
|
||||||
|
expect(path.normalize(resolved.file!.rootDir)).toBe(path.normalize('D:/NetaData/sessions'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **步骤 2:运行测试确认目前失败**
|
||||||
|
|
||||||
|
运行:
|
||||||
|
```bash
|
||||||
|
cd /c/Users/Administrator/Desktop/code/Neta-monorepo/packages/backend && npx jest test/windows-installer/data-dir.test.ts -i
|
||||||
|
```
|
||||||
|
预期:FAIL,因为当前仍然走 `<cwd>/dist`、`~/.neta`、`process.cwd()/skills`。
|
||||||
|
|
||||||
|
- [ ] **步骤 3:写最小实现,把路径改为统一根目录**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// packages/backend/src/comm/path.ts
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import { resolveDataDir } from './data-dir';
|
||||||
|
|
||||||
|
export const pDataPath = () => {
|
||||||
|
const dirPath = resolveDataDir();
|
||||||
|
if (!fs.existsSync(dirPath)) fs.mkdirSync(dirPath, { recursive: true });
|
||||||
|
return dirPath;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pUploadPath = () => {
|
||||||
|
const uploadPath = path.join(pDataPath(), 'uploads');
|
||||||
|
if (!fs.existsSync(uploadPath)) fs.mkdirSync(uploadPath, { recursive: true });
|
||||||
|
return uploadPath;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pPluginPath = () => {
|
||||||
|
const pluginPath = path.join(pDataPath(), 'plugins');
|
||||||
|
if (!fs.existsSync(pluginPath)) fs.mkdirSync(pluginPath, { recursive: true });
|
||||||
|
return pluginPath;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pSqlitePath = () => path.join(pDataPath(), 'cool.sqlite');
|
||||||
|
export const pCachePath = () => {
|
||||||
|
const cachePath = path.join(pDataPath(), 'cache');
|
||||||
|
if (!fs.existsSync(cachePath)) fs.mkdirSync(cachePath, { recursive: true });
|
||||||
|
return cachePath;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// packages/backend/src/modules/netaclaw/memory/sqlite_provider.ts
|
||||||
|
constructor(dbPath?: string) {
|
||||||
|
const baseDir = process.env.NETA_DATA_DIR ?? path.join(os.homedir(), '.neta');
|
||||||
|
const resolvedPath = dbPath ?? path.join(baseDir, 'memory', 'memory.db');
|
||||||
|
fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
|
||||||
|
this.db = new Database(resolvedPath);
|
||||||
|
this.db.pragma('journal_mode = WAL');
|
||||||
|
this.db.exec(INIT_SQL);
|
||||||
|
this.db.exec(FTS_SQL);
|
||||||
|
this.db.exec(TRIGGER_SQL);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// packages/backend/src/modules/netaclaw/session-tree/factory.ts
|
||||||
|
rootDir: agentConfig?.file?.rootDir ?? path.join(defaults.dataDir ?? process.env.NETA_DATA_DIR ?? '~/.neta', 'sessions')
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// packages/backend/src/modules/netaclaw/service/skill_installer.ts
|
||||||
|
@Init()
|
||||||
|
async init() {
|
||||||
|
const root = process.env.NETA_DATA_DIR ?? process.cwd();
|
||||||
|
this.skillsDir = path.resolve(root, 'skills');
|
||||||
|
await fs.mkdir(this.skillsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// packages/backend/src/modules/netaclaw/service/skill_registry.ts
|
||||||
|
@Init()
|
||||||
|
async init() {
|
||||||
|
const root = process.env.NETA_DATA_DIR ?? process.cwd();
|
||||||
|
this.hubDir = path.resolve(root, '.skillhub');
|
||||||
|
this.originsDir = path.join(this.hubDir, 'origins');
|
||||||
|
this.lockfilePath = path.join(this.hubDir, 'lock.json');
|
||||||
|
await fs.mkdir(this.originsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// packages/backend/src/config/config.default.ts
|
||||||
|
import { pCachePath, pUploadPath } from '../comm/path';
|
||||||
|
|
||||||
|
staticFile: {
|
||||||
|
buffer: true,
|
||||||
|
dirs: {
|
||||||
|
default: { prefix: '/', dir: path.join(__dirname, '..', '..', 'public') },
|
||||||
|
static: { prefix: '/upload', dir: pUploadPath() },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cacheManager: {
|
||||||
|
clients: {
|
||||||
|
default: {
|
||||||
|
store: CoolCacheStore,
|
||||||
|
options: { path: pCachePath(), ttl: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
netaclaw: {
|
||||||
|
...,
|
||||||
|
skillsDir: path.join(process.env.NETA_DATA_DIR ?? process.cwd(), 'skills'),
|
||||||
|
dataDir: process.env.NETA_DATA_DIR ?? '~/.neta',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// packages/backend/src/config/config.prod.ts
|
||||||
|
const external = global.__NETA_EXTERNAL_CONFIG__;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
typeorm: {
|
||||||
|
dataSource: {
|
||||||
|
default: {
|
||||||
|
type: external?.database?.type ?? 'mysql',
|
||||||
|
host: external?.database?.host ?? '',
|
||||||
|
port: external?.database?.port ?? 3306,
|
||||||
|
username: external?.database?.username ?? '',
|
||||||
|
password: external?.database?.password ?? '',
|
||||||
|
database: external?.database?.database ?? '',
|
||||||
|
synchronize: false,
|
||||||
|
logging: false,
|
||||||
|
charset: 'utf8mb4',
|
||||||
|
cache: true,
|
||||||
|
entities,
|
||||||
|
subscribers: [TenantSubscriber],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as MidwayConfig;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **步骤 4:运行测试确认路径收口成功**
|
||||||
|
|
||||||
|
运行:
|
||||||
|
```bash
|
||||||
|
cd /c/Users/Administrator/Desktop/code/Neta-monorepo/packages/backend && npx jest test/windows-installer/data-dir.test.ts -i
|
||||||
|
```
|
||||||
|
预期:PASS,所有路径都解析到 `D:/NetaData` 下。
|
||||||
|
|
||||||
|
- [ ] **步骤 5:提交路径统一改动**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/comm/path.ts packages/backend/src/config/config.default.ts packages/backend/src/config/config.prod.ts packages/backend/src/modules/netaclaw/memory/sqlite_provider.ts packages/backend/src/modules/netaclaw/session-tree/factory.ts packages/backend/src/modules/netaclaw/service/skill_installer.ts packages/backend/src/modules/netaclaw/service/skill_registry.ts packages/backend/test/windows-installer/data-dir.test.ts
|
||||||
|
git commit -m "feat: unify backend writable paths under data dir"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 任务 3:单实例锁、自动开浏览器、日志与数据目录初始化
|
||||||
|
|
||||||
|
**文件:**
|
||||||
|
- 新增:`packages/backend/src/comm/runtime-lock.ts`
|
||||||
|
- 新增:`packages/backend/src/comm/browser.ts`
|
||||||
|
- 新增:`packages/backend/test/windows-installer/runtime-lock.test.ts`
|
||||||
|
- 修改:`packages/backend/src/configuration.ts`
|
||||||
|
- 修改:`packages/backend/src/config/config.default.ts`
|
||||||
|
|
||||||
|
- [ ] **步骤 1:写失败测试,先定义锁文件行为**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// packages/backend/test/windows-installer/runtime-lock.test.ts
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
import * as os from 'node:os';
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import { acquireRuntimeLock, readRuntimeLock, releaseRuntimeLock } from '../../src/comm/runtime-lock';
|
||||||
|
|
||||||
|
describe('runtime lock', () => {
|
||||||
|
it('写入当前 pid,并在释放时删除锁文件', () => {
|
||||||
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'neta-lock-'));
|
||||||
|
const lockPath = path.join(tempDir, 'neta.lock');
|
||||||
|
|
||||||
|
acquireRuntimeLock(lockPath);
|
||||||
|
expect(readRuntimeLock(lockPath).pid).toBe(process.pid);
|
||||||
|
|
||||||
|
releaseRuntimeLock(lockPath);
|
||||||
|
expect(fs.existsSync(lockPath)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **步骤 2:运行测试确认当前失败**
|
||||||
|
|
||||||
|
运行:
|
||||||
|
```bash
|
||||||
|
cd /c/Users/Administrator/Desktop/code/Neta-monorepo/packages/backend && npx jest test/windows-installer/runtime-lock.test.ts -i
|
||||||
|
```
|
||||||
|
预期:FAIL,因为 `runtime-lock.ts` 尚不存在。
|
||||||
|
|
||||||
|
- [ ] **步骤 3:写最小实现,包含失效锁恢复与 onReady 行为**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// packages/backend/src/comm/runtime-lock.ts
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
|
||||||
|
export function acquireRuntimeLock(lockPath: string) {
|
||||||
|
if (fs.existsSync(lockPath)) {
|
||||||
|
const current = JSON.parse(fs.readFileSync(lockPath, 'utf8')) as { pid: number };
|
||||||
|
try {
|
||||||
|
process.kill(current.pid, 0);
|
||||||
|
throw new Error(`Neta 已在运行,PID=${current.pid}`);
|
||||||
|
} catch {
|
||||||
|
fs.rmSync(lockPath, { force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fs.writeFileSync(lockPath, JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }), 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readRuntimeLock(lockPath: string): { pid: number; startedAt: string } {
|
||||||
|
return JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function releaseRuntimeLock(lockPath: string) {
|
||||||
|
fs.rmSync(lockPath, { force: true });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// packages/backend/src/comm/browser.ts
|
||||||
|
import { exec } from 'node:child_process';
|
||||||
|
|
||||||
|
export function openBrowser(url: string) {
|
||||||
|
if (process.platform !== 'win32') return;
|
||||||
|
exec(`start "" "${url}"`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// packages/backend/src/configuration.ts
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import { acquireRuntimeLock, releaseRuntimeLock } from './comm/runtime-lock';
|
||||||
|
import { openBrowser } from './comm/browser';
|
||||||
|
import { pDataPath } from './comm/path';
|
||||||
|
|
||||||
|
async onReady() {
|
||||||
|
const lockPath = path.join(pDataPath(), 'neta.lock');
|
||||||
|
acquireRuntimeLock(lockPath);
|
||||||
|
process.on('exit', () => releaseRuntimeLock(lockPath));
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
releaseRuntimeLock(lockPath);
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
const channelService = await this.app.getApplicationContext().getAsync(NetaClawAgentChannelService);
|
||||||
|
await channelService.restoreConnectedRunners();
|
||||||
|
|
||||||
|
const port = this.app.getConfig('koa.port');
|
||||||
|
const autoOpenBrowser = this.app.getConfig('autoOpenBrowser');
|
||||||
|
if (process.pkg && autoOpenBrowser) {
|
||||||
|
openBrowser(`http://127.0.0.1:${port}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// packages/backend/src/config/config.default.ts
|
||||||
|
loggers: {
|
||||||
|
coreLogger: {
|
||||||
|
level: 'INFO',
|
||||||
|
consoleLevel: 'INFO',
|
||||||
|
dir: path.join(pDataPath(), 'logs'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
autoOpenBrowser: global.__NETA_EXTERNAL_CONFIG__?.autoOpenBrowser ?? false,
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **步骤 4:重新运行测试并做一次手工 smoke**
|
||||||
|
|
||||||
|
运行:
|
||||||
|
```bash
|
||||||
|
cd /c/Users/Administrator/Desktop/code/Neta-monorepo/packages/backend && npx jest test/windows-installer/runtime-lock.test.ts -i
|
||||||
|
```
|
||||||
|
预期:PASS,锁文件可创建并释放。
|
||||||
|
|
||||||
|
再运行:
|
||||||
|
```bash
|
||||||
|
cd /c/Users/Administrator/Desktop/code/Neta-monorepo/packages/backend && npm run start
|
||||||
|
```
|
||||||
|
预期:应用正常启动,首次启动时在数据目录创建 `logs/` 与 `neta.lock`。
|
||||||
|
|
||||||
|
- [ ] **步骤 5:提交运行时行为改动**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/comm/runtime-lock.ts packages/backend/src/comm/browser.ts packages/backend/src/configuration.ts packages/backend/src/config/config.default.ts packages/backend/test/windows-installer/runtime-lock.test.ts
|
||||||
|
git commit -m "feat: add installer runtime lifecycle helpers"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 任务 4:前端 + 后端 + pkg 的 Windows 打包流水线
|
||||||
|
|
||||||
|
**文件:**
|
||||||
|
- 新增:`packages/backend/scripts/pkg-build.js`
|
||||||
|
- 修改:`packages/backend/package.json`
|
||||||
|
- 修改:`packages/backend/scripts/pkg-build.sh`
|
||||||
|
- 新增:`packages/backend/installer/config.default.yaml`
|
||||||
|
|
||||||
|
- [ ] **步骤 1:先写一个 staging 断言,定义产物要求**
|
||||||
|
|
||||||
|
```js
|
||||||
|
// packages/backend/scripts/pkg-build.js
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
function assertStage(stageDir) {
|
||||||
|
const required = [
|
||||||
|
path.join(stageDir, 'public', 'index.html'),
|
||||||
|
path.join(stageDir, 'public', 'swagger', 'index.html'),
|
||||||
|
path.join(stageDir, 'dist', 'index.js'),
|
||||||
|
];
|
||||||
|
for (const file of required) {
|
||||||
|
if (!fs.existsSync(file)) {
|
||||||
|
throw new Error(`缺少 staging 文件: ${file}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **步骤 2:运行现有流程,确认它还没有完整前后端合包**
|
||||||
|
|
||||||
|
运行:
|
||||||
|
```bash
|
||||||
|
cd /c/Users/Administrator/Desktop/code/Neta-monorepo/packages/backend && node -e "const fs=require('fs'); console.log(fs.existsSync('build/pkg-stage/public/index.html') ? 'staged' : 'missing')"
|
||||||
|
```
|
||||||
|
预期:`missing` 或是旧产物,因为当前流程没有强制先构建前端再合并到 staging。
|
||||||
|
|
||||||
|
- [ ] **步骤 3:写最小实现,用 Node 脚本统一构建**
|
||||||
|
|
||||||
|
```js
|
||||||
|
// packages/backend/scripts/pkg-build.js
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
const cp = require('node:child_process');
|
||||||
|
|
||||||
|
const backendDir = path.resolve(__dirname, '..');
|
||||||
|
const repoDir = path.resolve(backendDir, '..', '..');
|
||||||
|
const frontendDir = path.join(repoDir, 'packages', 'frontend');
|
||||||
|
const stageDir = path.join(backendDir, 'build', 'pkg-stage');
|
||||||
|
const outputDir = path.join(backendDir, 'build', 'pkg-output');
|
||||||
|
|
||||||
|
function run(command, cwd) {
|
||||||
|
cp.execFileSync(process.platform === 'win32' ? 'cmd.exe' : 'bash', process.platform === 'win32' ? ['/c', command] : ['-lc', command], { cwd, stdio: 'inherit' });
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.rmSync(stageDir, { recursive: true, force: true });
|
||||||
|
fs.rmSync(outputDir, { recursive: true, force: true });
|
||||||
|
fs.mkdirSync(stageDir, { recursive: true });
|
||||||
|
|
||||||
|
run('pnpm build', frontendDir);
|
||||||
|
run('npm run build', backendDir);
|
||||||
|
|
||||||
|
fs.cpSync(path.join(backendDir, 'bootstrap.js'), path.join(stageDir, 'bootstrap.js'));
|
||||||
|
fs.cpSync(path.join(backendDir, 'dist'), path.join(stageDir, 'dist'), { recursive: true });
|
||||||
|
fs.cpSync(path.join(backendDir, 'public'), path.join(stageDir, 'public'), { recursive: true });
|
||||||
|
fs.cpSync(path.join(frontendDir, 'dist'), path.join(stageDir, 'public'), { recursive: true, force: true });
|
||||||
|
if (fs.existsSync(path.join(backendDir, 'typings'))) {
|
||||||
|
fs.cpSync(path.join(backendDir, 'typings'), path.join(stageDir, 'typings'), { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const stagePkg = {
|
||||||
|
name: '@neta/backend',
|
||||||
|
version: require(path.join(backendDir, 'package.json')).version,
|
||||||
|
private: true,
|
||||||
|
dependencies: require(path.join(backendDir, 'package.json')).dependencies,
|
||||||
|
bin: './bootstrap.js',
|
||||||
|
pkg: {
|
||||||
|
scripts: ['dist/**/*.js', 'node_modules/**/*.mjs'],
|
||||||
|
assets: [
|
||||||
|
'public/**/*',
|
||||||
|
'typings/**/*',
|
||||||
|
'node_modules/@img/sharp-win32-x64/**/*',
|
||||||
|
'node_modules/@img/colour/**/*',
|
||||||
|
'node_modules/better-sqlite3/build/Release/*.node',
|
||||||
|
'node_modules/@msgpackr-extract/msgpackr-extract-win32-x64/*.node',
|
||||||
|
'node_modules/@napi-rs/canvas-win32-x64-msvc/*.node',
|
||||||
|
],
|
||||||
|
targets: ['node20-win-x64'],
|
||||||
|
outputPath: outputDir,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
fs.writeFileSync(path.join(stageDir, 'package.json'), JSON.stringify(stagePkg, null, 2));
|
||||||
|
run('npm install --production --install-strategy=hoisted --legacy-peer-deps', stageDir);
|
||||||
|
run('npx @yao-pkg/pkg@6.14.1 .', stageDir);
|
||||||
|
assertStage(stageDir);
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
// packages/backend/package.json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"pkg": "node scripts/pkg-build.js",
|
||||||
|
"build:windows-installer": "node scripts/build-windows-installer.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# packages/backend/scripts/pkg-build.sh
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
node "$(cd "$(dirname "$0")" && pwd)/pkg-build.js"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# packages/backend/installer/config.default.yaml
|
||||||
|
server:
|
||||||
|
port: 8003
|
||||||
|
|
||||||
|
data:
|
||||||
|
dir: "C:\\NetaData"
|
||||||
|
|
||||||
|
autoOpenBrowser: true
|
||||||
|
|
||||||
|
database:
|
||||||
|
type: mysql
|
||||||
|
host: ""
|
||||||
|
port: 3306
|
||||||
|
username: ""
|
||||||
|
password: ""
|
||||||
|
database: ""
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **步骤 4:运行打包流水线确认 exe 可生成**
|
||||||
|
|
||||||
|
运行:
|
||||||
|
```bash
|
||||||
|
cd /c/Users/Administrator/Desktop/code/Neta-monorepo/packages/backend && npm run pkg
|
||||||
|
```
|
||||||
|
预期:PASS,生成 `packages/backend/build/pkg-output/backend.exe`,且 `public/index.html` 来自前端构建产物,`public/swagger/index.html` 仍保留。
|
||||||
|
|
||||||
|
- [ ] **步骤 5:提交打包脚本改动**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/scripts/pkg-build.js packages/backend/scripts/pkg-build.sh packages/backend/installer/config.default.yaml packages/backend/package.json
|
||||||
|
git commit -m "feat: package installer-ready backend exe"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 任务 5:离线安装器、卸载交互与重装安全
|
||||||
|
|
||||||
|
**文件:**
|
||||||
|
- 新增:`packages/backend/scripts/build-windows-installer.js`
|
||||||
|
- 新增:`packages/backend/installer/setup.iss`
|
||||||
|
- 手工验证:`packages/backend/build/pkg-output/backend.exe`
|
||||||
|
|
||||||
|
- [ ] **步骤 1:先写失败入口,定义安装器脚本必须存在**
|
||||||
|
|
||||||
|
```js
|
||||||
|
// packages/backend/scripts/build-windows-installer.js
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const issPath = path.resolve(__dirname, '..', 'installer', 'setup.iss');
|
||||||
|
if (!fs.existsSync(issPath)) {
|
||||||
|
throw new Error(`缺少安装器脚本: ${issPath}`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **步骤 2:运行安装器构建命令,确认当前失败**
|
||||||
|
|
||||||
|
运行:
|
||||||
|
```bash
|
||||||
|
cd /c/Users/Administrator/Desktop/code/Neta-monorepo/packages/backend && node scripts/build-windows-installer.js
|
||||||
|
```
|
||||||
|
预期:FAIL,提示缺少 `setup.iss`。
|
||||||
|
|
||||||
|
- [ ] **步骤 3:写最小可用的 Inno Setup 安装器实现**
|
||||||
|
|
||||||
|
```js
|
||||||
|
// packages/backend/scripts/build-windows-installer.js
|
||||||
|
const cp = require('node:child_process');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const backendDir = path.resolve(__dirname, '..');
|
||||||
|
const outputDir = path.join(backendDir, 'build', 'pkg-output');
|
||||||
|
const issPath = path.join(backendDir, 'installer', 'setup.iss');
|
||||||
|
|
||||||
|
if (!fs.existsSync(path.join(outputDir, 'backend.exe'))) {
|
||||||
|
cp.execFileSync('node', ['scripts/pkg-build.js'], { cwd: backendDir, stdio: 'inherit' });
|
||||||
|
}
|
||||||
|
|
||||||
|
cp.execFileSync('iscc', [issPath], { cwd: backendDir, stdio: 'inherit' });
|
||||||
|
```
|
||||||
|
|
||||||
|
```iss
|
||||||
|
; packages/backend/installer/setup.iss
|
||||||
|
#define MyAppName "Neta"
|
||||||
|
#define MyAppVersion "8.0.0"
|
||||||
|
|
||||||
|
[Setup]
|
||||||
|
AppId={{1B72B6C4-21A4-4C77-A6F6-1D4B98E7F1A1}
|
||||||
|
AppName={#MyAppName}
|
||||||
|
AppVersion={#MyAppVersion}
|
||||||
|
DefaultDirName={autopf}\Neta
|
||||||
|
DefaultGroupName=Neta
|
||||||
|
OutputDir=..\build\installer-output
|
||||||
|
OutputBaseFilename=neta-setup
|
||||||
|
Compression=lzma2
|
||||||
|
SolidCompression=yes
|
||||||
|
WizardStyle=modern
|
||||||
|
PrivilegesRequired=admin
|
||||||
|
|
||||||
|
[Files]
|
||||||
|
Source: "..\build\pkg-output\backend.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
Source: "config.default.yaml"; DestDir: "{app}"; DestName: "config.yaml"; Flags: onlyifdoesntexist
|
||||||
|
|
||||||
|
[Icons]
|
||||||
|
Name: "{autodesktop}\Neta"; Filename: "{app}\backend.exe"; WorkingDir: "{app}"
|
||||||
|
Name: "{group}\Neta"; Filename: "{app}\backend.exe"; WorkingDir: "{app}"
|
||||||
|
|
||||||
|
[Run]
|
||||||
|
Filename: "{app}\backend.exe"; Description: "启动 Neta"; Flags: nowait postinstall skipifsilent
|
||||||
|
|
||||||
|
[UninstallRun]
|
||||||
|
Filename: "taskkill"; Parameters: "/IM backend.exe /F"; Flags: runhidden skipifdoesntexist
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **步骤 4:运行安装器构建并做一次手工安装验证**
|
||||||
|
|
||||||
|
运行:
|
||||||
|
```bash
|
||||||
|
cd /c/Users/Administrator/Desktop/code/Neta-monorepo/packages/backend && node scripts/build-windows-installer.js
|
||||||
|
```
|
||||||
|
预期:PASS,生成 `packages/backend/build/installer-output/neta-setup.exe`。
|
||||||
|
|
||||||
|
再验证:
|
||||||
|
1. 双击安装器,确认安装成功
|
||||||
|
2. 安装完成后点击“立即启动”,浏览器能打开首页
|
||||||
|
3. 卸载后确认程序目录删除
|
||||||
|
4. 重装后确认数据目录未被误删
|
||||||
|
|
||||||
|
- [ ] **步骤 5:提交安装器实现**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/scripts/build-windows-installer.js packages/backend/installer/setup.iss
|
||||||
|
git commit -m "feat: add offline windows installer"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 自检
|
||||||
|
|
||||||
|
**设计覆盖检查:**
|
||||||
|
- 程序目录 / 数据目录分离:任务 1-2
|
||||||
|
- 外部 `config.yaml` + 校验 + 启动顺序:任务 1
|
||||||
|
- 数据库凭证外置:任务 2
|
||||||
|
- 所有本地持久化统一写入 `data.dir`:任务 2
|
||||||
|
- 单实例、自动开浏览器、日志、锁文件:任务 3
|
||||||
|
- 前端与后端合包为单 exe:任务 4
|
||||||
|
- Inno Setup 安装/卸载/重装:任务 5
|
||||||
|
|
||||||
|
**占位符检查:** 无 `TBD`、`TODO`、`后续补充` 一类占位内容。
|
||||||
|
|
||||||
|
**架构一致性检查:** 当前版本已经补上了交叉审查里指出的高优先级项:开发态回退、配置校验、路径统一、启动顺序、卸载/重装闭环。仍需注意的剩余架构问题只有一个:`config.yaml` 当前仍放在安装目录,和“普通用户手动编辑”存在权限冲突;第一版可以先接受“安装器生成、一般不手改”,后续若要开放用户编辑,再把配置迁到 `ProgramData` 或数据目录。
|
||||||
1221
docs/superpowers/plans/2026-04-25-windows-tray-mode.md
Normal file
1221
docs/superpowers/plans/2026-04-25-windows-tray-mode.md
Normal file
File diff suppressed because it is too large
Load Diff
1156
docs/superpowers/plans/2026-04-26-memory-management.md
Normal file
1156
docs/superpowers/plans/2026-04-26-memory-management.md
Normal file
File diff suppressed because it is too large
Load Diff
388
docs/superpowers/plans/2026-04-26-multimodal-tool.md
Normal file
388
docs/superpowers/plans/2026-04-26-multimodal-tool.md
Normal file
@ -0,0 +1,388 @@
|
|||||||
|
# 多模态图片识别工具 & 工具模型分类 & 对话附件上传 实施计划
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task.
|
||||||
|
|
||||||
|
**Goal:** 在工具管理中新增模型依赖分类和图片识别工具,在 Agent 对话页面新增附件上传功能。
|
||||||
|
|
||||||
|
**Architecture:** 后端扩展 tool entity 新增 requiresModel/modelChannelId/modelId 字段,新增 image_recognize 工具(工厂函数接收已解析凭证),复用现有 LLM provider 层调用模型。附件信息存 metadata,通过 prompt_builder 注入 LLM messages,content 保持纯净。前端附件功能拆分为 3 个独立子组件。
|
||||||
|
|
||||||
|
**Tech Stack:** Midway.js + TypeORM + TypeBox + Socket.IO, Vue 3 + Element Plus + Pinia, OpenAI 兼容 API (火山引擎)
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-04-26-multimodal-tool-design.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Tool Entity 新增模型配置字段
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/backend/src/modules/netaclaw/entity/tool.ts:48`
|
||||||
|
|
||||||
|
- [ ] **Step 1:** 在 `tool.ts` 的 `extra` 字段前(第 48 行前)新增:
|
||||||
|
```typescript
|
||||||
|
@Column({ comment: '是否需要大模型配置 0否 1是', default: 0 })
|
||||||
|
requiresModel: number;
|
||||||
|
|
||||||
|
@Column({ comment: '关联模型渠道ID', nullable: true })
|
||||||
|
modelChannelId: number;
|
||||||
|
|
||||||
|
@Column({ comment: '关联模型ID', length: 100, nullable: true })
|
||||||
|
modelId: string;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2:** 启动后端验证自动建表,用 MCP 验证 `DESCRIBE netaclaw_tool;`
|
||||||
|
|
||||||
|
- [ ] **Step 3:** Commit `feat(netaclaw): tool entity 新增 requiresModel/modelChannelId/modelId`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Catalog Schema 扩展 + Registry 同步
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/backend/src/modules/netaclaw/tools/catalog.ts:14`
|
||||||
|
- Modify: `packages/backend/src/modules/netaclaw/service/tool_registry.ts:28-49,96-128`
|
||||||
|
|
||||||
|
- [ ] **Step 1:** `catalog.ts` ToolSchema 接口第 14 行后新增 `requiresModel?: boolean;`
|
||||||
|
注意:`modelChannelId` 和 `modelId` 是运行时配置,只通过管理界面设置,不进 catalog。
|
||||||
|
|
||||||
|
- [ ] **Step 2:** `tool_registry.ts` createDefaults 返回对象中 `extra: null` 前新增:
|
||||||
|
```typescript
|
||||||
|
requiresModel: s.requiresModel ? 1 : 0,
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3:** `tool_registry.ts` syncCatalogToDb 更新对象中新增:
|
||||||
|
```typescript
|
||||||
|
requiresModel: typeof current.requiresModel === 'number' ? current.requiresModel : defaults.requiresModel,
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4:** `tool_registry.ts` update 方法后新增 getToolModelConfig:
|
||||||
|
```typescript
|
||||||
|
async getToolModelConfig(toolName: string): Promise<{
|
||||||
|
modelChannelId: number; modelId: string; promptHint: string | null;
|
||||||
|
} | null> {
|
||||||
|
const tool = await this.toolRepo.findOneBy({ name: toolName });
|
||||||
|
if (!tool?.modelChannelId || !tool?.modelId) return null;
|
||||||
|
return { modelChannelId: tool.modelChannelId, modelId: tool.modelId, promptHint: tool.promptHint };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5:** Commit `feat(netaclaw): catalog 扩展 requiresModel + registry 同步和查询`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Tool Controller 新增 requiresModel 筛选
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/backend/src/modules/netaclaw/controller/admin/tool.ts:26`
|
||||||
|
- Modify: `packages/backend/src/modules/netaclaw/service/tool_registry.ts:130-155`
|
||||||
|
|
||||||
|
- [ ] **Step 1:** controller page 参数第 26 行后新增 `requiresModel?: number;`
|
||||||
|
|
||||||
|
- [ ] **Step 2:** registry page 方法参数新增 `requiresModel?: number;`,解构加入,where 中新增:
|
||||||
|
```typescript
|
||||||
|
if (typeof requiresModel === 'number') where.requiresModel = requiresModel;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3:** Commit `feat(netaclaw): tool page 接口支持 requiresModel 筛选`
|
||||||
|
|
||||||
|
---
|
||||||
|
### Task 4: 实现 image_recognize 工具(复用 LLM Provider 层)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `packages/backend/src/modules/netaclaw/tools/builtin/image_recognize.ts`
|
||||||
|
- Modify: `packages/backend/src/modules/netaclaw/tools/catalog.ts:64`
|
||||||
|
|
||||||
|
- [ ] **Step 1:** 创建 `tools/builtin/image_recognize.ts`。工厂函数接收已解析的凭证对象(不是 service),通过项目现有 LLM provider 层调用模型:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Type, Static } from '@sinclair/typebox';
|
||||||
|
import { type AnyAgentTool, textResult } from '../common.js';
|
||||||
|
import { registerSchema } from '../catalog.js';
|
||||||
|
|
||||||
|
const DEFAULT_PROMPT = `你是一个专业的图像分析助手。请按以下步骤分析图片:
|
||||||
|
1. **图像分类**:首先识别图片类型(如:身份证、驾驶证、行驶证、营业执照、发票、商品图片、截图、照片、表格、图表、手写文字、印刷文字等)。
|
||||||
|
2. **结构化提取**:根据图片类型,提取关键信息:
|
||||||
|
- 证件类:提取所有字段(姓名、证件号、有效期、地址等)
|
||||||
|
- 票据类:提取金额、日期、项目明细等
|
||||||
|
- 商品类:提取品名、规格、价格、品牌等
|
||||||
|
- 表格/图表类:提取数据结构和关键数值
|
||||||
|
- 其他类:详细描述画面内容
|
||||||
|
3. **详细描述**:对图片内容进行全面、详细的文字描述,不遗漏任何可见信息。
|
||||||
|
4. **质量评估**:简要说明图片清晰度、是否有遮挡或模糊区域。
|
||||||
|
请以结构化格式输出分析结果。`;
|
||||||
|
|
||||||
|
const Params = Type.Object({
|
||||||
|
image: Type.String({ description: '图片URL或base64编码字符串' }),
|
||||||
|
prompt: Type.Optional(Type.String({ description: '分析提示词' })),
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface ImageRecognizeCredentials {
|
||||||
|
baseUrl: string;
|
||||||
|
apiKey: string;
|
||||||
|
supplier: string;
|
||||||
|
modelId: string;
|
||||||
|
promptHint: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createImageRecognizeTool(creds: ImageRecognizeCredentials): AnyAgentTool {
|
||||||
|
return {
|
||||||
|
name: 'image_recognize',
|
||||||
|
label: '图片识别',
|
||||||
|
description: '分析图片内容,支持证件识别、OCR、商品识别等。传入图片URL或base64。',
|
||||||
|
parameters: Params,
|
||||||
|
async execute(_id, params: Static<typeof Params>) {
|
||||||
|
const systemPrompt = creds.promptHint || DEFAULT_PROMPT;
|
||||||
|
const userPrompt = params.prompt
|
||||||
|
? `${systemPrompt}\n\n用户补充要求:${params.prompt}`
|
||||||
|
: systemPrompt;
|
||||||
|
|
||||||
|
const imageUrl = params.image.startsWith('http')
|
||||||
|
? params.image
|
||||||
|
: params.image.startsWith('data:')
|
||||||
|
? params.image
|
||||||
|
: `data:image/png;base64,${params.image}`;
|
||||||
|
|
||||||
|
// 复用项目 LLM provider 层(openai 兼容协议)
|
||||||
|
const { getProvider, supplierToProvider } = await import('../../plugins/llm_providers/index.js');
|
||||||
|
const providerName = supplierToProvider[creds.supplier] || 'openai';
|
||||||
|
const provider = getProvider(providerName);
|
||||||
|
|
||||||
|
const result = await provider.chat({
|
||||||
|
baseUrl: creds.baseUrl,
|
||||||
|
apiKey: creds.apiKey,
|
||||||
|
model: creds.modelId,
|
||||||
|
messages: [{
|
||||||
|
role: 'user',
|
||||||
|
content: [
|
||||||
|
{ type: 'text', text: userPrompt },
|
||||||
|
{ type: 'image_url', image_url: { url: imageUrl } },
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
maxTokens: 4096,
|
||||||
|
});
|
||||||
|
|
||||||
|
return textResult(result.content ?? '模型未返回内容');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
registerSchema({
|
||||||
|
name: 'image_recognize',
|
||||||
|
toolset: 'vision',
|
||||||
|
description: '分析图片内容,支持证件识别、OCR、商品识别等。',
|
||||||
|
capability: 'multimodal',
|
||||||
|
visibility: 'tool',
|
||||||
|
isCore: false,
|
||||||
|
canDisable: true,
|
||||||
|
supportsPromptHint: true,
|
||||||
|
requiresModel: true,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
注意:需要先确认 `plugins/llm_providers/` 的 provider.chat() 方法是否支持 multimodal content parts。如果不支持,需要在 provider 层扩展,而不是绕过它。
|
||||||
|
|
||||||
|
- [ ] **Step 2:** `catalog.ts` 末尾新增 `import './builtin/image_recognize.js';`
|
||||||
|
|
||||||
|
- [ ] **Step 3:** Commit `feat(netaclaw): 实现 image_recognize 工具(复用 LLM provider 层)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Tool Resolver 注入 image_recognize(resolve 阶段排除未配置工具)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/backend/src/modules/netaclaw/service/tool_resolver.ts:0-30,607-611`
|
||||||
|
|
||||||
|
- [ ] **Step 1:** `tool_resolver.ts` 顶部新增 import:
|
||||||
|
```typescript
|
||||||
|
import { createImageRecognizeTool } from '../tools/builtin/image_recognize.js';
|
||||||
|
import { NetaClawModelChannelService } from './model_channel.js';
|
||||||
|
```
|
||||||
|
|
||||||
|
在类中注入:
|
||||||
|
```typescript
|
||||||
|
@Inject()
|
||||||
|
modelChannelService: NetaClawModelChannelService;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2:** resolve() 方法中 escalate 注入后(约第 611 行后)新增。关键:模型未配置时不注入工具,LLM 不会看到它:
|
||||||
|
```typescript
|
||||||
|
if (filteredNames.includes('image_recognize')) {
|
||||||
|
const toolModelConfig = await this.toolRegistry.getToolModelConfig('image_recognize');
|
||||||
|
if (toolModelConfig) {
|
||||||
|
const channelCreds = await this.modelChannelService.resolveForAgent(toolModelConfig.modelChannelId);
|
||||||
|
if (channelCreds) {
|
||||||
|
runtimeTools.push(createImageRecognizeTool({
|
||||||
|
baseUrl: channelCreds.baseUrl,
|
||||||
|
apiKey: channelCreds.apiKey,
|
||||||
|
supplier: channelCreds.supplier,
|
||||||
|
modelId: toolModelConfig.modelId,
|
||||||
|
promptHint: toolModelConfig.promptHint,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
disabledReasons.push({ name: 'image_recognize', reason: 'model_channel_unavailable' });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
disabledReasons.push({ name: 'image_recognize', reason: 'model_not_configured' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3:** 启动后端验证,调用 `/admin/netaclaw/tool/sync` 确认 image_recognize 出现。
|
||||||
|
|
||||||
|
- [ ] **Step 4:** Commit `feat(netaclaw): tool resolver 注入 image_recognize(resolve 阶段排除未配置)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: 前端工具管理页改造
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/frontend/src/modules/agent/views/tools.vue`
|
||||||
|
|
||||||
|
- [ ] **Step 1:** 筛选栏新增"模型依赖"下拉(在 capability 筛选后):
|
||||||
|
```html
|
||||||
|
<el-select v-model="filters.requiresModel" placeholder="模型依赖" clearable style="width:140px">
|
||||||
|
<el-option label="需要模型" :value="1" />
|
||||||
|
<el-option label="不需要模型" :value="0" />
|
||||||
|
</el-select>
|
||||||
|
```
|
||||||
|
filters 对象新增 `requiresModel: undefined`,loadData 请求参数加入。
|
||||||
|
|
||||||
|
- [ ] **Step 2:** 表格新增"模型配置"列(capability 列后):
|
||||||
|
```html
|
||||||
|
<el-table-column label="模型配置" width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span v-if="!row.requiresModel">-</span>
|
||||||
|
<el-tag v-else-if="row.modelId" type="success" size="small">{{ row.modelId }}</el-tag>
|
||||||
|
<el-tag v-else type="warning" size="small">未配置</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3:** 编辑抽屉新增模型配置区域(当 requiresModel===1 时显示):渠道下拉 + 模型联动下拉 + 提示词 textarea。调用 `service.netaclaw.model_channel.allModels()` 获取多模态模型列表。
|
||||||
|
|
||||||
|
- [ ] **Step 4:** 启动前端验证:筛选、表格列、编辑抽屉模型配置。
|
||||||
|
|
||||||
|
- [ ] **Step 5:** Commit `feat(frontend): 工具管理页新增模型依赖筛选和模型配置编辑`
|
||||||
|
|
||||||
|
---
|
||||||
|
### Task 7: WebSocket 协议扩展附件 + 后端消息处理
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/backend/src/modules/netaclaw/gateway/protocol.ts:1-10`
|
||||||
|
- Modify: `packages/backend/src/modules/netaclaw/gateway/server.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1:** `protocol.ts` 顶部新增 ChatAttachment 接口:
|
||||||
|
```typescript
|
||||||
|
export interface ChatAttachment {
|
||||||
|
id: string;
|
||||||
|
type: 'image' | 'video' | 'pdf' | 'document' | 'other';
|
||||||
|
url: string;
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
mimeType: string;
|
||||||
|
role?: 'start_frame' | 'end_frame';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2:** ClientChatMessage 第 9 行后新增 `attachments?: ChatAttachment[];`
|
||||||
|
|
||||||
|
- [ ] **Step 3:** `server.ts` 中处理 chat 消息时,将 attachments 存入 message metadata(不修改 content):
|
||||||
|
```typescript
|
||||||
|
const metadata: Record<string, unknown> = {};
|
||||||
|
if (msg.attachments?.length) {
|
||||||
|
metadata.attachments = msg.attachments;
|
||||||
|
}
|
||||||
|
// 存储消息时传入 metadata
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4:** Commit `feat(netaclaw): WebSocket 协议扩展附件 + 消息 metadata 存储`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 8: Prompt Builder 附件信息注入
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/backend/src/modules/netaclaw/service/prompt_builder.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1:** 在 prompt_builder 构造 LLM messages 时,检查用户消息的 metadata.attachments。如果存在附件,在用户消息后追加一条附件提示 message:
|
||||||
|
```typescript
|
||||||
|
if (userMessage.metadata?.attachments?.length) {
|
||||||
|
const attachments = userMessage.metadata.attachments as ChatAttachment[];
|
||||||
|
const desc = attachments.map(a => {
|
||||||
|
const typeLabel = { image: '图片', video: '视频', pdf: 'PDF', document: '文件', other: '文件' }[a.type];
|
||||||
|
return `- ${typeLabel}: ${a.name} (URL: ${a.url})`;
|
||||||
|
}).join('\n');
|
||||||
|
messages.push({
|
||||||
|
role: 'user',
|
||||||
|
content: `[系统提示] 用户上传了以下附件:\n${desc}\n如需分析图片内容,请使用 image_recognize 工具,传入图片URL。`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这样 content 保持纯净,附件信息通过独立 message 注入 LLM。
|
||||||
|
|
||||||
|
- [ ] **Step 2:** Commit `feat(netaclaw): prompt builder 注入附件信息到 LLM messages`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 9: 前端类型定义 + WebSocket 适配
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/frontend/src/modules/agent/types/index.d.ts`
|
||||||
|
- Modify: `packages/frontend/src/modules/agent/hooks/websocket.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1:** `types/index.d.ts` 新增 ChatAttachment 接口(与后端 protocol.ts 一致)。WSClientMessage 的 chat 类型新增 `attachments?: ChatAttachment[]`。
|
||||||
|
|
||||||
|
- [ ] **Step 2:** 确认 `websocket.ts` 的 ExtendedWSClientMessage 类型能包含 attachments 字段。
|
||||||
|
|
||||||
|
- [ ] **Step 3:** Commit `feat(frontend): 前端类型定义新增 ChatAttachment`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 10: 前端对话附件上传组件
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `packages/frontend/src/modules/agent/components/chat/ChatAttachmentButton.vue`
|
||||||
|
- Create: `packages/frontend/src/modules/agent/components/chat/ChatAttachmentPreview.vue`
|
||||||
|
- Modify: `packages/frontend/src/modules/agent/components/chat/ChatComposer.vue`
|
||||||
|
|
||||||
|
- [ ] **Step 1:** 创建 ChatAttachmentButton.vue — 回形针按钮 + 隐藏 file input,emit `@select(files: File[])`
|
||||||
|
|
||||||
|
- [ ] **Step 2:** 创建 ChatAttachmentPreview.vue — 横向滚动预览条,缩略图/文件图标/删除/首尾帧标记/上传进度
|
||||||
|
|
||||||
|
- [ ] **Step 3:** 改造 ChatComposer.vue:
|
||||||
|
- 集成 ChatAttachmentButton(textarea 左侧)和 ChatAttachmentPreview(textarea 上方)
|
||||||
|
- 支持拖拽(@dragover + @drop)和粘贴(@paste 检测 clipboardData.files)
|
||||||
|
- 文件通过 `/admin/base/comm/upload` 上传到 Space(复用现有上传基础设施)
|
||||||
|
- 保持 `send` 事件名,通过可选 payload 传递附件:`emit('send', attachments)`
|
||||||
|
- 无附件时 `emit('send')` 仍然兼容
|
||||||
|
|
||||||
|
- [ ] **Step 4:** `chat.vue` 中 handleSend 方法适配附件参数:
|
||||||
|
```typescript
|
||||||
|
function handleSend(attachments?: ChatAttachment[]) {
|
||||||
|
const msg = {
|
||||||
|
type: 'chat', sessionId, content: inputText.value,
|
||||||
|
agentId, leafEntryId,
|
||||||
|
...(attachments?.length ? { attachments } : {}),
|
||||||
|
};
|
||||||
|
ws.send(msg);
|
||||||
|
inputText.value = '';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5:** Commit `feat(frontend): Agent 对话附件上传(按钮/预览/拖拽/粘贴)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 11: 消息气泡附件展示
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `packages/frontend/src/modules/agent/components/chat/MessageAttachments.vue`
|
||||||
|
- Modify: `packages/frontend/src/modules/agent/components/message-item.vue`
|
||||||
|
|
||||||
|
- [ ] **Step 1:** 创建 MessageAttachments.vue — 图片网格缩略图(el-image 放大)、视频/PDF/文档文件图标+文件名
|
||||||
|
|
||||||
|
- [ ] **Step 2:** `message-item.vue` 中用户消息气泡内,检查 metadata.attachments 渲染 MessageAttachments。content 保持原样显示,无需过滤。
|
||||||
|
|
||||||
|
- [ ] **Step 3:** 启动前后端,完整测试:上传图片 → 发送 → Agent 调用 image_recognize → 返回分析结果 → 消息气泡显示缩略图
|
||||||
|
|
||||||
|
- [ ] **Step 4:** Commit `feat(frontend): 消息气泡附件展示`
|
||||||
503
docs/superpowers/plans/2026-04-27-p0-skill-secrets.md
Normal file
503
docs/superpowers/plans/2026-04-27-p0-skill-secrets.md
Normal file
@ -0,0 +1,503 @@
|
|||||||
|
# P0: Skill-Scoped 密钥管理 实施计划
|
||||||
|
|
||||||
|
> **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:** 让每个 Skill 拥有独立的密钥/配置管理,通过 DB 加密存储,前端可视化配置,运行时自动注入到 skill 子进程环境变量。
|
||||||
|
|
||||||
|
**Architecture:** 新增 `SkillSecretService` 负责 AES-256-GCM 加密/解密。`netaclaw_skill` 表新增 `secrets`(加密 TEXT)和 `envSchema`(JSON 声明)字段。Admin controller 暴露配置端点,前端 skill-detail 抽屉新增配置 tab。bash 工具重构支持 env override,运行 skill 目录下脚本时自动注入 skill-scoped env。
|
||||||
|
|
||||||
|
**Tech Stack:** Node.js crypto (AES-256-GCM), TypeORM, Midway.js DI, Element Plus (前端)
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-04-27-skill-system-evolution-design.md` Section 3 + 10.2-10.4
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Entity 字段扩展
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/backend/src/modules/netaclaw/entity/skill.ts`
|
||||||
|
- Modify: `packages/shared/types/skill.types.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 在 entity/skill.ts 新增 secrets 和 envSchema 字段**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 在 fingerprint 字段之后添加
|
||||||
|
|
||||||
|
@Column({ type: 'text', comment: 'AES-256-GCM 加密的 secrets JSON', nullable: true })
|
||||||
|
secrets: string;
|
||||||
|
|
||||||
|
@Column({ type: 'json', comment: 'env 声明 schema', nullable: true })
|
||||||
|
envSchema: Array<{ name: string; required: boolean; description?: string; default?: string }>;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 在 shared/types/skill.types.ts 新增 EnvSchemaItem 类型**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface EnvSchemaItem {
|
||||||
|
name: string;
|
||||||
|
required: boolean;
|
||||||
|
description?: string;
|
||||||
|
default?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 重启开发服务器验证 TypeORM 自动同步**
|
||||||
|
|
||||||
|
Run: 重启 backend,检查 `netaclaw_skill` 表是否新增了 `secrets` 和 `envSchema` 列。可通过 MCP mysql `describe_table` 工具验证。
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/entity/skill.ts packages/shared/types/skill.types.ts
|
||||||
|
git commit -m "feat(skill): add secrets and envSchema columns to netaclaw_skill entity"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: SkillSecretService 实现
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `packages/backend/src/modules/netaclaw/service/skill_secret.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 创建 skill_secret.ts 文件,实现加密/解密**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Provide, Scope, ScopeEnum, Logger, Init } from '@midwayjs/core';
|
||||||
|
import { ILogger } from '@midwayjs/logger';
|
||||||
|
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
import { NetaClawSkillEntity } from '../entity/skill.js';
|
||||||
|
|
||||||
|
@Provide()
|
||||||
|
@Scope(ScopeEnum.Singleton)
|
||||||
|
export class SkillSecretService {
|
||||||
|
@Logger()
|
||||||
|
logger: ILogger;
|
||||||
|
|
||||||
|
@InjectEntityModel(NetaClawSkillEntity)
|
||||||
|
skillRepo: Repository<NetaClawSkillEntity>;
|
||||||
|
|
||||||
|
private readonly algorithm = 'aes-256-gcm' as const;
|
||||||
|
private readonly ivLength = 16;
|
||||||
|
private readonly authTagLength = 16;
|
||||||
|
|
||||||
|
private deriveKey(): Buffer {
|
||||||
|
const raw = process.env.SKILL_SECRET_KEY || process.env.APP_SECRET;
|
||||||
|
if (!raw) throw new Error('SKILL_SECRET_KEY or APP_SECRET environment variable must be set');
|
||||||
|
return crypto.createHash('sha256').update(raw).digest();
|
||||||
|
}
|
||||||
|
|
||||||
|
encrypt(plainObj: Record<string, string>): string {
|
||||||
|
const iv = crypto.randomBytes(this.ivLength);
|
||||||
|
const key = this.deriveKey();
|
||||||
|
const cipher = crypto.createCipheriv(this.algorithm, key, iv);
|
||||||
|
const encrypted = Buffer.concat([
|
||||||
|
cipher.update(JSON.stringify(plainObj), 'utf8'),
|
||||||
|
cipher.final(),
|
||||||
|
]);
|
||||||
|
const authTag = cipher.getAuthTag();
|
||||||
|
return Buffer.concat([iv, encrypted, authTag]).toString('base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
decrypt(cipherText: string): Record<string, string> {
|
||||||
|
const buf = Buffer.from(cipherText, 'base64');
|
||||||
|
const iv = buf.subarray(0, this.ivLength);
|
||||||
|
const authTag = buf.subarray(buf.length - this.authTagLength);
|
||||||
|
const encrypted = buf.subarray(this.ivLength, buf.length - this.authTagLength);
|
||||||
|
const key = this.deriveKey();
|
||||||
|
const decipher = crypto.createDecipheriv(this.algorithm, key, iv);
|
||||||
|
decipher.setAuthTag(authTag);
|
||||||
|
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
||||||
|
return JSON.parse(decrypted.toString('utf8'));
|
||||||
|
}
|
||||||
|
|
||||||
|
async resolveEnv(skillName: string): Promise<Record<string, string>> {
|
||||||
|
const entity = await this.skillRepo.findOneBy({ name: skillName });
|
||||||
|
if (!entity) return {};
|
||||||
|
|
||||||
|
const env: Record<string, string> = {};
|
||||||
|
|
||||||
|
// 填充 defaults
|
||||||
|
if (entity.envSchema) {
|
||||||
|
for (const item of entity.envSchema) {
|
||||||
|
if (item.default) env[item.name] = item.default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 覆盖 secrets
|
||||||
|
if (entity.secrets) {
|
||||||
|
try {
|
||||||
|
const secrets = this.decrypt(entity.secrets);
|
||||||
|
Object.assign(env, secrets);
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error('[SkillSecret] 解密 %s secrets 失败: %s', skillName, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveSecrets(skillName: string, secrets: Record<string, string>): Promise<void> {
|
||||||
|
const encrypted = this.encrypt(secrets);
|
||||||
|
await this.skillRepo.update({ name: skillName }, { secrets: encrypted });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getConfiguredKeys(skillName: string): Promise<Array<{ name: string; hasValue: boolean }>> {
|
||||||
|
const entity = await this.skillRepo.findOneBy({ name: skillName });
|
||||||
|
if (!entity?.envSchema) return [];
|
||||||
|
|
||||||
|
let configuredKeys = new Set<string>();
|
||||||
|
if (entity.secrets) {
|
||||||
|
try {
|
||||||
|
const secrets = this.decrypt(entity.secrets);
|
||||||
|
configuredKeys = new Set(Object.keys(secrets));
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
return entity.envSchema.map(item => ({
|
||||||
|
name: item.name,
|
||||||
|
hasValue: configuredKeys.has(item.name),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 验证编译通过**
|
||||||
|
|
||||||
|
Run: `cd packages/backend && npx tsc --noEmit`
|
||||||
|
Expected: 无错误
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/service/skill_secret.ts
|
||||||
|
git commit -m "feat(skill): add SkillSecretService with AES-256-GCM encryption"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Admin Controller 端点
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/backend/src/modules/netaclaw/controller/admin/skill.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 在 AdminNetaClawSkillController 中注入 SkillSecretService**
|
||||||
|
|
||||||
|
在文件顶部 import 区域添加:
|
||||||
|
```typescript
|
||||||
|
import { SkillSecretService } from '../../service/skill_secret.js';
|
||||||
|
```
|
||||||
|
|
||||||
|
在 class 内部添加注入:
|
||||||
|
```typescript
|
||||||
|
@Inject()
|
||||||
|
skillSecret: SkillSecretService;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 新增 envSchema 端点**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Get('/envSchema', { summary: '获取 skill 的 env 声明和配置状态' })
|
||||||
|
async envSchema(@Query('name') name: string) {
|
||||||
|
if (!name) return this.fail('缺少 name 参数');
|
||||||
|
const keys = await this.skillSecret.getConfiguredKeys(name);
|
||||||
|
const skill = await this.skillLoader.getSkill(name);
|
||||||
|
const schema = skill?.metadata?.env || [];
|
||||||
|
return this.ok({ name, schema, configured: keys });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 新增 secrets 保存端点**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Post('/secrets', { summary: '保存 skill secrets(加密存储)' })
|
||||||
|
async saveSecrets(@Body() body: { name: string; secrets: Record<string, string> }) {
|
||||||
|
if (!body.name || !body.secrets) return this.fail('缺少 name 或 secrets');
|
||||||
|
try {
|
||||||
|
await this.skillSecret.saveSecrets(body.name, body.secrets);
|
||||||
|
return this.ok();
|
||||||
|
} catch (e: any) {
|
||||||
|
return this.fail(e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 验证编译通过**
|
||||||
|
|
||||||
|
Run: `cd packages/backend && npx tsc --noEmit`
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/controller/admin/skill.ts
|
||||||
|
git commit -m "feat(skill): add envSchema and secrets admin endpoints"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: SkillLoaderService 新增 resolveSkillByPath
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/backend/src/modules/netaclaw/service/skill_loader.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 在 SkillLoaderService 中新增 resolveSkillByPath 方法**
|
||||||
|
|
||||||
|
在 class 末尾(`getSkillFilePath` 方法之后)添加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/** 根据绝对路径判断是否属于某个 skill 目录,返回 skill name 或 null */
|
||||||
|
resolveSkillByPath(absPath: string): string | null {
|
||||||
|
if (!absPath) return null;
|
||||||
|
const normalized = path.resolve(absPath);
|
||||||
|
const skillsDirNorm = path.resolve(this.skillsDir);
|
||||||
|
if (!normalized.startsWith(skillsDirNorm + path.sep)) return null;
|
||||||
|
const relative = normalized.slice(skillsDirNorm.length + 1);
|
||||||
|
const skillName = relative.split(path.sep)[0];
|
||||||
|
if (!skillName || !this.skills.has(skillName)) return null;
|
||||||
|
return skillName;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 验证编译通过**
|
||||||
|
|
||||||
|
Run: `cd packages/backend && npx tsc --noEmit`
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/service/skill_loader.ts
|
||||||
|
git commit -m "feat(skill): add resolveSkillByPath for skill directory detection"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Bash 工具 env 注入重构
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/backend/src/modules/netaclaw/tools/builtin/bash.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 新增 BashEnvProvider 接口并修改 createLocalBashOperations 签名**
|
||||||
|
|
||||||
|
在 `BashToolOptions` 接口之前添加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface BashEnvProvider {
|
||||||
|
getAdditionalEnv(cwd: string): Promise<Record<string, string>>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
修改 `createLocalBashOperations` 签名:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function createLocalBashOperations(envProvider?: BashEnvProvider): BashOperations {
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 在 exec 方法中注入额外 env**
|
||||||
|
|
||||||
|
修改 `exec` 方法内部,在 spawn 之前获取额外 env:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
exec: async (command, cwd, options) => {
|
||||||
|
const shellConfig = resolveShellConfig();
|
||||||
|
const shell = shellConfig.shell;
|
||||||
|
const shellArgs = [...shellConfig.args, command];
|
||||||
|
|
||||||
|
// 获取 skill-scoped env
|
||||||
|
let envVars: Record<string, string> = { ...process.env } as any;
|
||||||
|
if (envProvider) {
|
||||||
|
try {
|
||||||
|
const additional = await envProvider.getAdditionalEnv(cwd);
|
||||||
|
Object.assign(envVars, additional);
|
||||||
|
} catch { /* ignore env resolution failures */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
const child = await spawnWithFallback(shell, shellArgs, {
|
||||||
|
cwd,
|
||||||
|
env: envVars,
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
...withHiddenWindow({}),
|
||||||
|
}).catch(reject);
|
||||||
|
// ... rest unchanged
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 找到 createLocalBashOperations 的调用点,传入 envProvider**
|
||||||
|
|
||||||
|
搜索 `createLocalBashOperations()` 的调用位置(通常在 bash 工具的工厂函数或 tool_resolver 中),将 `SkillSecretService` + `SkillLoaderService` 组合为 `BashEnvProvider` 传入。
|
||||||
|
|
||||||
|
实现一个适配器:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 在 bash.ts 底部或单独文件
|
||||||
|
export function createSkillBashEnvProvider(
|
||||||
|
skillLoader: SkillLoaderService,
|
||||||
|
skillSecret: SkillSecretService,
|
||||||
|
): BashEnvProvider {
|
||||||
|
return {
|
||||||
|
async getAdditionalEnv(cwd: string): Promise<Record<string, string>> {
|
||||||
|
const skillName = skillLoader.resolveSkillByPath(cwd);
|
||||||
|
if (!skillName) return {};
|
||||||
|
return skillSecret.resolveEnv(skillName);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3.5: 在 tool_resolver.ts 中接线 BashEnvProvider**
|
||||||
|
|
||||||
|
找到 `tool_resolver.ts` 中创建 bash 工具的位置(搜索 `createBashTool` 或 `createLocalBashOperations`)。将 `SkillLoaderService` 和 `SkillSecretService` 组合为 `BashEnvProvider` 传入:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// tool_resolver.ts 中
|
||||||
|
import { createSkillBashEnvProvider } from '../tools/builtin/bash.js';
|
||||||
|
import { SkillSecretService } from './skill_secret.js';
|
||||||
|
|
||||||
|
// class 内部注入
|
||||||
|
@Inject()
|
||||||
|
skillSecret: SkillSecretService;
|
||||||
|
|
||||||
|
// 在 bash 工具创建处传入 envProvider
|
||||||
|
const bashEnvProvider = createSkillBashEnvProvider(this.skillLoader, this.skillSecret);
|
||||||
|
```
|
||||||
|
|
||||||
|
具体接线方式取决于 bash 工具的创建路径——在实施时需要 trace `createLocalBashOperations()` 的调用链,将 `bashEnvProvider` 参数传递到位。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 验证编译通过(bash.ts)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: 前端 skill-detail 配置 Tab
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/frontend/src/modules/agent/components/skill-detail.vue`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 重构 skill-detail.vue 为 tab 布局**
|
||||||
|
|
||||||
|
将现有内容包裹在 `<el-tabs>` 中,第一个 tab 保留原有内容(基本信息 + SKILL.md),新增第二个 tab "配置":
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<el-tabs v-model="activeTab">
|
||||||
|
<el-tab-pane label="基本信息" name="info">
|
||||||
|
<!-- 现有内容移入此处 -->
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="配置" name="config">
|
||||||
|
<div v-if="envSchema.length === 0" class="empty-hint">此 Skill 无需配置环境变量</div>
|
||||||
|
<div v-else>
|
||||||
|
<div v-for="item in envSchema" :key="item.name" class="env-row">
|
||||||
|
<div class="env-label">
|
||||||
|
<span>{{ item.name }}</span>
|
||||||
|
<el-tag v-if="item.required" size="small" type="danger">必填</el-tag>
|
||||||
|
</div>
|
||||||
|
<div v-if="item.description" class="env-desc">{{ item.description }}</div>
|
||||||
|
<el-input
|
||||||
|
v-model="secretValues[item.name]"
|
||||||
|
:placeholder="getConfiguredStatus(item.name) ? '已配置(留空则不修改)' : item.default || '请输入'"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" style="margin-top: 16px" @click="handleSaveSecrets">保存配置</el-button>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 新增配置相关的 script 逻辑**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const activeTab = ref('info');
|
||||||
|
const envSchema = ref<any[]>([]);
|
||||||
|
const configuredKeys = ref<any[]>([]);
|
||||||
|
const secretValues = ref<Record<string, string>>({});
|
||||||
|
|
||||||
|
function getConfiguredStatus(name: string): boolean {
|
||||||
|
return configuredKeys.value.find(k => k.name === name)?.hasValue || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadEnvSchema() {
|
||||||
|
if (!props.skill?.name) return;
|
||||||
|
try {
|
||||||
|
const res = await service.request({
|
||||||
|
url: '/admin/netaclaw/skill/envSchema',
|
||||||
|
params: { name: props.skill.name },
|
||||||
|
});
|
||||||
|
envSchema.value = res?.schema || [];
|
||||||
|
configuredKeys.value = res?.configured || [];
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSaveSecrets() {
|
||||||
|
const nonEmpty = Object.fromEntries(
|
||||||
|
Object.entries(secretValues.value).filter(([_, v]) => v.trim())
|
||||||
|
);
|
||||||
|
if (Object.keys(nonEmpty).length === 0) {
|
||||||
|
ElMessage.warning('请至少填写一个配置项');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await service.request({
|
||||||
|
url: '/admin/netaclaw/skill/secrets',
|
||||||
|
method: 'POST',
|
||||||
|
data: { name: props.skill.name, secrets: nonEmpty },
|
||||||
|
});
|
||||||
|
ElMessage.success('配置已保存');
|
||||||
|
secretValues.value = {};
|
||||||
|
await loadEnvSchema();
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e.message || '保存失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.skill, () => {
|
||||||
|
activeTab.value = 'info';
|
||||||
|
secretValues.value = {};
|
||||||
|
loadEnvSchema();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 验证前端编译通过**
|
||||||
|
|
||||||
|
Run: `cd packages/frontend && npx vue-tsc --noEmit`
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/frontend/src/modules/agent/components/skill-detail.vue
|
||||||
|
git commit -m "feat(skill): add configuration tab to skill-detail drawer"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: SkillLoaderService 解析 envSchema 并同步到 DB
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/backend/src/modules/netaclaw/service/skill_loader.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 在 scanSkills 中解析 metadata.env 并同步 envSchema 到 DB**
|
||||||
|
|
||||||
|
在 `getSkillMetas()` 方法中,当同步到 DB 时,同时写入 envSchema:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 在 getSkillMetas() 的 skillRepo.save 调用中,添加 envSchema 字段
|
||||||
|
const envFromMeta = (fs.metadata as any)?.env;
|
||||||
|
const envSchema = Array.isArray(envFromMeta) ? envFromMeta.map((e: any) => ({
|
||||||
|
name: e.name,
|
||||||
|
required: !!e.required,
|
||||||
|
description: e.description || undefined,
|
||||||
|
default: e.default || undefined,
|
||||||
|
})) : null;
|
||||||
|
|
||||||
|
// 在 save/update 的 entityData 中加入
|
||||||
|
envSchema,
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 验证编译通过**
|
||||||
|
|
||||||
|
Run: `cd packages/backend && npx tsc --noEmit`
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/service/skill_loader.ts
|
||||||
|
git commit -m "feat(skill): sync envSchema from SKILL.md metadata to DB during scan"
|
||||||
|
```
|
||||||
684
docs/superpowers/plans/2026-04-27-p1-skill-executor.md
Normal file
684
docs/superpowers/plans/2026-04-27-p1-skill-executor.md
Normal file
@ -0,0 +1,684 @@
|
|||||||
|
# P1: Skill Runtime Executor 实施计划
|
||||||
|
|
||||||
|
> **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:** 新增 `execute_skill` Agent 工具和 `SkillExecutorService`,让 Agent 通过标准化接口调用 compute-entry skill,自动处理运行时选择、env 注入、子进程管理。同时新增 `skill.config.yaml` 解析器和依赖安装改造。
|
||||||
|
|
||||||
|
**Architecture:** 新增 `SkillConfigService` 解析 skill.config.yaml。`SkillExecutorService` 根据 config 的 runtime/entrypoint 声明 spawn 子进程,通过 stdin/stdout JSON 协议通信。`execute_skill` 工具在 `tool_resolver.ts` 中条件注入。依赖安装改造为 skill 级隔离(venv/node_modules)。
|
||||||
|
|
||||||
|
**Tech Stack:** Node.js child_process, js-yaml, TypeBox (参数校验), Midway.js DI
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-04-27-skill-system-evolution-design.md` Section 4 + 10.1
|
||||||
|
|
||||||
|
**Depends on:** P0 (SkillSecretService)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: SkillConfigService — skill.config.yaml 解析器
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `packages/backend/src/modules/netaclaw/service/skill_config.ts`
|
||||||
|
- Modify: `packages/shared/types/skill.types.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 在 shared/types/skill.types.ts 新增 SkillConfig 类型**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export type SkillRuntime = 'node' | 'python' | 'bash' | 'dotnet';
|
||||||
|
export type SkillClassification = 'prompt' | 'compute-entry' | 'compute-toolkit';
|
||||||
|
|
||||||
|
export interface SkillSystemDep {
|
||||||
|
name: string;
|
||||||
|
check: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SkillInterfaceField {
|
||||||
|
type: string;
|
||||||
|
required?: boolean;
|
||||||
|
default?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SkillRouteRule {
|
||||||
|
match: string[];
|
||||||
|
required_refs: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SkillConfig {
|
||||||
|
runtime?: SkillRuntime;
|
||||||
|
entrypoint?: string;
|
||||||
|
timeout?: number;
|
||||||
|
env?: Array<{ name: string; required: boolean; description?: string; default?: string }>;
|
||||||
|
dependencies?: {
|
||||||
|
system?: SkillSystemDep[];
|
||||||
|
python?: { source?: string; inline?: string[] };
|
||||||
|
node?: { packages?: string[] };
|
||||||
|
dotnet?: { project?: string };
|
||||||
|
};
|
||||||
|
setup?: { posix?: string; win32?: string };
|
||||||
|
interface?: {
|
||||||
|
input?: Record<string, SkillInterfaceField>;
|
||||||
|
output?: Record<string, SkillInterfaceField>;
|
||||||
|
};
|
||||||
|
references?: {
|
||||||
|
required?: string[];
|
||||||
|
optional?: string[];
|
||||||
|
routes?: SkillRouteRule[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 创建 skill_config.ts**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Provide, Scope, ScopeEnum, Logger } from '@midwayjs/core';
|
||||||
|
import { ILogger } from '@midwayjs/logger';
|
||||||
|
import * as fs from 'fs/promises';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as yaml from 'js-yaml';
|
||||||
|
import type { SkillConfig, SkillClassification } from '../../../../shared/types/skill.types.js';
|
||||||
|
|
||||||
|
@Provide()
|
||||||
|
@Scope(ScopeEnum.Singleton)
|
||||||
|
export class SkillConfigService {
|
||||||
|
@Logger()
|
||||||
|
logger: ILogger;
|
||||||
|
|
||||||
|
private configs: Map<string, SkillConfig> = new Map();
|
||||||
|
private parseErrors: Map<string, string> = new Map();
|
||||||
|
|
||||||
|
async loadConfig(skillDir: string, skillName: string): Promise<SkillConfig | null> {
|
||||||
|
const configPath = path.join(skillDir, 'skill.config.yaml');
|
||||||
|
try {
|
||||||
|
const raw = await fs.readFile(configPath, 'utf-8');
|
||||||
|
const config = yaml.load(raw) as SkillConfig;
|
||||||
|
this.configs.set(skillName, config);
|
||||||
|
return config;
|
||||||
|
} catch (e: any) {
|
||||||
|
// 文件不存在 → 正常(prompt skill),解析失败 → 记录错误
|
||||||
|
if (e?.code !== 'ENOENT') {
|
||||||
|
this.logger.warn('[SkillConfig] Failed to parse %s: %s', configPath, e.message);
|
||||||
|
this.parseErrors.set(skillName, e.message);
|
||||||
|
}
|
||||||
|
this.configs.delete(skillName);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getParseError(skillName: string): string | null {
|
||||||
|
return this.parseErrors.get(skillName) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getConfig(skillName: string): SkillConfig | null {
|
||||||
|
return this.configs.get(skillName) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
classify(skillName: string): SkillClassification {
|
||||||
|
const config = this.configs.get(skillName);
|
||||||
|
if (!config) return 'prompt';
|
||||||
|
if (config.entrypoint) return 'compute-entry';
|
||||||
|
if (config.runtime) return 'compute-toolkit';
|
||||||
|
return 'prompt';
|
||||||
|
}
|
||||||
|
|
||||||
|
hasComputeEntrySkills(skillNames: string[]): boolean {
|
||||||
|
return skillNames.some(name => this.classify(name) === 'compute-entry');
|
||||||
|
}
|
||||||
|
|
||||||
|
clearAll(): void {
|
||||||
|
this.configs.clear();
|
||||||
|
this.parseErrors.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 验证编译通过**
|
||||||
|
|
||||||
|
Run: `cd packages/backend && npx tsc --noEmit`
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/shared/types/skill.types.ts packages/backend/src/modules/netaclaw/service/skill_config.ts
|
||||||
|
git commit -m "feat(skill): add SkillConfigService for skill.config.yaml parsing"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: SkillLoaderService 集成 SkillConfigService
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/backend/src/modules/netaclaw/service/skill_loader.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 注入 SkillConfigService 并在 scanSkills 中加载 config**
|
||||||
|
|
||||||
|
在 class 顶部添加注入:
|
||||||
|
```typescript
|
||||||
|
import { SkillConfigService } from './skill_config.js';
|
||||||
|
|
||||||
|
// class 内部
|
||||||
|
@Inject()
|
||||||
|
skillConfig: SkillConfigService;
|
||||||
|
```
|
||||||
|
|
||||||
|
在 `scanSkills()` 的循环中,加载完 SKILL.md 后尝试加载 skill.config.yaml:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 在 this.skills.set(skill.name, skill) 之后添加
|
||||||
|
const skillDir = path.join(this.skillsDir, entry.name);
|
||||||
|
await this.skillConfig.loadConfig(skillDir, skill.name);
|
||||||
|
```
|
||||||
|
|
||||||
|
在 `scanSkills()` 开头的 `this.skills.clear()` 之后添加:
|
||||||
|
```typescript
|
||||||
|
this.skillConfig.clearAll();
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 新增 getSkillConfig 代理方法**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
getSkillConfig(name: string): SkillConfig | null {
|
||||||
|
return this.skillConfig.getConfig(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSkillClassification(name: string): SkillClassification {
|
||||||
|
return this.skillConfig.classify(name);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 验证编译通过**
|
||||||
|
|
||||||
|
Run: `cd packages/backend && npx tsc --noEmit`
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/service/skill_loader.ts
|
||||||
|
git commit -m "feat(skill): integrate SkillConfigService into SkillLoaderService scan"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: SkillExecutorService 实现
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `packages/backend/src/modules/netaclaw/service/skill_executor.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 创建 skill_executor.ts**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Provide, Inject, Scope, ScopeEnum, Logger } from '@midwayjs/core';
|
||||||
|
import { ILogger } from '@midwayjs/logger';
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { SkillLoaderService } from './skill_loader.js';
|
||||||
|
import { SkillSecretService } from './skill_secret.js';
|
||||||
|
import { SkillConfigService } from './skill_config.js';
|
||||||
|
|
||||||
|
const ENV_WHITELIST = [
|
||||||
|
'PATH', 'HOME', 'USER', 'LANG', 'LC_ALL', 'TZ',
|
||||||
|
'TEMP', 'TMP', 'TMPDIR',
|
||||||
|
'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY',
|
||||||
|
'SystemRoot', 'APPDATA', 'LOCALAPPDATA', // Windows
|
||||||
|
];
|
||||||
|
|
||||||
|
export interface SkillExecuteParams {
|
||||||
|
skillName: string;
|
||||||
|
input: Record<string, unknown>;
|
||||||
|
agentId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SkillExecuteResult {
|
||||||
|
success: boolean;
|
||||||
|
output?: Record<string, unknown>;
|
||||||
|
error?: string;
|
||||||
|
duration: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provide()
|
||||||
|
@Scope(ScopeEnum.Singleton)
|
||||||
|
export class SkillExecutorService {
|
||||||
|
@Logger()
|
||||||
|
logger: ILogger;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
skillLoader: SkillLoaderService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
skillSecret: SkillSecretService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
skillConfig: SkillConfigService;
|
||||||
|
|
||||||
|
async execute(params: SkillExecuteParams): Promise<SkillExecuteResult> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
const { skillName, input } = params;
|
||||||
|
|
||||||
|
const skill = this.skillLoader.getSkill(skillName);
|
||||||
|
if (!skill) {
|
||||||
|
return { success: false, error: `Skill "${skillName}" not found`, duration: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = this.skillConfig.getConfig(skillName);
|
||||||
|
if (!config?.entrypoint) {
|
||||||
|
return { success: false, error: `Skill "${skillName}" is not a compute-entry skill`, duration: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const skillDir = path.join(this.skillLoader.getSkillsDir(), skillName);
|
||||||
|
const timeout = config.timeout || 30000;
|
||||||
|
|
||||||
|
// 安全校验:entrypoint 路径穿越防护
|
||||||
|
const resolvedEntry = path.resolve(skillDir, config.entrypoint);
|
||||||
|
if (!resolvedEntry.startsWith(path.resolve(skillDir) + path.sep)) {
|
||||||
|
return { success: false, error: 'Entrypoint path traversal detected', duration: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预检:Python runtime 检查 .venv 是否存在
|
||||||
|
if ((config.runtime || 'python') === 'python') {
|
||||||
|
const venvPath = path.join(skillDir, '.venv');
|
||||||
|
try {
|
||||||
|
const fs = await import('fs/promises');
|
||||||
|
await fs.access(venvPath);
|
||||||
|
} catch {
|
||||||
|
return { success: false, error: `Python venv not found at ${venvPath}. Run dependency install first (POST /admin/netaclaw/skill/installDeps).`, duration: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build command based on runtime
|
||||||
|
const { cmd, args } = this.buildCommand(config.runtime || 'python', config.entrypoint, skillDir);
|
||||||
|
|
||||||
|
// Build env: whitelist + skill-scoped secrets
|
||||||
|
const baseEnv: Record<string, string> = {};
|
||||||
|
for (const key of ENV_WHITELIST) {
|
||||||
|
if (process.env[key]) baseEnv[key] = process.env[key]!;
|
||||||
|
}
|
||||||
|
const skillEnv = await this.skillSecret.resolveEnv(skillName);
|
||||||
|
const env = { ...baseEnv, ...skillEnv };
|
||||||
|
|
||||||
|
return new Promise<SkillExecuteResult>((resolve) => {
|
||||||
|
let stdout = '';
|
||||||
|
let stderr = '';
|
||||||
|
let timedOut = false;
|
||||||
|
|
||||||
|
const child = spawn(cmd, args, {
|
||||||
|
cwd: skillDir,
|
||||||
|
env,
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
|
timeout: 0, // we handle timeout manually
|
||||||
|
});
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
timedOut = true;
|
||||||
|
child.kill('SIGTERM');
|
||||||
|
}, timeout);
|
||||||
|
|
||||||
|
child.stdout.on('data', (chunk: Buffer) => { stdout += chunk.toString(); });
|
||||||
|
child.stderr.on('data', (chunk: Buffer) => { stderr += chunk.toString(); });
|
||||||
|
|
||||||
|
// Send input via stdin
|
||||||
|
child.stdin.write(JSON.stringify(input));
|
||||||
|
child.stdin.end();
|
||||||
|
|
||||||
|
child.on('close', (code) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
|
this.logger.info('[SkillExecutor] %s executed agent=%s duration=%dms exit=%d',
|
||||||
|
skillName, params.agentId || 'unknown', duration, code);
|
||||||
|
|
||||||
|
if (timedOut) {
|
||||||
|
resolve({ success: false, error: `Execution timed out after ${timeout}ms`, duration });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code !== 0) {
|
||||||
|
const errMsg = stderr.slice(0, 500) || `Process exited with code ${code}`;
|
||||||
|
resolve({ success: false, error: errMsg, duration });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const output = JSON.parse(stdout.trim());
|
||||||
|
resolve({ success: true, output, duration });
|
||||||
|
} catch {
|
||||||
|
resolve({ success: false, error: `Invalid JSON output: ${stdout.slice(0, 200)}`, duration });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('error', (err) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve({ success: false, error: err.message, duration: Date.now() - startTime });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildCommand(runtime: string, entrypoint: string, skillDir: string): { cmd: string; args: string[] } {
|
||||||
|
const isWin = process.platform === 'win32';
|
||||||
|
switch (runtime) {
|
||||||
|
case 'python': {
|
||||||
|
const python = isWin
|
||||||
|
? path.join(skillDir, '.venv', 'Scripts', 'python.exe')
|
||||||
|
: path.join(skillDir, '.venv', 'bin', 'python');
|
||||||
|
return { cmd: python, args: [entrypoint] };
|
||||||
|
}
|
||||||
|
case 'node':
|
||||||
|
return { cmd: 'node', args: [entrypoint] };
|
||||||
|
case 'bash':
|
||||||
|
return { cmd: isWin ? 'bash' : '/bin/bash', args: [entrypoint] };
|
||||||
|
case 'dotnet':
|
||||||
|
return { cmd: 'dotnet', args: ['run', '--project', entrypoint] };
|
||||||
|
default:
|
||||||
|
return { cmd: runtime, args: [entrypoint] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 验证编译通过**
|
||||||
|
|
||||||
|
Run: `cd packages/backend && npx tsc --noEmit`
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/service/skill_executor.ts
|
||||||
|
git commit -m "feat(skill): add SkillExecutorService for compute-entry skill execution"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: execute_skill Agent 工具
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `packages/backend/src/modules/netaclaw/tools/builtin/execute_skill.ts`
|
||||||
|
- Modify: `packages/backend/src/modules/netaclaw/tools/catalog.ts`
|
||||||
|
- Modify: `packages/backend/src/modules/netaclaw/service/tool_resolver.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 创建 execute_skill.ts**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Type } from '@sinclair/typebox';
|
||||||
|
import { AgentToolWithMeta, textResult } from '../common.js';
|
||||||
|
import { SkillExecutorService } from '../../service/skill_executor.js';
|
||||||
|
|
||||||
|
const ExecuteSkillParams = Type.Object({
|
||||||
|
name: Type.String({ description: 'compute-entry skill 名称' }),
|
||||||
|
input: Type.Record(Type.String(), Type.Unknown(), { description: '输入参数 JSON' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export function createExecuteSkillTool(executor: SkillExecutorService): AgentToolWithMeta<typeof ExecuteSkillParams, unknown> {
|
||||||
|
return {
|
||||||
|
name: 'execute_skill',
|
||||||
|
label: '执行 Skill',
|
||||||
|
description: '执行 compute-entry 类型的 skill。传入 skill 名称和输入参数,返回执行结果。',
|
||||||
|
parameters: ExecuteSkillParams,
|
||||||
|
async execute(_id, params) {
|
||||||
|
const result = await executor.execute({
|
||||||
|
skillName: params.name,
|
||||||
|
input: params.input as Record<string, unknown>,
|
||||||
|
});
|
||||||
|
if (result.success) {
|
||||||
|
return textResult(JSON.stringify(result.output, null, 2));
|
||||||
|
} else {
|
||||||
|
return textResult(`执行失败: ${result.error}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
import { registerSchema } from '../catalog.js';
|
||||||
|
registerSchema({
|
||||||
|
name: 'execute_skill',
|
||||||
|
toolset: 'skill',
|
||||||
|
description: '执行 compute-entry 类型的技能',
|
||||||
|
capability: 'text',
|
||||||
|
visibility: 'skill',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 在 catalog.ts 中导入 execute_skill**
|
||||||
|
|
||||||
|
在 catalog.ts 的 import 区域(约 line 49-65)添加:
|
||||||
|
```typescript
|
||||||
|
import '../tools/builtin/execute_skill.js';
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 在 tool_resolver.ts 中注入并条件添加 execute_skill**
|
||||||
|
|
||||||
|
在 import 区域添加:
|
||||||
|
```typescript
|
||||||
|
import { createExecuteSkillTool } from '../tools/builtin/execute_skill.js';
|
||||||
|
import { SkillExecutorService } from './skill_executor.js';
|
||||||
|
```
|
||||||
|
|
||||||
|
在 class 内部添加注入:
|
||||||
|
```typescript
|
||||||
|
@Inject()
|
||||||
|
skillExecutor: SkillExecutorService;
|
||||||
|
```
|
||||||
|
|
||||||
|
在 `resolve()` 方法中,现有 skill 工具注入块(约 line 602-606)之后添加:
|
||||||
|
```typescript
|
||||||
|
// 仅当 Agent 配置的 skills 中存在 compute-entry 类型时才注入
|
||||||
|
if (filteredNames.includes('execute_skill')) {
|
||||||
|
const agentSkills = params.agent?.skills || [];
|
||||||
|
const hasComputeEntry = agentSkills.some(name => this.skillConfig.classify(name) === 'compute-entry');
|
||||||
|
if (hasComputeEntry) {
|
||||||
|
runtimeTools.push(createExecuteSkillTool(this.skillExecutor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 验证编译通过**
|
||||||
|
|
||||||
|
Run: `cd packages/backend && npx tsc --noEmit`
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/tools/builtin/execute_skill.ts \
|
||||||
|
packages/backend/src/modules/netaclaw/tools/catalog.ts \
|
||||||
|
packages/backend/src/modules/netaclaw/service/tool_resolver.ts
|
||||||
|
git commit -m "feat(skill): add execute_skill tool with conditional injection in tool_resolver"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: 依赖安装改造
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/backend/src/modules/netaclaw/service/skill_installer.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 重构 installDependencies 方法**
|
||||||
|
|
||||||
|
替换现有 `installDependencies` 方法体,改为 skill 级隔离安装:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async installDependencies(
|
||||||
|
skillName: string,
|
||||||
|
installSpecs?: Record<string, unknown>[],
|
||||||
|
): Promise<{ success: boolean; logs: string[] }> {
|
||||||
|
const logs: string[] = [];
|
||||||
|
const skillDir = path.join(this.skillsDir, skillName);
|
||||||
|
|
||||||
|
// 读取 skill.config.yaml 的 dependencies
|
||||||
|
const configPath = path.join(skillDir, 'skill.config.yaml');
|
||||||
|
let config: any = null;
|
||||||
|
try {
|
||||||
|
const raw = await fs.readFile(configPath, 'utf-8');
|
||||||
|
const yaml = await import('js-yaml');
|
||||||
|
config = yaml.load(raw);
|
||||||
|
} catch { /* no config file */ }
|
||||||
|
|
||||||
|
const deps = config?.dependencies;
|
||||||
|
|
||||||
|
// Python: skill-level venv
|
||||||
|
if (deps?.python) {
|
||||||
|
try {
|
||||||
|
const reqSource = deps.python.source || 'requirements.txt';
|
||||||
|
const reqPath = path.join(skillDir, reqSource);
|
||||||
|
await fs.access(reqPath);
|
||||||
|
await execAsync(`uv venv .venv`, { cwd: skillDir, timeout: 60000 });
|
||||||
|
const pip = process.platform === 'win32'
|
||||||
|
? path.join('.venv', 'Scripts', 'pip')
|
||||||
|
: path.join('.venv', 'bin', 'pip');
|
||||||
|
const { stdout } = await execAsync(`uv pip install -r ${reqSource} -p ${pip}`, {
|
||||||
|
cwd: skillDir, timeout: 120000,
|
||||||
|
});
|
||||||
|
logs.push(`[python] venv created and deps installed: ${stdout.trim()}`);
|
||||||
|
} catch (e: any) {
|
||||||
|
logs.push(`[python] install failed: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Node: skill-level node_modules
|
||||||
|
if (deps?.node?.packages?.length) {
|
||||||
|
try {
|
||||||
|
const pkgs = deps.node.packages.join(' ');
|
||||||
|
const { stdout } = await execAsync(`npm install ${pkgs}`, {
|
||||||
|
cwd: skillDir, timeout: 120000,
|
||||||
|
});
|
||||||
|
logs.push(`[node] installed: ${stdout.trim()}`);
|
||||||
|
} catch (e: any) {
|
||||||
|
logs.push(`[node] install failed: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dotnet: restore
|
||||||
|
if (deps?.dotnet?.project) {
|
||||||
|
try {
|
||||||
|
const { stdout } = await execAsync(`dotnet restore ${deps.dotnet.project}`, {
|
||||||
|
cwd: skillDir, timeout: 120000,
|
||||||
|
});
|
||||||
|
logs.push(`[dotnet] restored: ${stdout.trim()}`);
|
||||||
|
} catch (e: any) {
|
||||||
|
logs.push(`[dotnet] restore failed: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// System deps: check only
|
||||||
|
if (deps?.system) {
|
||||||
|
for (const dep of deps.system) {
|
||||||
|
try {
|
||||||
|
await execAsync(dep.check, { timeout: 10000 });
|
||||||
|
logs.push(`[system] ${dep.name}: OK`);
|
||||||
|
} catch {
|
||||||
|
logs.push(`[system] ${dep.name}: NOT FOUND (run "${dep.check}" to verify)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup script — 安全约束(Spec 10.9)
|
||||||
|
const setupKey = process.platform === 'win32' ? 'win32' : 'posix';
|
||||||
|
const setupScript = config?.setup?.[setupKey];
|
||||||
|
if (setupScript) {
|
||||||
|
// 路径穿越防护
|
||||||
|
if (setupScript.includes('..')) {
|
||||||
|
logs.push(`[setup] 跳过: 路径包含 ".." (${setupScript})`);
|
||||||
|
} else {
|
||||||
|
// 来源校验:仅 github/zip 安装的 skill 允许执行 setup
|
||||||
|
const origin = await this.registry.readOrigin(skillName);
|
||||||
|
const allowedSources = ['github', 'zip'];
|
||||||
|
if (!origin || !allowedSources.includes(origin.source)) {
|
||||||
|
logs.push(`[setup] 跳过: 仅 GitHub/ZIP 安装的 skill 允许执行 setup 脚本 (source=${origin?.source || 'local'})`);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const { stdout } = await execAsync(
|
||||||
|
process.platform === 'win32' ? `powershell -File ${setupScript}` : `bash ${setupScript}`,
|
||||||
|
{ cwd: skillDir, timeout: 120000 },
|
||||||
|
);
|
||||||
|
logs.push(`[setup] executed ${setupScript}: ${stdout.trim()}`);
|
||||||
|
} catch (e: any) {
|
||||||
|
logs.push(`[setup] ${setupScript} failed: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: legacy installSpecs from SKILL.md metadata
|
||||||
|
if (!config && installSpecs && Array.isArray(installSpecs)) {
|
||||||
|
for (const spec of installSpecs) {
|
||||||
|
const kind = spec.kind as string;
|
||||||
|
const pkg = spec.package as string;
|
||||||
|
if (!kind || !pkg) continue;
|
||||||
|
try {
|
||||||
|
if (kind === 'node' && SAFE_NODE_PACKAGE.test(pkg)) {
|
||||||
|
const { stdout } = await execAsync(`npm install ${pkg}`, { cwd: skillDir, timeout: 120000 });
|
||||||
|
logs.push(`[node-legacy] installed ${pkg}: ${stdout.trim()}`);
|
||||||
|
} else if (kind === 'uv' && SAFE_UV_PACKAGE.test(pkg)) {
|
||||||
|
await execAsync(`uv venv .venv`, { cwd: skillDir, timeout: 60000 });
|
||||||
|
const { stdout } = await execAsync(`uv pip install ${pkg}`, { cwd: skillDir, timeout: 120000 });
|
||||||
|
logs.push(`[uv-legacy] installed ${pkg}: ${stdout.trim()}`);
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
logs.push(`[${kind}] install ${pkg} failed: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: logs.every(l => !l.includes('failed')), logs };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 验证编译通过**
|
||||||
|
|
||||||
|
Run: `cd packages/backend && npx tsc --noEmit`
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/service/skill_installer.ts
|
||||||
|
git commit -m "feat(skill): refactor dependency installation to skill-level isolation"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: 前端安装对话框 setup 脚本确认(Spec 10.9)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/frontend/src/modules/agent/views/skills.vue`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 安装完成后检查是否有 setup 脚本并弹出确认**
|
||||||
|
|
||||||
|
修改 `handleInstallFromGitHub` 和 `handleInstallFromZip` 函数,安装成功后检查返回的 meta 是否包含 setup 脚本:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function handleInstallFromGitHub() {
|
||||||
|
// ... 现有安装逻辑 ...
|
||||||
|
const meta = res; // 安装返回的 skillMeta
|
||||||
|
installDialogVisible.value = false;
|
||||||
|
await refresh();
|
||||||
|
|
||||||
|
// 检查是否有 setup 脚本需要执行
|
||||||
|
if (meta?.metadata?.setup) {
|
||||||
|
const setupKey = navigator.platform.startsWith('Win') ? 'win32' : 'posix';
|
||||||
|
const scriptName = meta.metadata.setup[setupKey];
|
||||||
|
if (scriptName) {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`此 Skill 包含安装脚本(${scriptName}),是否执行?执行将安装 Skill 所需的系统依赖。`,
|
||||||
|
'安装脚本确认',
|
||||||
|
{ confirmButtonText: '执行', cancelButtonText: '跳过', type: 'warning' },
|
||||||
|
);
|
||||||
|
// 用户确认后执行依赖安装(包含 setup 脚本)
|
||||||
|
try {
|
||||||
|
await service.request({
|
||||||
|
url: '/admin/netaclaw/skill/installDeps',
|
||||||
|
method: 'POST',
|
||||||
|
data: { name: meta.name },
|
||||||
|
});
|
||||||
|
ElMessage.success('安装脚本执行完成');
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e.message || '安装脚本执行失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
对 `handleInstallFromZip` 做同样的改造。
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/frontend/src/modules/agent/views/skills.vue
|
||||||
|
git commit -m "feat(skill): add setup script confirmation dialog after skill installation"
|
||||||
|
```
|
||||||
422
docs/superpowers/plans/2026-04-27-p2-standard-compat.md
Normal file
422
docs/superpowers/plans/2026-04-27-p2-standard-compat.md
Normal file
@ -0,0 +1,422 @@
|
|||||||
|
# P2: Agent Skills 标准兼容 + References 加载 实施计划
|
||||||
|
|
||||||
|
> **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 Skills 社区标准(名称验证、hidden skill、字段映射),改造 prompt 索引区分三种 skill 类型,改造 read_skill 返回格式强制 Agent 读取 required references。
|
||||||
|
|
||||||
|
**Architecture:** 新增 `validateSkillName()` 验证函数。`buildSkillsPrompt()` 输出增加 type 属性和 compute-entry 的 interface 摘要。`read_skill` 工具返回区分 `<skill_required_references>` 和 `<skill_optional_references>`。基础设施文件从 collectFiles 中过滤。
|
||||||
|
|
||||||
|
**Tech Stack:** TypeBox, XML 模板, Midway.js
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-04-27-skill-system-evolution-design.md` Section 5 + 10.5-10.7 + 10.10
|
||||||
|
|
||||||
|
**Depends on:** P1 (SkillConfigService, SkillClassification must be integrated into SkillLoaderService before P2 Task 2 can work. Execute P1 Tasks 1-2 first.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: 名称验证函数(无依赖,可独立执行)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/backend/src/modules/netaclaw/service/skill_loader.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 在 skill_loader.ts 顶部新增 validateSkillName 函数**
|
||||||
|
|
||||||
|
在 class 定义之前添加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const MAX_NAME_LENGTH = 64;
|
||||||
|
|
||||||
|
export function validateSkillName(name: string, parentDirName?: string): string[] {
|
||||||
|
const errors: string[] = [];
|
||||||
|
if (!name) { errors.push('name is required'); return errors; }
|
||||||
|
if (name.length > MAX_NAME_LENGTH) errors.push(`name exceeds ${MAX_NAME_LENGTH} characters`);
|
||||||
|
if (!/^[a-z0-9-]+$/.test(name)) errors.push('name must be lowercase a-z, 0-9, hyphens only');
|
||||||
|
if (name.startsWith('-') || name.endsWith('-')) errors.push('name must not start or end with hyphen');
|
||||||
|
if (name.includes('--')) errors.push('name must not contain consecutive hyphens');
|
||||||
|
if (parentDirName && name !== parentDirName) errors.push(`name "${name}" does not match directory "${parentDirName}"`);
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 在 parseSkillMd 中调用验证(warning 级别,不阻塞加载)**
|
||||||
|
|
||||||
|
在 `parseSkillMd` 方法中,解析出 name 后添加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const nameErrors = validateSkillName(name);
|
||||||
|
if (nameErrors.length > 0) {
|
||||||
|
this.logger.warn('[SkillLoader] Skill "%s" name validation: %s', name, nameErrors.join(', '));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 在 skill_manage.ts 和 skill_installer.ts 的创建/安装流程中调用验证(error 级别,阻塞)**
|
||||||
|
|
||||||
|
在 `skill_manage.ts` 的 `execute` 方法中,`create` 分支的 `parseSkillMd` 之后添加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const nameErrors = validateSkillName(parsed.name, params.name);
|
||||||
|
if (nameErrors.length > 0) {
|
||||||
|
return textResult(`名称不合规: ${nameErrors.join('; ')}`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
在 `skill_installer.ts` 的 `installFromGitHub` 和 `installFromZip` 中,`parseSkillMd` 之后添加类似校验。
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/service/skill_loader.ts \
|
||||||
|
packages/backend/src/modules/netaclaw/tools/builtin/skill_manage.ts \
|
||||||
|
packages/backend/src/modules/netaclaw/service/skill_installer.ts
|
||||||
|
git commit -m "feat(skill): add Agent Skills standard name validation"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: buildSkillsPrompt 输出改造
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/backend/src/modules/netaclaw/service/skill_loader.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 改造 buildSkillsPrompt 方法**
|
||||||
|
|
||||||
|
替换现有 `buildSkillsPrompt` 方法:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
buildSkillsPrompt(skillNames: string[], availableTools: string[]): string {
|
||||||
|
const filtered = this.filterByConditions(skillNames, availableTools);
|
||||||
|
if (filtered.length === 0) return '';
|
||||||
|
|
||||||
|
const lines: string[] = [];
|
||||||
|
let totalChars = 0;
|
||||||
|
|
||||||
|
for (const s of filtered) {
|
||||||
|
// 跳过 hidden skill
|
||||||
|
const fm = s.metadata as any;
|
||||||
|
if (fm?.['disable-model-invocation'] === true) continue;
|
||||||
|
|
||||||
|
const classification = this.skillConfig.classify(s.name);
|
||||||
|
const category = fm?.category || '通用';
|
||||||
|
const tags = Array.isArray(fm?.tags) ? fm.tags.join(',') : '';
|
||||||
|
|
||||||
|
let body = ` ${s.description}`;
|
||||||
|
|
||||||
|
// compute-entry: 附带 interface.input 摘要
|
||||||
|
if (classification === 'compute-entry') {
|
||||||
|
const config = this.skillConfig.getConfig(s.name);
|
||||||
|
if (config?.interface?.input) {
|
||||||
|
const inputDesc = Object.entries(config.interface.input)
|
||||||
|
.map(([k, v]) => `${k}(${v.type}${v.required ? ',必填' : ''}${v.default ? ',默认' + v.default : ''})`)
|
||||||
|
.join(', ');
|
||||||
|
body += `\n 输入: ${inputDesc}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const line = ` <skill name="${s.name}" type="${classification}" category="${category}" tags="${tags}">\n${body}\n </skill>`;
|
||||||
|
if (totalChars + line.length > SkillLoaderService.MAX_SKILLS_PROMPT_CHARS) break;
|
||||||
|
lines.push(line);
|
||||||
|
totalChars += line.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lines.length === 0) return '';
|
||||||
|
|
||||||
|
return `\n\n## 技能(必须扫描)\n回复前,扫描以下技能列表。prompt 类型用 read_skill 加载指令,compute-entry 类型用 execute_skill 直接调用,compute-toolkit 类型用 read_skill 加载指令后通过 bash 执行。\n读取 skill 后,如果返回中包含 <skill_required_references>,你必须在执行任何操作前先用 read_skill_file 逐一读取列出的所有文档。这不是建议,是强制要求。\n\n<available_skills>\n${lines.join('\n')}\n</available_skills>`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 验证编译通过**
|
||||||
|
|
||||||
|
Run: `cd packages/backend && npx tsc --noEmit`
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/service/skill_loader.ts
|
||||||
|
git commit -m "feat(skill): enhance buildSkillsPrompt with type classification and required references guidance"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: read_skill 返回格式改造
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/backend/src/modules/netaclaw/tools/builtin/read_skill.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 改造 read_skill 的 execute 方法**
|
||||||
|
|
||||||
|
替换现有 `execute` 方法体:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async execute(_id, params) {
|
||||||
|
const skill = skillLoader.getSkill(params.name);
|
||||||
|
if (!skill) {
|
||||||
|
return textResult(`未找到名为 "${params.name}" 的 skill`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = skill.content;
|
||||||
|
|
||||||
|
// 获取 references 配置
|
||||||
|
const config = skillLoader.getSkillConfig(params.name);
|
||||||
|
const configRefs = config?.references;
|
||||||
|
const fmRefs = (skill.metadata as any)?.references;
|
||||||
|
const refs = configRefs || fmRefs || null;
|
||||||
|
|
||||||
|
const requiredRefs: string[] = refs?.required || [];
|
||||||
|
const allFiles = skill.files || [];
|
||||||
|
|
||||||
|
// 基础设施文件过滤
|
||||||
|
const INFRA_FILES = new Set([
|
||||||
|
'skill.config.yaml', 'requirements.txt', 'package.json',
|
||||||
|
'package-lock.json', 'tsconfig.json', '.env',
|
||||||
|
]);
|
||||||
|
const referenceFiles = allFiles.filter(f => !INFRA_FILES.has(f) && !requiredRefs.includes(f));
|
||||||
|
|
||||||
|
if (requiredRefs.length > 0) {
|
||||||
|
result += `\n\n<skill_required_references>`;
|
||||||
|
result += `\n⚠️ 执行此 skill 的任务前,你必须先用 read_skill_file 读取以下文档:`;
|
||||||
|
for (const ref of requiredRefs) {
|
||||||
|
result += `\n- ${ref}`;
|
||||||
|
}
|
||||||
|
result += `\n未读取这些文档就执行操作会导致错误。`;
|
||||||
|
result += `\n</skill_required_references>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (referenceFiles.length > 0) {
|
||||||
|
result += `\n\n<skill_optional_references>`;
|
||||||
|
result += `\n以下文档可按需读取(用 read_skill_file 工具):`;
|
||||||
|
for (const ref of referenceFiles) {
|
||||||
|
result += `\n- ${ref}`;
|
||||||
|
}
|
||||||
|
result += `\n</skill_optional_references>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return textResult(result);
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 验证编译通过**
|
||||||
|
|
||||||
|
Run: `cd packages/backend && npx tsc --noEmit`
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/tools/builtin/read_skill.ts
|
||||||
|
git commit -m "feat(skill): enhance read_skill with required/optional references separation"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: collectFiles 基础设施文件过滤
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/backend/src/modules/netaclaw/service/skill_loader.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 在 collectFiles 方法中过滤基础设施文件**
|
||||||
|
|
||||||
|
在 `collectFiles` 方法的 `else if (entry.name !== 'SKILL.md')` 条件中,增加过滤:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const INFRA_FILES = new Set([
|
||||||
|
'skill.config.yaml', 'requirements.txt', 'package.json',
|
||||||
|
'package-lock.json', 'tsconfig.json', '.env',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 在 else if 分支中
|
||||||
|
} else if (entry.name !== 'SKILL.md' && !INFRA_FILES.has(entry.name)) {
|
||||||
|
const rel = path.relative(baseDir, fullPath).replace(/\\/g, '/');
|
||||||
|
results.push(rel);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/service/skill_loader.ts
|
||||||
|
git commit -m "feat(skill): filter infrastructure files from skill file listing"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: read_skill task 参数与 routes 匹配(Spec 10.10)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/backend/src/modules/netaclaw/tools/builtin/read_skill.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 给 ReadSkillParams 新增可选 task 参数**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const ReadSkillParams = Type.Object({
|
||||||
|
name: Type.String({ description: '要读取的 skill 名称' }),
|
||||||
|
task: Type.Optional(Type.String({ description: '当前任务描述,用于自动匹配需要读取的文档' })),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 在 execute 方法中实现 routes 匹配**
|
||||||
|
|
||||||
|
在获取 `requiredRefs` 之后、构建返回结果之前,添加 routes 匹配逻辑:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// routes 匹配:如果传入了 task 且 config 有 routes,尝试关键词匹配
|
||||||
|
let routeMatchedRefs: string[] = [];
|
||||||
|
if (params.task && refs?.routes) {
|
||||||
|
const taskLower = params.task.toLowerCase();
|
||||||
|
for (const route of refs.routes) {
|
||||||
|
if (route.match.some(keyword => taskLower.includes(keyword.toLowerCase()))) {
|
||||||
|
routeMatchedRefs.push(...route.required_refs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
routeMatchedRefs = [...new Set(routeMatchedRefs)]; // 去重
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合并 required + route matched
|
||||||
|
const allRequired = [...new Set([...requiredRefs, ...routeMatchedRefs])];
|
||||||
|
```
|
||||||
|
|
||||||
|
如果 `routeMatchedRefs` 命中了文档,尝试直接读取并拼接到返回结果中(减少 Agent 二次调用):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
if (routeMatchedRefs.length > 0) {
|
||||||
|
result += `\n\n<skill_route_matched_references>`;
|
||||||
|
for (const ref of routeMatchedRefs) {
|
||||||
|
const filePath = skillLoader.getSkillFilePath(params.name, ref);
|
||||||
|
if (filePath) {
|
||||||
|
try {
|
||||||
|
const content = await fs.readFile(filePath, 'utf-8');
|
||||||
|
result += `\n\n--- ${ref} ---\n${content}`;
|
||||||
|
} catch { /* skip unreadable */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result += `\n</skill_route_matched_references>`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/tools/builtin/read_skill.ts
|
||||||
|
git commit -m "feat(skill): add task parameter and routes matching to read_skill tool"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Agent 分配 skill 时的类型校验(Spec 10.6)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/frontend/src/modules/agent/views/agent-edit.vue`
|
||||||
|
- Modify: `packages/backend/src/modules/netaclaw/controller/agent.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: metas 端点返回 classification 字段**
|
||||||
|
|
||||||
|
在 `SkillLoaderService.getSkillMetas()` 中,返回对象添加(如果 P3 Task 5 尚未添加):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
classification: this.skillConfig.classify(fs.name),
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 前端 skill 选择器显示分类标签**
|
||||||
|
|
||||||
|
在 `agent-edit.vue` 的可选 Skill 列表项中(约 line 38),在现有 `skillType` 标签旁新增分类标签:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<el-tag v-if="sk.classification" size="small"
|
||||||
|
:type="sk.classification === 'compute-entry' ? 'success' : sk.classification === 'compute-toolkit' ? '' : 'info'">
|
||||||
|
{{ sk.classification }}
|
||||||
|
</el-tag>
|
||||||
|
```
|
||||||
|
|
||||||
|
已选择列表中同样添加:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<span class="item-main">{{ getSkillLabel(name) }}</span>
|
||||||
|
<el-tag size="small" :type="getSkillClassification(name) === 'compute-entry' ? 'success' : 'info'">
|
||||||
|
{{ getSkillClassification(name) }}
|
||||||
|
</el-tag>
|
||||||
|
```
|
||||||
|
|
||||||
|
新增辅助函数:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function getSkillClassification(name: string): string {
|
||||||
|
const meta = skillMetas.value.find((s) => s.name === name);
|
||||||
|
return meta?.classification || 'prompt';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 前端选择 compute skill 时显示工具权限 warning**
|
||||||
|
|
||||||
|
在 `addSkill` 函数中添加校验:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function addSkill(name: string) {
|
||||||
|
if (!form.value.skills.includes(name)) {
|
||||||
|
form.value.skills.push(name);
|
||||||
|
// 校验工具权限
|
||||||
|
const classification = getSkillClassification(name);
|
||||||
|
if (classification === 'compute-entry') {
|
||||||
|
ElMessage.warning(`"${name}" 是 compute-entry 类型,请确保该 Agent 已启用 execute_skill 工具`);
|
||||||
|
} else if (classification === 'compute-toolkit') {
|
||||||
|
ElMessage.warning(`"${name}" 是 compute-toolkit 类型,请确保该 Agent 已启用 bash 工具`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 后端保存时返回 warnings**
|
||||||
|
|
||||||
|
在 `controller/agent.ts` 的 `update` 方法中,注入 `SkillLoaderService`,保存前校验:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Inject()
|
||||||
|
skillLoader: SkillLoaderService;
|
||||||
|
|
||||||
|
@Post('/update')
|
||||||
|
async update(@Body() body: any) {
|
||||||
|
const warnings: string[] = [];
|
||||||
|
if (body.skills?.length) {
|
||||||
|
for (const skillName of body.skills) {
|
||||||
|
const classification = this.skillLoader.getSkillClassification(skillName);
|
||||||
|
if (classification === 'compute-entry') {
|
||||||
|
warnings.push(`Skill "${skillName}" 需要 execute_skill 工具`);
|
||||||
|
} else if (classification === 'compute-toolkit') {
|
||||||
|
warnings.push(`Skill "${skillName}" 需要 bash 工具`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await this.agentService.update(body);
|
||||||
|
return { code: 1000, message: 'success', warnings };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/frontend/src/modules/agent/views/agent-edit.vue \
|
||||||
|
packages/backend/src/modules/netaclaw/controller/agent.ts
|
||||||
|
git commit -m "feat(skill): add skill classification display and tool permission warnings in agent editor"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: skill_context.ts 废弃标记(Spec 10.12)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/backend/src/modules/netaclaw/service/skill_context.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 添加 @deprecated 注释**
|
||||||
|
|
||||||
|
在 `buildSkillContext` 函数上方添加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* @deprecated 使用 tool_resolver.ts 中的 skill 工具注入逻辑替代。
|
||||||
|
* 此函数不再被主链路调用,保留仅为兼容可能的外部引用。
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/service/skill_context.ts
|
||||||
|
git commit -m "chore(skill): mark buildSkillContext as deprecated"
|
||||||
|
```
|
||||||
360
docs/superpowers/plans/2026-04-27-p3-diagnostics.md
Normal file
360
docs/superpowers/plans/2026-04-27-p3-diagnostics.md
Normal file
@ -0,0 +1,360 @@
|
|||||||
|
# P3: 碰撞检测与诊断系统 实施计划
|
||||||
|
|
||||||
|
> **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:** 在 skill 扫描时收集诊断信息(名称碰撞、验证警告、环境缺失),通过 API 暴露,前端展示诊断横幅和 per-skill 诊断详情。
|
||||||
|
|
||||||
|
**Architecture:** `SkillLoaderService` 内部维护 `diagnostics: SkillDiagnostic[]` 数组,`scanSkills()` 时清空并重新收集。Admin controller 新增 `/diagnostics` 端点。前端 skills.vue 顶部新增诊断横幅,skill-detail.vue 新增诊断 tab。
|
||||||
|
|
||||||
|
**Tech Stack:** TypeORM, Midway.js, Element Plus
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-04-27-skill-system-evolution-design.md` Section 6
|
||||||
|
|
||||||
|
**Depends on:** P0 (envSchema for ENV_NOT_CONFIGURED check), P1 Tasks 1-2 (SkillConfigService integrated into SkillLoaderService), P2 Task 1 (validateSkillName function). Execute these prerequisites first.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: 诊断数据结构和收集
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/shared/types/skill.types.ts`
|
||||||
|
- Modify: `packages/backend/src/modules/netaclaw/service/skill_loader.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 在 shared/types/skill.types.ts 新增 SkillDiagnostic 类型**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface SkillDiagnostic {
|
||||||
|
level: 'error' | 'warning' | 'info';
|
||||||
|
code: string;
|
||||||
|
skillName: string;
|
||||||
|
message: string;
|
||||||
|
path?: string;
|
||||||
|
detail?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 在 SkillLoaderService 中新增诊断收集**
|
||||||
|
|
||||||
|
在 class 内部新增:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private diagnostics: SkillDiagnostic[] = [];
|
||||||
|
|
||||||
|
getDiagnostics(level?: string): SkillDiagnostic[] {
|
||||||
|
if (level) return this.diagnostics.filter(d => d.level === level);
|
||||||
|
return [...this.diagnostics];
|
||||||
|
}
|
||||||
|
|
||||||
|
private addDiagnostic(diag: SkillDiagnostic): void {
|
||||||
|
this.diagnostics.push(diag);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 在 scanSkills 开头清空诊断**
|
||||||
|
|
||||||
|
在 `scanSkills()` 方法的 `this.skills.clear()` 之后添加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
this.diagnostics = [];
|
||||||
|
const realPathSet = new Set<string>();
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3.5: 在 scanSkills 循环中添加 symlink 去重(Spec 10.11)**
|
||||||
|
|
||||||
|
在解析每个 skill 目录时,碰撞检测之前添加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// symlink 去重
|
||||||
|
const skillDirPath = path.join(this.skillsDir, entry.name);
|
||||||
|
let realPath: string;
|
||||||
|
try {
|
||||||
|
realPath = await fs.realpath(skillDirPath);
|
||||||
|
} catch {
|
||||||
|
realPath = skillDirPath;
|
||||||
|
}
|
||||||
|
if (realPathSet.has(realPath)) continue; // 同一物理目录,静默跳过
|
||||||
|
realPathSet.add(realPath);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 在 scanSkills 循环中添加诊断收集**
|
||||||
|
|
||||||
|
在解析每个 skill 的过程中,添加以下检查:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 名称验证
|
||||||
|
const nameErrors = validateSkillName(skill.name, entry.name);
|
||||||
|
for (const err of nameErrors) {
|
||||||
|
if (err.includes('does not match')) {
|
||||||
|
this.addDiagnostic({ level: 'warning', code: 'NAME_MISMATCH', skillName: skill.name, message: err, path: skillMdPath });
|
||||||
|
} else {
|
||||||
|
this.addDiagnostic({ level: 'warning', code: 'NAME_INVALID', skillName: skill.name, message: err, path: skillMdPath });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 碰撞检测
|
||||||
|
if (this.skills.has(skill.name)) {
|
||||||
|
const existing = this.skills.get(skill.name)!;
|
||||||
|
this.addDiagnostic({
|
||||||
|
level: 'warning', code: 'NAME_COLLISION', skillName: skill.name,
|
||||||
|
message: `Name collision: "${skill.name}" already loaded`,
|
||||||
|
detail: { winnerPath: path.join(this.skillsDir, existing.name), loserPath: skillMdPath },
|
||||||
|
});
|
||||||
|
continue; // 跳过,保留先发现的
|
||||||
|
}
|
||||||
|
|
||||||
|
// description 检查
|
||||||
|
if (!skill.description) {
|
||||||
|
this.addDiagnostic({ level: 'error', code: 'DESC_MISSING', skillName: skill.name, message: 'Missing description', path: skillMdPath });
|
||||||
|
continue; // 不加载
|
||||||
|
}
|
||||||
|
if (skill.description.length > 1024) {
|
||||||
|
this.addDiagnostic({ level: 'warning', code: 'DESC_TOO_LONG', skillName: skill.name, message: `Description exceeds 1024 chars (${skill.description.length})`, path: skillMdPath });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: 在 scanSkills 末尾添加 config 相关诊断**
|
||||||
|
|
||||||
|
在所有 skill 加载完成后,遍历检查 compute skill 的环境就绪状态:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 扫描完成后检查 compute skill 状态
|
||||||
|
for (const [name, _skill] of this.skills) {
|
||||||
|
const config = this.skillConfig.getConfig(name);
|
||||||
|
if (!config) {
|
||||||
|
// 检查是否有解析错误(区分"无 config 文件"和"config 解析失败")
|
||||||
|
const parseErr = this.skillConfig.getParseError(name);
|
||||||
|
if (parseErr) {
|
||||||
|
this.addDiagnostic({ level: 'error', code: 'CONFIG_PARSE_ERROR', skillName: name, message: `skill.config.yaml parse failed: ${parseErr}` });
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// config 解析错误已在 loadConfig 中处理
|
||||||
|
|
||||||
|
// runtime 可用性检查
|
||||||
|
if (config.runtime === 'python') {
|
||||||
|
const venvPath = path.join(this.skillsDir, name, '.venv');
|
||||||
|
try { await fs.access(venvPath); } catch {
|
||||||
|
this.addDiagnostic({ level: 'warning', code: 'VENV_MISSING', skillName: name, message: 'Python .venv not found, run dependency install' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (config.runtime) {
|
||||||
|
const runtimeChecks: Record<string, string> = { python: 'python3 --version', node: 'node --version', dotnet: 'dotnet --version' };
|
||||||
|
const checkCmd = runtimeChecks[config.runtime];
|
||||||
|
if (checkCmd) {
|
||||||
|
try { await execAsync(checkCmd, { timeout: 5000 }); } catch {
|
||||||
|
this.addDiagnostic({ level: 'error', code: 'RUNTIME_UNAVAILABLE', skillName: name, message: `Runtime "${config.runtime}" not available on this system` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 系统依赖检查
|
||||||
|
if (config.dependencies?.system) {
|
||||||
|
for (const dep of config.dependencies.system) {
|
||||||
|
try { await execAsync(dep.check, { timeout: 5000 }); } catch {
|
||||||
|
this.addDiagnostic({ level: 'warning', code: 'SYSTEM_DEP_MISSING', skillName: name, message: `System dependency "${dep.name}" not found (check: ${dep.check})` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fingerprint 变更检查
|
||||||
|
const lockfile = await this.registry?.readLockfile?.();
|
||||||
|
if (lockfile?.skills?.[name]) {
|
||||||
|
const currentFp = await this.registry.computeFingerprint(path.join(this.skillsDir, name));
|
||||||
|
if (currentFp !== lockfile.skills[name].fingerprint) {
|
||||||
|
this.addDiagnostic({ level: 'info', code: 'FINGERPRINT_CHANGED', skillName: name, message: 'SKILL.md content changed since last install/update' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// env 配置检查
|
||||||
|
if (config.env) {
|
||||||
|
const entity = await this.skillRepo.findOneBy({ name });
|
||||||
|
const hasSecrets = !!entity?.secrets;
|
||||||
|
for (const envItem of config.env) {
|
||||||
|
if (envItem.required && !hasSecrets) {
|
||||||
|
this.addDiagnostic({ level: 'warning', code: 'ENV_NOT_CONFIGURED', skillName: name, message: `Required env "${envItem.name}" not configured` });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/shared/types/skill.types.ts packages/backend/src/modules/netaclaw/service/skill_loader.ts
|
||||||
|
git commit -m "feat(skill): add diagnostic collection during skill scanning"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Admin Controller 诊断端点
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/backend/src/modules/netaclaw/controller/admin/skill.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 新增 diagnostics 端点**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Get('/diagnostics', { summary: '获取 skill 诊断信息' })
|
||||||
|
async diagnostics(@Query('level') level?: string) {
|
||||||
|
const diags = this.skillLoader.getDiagnostics(level);
|
||||||
|
return this.ok(diags);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/controller/admin/skill.ts
|
||||||
|
git commit -m "feat(skill): add diagnostics admin endpoint"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: 前端诊断横幅
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/frontend/src/modules/agent/views/skills.vue`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 在 skills.vue 中新增诊断数据加载**
|
||||||
|
|
||||||
|
在 script 区域添加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const diagnostics = ref<any[]>([]);
|
||||||
|
const errorCount = computed(() => diagnostics.value.filter(d => d.level === 'error').length);
|
||||||
|
const warningCount = computed(() => diagnostics.value.filter(d => d.level === 'warning').length);
|
||||||
|
const showDiagDetails = ref(false);
|
||||||
|
|
||||||
|
async function loadDiagnostics() {
|
||||||
|
try {
|
||||||
|
const res = await service.request({ url: '/admin/netaclaw/skill/diagnostics' });
|
||||||
|
diagnostics.value = res || [];
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
在 `refresh()` 函数末尾添加 `await loadDiagnostics();`
|
||||||
|
|
||||||
|
- [ ] **Step 2: 在 template 中 skill-header 之后添加诊断横幅**
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- 诊断横幅 -->
|
||||||
|
<div v-if="errorCount > 0" class="diag-banner diag-error" @click="showDiagDetails = !showDiagDetails">
|
||||||
|
⚠ {{ errorCount }} 个错误需要处理
|
||||||
|
</div>
|
||||||
|
<div v-else-if="warningCount > 0" class="diag-banner diag-warning" @click="showDiagDetails = !showDiagDetails">
|
||||||
|
{{ warningCount }} 个警告
|
||||||
|
</div>
|
||||||
|
<div v-if="showDiagDetails && diagnostics.length > 0" class="diag-list">
|
||||||
|
<div v-for="(d, i) in diagnostics" :key="i" class="diag-item" :class="'diag-' + d.level">
|
||||||
|
<el-tag :type="d.level === 'error' ? 'danger' : d.level === 'warning' ? 'warning' : 'info'" size="small">
|
||||||
|
{{ d.code }}
|
||||||
|
</el-tag>
|
||||||
|
<span class="diag-skill">{{ d.skillName }}</span>
|
||||||
|
<span>{{ d.message }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 添加样式**
|
||||||
|
|
||||||
|
```css
|
||||||
|
.diag-banner { padding: 8px 16px; border-radius: 6px; margin-bottom: 16px; cursor: pointer; font-size: 13px; }
|
||||||
|
.diag-error { background: #fef0f0; color: #f56c6c; border: 1px solid #fbc4c4; }
|
||||||
|
.diag-warning { background: #fdf6ec; color: #e6a23c; border: 1px solid #f5dab1; }
|
||||||
|
.diag-list { background: #f5f7fa; border-radius: 6px; padding: 12px; margin-bottom: 16px; max-height: 200px; overflow-y: auto; }
|
||||||
|
.diag-item { display: flex; align-items: center; gap: 8px; padding: 4px 0; font-size: 12px; }
|
||||||
|
.diag-skill { font-weight: 600; color: #303133; }
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/frontend/src/modules/agent/views/skills.vue
|
||||||
|
git commit -m "feat(skill): add diagnostic banner to skills management page"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: skill-detail 诊断 Tab
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/frontend/src/modules/agent/components/skill-detail.vue`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 在 el-tabs 中新增诊断 tab**
|
||||||
|
|
||||||
|
在 P0 Task 6 已创建的 tab 结构中,添加第三个 tab:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<el-tab-pane label="诊断" name="diagnostics">
|
||||||
|
<div v-if="skillDiagnostics.length === 0" class="empty-hint">无诊断信息</div>
|
||||||
|
<div v-else>
|
||||||
|
<div v-for="(d, i) in skillDiagnostics" :key="i" class="diag-item">
|
||||||
|
<el-tag :type="d.level === 'error' ? 'danger' : d.level === 'warning' ? 'warning' : 'info'" size="small">
|
||||||
|
{{ d.code }}
|
||||||
|
</el-tag>
|
||||||
|
<span>{{ d.message }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 新增诊断数据加载逻辑**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const skillDiagnostics = ref<any[]>([]);
|
||||||
|
|
||||||
|
async function loadSkillDiagnostics() {
|
||||||
|
if (!props.skill?.name) return;
|
||||||
|
try {
|
||||||
|
const res = await service.request({ url: '/admin/netaclaw/skill/diagnostics' });
|
||||||
|
skillDiagnostics.value = (res || []).filter((d: any) => d.skillName === props.skill.name);
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在 watch skill 的回调中添加
|
||||||
|
loadSkillDiagnostics();
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/frontend/src/modules/agent/components/skill-detail.vue
|
||||||
|
git commit -m "feat(skill): add diagnostics tab to skill-detail drawer"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: skills.vue 卡片 type 标签
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/frontend/src/modules/agent/views/skills.vue`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 在 metas API 返回中包含 skillTypeV2**
|
||||||
|
|
||||||
|
在 `SkillLoaderService.getSkillMetas()` 中,返回对象添加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
skillTypeV2: this.skillConfig.classify(fs.name),
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 在 skill 卡片的 tags 区域添加 type 标签**
|
||||||
|
|
||||||
|
在 `skill-tags` div 中,现有标签之前添加:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<el-tag size="small" :type="skill.skillTypeV2 === 'compute-entry' ? 'success' : skill.skillTypeV2 === 'compute-toolkit' ? '' : 'info'">
|
||||||
|
{{ skill.skillTypeV2 || 'prompt' }}
|
||||||
|
</el-tag>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/backend/src/modules/netaclaw/service/skill_loader.ts \
|
||||||
|
packages/frontend/src/modules/agent/views/skills.vue
|
||||||
|
git commit -m "feat(skill): show skill classification type tag on cards"
|
||||||
|
```
|
||||||
1346
docs/superpowers/plans/2026-05-02-image-generation-tools.md
Normal file
1346
docs/superpowers/plans/2026-05-02-image-generation-tools.md
Normal file
File diff suppressed because it is too large
Load Diff
2679
docs/superpowers/plans/2026-05-03-geo-s1-infrastructure-plan.md
Normal file
2679
docs/superpowers/plans/2026-05-03-geo-s1-infrastructure-plan.md
Normal file
File diff suppressed because it is too large
Load Diff
3173
docs/superpowers/plans/2026-05-04-netabrowser-cli-s1-plan.md
Normal file
3173
docs/superpowers/plans/2026-05-04-netabrowser-cli-s1-plan.md
Normal file
File diff suppressed because it is too large
Load Diff
2153
docs/superpowers/plans/2026-05-04-vehicle-damage-inspection-skill.md
Normal file
2153
docs/superpowers/plans/2026-05-04-vehicle-damage-inspection-skill.md
Normal file
File diff suppressed because it is too large
Load Diff
1547
docs/superpowers/plans/2026-05-06-tool-process-events.md
Normal file
1547
docs/superpowers/plans/2026-05-06-tool-process-events.md
Normal file
File diff suppressed because it is too large
Load Diff
2174
docs/superpowers/plans/2026-05-09-wechat-uia-a-bridge-skeleton.md
Normal file
2174
docs/superpowers/plans/2026-05-09-wechat-uia-a-bridge-skeleton.md
Normal file
File diff suppressed because it is too large
Load Diff
3236
docs/superpowers/plans/2026-05-09-wechat-uia-b-bridge-runtime.md
Normal file
3236
docs/superpowers/plans/2026-05-09-wechat-uia-b-bridge-runtime.md
Normal file
File diff suppressed because it is too large
Load Diff
2161
docs/superpowers/plans/2026-05-09-wechat-uia-c-backend-adapter.md
Normal file
2161
docs/superpowers/plans/2026-05-09-wechat-uia-c-backend-adapter.md
Normal file
File diff suppressed because it is too large
Load Diff
1607
docs/superpowers/plans/2026-05-09-wechat-uia-d-frontend-tray-e2e.md
Normal file
1607
docs/superpowers/plans/2026-05-09-wechat-uia-d-frontend-tray-e2e.md
Normal file
File diff suppressed because it is too large
Load Diff
2656
docs/superpowers/plans/2026-05-12-weixin-db-channel.md
Normal file
2656
docs/superpowers/plans/2026-05-12-weixin-db-channel.md
Normal file
File diff suppressed because it is too large
Load Diff
1841
docs/superpowers/plans/2026-05-13-weixin-archive-sync.md
Normal file
1841
docs/superpowers/plans/2026-05-13-weixin-archive-sync.md
Normal file
File diff suppressed because it is too large
Load Diff
1518
docs/superpowers/plans/2026-05-14-neta-desktop-op.md
Normal file
1518
docs/superpowers/plans/2026-05-14-neta-desktop-op.md
Normal file
File diff suppressed because it is too large
Load Diff
2041
docs/superpowers/plans/2026-05-15-mysql-question-answering.md
Normal file
2041
docs/superpowers/plans/2026-05-15-mysql-question-answering.md
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,360 @@
|
|||||||
|
# NetaClaw 电商浏览器自动化平台设计文档
|
||||||
|
|
||||||
|
> **日期**: 2026-04-11
|
||||||
|
> **状态**: 设计阶段
|
||||||
|
> **范围**: Neta-monorepo 架构转型 — 从保险审核 AI 平台转为电商浏览器自动化运营平台
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 项目定位
|
||||||
|
|
||||||
|
Neta 电商浏览器自动化运营平台,基于 OpenClaw Agent 架构(命名为 NetaClaw),结合 Playwright 浏览器自动化,实现电商平台的智能化运营操作(商品管理、订单处理、竞品监控、营销投流、文生图等)。
|
||||||
|
|
||||||
|
## 2. 核心决策
|
||||||
|
|
||||||
|
| 决策项 | 结论 | 理由 |
|
||||||
|
|--------|------|------|
|
||||||
|
| Agent 引擎 | NetaClaw(迁移自 OpenClaw) | 微内核+插件化,Skill 懒加载,比 LangChain 更适合工具驱动场景 |
|
||||||
|
| LangChain | 完全移除 | 不符合最终需求,OpenClaw 的 ReAct 循环更直接 |
|
||||||
|
| 浏览器自动化 | Playwright | 模拟真人操作,Node.js 生态最成熟,反检测插件丰富 |
|
||||||
|
| 架构模式 | 纯 B/S(去掉 Tauri) | 后端本地运行可直接操作文件系统,无需 Tauri IPC |
|
||||||
|
| 业务框架 | Midway.js(保留) | CRUD、TypeORM、权限系统继续使用 |
|
||||||
|
| 前端 | Vue 3 + Element Plus(保留) | 改造 UI 适配电商运营场景 |
|
||||||
|
| 桌面体验 | pkg 打包 .exe + NSIS 安装包 | 非技术用户双击启动,系统托盘图标 |
|
||||||
|
| 现有 Skill | 全部清理重建 | 旧 Skill 面向保险审核,与电商无关 |
|
||||||
|
| 目标平台 | 淘宝/天猫、拼多多、京东、抖音/快手 | 四大电商平台全覆盖 |
|
||||||
|
| 数据存储 | 本地文件 + MySQL + SQLite | 对话/图片存本地,业务数据存 MySQL,会话检查点存 SQLite |
|
||||||
|
|
||||||
|
## 3. 系统架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─ 云端服务器 ─────────────────────────────────┐
|
||||||
|
│ 用户认证 / License 验证 / 共享配置 / 模型API代理 │
|
||||||
|
└──────────────────────┬───────────────────────┘
|
||||||
|
│ HTTPS
|
||||||
|
┌──────────────────────┴───────────────────────┐
|
||||||
|
│ 用户本地 (Neta.exe) │
|
||||||
|
│ │
|
||||||
|
│ ┌─── Vue 3 前端 (localhost:9001) ─────────┐ │
|
||||||
|
│ │ 操作面板 │ 任务监控 │ Agent对话 │ 数据看板 │ │
|
||||||
|
│ └────────────────┬────────────────────────┘ │
|
||||||
|
│ │ HTTP / WebSocket │
|
||||||
|
│ ┌────────────────┴────────────────────────┐ │
|
||||||
|
│ │ Midway.js 后端 (localhost:8003) │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ┌── NetaClaw Agent Engine ───────────┐ │ │
|
||||||
|
│ │ │ Gateway (WS) → Runtime (ReAct) │ │ │
|
||||||
|
│ │ │ → Tools → Plugins → Subagents │ │ │
|
||||||
|
│ │ └──────────────┬─────────────────────┘ │ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ ┌──────────────┴─────────────────────┐ │ │
|
||||||
|
│ │ │ Playwright Engine │ │ │
|
||||||
|
│ │ │ 反检测 │ 多店铺并发 │ 平台适配 │ │ │
|
||||||
|
│ │ └────────────────────────────────────┘ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ┌─ Skills (SKILL.md) ────────────────┐ │ │
|
||||||
|
│ │ │ 商品管理│订单处理│竞品监控│营销投流 │ │ │
|
||||||
|
│ │ └────────────────────────────────────┘ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ┌─ 存储 ─────────────────────────────┐ │ │
|
||||||
|
│ │ │ 本地文件 │ MySQL │ SQLite │ │ │
|
||||||
|
│ │ └────────────────────────────────────┘ │ │
|
||||||
|
│ └──────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─ 系统托盘 ─────────────────────────────┐ │
|
||||||
|
│ │ 状态指示 │ 打开面板 │ 设置 │ 退出 │ │
|
||||||
|
│ └─────────────────────────────────────────┘ │
|
||||||
|
└───────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Monorepo 结构重组
|
||||||
|
|
||||||
|
### 移除
|
||||||
|
- `packages/desktop/` — 整个 Tauri 桌面端
|
||||||
|
- `packages/skills/` — 旧的 Python/Node skill 目录
|
||||||
|
- `packages/backend/src/modules/agent/` — 旧的 LangChain Agent 模块
|
||||||
|
- 所有 `@langchain/*` 依赖
|
||||||
|
|
||||||
|
### 保留
|
||||||
|
- `packages/backend/` — Midway.js 后端(base/dict/task/user/notification 等模块)
|
||||||
|
- `packages/frontend/` — Vue 3 前端(改造 UI)
|
||||||
|
- `packages/shared/` — 共享类型
|
||||||
|
|
||||||
|
### 新增
|
||||||
|
```
|
||||||
|
packages/
|
||||||
|
├── backend/
|
||||||
|
│ └── src/modules/
|
||||||
|
│ ├── netaclaw/ # ★ NetaClaw Agent 引擎(从 OpenClaw 迁移)
|
||||||
|
│ └── ... (保留的业务模块)
|
||||||
|
├── frontend/ # 改造 UI
|
||||||
|
├── shared/ # 共享类型
|
||||||
|
├── skills/ # ★ 电商 Skill 文档(SKILL.md 格式)
|
||||||
|
└── scripts/
|
||||||
|
├── build.ts # ★ pkg 打包脚本
|
||||||
|
└── installer/ # ★ NSIS 安装包配置
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. NetaClaw 模块设计
|
||||||
|
|
||||||
|
### 5.1 OpenClaw → NetaClaw 迁移映射
|
||||||
|
|
||||||
|
| OpenClaw 源码路径 | NetaClaw 目标路径 | 迁移策略 |
|
||||||
|
|-------------------|-------------------|----------|
|
||||||
|
| `src/gateway/server.impl.ts` | `netaclaw/gateway/server.ts` | 精简迁移,去掉 Channel 路由,保留 WS + 会话管理 |
|
||||||
|
| `src/gateway/session-utils.ts` | `netaclaw/gateway/session.ts` | 迁移会话 CRUD 和历史管理 |
|
||||||
|
| `src/gateway/net.ts` | `netaclaw/gateway/ws.ts` | 迁移 WebSocket 服务器 |
|
||||||
|
| `src/agents/pi-embedded-runner/` | `netaclaw/runtime/` | 核心迁移,保留 ReAct 循环、工具执行、模型调用 |
|
||||||
|
| `src/agents/pi-tools.ts` | `netaclaw/tools/common.ts` | 迁移 AgentTool 接口 + TypeBox Schema |
|
||||||
|
| `src/agents/bash-tools.ts` | `netaclaw/tools/builtin/bash.ts` | 迁移命令执行工具 |
|
||||||
|
| `src/agents/acp-spawn.ts` | `netaclaw/runtime/subagent.ts` | 迁移子代理生成(多店铺并行) |
|
||||||
|
| `src/agents/model-selection.ts` | `netaclaw/runtime/model_selection.ts` | 迁移模型选择与故障转移 |
|
||||||
|
| `src/agents/tool-policy.ts` | `netaclaw/tools/policy.ts` | 迁移工具访问控制 |
|
||||||
|
| `extensions/browser/` | `netaclaw/browser/` | 核心迁移,新增反检测和电商适配 |
|
||||||
|
| `src/plugin-sdk/` | `netaclaw/plugins/` | 精简迁移,保留工具工厂和钩子 |
|
||||||
|
| `src/config/` | 不迁移 | 使用 Midway.js 配置体系 |
|
||||||
|
| `src/channels/` | 不迁移 | 电商场景不需要 IM 渠道 |
|
||||||
|
| `skills/` | 不迁移内容 | 保留 SKILL.md 格式,内容全部替换 |
|
||||||
|
|
||||||
|
### 5.2 NetaClaw 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
packages/backend/src/modules/netaclaw/
|
||||||
|
├── gateway/ # WebSocket Gateway
|
||||||
|
│ ├── server.ts # WS 服务器(精简版)
|
||||||
|
│ ├── session.ts # 会话管理
|
||||||
|
│ └── protocol.ts # 消息协议定义
|
||||||
|
│
|
||||||
|
├── runtime/ # Agent 运行时
|
||||||
|
│ ├── agent.ts # Agent 核心循环(ReAct loop)
|
||||||
|
│ ├── attempt.ts # 单次执行尝试
|
||||||
|
│ ├── model_selection.ts # 模型选择与故障转移
|
||||||
|
│ ├── subagent.ts # 子代理生成
|
||||||
|
│ └── thinking.ts # 思考级别控制
|
||||||
|
│
|
||||||
|
├── tools/ # 工具系统
|
||||||
|
│ ├── common.ts # AgentTool 接口 + TypeBox Schema
|
||||||
|
│ ├── policy.ts # 工具访问控制
|
||||||
|
│ ├── builtin/ # 内置工具
|
||||||
|
│ │ ├── bash.ts # 命令执行
|
||||||
|
│ │ ├── file.ts # 文件读写
|
||||||
|
│ │ └── web_search.ts # 网页搜索
|
||||||
|
│ ├── browser/ # 浏览器工具(迁移自 extensions/browser/)
|
||||||
|
│ │ ├── pw_tools_core.ts # Playwright 核心
|
||||||
|
│ │ ├── interactions.ts # 点击/输入/滚动
|
||||||
|
│ │ ├── snapshot.ts # 页面快照
|
||||||
|
│ │ └── stealth.ts # 反检测配置
|
||||||
|
│ └── ecommerce/ # 电商专用工具
|
||||||
|
│ ├── product.ts # 商品操作(上架/下架/改价)
|
||||||
|
│ ├── order.ts # 订单操作
|
||||||
|
│ └── data_collect.ts # 数据采集
|
||||||
|
│
|
||||||
|
├── plugins/ # 插件系统
|
||||||
|
│ ├── plugin_entry.ts # 插件定义接口
|
||||||
|
│ ├── plugin_api.ts # 插件 API
|
||||||
|
│ └── llm_providers/ # LLM 提供商
|
||||||
|
│ ├── openai.ts
|
||||||
|
│ ├── anthropic.ts
|
||||||
|
│ └── deepseek.ts
|
||||||
|
│
|
||||||
|
├── platforms/ # 电商平台适配层
|
||||||
|
│ ├── base_platform.ts # 平台基类(登录/反检测/选择器)
|
||||||
|
│ ├── taobao/ # 淘宝/天猫
|
||||||
|
│ │ ├── login.ts
|
||||||
|
│ │ ├── selectors.ts
|
||||||
|
│ │ └── actions.ts
|
||||||
|
│ ├── pdd/ # 拼多多
|
||||||
|
│ ├── jd/ # 京东
|
||||||
|
│ └── douyin/ # 抖音
|
||||||
|
│
|
||||||
|
├── config.ts # NetaClaw 配置(接入 Midway config)
|
||||||
|
└── module.ts # Midway.js 模块注册
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 核心接口设计
|
||||||
|
|
||||||
|
#### AgentTool 接口(迁移自 OpenClaw)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Type, TSchema } from '@sinclair/typebox';
|
||||||
|
|
||||||
|
export type AgentTool<TParams extends TSchema, TResult> = {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
parameters: TParams;
|
||||||
|
execute(id: string, params: Static<TParams>): Promise<TResult>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AgentToolWithMeta<TParams extends TSchema, TResult> =
|
||||||
|
AgentTool<TParams, TResult> & {
|
||||||
|
ownerOnly?: boolean;
|
||||||
|
displaySummary?: string;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Skill 格式(SKILL.md)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
name: product-listing
|
||||||
|
description: 电商平台商品上架自动化
|
||||||
|
metadata:
|
||||||
|
netaclaw:
|
||||||
|
platforms: [taobao, pdd, jd, douyin]
|
||||||
|
requires: { tools: [browser, file] }
|
||||||
|
---
|
||||||
|
|
||||||
|
# 商品上架 Skill
|
||||||
|
|
||||||
|
## 触发条件
|
||||||
|
用户要求上架商品、批量上架、商品发布
|
||||||
|
|
||||||
|
## 工作流程
|
||||||
|
1. 确认目标平台和商品信息
|
||||||
|
2. 打开平台卖家后台
|
||||||
|
3. 导航到商品发布页面
|
||||||
|
4. 填写商品信息(标题、价格、库存、描述)
|
||||||
|
5. 上传商品图片
|
||||||
|
6. 提交并确认发布状态
|
||||||
|
|
||||||
|
## 规则约束
|
||||||
|
- 操作前必须确认已登录目标平台
|
||||||
|
- 价格修改需要用户二次确认
|
||||||
|
- 批量操作间隔随机 3-8 秒,模拟人工节奏
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 平台适配器基类
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export abstract class BasePlatform {
|
||||||
|
abstract name: string;
|
||||||
|
abstract loginUrl: string;
|
||||||
|
|
||||||
|
abstract login(page: Page, credentials: Credentials): Promise<void>;
|
||||||
|
abstract isLoggedIn(page: Page): Promise<boolean>;
|
||||||
|
abstract getSelectors(): PlatformSelectors;
|
||||||
|
|
||||||
|
// 反检测配置
|
||||||
|
getStealthConfig(): StealthConfig {
|
||||||
|
return { humanDelay: [100, 300], randomMouseMove: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Playwright 浏览器引擎
|
||||||
|
|
||||||
|
### 6.1 反检测策略
|
||||||
|
|
||||||
|
| 策略 | 实现方式 |
|
||||||
|
|------|----------|
|
||||||
|
| 指纹伪装 | playwright-extra + stealth plugin |
|
||||||
|
| 人工延迟 | 随机间隔 100-500ms,高斯分布 |
|
||||||
|
| 鼠标轨迹 | 贝塞尔曲线模拟真人移动 |
|
||||||
|
| 键盘输入 | 逐字输入,随机打字速度 |
|
||||||
|
| 浏览器指纹 | 使用用户已安装的 Chrome(`channel: 'chrome'`) |
|
||||||
|
| WebDriver 标志 | 移除 `navigator.webdriver` 等自动化特征 |
|
||||||
|
| 行为模式 | 随机滚动、页面停留、Tab 切换 |
|
||||||
|
|
||||||
|
### 6.2 多店铺并发
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 浏览器实例池
|
||||||
|
export class BrowserPool {
|
||||||
|
private contexts: Map<string, BrowserContext> = new Map();
|
||||||
|
|
||||||
|
// 每个店铺一个独立的 BrowserContext(隔离 Cookie/Storage)
|
||||||
|
async getContext(shopId: string): Promise<BrowserContext>;
|
||||||
|
|
||||||
|
// 并发控制(信号量,默认 max=3)
|
||||||
|
async executeWithLimit<T>(shopId: string, fn: (page: Page) => Promise<T>): Promise<T>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 数据存储
|
||||||
|
|
||||||
|
| 数据类型 | 存储位置 | 说明 |
|
||||||
|
|----------|----------|------|
|
||||||
|
| Agent 对话记录 | 本地文件 (`~/.neta/sessions/`) | 默认本地,用户可选同步到 MySQL |
|
||||||
|
| 生成的图片 | 本地文件 (`~/.neta/workspace/`) | 文生图等产出物 |
|
||||||
|
| 业务数据 | MySQL (`neta_ecommerce`) | 店铺配置、任务记录、操作日志 |
|
||||||
|
| 会话检查点 | SQLite (`~/.neta/checkpoints.sqlite`) | Agent 会话持久化 |
|
||||||
|
| 浏览器状态 | 本地文件 (`~/.neta/browser/`) | Cookie、Storage、登录态 |
|
||||||
|
|
||||||
|
## 8. 打包与分发
|
||||||
|
|
||||||
|
### 8.1 pkg 打包
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 构建
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# 打包为 .exe
|
||||||
|
pkg dist/bootstrap.js \
|
||||||
|
--targets node20-win-x64 \
|
||||||
|
--output Neta.exe \
|
||||||
|
--assets "skills/**/*"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 启动流程
|
||||||
|
|
||||||
|
```
|
||||||
|
Neta.exe 双击启动
|
||||||
|
→ 启动 Midway.js 后端 (localhost:8003)
|
||||||
|
→ 启动系统托盘图标
|
||||||
|
→ 自动打开默认浏览器到 localhost:9001
|
||||||
|
→ 前端加载,连接后端 WebSocket
|
||||||
|
→ 用户开始使用
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.3 NSIS 安装包
|
||||||
|
|
||||||
|
- 安装 Neta.exe 到 Program Files
|
||||||
|
- 创建桌面快捷方式和开始菜单项
|
||||||
|
- 捆绑 Playwright Chromium(可选,或首次启动时下载)
|
||||||
|
- 注册开机自启(可选)
|
||||||
|
|
||||||
|
## 9. 电商 Skill 清单(首批)
|
||||||
|
|
||||||
|
| Skill | 描述 | 目标平台 |
|
||||||
|
|-------|------|----------|
|
||||||
|
| `product-listing` | 商品上架(单个/批量) | 全平台 |
|
||||||
|
| `product-delist` | 商品下架 | 全平台 |
|
||||||
|
| `price-update` | 价格修改 | 全平台 |
|
||||||
|
| `inventory-sync` | 库存同步 | 全平台 |
|
||||||
|
| `order-process` | 订单处理(发货/备注) | 全平台 |
|
||||||
|
| `customer-reply` | 客服自动回复 | 全平台 |
|
||||||
|
| `competitor-monitor` | 竞品价格/销量监控 | 全平台 |
|
||||||
|
| `ad-campaign` | 直通车/推广投流 | 淘宝/拼多多 |
|
||||||
|
| `image-gen` | 产品详情页文生图 | 不限平台 |
|
||||||
|
| `data-export` | 运营数据导出 | 全平台 |
|
||||||
|
|
||||||
|
## 10. 需要移除的依赖
|
||||||
|
|
||||||
|
```
|
||||||
|
@langchain/classic
|
||||||
|
@langchain/cohere
|
||||||
|
@langchain/community
|
||||||
|
@langchain/core
|
||||||
|
@langchain/deepseek
|
||||||
|
@langchain/langgraph
|
||||||
|
@langchain/langgraph-checkpoint
|
||||||
|
@langchain/mcp-adapters
|
||||||
|
@langchain/ollama
|
||||||
|
@langchain/openai
|
||||||
|
@langchain/textsplitters
|
||||||
|
langchain
|
||||||
|
```
|
||||||
|
|
||||||
|
## 11. 需要新增的依赖
|
||||||
|
|
||||||
|
```
|
||||||
|
playwright-core # 浏览器自动化
|
||||||
|
playwright-extra # 反检测插件框架
|
||||||
|
puppeteer-extra-plugin-stealth # 反检测插件(兼容 playwright-extra)
|
||||||
|
@sinclair/typebox # JSON Schema(AgentTool 参数定义)
|
||||||
|
@anthropic-ai/sdk # Anthropic Claude API
|
||||||
|
node-systray # 系统托盘图标
|
||||||
|
open # 自动打开浏览器
|
||||||
|
pkg # 打包为 .exe(devDependency)
|
||||||
|
```
|
||||||
333
docs/superpowers/specs/2026-04-12-agent-memory-system-design.md
Normal file
333
docs/superpowers/specs/2026-04-12-agent-memory-system-design.md
Normal file
@ -0,0 +1,333 @@
|
|||||||
|
# Agent 长期记忆系统设计
|
||||||
|
|
||||||
|
> 日期: 2026-04-12
|
||||||
|
> 模块: netaclaw/memory
|
||||||
|
> 状态: 设计阶段
|
||||||
|
|
||||||
|
## 1. 目标
|
||||||
|
|
||||||
|
为 NetaClaw Agent 添加长期记忆能力,使 Agent 能够跨会话记住用户偏好、项目上下文、行为反馈和外部引用。记忆按 Agent + 用户组合隔离,支持 MySQL 和 SQLite 两种存储后端,由用户在 Agent 管理界面自由选择。
|
||||||
|
|
||||||
|
## 2. 核心决策
|
||||||
|
|
||||||
|
| 决策项 | 选择 | 理由 |
|
||||||
|
|--------|------|------|
|
||||||
|
| 记忆归属粒度 | Agent + 用户组合 | 最细粒度隔离,互不干扰 |
|
||||||
|
| 存储后端 | MySQL FULLTEXT + SQLite FTS5 双后端 | 用户可按 Agent 选择持久化方式 |
|
||||||
|
| 记忆分类 | user / project / feedback / reference | 覆盖四种典型记忆场景 |
|
||||||
|
| 写入方式 | Agent 工具主动写入 | 简单可控,Agent 自主决策存什么 |
|
||||||
|
| 检索注入 | 每轮对话前 prefetch,不持久化 | 避免污染消息历史 |
|
||||||
|
|
||||||
|
## 3. 数据模型
|
||||||
|
|
||||||
|
### 3.1 MemoryEntry 接口
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface MemoryEntry {
|
||||||
|
id: number;
|
||||||
|
agentName: string; // 归属 Agent
|
||||||
|
userId: string; // 归属用户
|
||||||
|
type: 'user' | 'project' | 'feedback' | 'reference';
|
||||||
|
name: string; // 记忆标题
|
||||||
|
content: string; // 记忆正文
|
||||||
|
description: string; // 一行描述,用于检索时快速判断相关性
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 MySQL 表结构 (netaclaw_memory)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE netaclaw_memory (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
agent_name VARCHAR(100) NOT NULL,
|
||||||
|
user_id VARCHAR(100) NOT NULL,
|
||||||
|
type ENUM('user', 'project', 'feedback', 'reference') NOT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
description VARCHAR(500) NOT NULL DEFAULT '',
|
||||||
|
metadata JSON,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_agent_user (agent_name, user_id),
|
||||||
|
INDEX idx_type (type),
|
||||||
|
FULLTEXT INDEX ft_content (name, content, description) WITH PARSER ngram
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 SQLite 表结构
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 主表
|
||||||
|
CREATE TABLE memory (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
agent_name TEXT NOT NULL,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
type TEXT NOT NULL CHECK(type IN ('user', 'project', 'feedback', 'reference')),
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL DEFAULT '',
|
||||||
|
metadata TEXT, -- JSON string
|
||||||
|
created_at TEXT DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_agent_user ON memory(agent_name, user_id);
|
||||||
|
|
||||||
|
-- FTS5 虚拟表
|
||||||
|
CREATE VIRTUAL TABLE memory_fts USING fts5(
|
||||||
|
name, content, description,
|
||||||
|
content='memory', content_rowid='id',
|
||||||
|
tokenize='trigram'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 同步触发器
|
||||||
|
CREATE TRIGGER memory_ai AFTER INSERT ON memory BEGIN
|
||||||
|
INSERT INTO memory_fts(rowid, name, content, description)
|
||||||
|
VALUES (new.id, new.name, new.content, new.description);
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER memory_ad AFTER DELETE ON memory BEGIN
|
||||||
|
INSERT INTO memory_fts(memory_fts, rowid, name, content, description)
|
||||||
|
VALUES ('delete', old.id, old.name, old.content, old.description);
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER memory_au AFTER UPDATE ON memory BEGIN
|
||||||
|
INSERT INTO memory_fts(memory_fts, rowid, name, content, description)
|
||||||
|
VALUES ('delete', old.id, old.name, old.content, old.description);
|
||||||
|
INSERT INTO memory_fts(rowid, name, content, description)
|
||||||
|
VALUES (new.id, new.name, new.content, new.description);
|
||||||
|
END;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Provider 抽象层
|
||||||
|
|
||||||
|
### 4.1 MemoryProvider 接口
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// memory/provider.ts
|
||||||
|
export interface MemorySearchOpts {
|
||||||
|
agentName: string;
|
||||||
|
userId: string;
|
||||||
|
type?: MemoryEntry['type'];
|
||||||
|
limit?: number; // 默认 10
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MemoryProvider {
|
||||||
|
save(entry: Omit<MemoryEntry, 'id' | 'createdAt' | 'updatedAt'>): Promise<MemoryEntry>;
|
||||||
|
update(id: number | string, partial: Partial<Pick<MemoryEntry, 'name' | 'content' | 'description' | 'type' | 'metadata'>>): Promise<MemoryEntry>;
|
||||||
|
delete(id: number | string): Promise<void>;
|
||||||
|
search(query: string, opts: MemorySearchOpts): Promise<MemoryEntry[]>;
|
||||||
|
list(opts: MemorySearchOpts): Promise<MemoryEntry[]>;
|
||||||
|
getById(id: number | string): Promise<MemoryEntry | null>;
|
||||||
|
close?(): Promise<void>; // SQLite 需要关闭连接
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 工厂函数
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// memory/factory.ts
|
||||||
|
export function createMemoryProvider(config: AgentMemoryConfig, mysqlRepo?: Repository<NetaClawMemoryEntity>): MemoryProvider {
|
||||||
|
if (config.backend === 'sqlite') {
|
||||||
|
return new SqliteMemoryProvider(config.sqlitePath);
|
||||||
|
}
|
||||||
|
return new MysqlMemoryProvider(mysqlRepo);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 MysqlMemoryProvider
|
||||||
|
|
||||||
|
- 注入 TypeORM `Repository<NetaClawMemoryEntity>`
|
||||||
|
- search 实现: `SELECT * FROM netaclaw_memory WHERE MATCH(name, content, description) AGAINST(? IN BOOLEAN MODE) AND agent_name = ? AND user_id = ? LIMIT ?`
|
||||||
|
- 复用现有数据库连接,无额外依赖
|
||||||
|
|
||||||
|
### 4.4 SqliteMemoryProvider
|
||||||
|
|
||||||
|
- 使用 `better-sqlite3` 管理独立 .db 文件
|
||||||
|
- 默认路径: `{dataDir}/memory/memory.db`(所有 Agent 共用一个 db,按字段过滤)
|
||||||
|
- search 实现: `SELECT m.* FROM memory m JOIN memory_fts f ON m.id = f.rowid WHERE memory_fts MATCH ? AND m.agent_name = ? AND m.user_id = ? LIMIT ?`
|
||||||
|
- 初始化时自动建表(如不存在)
|
||||||
|
|
||||||
|
## 5. Agent 工具定义
|
||||||
|
|
||||||
|
### 5.1 memory_save 工具
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// tools/builtin/memory.ts
|
||||||
|
const memorySaveTool: AgentTool = {
|
||||||
|
name: 'memory_save',
|
||||||
|
label: '保存记忆',
|
||||||
|
description: '存储、更新或删除长期记忆。记忆会在未来对话中自动召回。',
|
||||||
|
parameters: Type.Object({
|
||||||
|
action: Type.Union([Type.Literal('create'), Type.Literal('update'), Type.Literal('delete')]),
|
||||||
|
name: Type.String({ description: '记忆标题' }),
|
||||||
|
type: Type.Union([
|
||||||
|
Type.Literal('user'), Type.Literal('project'),
|
||||||
|
Type.Literal('feedback'), Type.Literal('reference')
|
||||||
|
]),
|
||||||
|
content: Type.Optional(Type.String({ description: '记忆正文' })),
|
||||||
|
description: Type.Optional(Type.String({ description: '一行描述' })),
|
||||||
|
id: Type.Optional(Type.Number({ description: '更新/删除时的记忆 ID' })),
|
||||||
|
}),
|
||||||
|
async execute(id, params) {
|
||||||
|
// 由运行时注入 agentName + userId + provider
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 memory_recall 工具
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const memoryRecallTool: AgentTool = {
|
||||||
|
name: 'memory_recall',
|
||||||
|
label: '检索记忆',
|
||||||
|
description: '搜索长期记忆中的相关信息。',
|
||||||
|
parameters: Type.Object({
|
||||||
|
query: Type.String({ description: '搜索关键词' }),
|
||||||
|
type: Type.Optional(Type.Union([
|
||||||
|
Type.Literal('user'), Type.Literal('project'),
|
||||||
|
Type.Literal('feedback'), Type.Literal('reference')
|
||||||
|
])),
|
||||||
|
limit: Type.Optional(Type.Number({ description: '返回条数,默认 5', default: 5 })),
|
||||||
|
}),
|
||||||
|
async execute(id, params) {
|
||||||
|
// 由运行时注入 agentName + userId + provider
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Prefetch 注入流程
|
||||||
|
|
||||||
|
### 6.1 触发时机
|
||||||
|
|
||||||
|
在 `controller/chat.ts` 的 chat 方法中,调用 `runAgent()` 之前:
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 检查 Agent 配置 memory.enabled
|
||||||
|
2. 如果启用 → createMemoryProvider(agentConfig.memory)
|
||||||
|
3. 用用户消息作为 query → provider.search(userMessage, { agentName, userId, limit: 5 })
|
||||||
|
4. 格式化为 memoryContext 字符串
|
||||||
|
5. 传入 runAgent() 的参数中
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 注入位置
|
||||||
|
|
||||||
|
在 `runtime/agent.ts` 消息组装处,将 memoryContext 拼接到 systemPrompt 中(而非添加第二个 system 消息,因为 Anthropic provider 只读取第一个 system 消息):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const systemContent = memoryContext
|
||||||
|
? `${agentConfig.systemPrompt}\n\n<memory-context>\n${memoryContext}\n</memory-context>`
|
||||||
|
: agentConfig.systemPrompt;
|
||||||
|
|
||||||
|
const messages: LLMMessage[] = [
|
||||||
|
{ role: 'system', content: systemContent },
|
||||||
|
...history,
|
||||||
|
{ role: 'user', content: userMessage },
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 memoryContext 格式
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<memory-context>
|
||||||
|
以下是与当前对话可能相关的长期记忆:
|
||||||
|
|
||||||
|
[user] 用户偏好-简洁风格
|
||||||
|
用户偏好简洁直接的回答风格,不喜欢冗长的解释。
|
||||||
|
|
||||||
|
[project] 当前冲刺目标
|
||||||
|
本周冲刺目标是完成支付模块重构,截止日期 2026-04-15。
|
||||||
|
|
||||||
|
[feedback] 不要自动格式化代码
|
||||||
|
用户明确要求不要自动格式化代码,保持原有风格。
|
||||||
|
</memory-context>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.4 关键约束
|
||||||
|
|
||||||
|
- memoryContext 消息不写入 `netaclaw_message` 表
|
||||||
|
- 每次请求重新 prefetch,保证记忆是最新的
|
||||||
|
- 如果 search 返回空,不注入任何内容
|
||||||
|
|
||||||
|
## 7. 系统提示词扩展
|
||||||
|
|
||||||
|
当 Agent 启用记忆时,在 systemPrompt 末尾追加:
|
||||||
|
|
||||||
|
```
|
||||||
|
## 记忆系统
|
||||||
|
你拥有长期记忆能力。使用 memory_save 工具存储重要信息,使用 memory_recall 工具检索过往记忆。
|
||||||
|
|
||||||
|
记忆类型:
|
||||||
|
- user: 用户画像(偏好、角色、习惯)
|
||||||
|
- project: 项目知识(进展、决策、约束)
|
||||||
|
- feedback: 行为反馈(用户对你行为的纠正或确认)
|
||||||
|
- reference: 引用(外部资源链接、文档地址)
|
||||||
|
|
||||||
|
存储原则:
|
||||||
|
- 当用户透露个人偏好、角色、习惯时,存为 user 类型
|
||||||
|
- 当了解到项目进展、决策、约束时,存为 project 类型
|
||||||
|
- 当用户纠正或确认你的行为时,存为 feedback 类型
|
||||||
|
- 当提到外部资源链接时,存为 reference 类型
|
||||||
|
- 更新已有记忆而非创建重复条目
|
||||||
|
- 只存储对未来对话有价值的信息
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Agent 配置扩展
|
||||||
|
|
||||||
|
### 8.1 AgentEntity.config 扩展
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// entity/agent.ts config 字段新增
|
||||||
|
interface AgentConfig {
|
||||||
|
// ...现有字段
|
||||||
|
memory?: {
|
||||||
|
enabled: boolean;
|
||||||
|
backend: 'mysql' | 'sqlite';
|
||||||
|
sqlitePath?: string; // 仅 sqlite 后端,默认 dataDir/memory/memory.db
|
||||||
|
prefetchLimit?: number; // prefetch 返回条数,默认 5
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 Agent 管理界面
|
||||||
|
|
||||||
|
在 Agent 编辑页面新增"记忆配置"区域:
|
||||||
|
- 开关:启用/禁用记忆
|
||||||
|
- 下拉:存储后端(MySQL / 本地 SQLite)
|
||||||
|
- 数字输入:prefetch 条数(高级选项)
|
||||||
|
|
||||||
|
## 9. 新增文件清单
|
||||||
|
|
||||||
|
```
|
||||||
|
src/modules/netaclaw/
|
||||||
|
├── memory/
|
||||||
|
│ ├── provider.ts # MemoryProvider 接口 + MemoryEntry 类型
|
||||||
|
│ ├── factory.ts # createMemoryProvider 工厂
|
||||||
|
│ ├── mysql_provider.ts # MysqlMemoryProvider 实现
|
||||||
|
│ ├── sqlite_provider.ts # SqliteMemoryProvider 实现
|
||||||
|
│ └── prefetch.ts # prefetchMemory() 函数,格式化注入内容
|
||||||
|
├── entity/
|
||||||
|
│ └── memory.ts # NetaClawMemoryEntity (MySQL)
|
||||||
|
├── tools/builtin/
|
||||||
|
│ └── memory.ts # memory_save + memory_recall 工具定义
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. 修改文件清单
|
||||||
|
|
||||||
|
| 文件 | 修改内容 |
|
||||||
|
|------|---------|
|
||||||
|
| `runtime/agent.ts` | AgentRunParams 新增 memoryContext 参数,拼接到 systemPrompt 中 |
|
||||||
|
| `controller/chat.ts` | 注入 AgentService + MemoryRepo,加载 agent 配置读取 memory config,执行 prefetch,注入 memory 工具,body 新增 userId 字段 |
|
||||||
|
| `src/entities.ts` | 注册 NetaClawMemoryEntity |
|
||||||
|
|
||||||
|
## 11. 依赖
|
||||||
|
|
||||||
|
| 依赖 | 用途 | 条件 |
|
||||||
|
|------|------|------|
|
||||||
|
| `better-sqlite3` | SQLite FTS5 全文检索 | 仅 sqlite 后端需要 |
|
||||||
|
| `@types/better-sqlite3` | TypeScript 类型 | 开发依赖 |
|
||||||
|
|
||||||
|
MySQL FULLTEXT 无额外依赖,复用现有 TypeORM 连接。
|
||||||
@ -0,0 +1,250 @@
|
|||||||
|
# 模型渠道管理设计文档
|
||||||
|
|
||||||
|
> **日期**: 2026-04-12
|
||||||
|
> **范围**: Neta-monorepo 后端 + 前端
|
||||||
|
> **参照**: AI_flow_BackEnd 模型渠道管理实现
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 目标
|
||||||
|
|
||||||
|
为 Neta-monorepo 新增集中式 LLM 模型渠道管理能力,参照 AI_flow 的三级结构(供应商 → 渠道 → 模型),同时适配 Neta 的 `provider:model` 运行时机制。改造 Agent 和 Skill 的模型配置为渠道级联选择器。
|
||||||
|
|
||||||
|
**核心价值**:
|
||||||
|
- API Key 集中管理,一处修改全局生效
|
||||||
|
- 支持同一供应商多套渠道(如测试/生产各一套 Key)
|
||||||
|
- 渠道启停、连通性测试等运维能力
|
||||||
|
- Agent/Skill 编辑时通过选择器引用,避免手动填凭证
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 数据模型
|
||||||
|
|
||||||
|
### 2.1 新增表: `netaclaw_model_channel`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| id | int | PK, auto | 主键 |
|
||||||
|
| name | varchar(100) | unique | 渠道名称,如"Anthropic-生产" |
|
||||||
|
| supplier | varchar(50) | indexed | 供应商类型代码 |
|
||||||
|
| baseUrl | varchar(500) | not null | API 基础地址 |
|
||||||
|
| apiKey | varchar(500) | nullable | API 密钥 |
|
||||||
|
| models | json | not null | 模型列表 `[{name, capability}]` |
|
||||||
|
| description | text | nullable | 描述 |
|
||||||
|
| status | int | indexed, default 1 | 0=禁用 1=启用 |
|
||||||
|
| createTime | datetime | auto | 创建时间 |
|
||||||
|
| updateTime | datetime | auto | 更新时间 |
|
||||||
|
| deleteTime | datetime | nullable | 软删除时间 |
|
||||||
|
|
||||||
|
**models JSON 结构**:
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{ "name": "claude-sonnet-4-20250514", "capability": "text" },
|
||||||
|
{ "name": "claude-sonnet-4-20250514", "capability": "multimodal" }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 系统参数 (`base_sys_param`)
|
||||||
|
|
||||||
|
新增 2 条参数记录:
|
||||||
|
|
||||||
|
| keyName | name | dataType | data (JSON) |
|
||||||
|
|---------|------|----------|-------------|
|
||||||
|
| `model_suppliers` | AI模型供应商 | 0 | `[{"value":"openai","label":"OpenAI","tagType":""},{"value":"anthropic","label":"Anthropic","tagType":"purple"},{"value":"deepseek","label":"DeepSeek","tagType":"success"},{"value":"zhipu","label":"智谱AI","tagType":"success"},{"value":"tongyi","label":"通义千问","tagType":"warning"},{"value":"minimax","label":"MiniMax","tagType":"danger"},{"value":"volcengine","label":"火山引擎","tagType":"warning"},{"value":"ollama","label":"Ollama","tagType":"info"},{"value":"azure","label":"Azure OpenAI","tagType":""}]` |
|
||||||
|
| `model_capabilities` | 模型能力类型 | 0 | `[{"value":"text","label":"纯文本"},{"value":"multimodal","label":"多模态"},{"value":"vision","label":"视觉"}]` |
|
||||||
|
|
||||||
|
供应商和能力类型均通过系统参数表管理,前端不做硬编码。供应商配置包含 `tagType` 字段用于渲染标签颜色,后续可在参数列表页面自由扩展新供应商或新能力类型。
|
||||||
|
|
||||||
|
### 2.3 Agent `modelConfig` 字段语义扩展
|
||||||
|
|
||||||
|
**新格式** (选择渠道时):
|
||||||
|
```json
|
||||||
|
{ "channelId": 5, "modelId": "claude-sonnet-4-20250514", "contextWindow": 200 }
|
||||||
|
```
|
||||||
|
|
||||||
|
**自定义格式** (channelId 为 0 或 null 时,回退手动模式):
|
||||||
|
```json
|
||||||
|
{ "channelId": 0, "apiUrl": "https://...", "apiKey": "sk-xxx", "modelId": "custom-model", "contextWindow": 200 }
|
||||||
|
```
|
||||||
|
|
||||||
|
**兼容性**: 已有 Agent 的 modelConfig 中无 channelId,视为自定义配置模式,无需数据迁移。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 后端架构
|
||||||
|
|
||||||
|
### 3.1 新增文件
|
||||||
|
|
||||||
|
| 文件 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| Entity | `packages/backend/src/modules/netaclaw/entity/model_channel.ts` | TypeORM 实体 |
|
||||||
|
| Service | `packages/backend/src/modules/netaclaw/service/model_channel.ts` | 业务逻辑 |
|
||||||
|
| Controller | `packages/backend/src/modules/netaclaw/controller/admin/model_channel.ts` | Admin API |
|
||||||
|
|
||||||
|
### 3.2 API 端点
|
||||||
|
|
||||||
|
| 端点 | 方法 | 功能 | 来源 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `/admin/netaclaw/model_channel/page` | POST | 分页查询 | @CoolController 自动生成 |
|
||||||
|
| `/admin/netaclaw/model_channel/add` | POST | 新增渠道 | @CoolController 自动生成 |
|
||||||
|
| `/admin/netaclaw/model_channel/update` | POST | 更新渠道 | @CoolController 自动生成 |
|
||||||
|
| `/admin/netaclaw/model_channel/delete` | POST | 删除渠道 | @CoolController 自动生成 |
|
||||||
|
| `/admin/netaclaw/model_channel/info` | GET | 渠道详情 | @CoolController 自动生成 |
|
||||||
|
| `/admin/netaclaw/model_channel/allModels` | GET | 所有启用渠道的模型列表 | 自定义方法 |
|
||||||
|
| `/admin/netaclaw/model_channel/testConnection` | POST | 测试渠道连通性 | 自定义方法 |
|
||||||
|
|
||||||
|
### 3.3 Service 关键方法
|
||||||
|
|
||||||
|
**`allModels()`**:
|
||||||
|
- 查询所有 `status=1` 的渠道
|
||||||
|
- 标准化 models 字段(兼容旧格式字符串数组 → `{name, capability}` 对象)
|
||||||
|
- 返回渠道列表及其模型
|
||||||
|
|
||||||
|
**`testConnection(id: number)`**:
|
||||||
|
- 根据 supplier 类型初始化对应 LLM 客户端
|
||||||
|
- 发送简单测试请求验证连通性
|
||||||
|
- 返回 `{ success, elapsed, message }`
|
||||||
|
- 供应商到 Provider 的映射:
|
||||||
|
- openai/azure → OpenAI SDK
|
||||||
|
- anthropic → Anthropic SDK
|
||||||
|
- deepseek → OpenAI SDK (自定义 baseUrl)
|
||||||
|
- zhipu/tongyi/minimax/volcengine → OpenAI SDK (自定义 baseUrl)
|
||||||
|
- ollama → OpenAI SDK (本地 baseUrl)
|
||||||
|
|
||||||
|
**`resolveForAgent(channelId: number, modelId: string)`**:
|
||||||
|
- 查询渠道获取 `supplier`、`baseUrl`、`apiKey`
|
||||||
|
- 将 supplier 映射为 Neta 的 provider 名称
|
||||||
|
- 返回 `{ provider, model, apiKey, baseUrl }` 供 Agent 运行时使用
|
||||||
|
- supplier → provider 映射规则:
|
||||||
|
- `openai` → `openai`
|
||||||
|
- `anthropic` → `anthropic`
|
||||||
|
- `deepseek` → `deepseek`
|
||||||
|
- 其他 (zhipu/tongyi/minimax/volcengine/ollama/azure) → `openai`(均使用 OpenAI 兼容协议)
|
||||||
|
|
||||||
|
### 3.4 运行时改造
|
||||||
|
|
||||||
|
**文件**: `packages/backend/src/modules/netaclaw/runtime/agent.ts`
|
||||||
|
|
||||||
|
在 `runAgent()` 函数中,执行前解析模型配置:
|
||||||
|
1. 读取 Agent 的 `modelConfig`
|
||||||
|
2. 若 `channelId > 0`,调用 `resolveForAgent(channelId, modelId)` 获取凭证
|
||||||
|
3. 若 `channelId` 为 0/null 或无该字段,使用 modelConfig 中的 `apiUrl`/`apiKey`(向后兼容)
|
||||||
|
4. 构建 `provider:model` 格式字符串传入运行时
|
||||||
|
|
||||||
|
### 3.5 Entity 注册
|
||||||
|
|
||||||
|
在 `packages/backend/src/entities.ts` 中添加新 Entity 的导入。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 前端架构
|
||||||
|
|
||||||
|
### 4.1 新增文件
|
||||||
|
|
||||||
|
| 文件 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 模型渠道管理页 | `packages/frontend/src/modules/agent/views/model-channel.vue` | 渠道 CRUD 页面 |
|
||||||
|
| 渠道选择器组件 | `packages/frontend/src/modules/agent/components/model-channel-selector.vue` | 级联选择器 |
|
||||||
|
|
||||||
|
### 4.2 模型渠道管理页面 (`model-channel.vue`)
|
||||||
|
|
||||||
|
**布局**:
|
||||||
|
- 顶部工具栏: 新增渠道按钮、供应商筛选下拉、关键词搜索、查询/重置
|
||||||
|
- 数据表格: 渠道名称、供应商(Tag)、Base URL、模型汇总(按能力分组计数)、状态(Switch)、操作(编辑/测试/删除)
|
||||||
|
- 分页控件
|
||||||
|
- 编辑抽屉 (600px 宽度):
|
||||||
|
- 渠道名称 (必填)
|
||||||
|
- 供应商类型 (下拉,选项从 `model_suppliers` 参数加载,必填)
|
||||||
|
- Base URL (必填)
|
||||||
|
- API Key (密码输入)
|
||||||
|
- 描述
|
||||||
|
- 模型列表 (表格式编辑: 模型名 + 能力类型下拉)
|
||||||
|
- 添加单行 + 批量导入按钮
|
||||||
|
- 批量导入对话框: 默认能力类型 + 文本域(逗号/换行分隔模型名)
|
||||||
|
|
||||||
|
**供应商 Tag 颜色**: 从 `model_suppliers` 参数的 `tagType` 字段读取,不硬编码。
|
||||||
|
|
||||||
|
**API 调用**:
|
||||||
|
- 参数加载: `GET /admin/base/sys/param/dataByKey?key=model_suppliers`
|
||||||
|
- 参数加载: `GET /admin/base/sys/param/dataByKey?key=model_capabilities`
|
||||||
|
- 分页查询: `POST /admin/netaclaw/model_channel/page`
|
||||||
|
- 新增: `POST /admin/netaclaw/model_channel/add`
|
||||||
|
- 更新: `POST /admin/netaclaw/model_channel/update`
|
||||||
|
- 删除: `POST /admin/netaclaw/model_channel/delete`
|
||||||
|
- 测试: `POST /admin/netaclaw/model_channel/testConnection`
|
||||||
|
|
||||||
|
### 4.3 渠道选择器组件 (`model-channel-selector.vue`)
|
||||||
|
|
||||||
|
**Props**:
|
||||||
|
- `modelValue?: { channelId?, modelId?, apiUrl?, apiKey?, contextWindow? }` (v-model)
|
||||||
|
- `capabilityFilter?: string` (按能力类型过滤可选模型)
|
||||||
|
|
||||||
|
**Emits**:
|
||||||
|
- `update:modelValue`
|
||||||
|
- `change`
|
||||||
|
|
||||||
|
**UI**:
|
||||||
|
- 渠道选择下拉 (显示: "渠道名称(N个模型)")
|
||||||
|
- 额外选项: "自定义配置(手动输入)" (value=0)
|
||||||
|
- 模型选择下拉 (filterable,显示: 模型名 + 能力类型 Tag)
|
||||||
|
- 自定义模式时展示: API URL + API Key + Model ID 手动输入框
|
||||||
|
|
||||||
|
**数据流**:
|
||||||
|
1. 初始化时调用 `GET /admin/netaclaw/model_channel/allModels` 获取所有启用渠道
|
||||||
|
2. 按 `capabilityFilter` 过滤渠道和模型
|
||||||
|
3. 用户选择渠道 → 展示该渠道的模型列表
|
||||||
|
4. 用户选择模型 → emit `{ channelId, modelId }`
|
||||||
|
5. 若选「自定义配置」→ 展示手动输入表单 → emit `{ channelId: 0, apiUrl, apiKey, modelId }`
|
||||||
|
|
||||||
|
### 4.4 改造: Agent 编辑页
|
||||||
|
|
||||||
|
**文件**: `packages/frontend/src/modules/agent/views/agent-edit.vue`
|
||||||
|
|
||||||
|
**「模型配置」Tab 改造**:
|
||||||
|
- 移除原有 apiUrl/apiKey/modelId/contextWindow 4 个输入框
|
||||||
|
- 替换为 `<model-channel-selector v-model="form.modelConfig" />`
|
||||||
|
- 选择器下方保留 contextWindow 输入框
|
||||||
|
- 保留「填入默认」按钮(从 netaclaw 配置读取默认渠道)和「清空」按钮
|
||||||
|
|
||||||
|
### 4.5 改造: Skill 模型配置
|
||||||
|
|
||||||
|
**文件**: `packages/frontend/src/modules/agent/components/skill-model.vue`
|
||||||
|
|
||||||
|
- 将手动输入替换为 `<model-channel-selector :capability-filter="skillType" />`
|
||||||
|
- Skill 类型为 `multimodal` 时只展示 multimodal 能力的模型
|
||||||
|
- Skill 类型为 `llm` 时展示 text 能力的模型
|
||||||
|
|
||||||
|
### 4.6 路由配置
|
||||||
|
|
||||||
|
**文件**: `packages/frontend/src/modules/agent/config.ts`
|
||||||
|
|
||||||
|
新增路由:
|
||||||
|
```typescript
|
||||||
|
{ path: '/agent/model-channel', meta: { label: '模型管理' } }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 文件变更清单
|
||||||
|
|
||||||
|
| 操作 | 位置 | 文件 |
|
||||||
|
|------|------|------|
|
||||||
|
| 新增 | backend | `src/modules/netaclaw/entity/model_channel.ts` |
|
||||||
|
| 新增 | backend | `src/modules/netaclaw/service/model_channel.ts` |
|
||||||
|
| 新增 | backend | `src/modules/netaclaw/controller/admin/model_channel.ts` |
|
||||||
|
| 新增 | frontend | `src/modules/agent/views/model-channel.vue` |
|
||||||
|
| 新增 | frontend | `src/modules/agent/components/model-channel-selector.vue` |
|
||||||
|
| 修改 | frontend | `src/modules/agent/views/agent-edit.vue` |
|
||||||
|
| 修改 | frontend | `src/modules/agent/components/skill-model.vue` |
|
||||||
|
| 修改 | frontend | `src/modules/agent/config.ts` |
|
||||||
|
| 修改 | backend | `src/modules/netaclaw/runtime/agent.ts` |
|
||||||
|
| 修改 | backend | `src/entities.ts` |
|
||||||
|
| 数据 | database | `base_sys_param` 表新增 2 条参数记录 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 兼容性
|
||||||
|
|
||||||
|
- **无数据迁移**: 已有 Agent 的 modelConfig 中无 channelId,自动视为自定义配置模式
|
||||||
|
- **渐进式采用**: 新建/编辑 Agent 时可选择渠道或自定义,不影响已有 Agent 运行
|
||||||
|
- **运行时兼容**: channelId 有值走渠道查询,无值走原有 apiUrl/apiKey 逻辑
|
||||||
@ -0,0 +1,234 @@
|
|||||||
|
# NetaClaw Agent/Skill 管理迁移设计文档
|
||||||
|
|
||||||
|
> **日期**: 2026-04-12
|
||||||
|
> **状态**: 已批准
|
||||||
|
> **范围**: 将 Agent 管理、Skill 管理、Agent 对话页面从旧接口迁移到 NetaClaw 运行时
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 背景与目标
|
||||||
|
|
||||||
|
现有前端 3 个页面(Agent 管理、Skill 管理、Agent 对话)调用的是旧的 `/admin/agent/*` 和 `/open/agent/*` 接口体系,后端对应旧的 LangChain Agent 模块。
|
||||||
|
|
||||||
|
NetaClaw 模块已从 OpenClaw(小龙虾)迁移了 Agent 运行时(ReAct 循环、工具系统、LLM 提供商),但缺少管理层接口。
|
||||||
|
|
||||||
|
**目标:**
|
||||||
|
- 后端在 NetaClaw 模块补全 Agent CRUD、Skill 管理、会话管理接口
|
||||||
|
- 前端统一切到 NetaClaw 接口
|
||||||
|
- 对话通信从 SSE 改为 WebSocket
|
||||||
|
- 删除旧 agent 模块代码和数据库表
|
||||||
|
|
||||||
|
## 2. 核心决策
|
||||||
|
|
||||||
|
| 决策项 | 结论 | 理由 |
|
||||||
|
|--------|------|------|
|
||||||
|
| 旧模块处理 | 直接删除 | 旧数据是保险审核场景,电商场景用不上 |
|
||||||
|
| Agent 存储 | 数据库 + 后台管理 | 保持管理页面 CRUD 体验 |
|
||||||
|
| 通信方式 | WebSocket 流式 | 与 NetaClaw 现有 WS Gateway 一致 |
|
||||||
|
| Skill 管理 | SKILL.md + 数据库状态 | 文件定义内容,数据库控制启用/禁用 |
|
||||||
|
| 数据库表 | 全部新建 netaclaw_ 前缀 | 旧 agent_* 表和数据全部删除 |
|
||||||
|
|
||||||
|
## 3. 数据库设计
|
||||||
|
|
||||||
|
### 3.1 新建表
|
||||||
|
|
||||||
|
#### `netaclaw_agent` — Agent 配置
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| id | int | PK, AUTO_INCREMENT | 主键 |
|
||||||
|
| createTime | varchar(255) | NOT NULL | 创建时间 |
|
||||||
|
| updateTime | varchar(255) | NOT NULL | 更新时间 |
|
||||||
|
| tenantId | int | nullable, INDEX | 租户ID |
|
||||||
|
| name | varchar(100) | UNIQUE, NOT NULL | 唯一标识,如 `agent_xxxxx` |
|
||||||
|
| label | varchar(200) | NOT NULL | 显示名称 |
|
||||||
|
| description | text | nullable | 描述 |
|
||||||
|
| icon | varchar(100) | nullable | 图标 |
|
||||||
|
| systemPrompt | text | nullable | 系统提示词 |
|
||||||
|
| skills | json | nullable | 关联 Skill 名称列表 `string[]` |
|
||||||
|
| modelConfig | json | nullable | `{apiUrl, apiKey, modelId, contextWindow}` |
|
||||||
|
| config | json | nullable | `{welcomeMessage, middleware: {maxToolRounds, ...}}` |
|
||||||
|
| status | int | NOT NULL, DEFAULT 0, INDEX | 0=草稿, 1=已发布 |
|
||||||
|
|
||||||
|
#### `netaclaw_skill` — Skill 状态管理
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| id | int | PK, AUTO_INCREMENT | 主键 |
|
||||||
|
| createTime | varchar(255) | NOT NULL | 创建时间 |
|
||||||
|
| updateTime | varchar(255) | NOT NULL | 更新时间 |
|
||||||
|
| tenantId | int | nullable, INDEX | 租户ID |
|
||||||
|
| name | varchar(100) | UNIQUE, NOT NULL | 对应 SKILL.md 的 name |
|
||||||
|
| label | varchar(200) | NOT NULL | 显示名称 |
|
||||||
|
| description | text | nullable | 描述 |
|
||||||
|
| icon | varchar(100) | nullable | 图标 |
|
||||||
|
| category | varchar(50) | nullable | 分类 |
|
||||||
|
| skillType | varchar(20) | nullable | compute/llm/multimodal |
|
||||||
|
| tags | json | nullable | 标签 |
|
||||||
|
| config | json | nullable | 配置 |
|
||||||
|
| status | int | NOT NULL, DEFAULT 1, INDEX | 0=禁用, 1=启用 |
|
||||||
|
| version | varchar(20) | nullable | 版本号 |
|
||||||
|
|
||||||
|
### 3.2 复用表(已存在,无数据)
|
||||||
|
|
||||||
|
- **`netaclaw_session`** — 增加 `agentId` int 字段关联 Agent
|
||||||
|
- **`netaclaw_message`** — 不改
|
||||||
|
|
||||||
|
### 3.3 删除表
|
||||||
|
|
||||||
|
- `agent_info`, `agent_skill`, `agent_session`, `agent_message`
|
||||||
|
- `agent_checkpoints`, `agent_checkpoint_writes`, `agent_configs`, `skill_configs`
|
||||||
|
|
||||||
|
## 4. 后端接口设计
|
||||||
|
|
||||||
|
### 4.1 Agent 管理 Controller
|
||||||
|
|
||||||
|
文件:`netaclaw/controller/agent.ts`
|
||||||
|
|
||||||
|
| 方法 | 路径 | 参数 | 返回 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| POST | `/admin/netaclaw/agent/page` | `{page, size, keyWord?}` | 分页列表 |
|
||||||
|
| POST | `/admin/netaclaw/agent/add` | Agent 完整对象 | `{id}` |
|
||||||
|
| POST | `/admin/netaclaw/agent/update` | Agent 完整对象 | 成功/失败 |
|
||||||
|
| POST | `/admin/netaclaw/agent/delete` | `{ids: number[]}` | 成功/失败 |
|
||||||
|
| GET | `/admin/netaclaw/agent/info` | `?id=number` | Agent 详情 |
|
||||||
|
| POST | `/open/netaclaw/agent/list` | 无 | 已发布 Agent 列表 |
|
||||||
|
|
||||||
|
### 4.2 Skill 管理 Controller
|
||||||
|
|
||||||
|
文件:`netaclaw/controller/skill.ts`
|
||||||
|
|
||||||
|
| 方法 | 路径 | 参数 | 返回 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| GET | `/admin/netaclaw/skill/metas` | 无 | Skill 元数据列表(SKILL.md + DB 合并) |
|
||||||
|
| POST | `/admin/netaclaw/skill/setStatus` | `{name, status}` | 成功/失败 |
|
||||||
|
|
||||||
|
**合并逻辑:** SkillLoader 扫描 SKILL.md 文件获取内容,与数据库 `netaclaw_skill` 表的 status 字段合并。如果 SKILL.md 存在但数据库无记录,自动创建记录(默认启用)。
|
||||||
|
|
||||||
|
### 4.3 会话管理 Controller
|
||||||
|
|
||||||
|
文件:`netaclaw/controller/session.ts`
|
||||||
|
|
||||||
|
| 方法 | 路径 | 参数 | 返回 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| POST | `/open/netaclaw/session/list` | `{userId?}` | 会话列表 |
|
||||||
|
| POST | `/open/netaclaw/session/messages` | `{sessionId}` | 消息历史 |
|
||||||
|
| POST | `/open/netaclaw/session/delete` | `{sessionId}` | 成功/失败 |
|
||||||
|
| POST | `/open/netaclaw/session/deleteAll` | `{userId?}` | 成功/失败 |
|
||||||
|
|
||||||
|
### 4.4 WebSocket Gateway 增强
|
||||||
|
|
||||||
|
文件:`netaclaw/gateway/server.ts`
|
||||||
|
|
||||||
|
**客户端 → 服务端:**
|
||||||
|
```typescript
|
||||||
|
// chat 事件增加 agentId
|
||||||
|
{ type: 'chat', sessionId?: string, content: string, agentId?: number }
|
||||||
|
```
|
||||||
|
|
||||||
|
**服务端 → 客户端(新增事件):**
|
||||||
|
```typescript
|
||||||
|
{ type: 'skill_start', sessionId, name, label }
|
||||||
|
{ type: 'skill_end', sessionId, name, status, result?, tokens? }
|
||||||
|
{ type: 'progress', sessionId, name, step?, detail?, percent? }
|
||||||
|
{ type: 'token_update', sessionId, input, output, total, apiCalls }
|
||||||
|
```
|
||||||
|
|
||||||
|
**agentId 处理流程:**
|
||||||
|
1. 收到 chat 事件时,如果有 agentId,从 `netaclaw_agent` 表读取配置
|
||||||
|
2. 用 Agent 的 modelConfig 初始化 LLM 提供商
|
||||||
|
3. 用 Agent 的 systemPrompt + skills 构建系统提示
|
||||||
|
4. 调用 `runAgent()` 执行
|
||||||
|
|
||||||
|
## 5. 前端迁移设计
|
||||||
|
|
||||||
|
### 5.1 Store 改造 (`store/chat.ts`)
|
||||||
|
|
||||||
|
**API 路径替换:**
|
||||||
|
|
||||||
|
| 旧路径 | 新路径 |
|
||||||
|
|--------|--------|
|
||||||
|
| `POST /open/agent/info/list` | `POST /open/netaclaw/agent/list` |
|
||||||
|
| `POST /open/agent/chat/chat` (SSE) | WebSocket `/netaclaw` (chat 事件) |
|
||||||
|
| `POST /open/agent/chat/messages` | `POST /open/netaclaw/session/messages` |
|
||||||
|
| `POST /open/agent/chat/sessions` | `POST /open/netaclaw/session/list` |
|
||||||
|
| `POST /open/agent/chat/deleteSession` | `POST /open/netaclaw/session/delete` |
|
||||||
|
| `POST /open/agent/chat/deleteAllSessions` | `POST /open/netaclaw/session/deleteAll` |
|
||||||
|
| `POST /open/agent/chat/contextTokens` | 移除(WS token_update 事件替代) |
|
||||||
|
| `POST /open/agent/chat/status` | 移除(WS 连接状态替代) |
|
||||||
|
| `POST /open/agent/chat/reconnect` | 移除(WS 自动重连替代) |
|
||||||
|
|
||||||
|
**通信方式改造:**
|
||||||
|
- 移除 SSE(fetch stream + EventSource)逻辑
|
||||||
|
- 新增 WebSocket 连接管理:
|
||||||
|
- 连接:页面加载时建立 WS 连接到 `ws://localhost:8003/netaclaw`
|
||||||
|
- 心跳:定时发送 ping,收到 pong 确认
|
||||||
|
- 断线重连:连接断开后自动重连,指数退避
|
||||||
|
- 发送消息:通过 WS 发送 `{type: 'chat', sessionId, content, agentId}`
|
||||||
|
- WS 事件映射到 store 状态:
|
||||||
|
- `token` → 追加 assistant 消息内容
|
||||||
|
- `thinking` → 追加思考内容
|
||||||
|
- `tool_call` → 记录工具调用
|
||||||
|
- `tool_result` → 记录工具结果
|
||||||
|
- `skill_start` → 添加 skillProgress 条目
|
||||||
|
- `progress` → 更新 skillProgress
|
||||||
|
- `skill_end` → 完成 skillProgress
|
||||||
|
- `token_update` → 更新 token 统计
|
||||||
|
- `done` → 标记完成,更新 sessionId
|
||||||
|
- `error` → 显示错误
|
||||||
|
|
||||||
|
### 5.2 页面改造
|
||||||
|
|
||||||
|
**`agent-list.vue`:**
|
||||||
|
- `/admin/agent/info/page` → `/admin/netaclaw/agent/page`
|
||||||
|
- `/admin/agent/info/update` → `/admin/netaclaw/agent/update`
|
||||||
|
- `/admin/agent/info/delete` → `/admin/netaclaw/agent/delete`
|
||||||
|
|
||||||
|
**`skills.vue`:**
|
||||||
|
- `/admin/agent/skill/metas` → `/admin/netaclaw/skill/metas`
|
||||||
|
- `/admin/agent/skill/setStatus` → `/admin/netaclaw/skill/setStatus`
|
||||||
|
|
||||||
|
**`chat.vue`:**
|
||||||
|
- 移除 SSE 相关代码
|
||||||
|
- 使用 store 中的 WS 连接发送/接收消息
|
||||||
|
- `/admin/agent/info/info` → `/admin/netaclaw/agent/info`
|
||||||
|
- `/admin/base/comm/upload` → 保持不变(通用上传接口)
|
||||||
|
|
||||||
|
**`agent-edit.vue`:**
|
||||||
|
- `/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`
|
||||||
|
|
||||||
|
## 6. 删除清单
|
||||||
|
|
||||||
|
### 6.1 后端删除
|
||||||
|
|
||||||
|
- 旧 agent 模块目录(如果存在 `src/modules/agent/`)
|
||||||
|
- 旧 agent 相关 Entity 文件
|
||||||
|
- 旧 LangChain 相关依赖(已在之前的设计文档中列出)
|
||||||
|
|
||||||
|
### 6.2 数据库删除
|
||||||
|
|
||||||
|
```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;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 前端
|
||||||
|
|
||||||
|
不删除目录,只改内部实现(API 路径和通信方式)。
|
||||||
|
|
||||||
|
## 7. 联调要点
|
||||||
|
|
||||||
|
1. **后端先行**:先完成 Entity + Controller + Service,确保接口可用
|
||||||
|
2. **WS 联调**:用 WebSocket 客户端工具测试 chat 事件的完整流程
|
||||||
|
3. **前端适配**:逐页面替换 API 路径,先改管理页面(简单),再改对话页面(复杂)
|
||||||
|
4. **数据验证**:确保 Agent 创建/编辑/删除、Skill 启用/禁用、会话 CRUD 全部正常
|
||||||
|
5. **流式验证**:确保 WS 的 token/thinking/tool_call/skill_start/done 等事件正确推送和渲染
|
||||||
292
docs/superpowers/specs/2026-04-12-project-management-design.md
Normal file
292
docs/superpowers/specs/2026-04-12-project-management-design.md
Normal file
@ -0,0 +1,292 @@
|
|||||||
|
# 项目管理模块设计文档
|
||||||
|
|
||||||
|
> 日期: 2026-04-12
|
||||||
|
> 位置: 系统管理 → 项目管理
|
||||||
|
> 使用者: 管理员/项目经理
|
||||||
|
|
||||||
|
## 1. 概述
|
||||||
|
|
||||||
|
在系统管理菜单下新增「项目管理」模块,用于管理整个产品的研发与非研发任务进度。支持甘特图、日历、表格列表、Kanban 看板四种视图,覆盖项目 → 阶段 → 任务 → 子任务的四层结构。
|
||||||
|
|
||||||
|
核心场景:电商自动化运营 Agent 平台的全生命周期管理,包含研发任务(前端、后端、AI)和运营任务(官网、隐私条款、市场推广等)。
|
||||||
|
|
||||||
|
## 2. 技术选型
|
||||||
|
|
||||||
|
| 组件 | 方案 | 理由 |
|
||||||
|
|------|------|------|
|
||||||
|
| 甘特图 | dhtmlx-gantt ^8.x (GPL) | 最成熟的甘特图库,原生支持依赖箭头、拖拽、树形层级 |
|
||||||
|
| 日历 | @fullcalendar/vue3 ^6.x | 日历视图事实标准,月/周视图开箱即用 |
|
||||||
|
| 看板拖拽 | vuedraggable ^4.x | Vue 3 拖拽排序,轻量成熟 |
|
||||||
|
| 表格 | cl-crud (已有) | 复用现有 Cool Admin CRUD 组件 |
|
||||||
|
|
||||||
|
安装位置:均安装到 `@neta/frontend`。
|
||||||
|
|
||||||
|
## 3. 数据模型
|
||||||
|
|
||||||
|
### 3.1 project_info(项目表)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| id | int, PK, auto | 主键 |
|
||||||
|
| name | varchar(100) | 项目名称 |
|
||||||
|
| description | text, nullable | 项目描述 |
|
||||||
|
| status | tinyint | 状态:0未开始 1进行中 2已完成 3已归档 |
|
||||||
|
| startDate | date, nullable | 计划开始日期 |
|
||||||
|
| endDate | date, nullable | 计划结束日期 |
|
||||||
|
| progress | int, default 0 | 进度百分比 0-100 |
|
||||||
|
| ownerId | int, nullable | 项目经理(base_sys_user.id) |
|
||||||
|
| ownerName | varchar(50), nullable | 项目经理姓名(冗余) |
|
||||||
|
| color | varchar(20), nullable | 主题色(如 #409EFF) |
|
||||||
|
| createTime | datetime | 创建时间(BaseEntity) |
|
||||||
|
| updateTime | datetime | 更新时间(BaseEntity) |
|
||||||
|
| tenantId | int | 租户ID(BaseEntity) |
|
||||||
|
|
||||||
|
### 3.2 project_phase(阶段表)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| id | int, PK, auto | 主键 |
|
||||||
|
| projectId | int | 所属项目 ID |
|
||||||
|
| name | varchar(100) | 阶段名称 |
|
||||||
|
| type | varchar(50), nullable | 分类(来自系统参数 project_task_category) |
|
||||||
|
| status | tinyint, default 0 | 状态:0未开始 1进行中 2已完成 |
|
||||||
|
| startDate | date, nullable | 开始日期 |
|
||||||
|
| endDate | date, nullable | 结束日期 |
|
||||||
|
| progress | int, default 0 | 进度 0-100 |
|
||||||
|
| sortOrder | int, default 0 | 排序序号 |
|
||||||
|
| createTime | datetime | BaseEntity |
|
||||||
|
| updateTime | datetime | BaseEntity |
|
||||||
|
| tenantId | int | BaseEntity |
|
||||||
|
|
||||||
|
### 3.3 project_task(任务表)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| id | int, PK, auto | 主键 |
|
||||||
|
| projectId | int | 所属项目 ID |
|
||||||
|
| phaseId | int, nullable | 所属阶段 ID |
|
||||||
|
| parentId | int, nullable | 父任务 ID(null 为顶级任务) |
|
||||||
|
| name | varchar(200) | 任务名称 |
|
||||||
|
| description | text, nullable | 任务描述 |
|
||||||
|
| status | tinyint, default 0 | 0待办 1进行中 2已完成 3已关闭 |
|
||||||
|
| priority | tinyint, default 2 | P0(0)紧急 P1(1)高 P2(2)中 P3(3)低 |
|
||||||
|
| category | varchar(50), nullable | 分类(来自系统参数) |
|
||||||
|
| assigneeId | int, nullable | 负责人 ID |
|
||||||
|
| assigneeName | varchar(50), nullable | 负责人姓名(冗余) |
|
||||||
|
| startDate | date, nullable | 计划开始日期 |
|
||||||
|
| endDate | date, nullable | 计划结束日期 |
|
||||||
|
| estimatedHours | decimal(8,1), default 0 | 预估工时(小时) |
|
||||||
|
| actualHours | decimal(8,1), default 0 | 实际工时(由 time_log 汇总) |
|
||||||
|
| progress | int, default 0 | 进度 0-100 |
|
||||||
|
| sortOrder | int, default 0 | 排序序号 |
|
||||||
|
| color | varchar(20), nullable | 自定义颜色(覆盖项目色) |
|
||||||
|
| createTime | datetime | BaseEntity |
|
||||||
|
| updateTime | datetime | BaseEntity |
|
||||||
|
| tenantId | int | BaseEntity |
|
||||||
|
|
||||||
|
### 3.4 project_task_dependency(任务依赖表)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| id | int, PK, auto | 主键 |
|
||||||
|
| taskId | int | 当前任务 ID |
|
||||||
|
| dependsOnTaskId | int | 前置任务 ID |
|
||||||
|
| type | tinyint, default 0 | 0:FS(完成后开始) 1:SS 2:FF 3:SF |
|
||||||
|
| createTime | datetime | BaseEntity |
|
||||||
|
| updateTime | datetime | BaseEntity |
|
||||||
|
| tenantId | int | BaseEntity |
|
||||||
|
|
||||||
|
### 3.5 project_time_log(工时记录表)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| id | int, PK, auto | 主键 |
|
||||||
|
| taskId | int | 所属任务 ID |
|
||||||
|
| userId | int | 记录人 ID |
|
||||||
|
| userName | varchar(50) | 记录人姓名(冗余) |
|
||||||
|
| logDate | date | 工作日期 |
|
||||||
|
| hours | decimal(5,1) | 工时(小时) |
|
||||||
|
| description | varchar(500), nullable | 工作内容描述 |
|
||||||
|
| createTime | datetime | BaseEntity |
|
||||||
|
| updateTime | datetime | BaseEntity |
|
||||||
|
| tenantId | int | BaseEntity |
|
||||||
|
|
||||||
|
## 4. 后端模块结构
|
||||||
|
|
||||||
|
```
|
||||||
|
packages/backend/src/modules/project/
|
||||||
|
├── config.ts
|
||||||
|
├── entity/
|
||||||
|
│ ├── info.ts -- ProjectInfoEntity
|
||||||
|
│ ├── phase.ts -- ProjectPhaseEntity
|
||||||
|
│ ├── task.ts -- ProjectTaskEntity
|
||||||
|
│ ├── task_dependency.ts -- ProjectTaskDependencyEntity
|
||||||
|
│ └── time_log.ts -- ProjectTimeLogEntity
|
||||||
|
├── controller/admin/
|
||||||
|
│ ├── info.ts -- 项目 CRUD
|
||||||
|
│ ├── phase.ts -- 阶段 CRUD
|
||||||
|
│ ├── task.ts -- 任务 CRUD + 树形查询
|
||||||
|
│ ├── task_dependency.ts -- 依赖关系管理
|
||||||
|
│ └── time_log.ts -- 工时记录 CRUD
|
||||||
|
└── service/
|
||||||
|
├── info.ts -- 项目统计、进度汇总
|
||||||
|
├── task.ts -- 任务树构建、状态流转、进度计算
|
||||||
|
└── gantt.ts -- 甘特图聚合接口
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.1 核心 API
|
||||||
|
|
||||||
|
| 路径 | 方法 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| `/admin/project/info/page` | GET | 项目列表分页 |
|
||||||
|
| `/admin/project/info/add` | POST | 创建项目 |
|
||||||
|
| `/admin/project/info/update` | POST | 更新项目 |
|
||||||
|
| `/admin/project/info/delete` | POST | 删除项目 |
|
||||||
|
| `/admin/project/phase/list` | GET | 某项目的阶段列表 |
|
||||||
|
| `/admin/project/phase/add` | POST | 创建阶段 |
|
||||||
|
| `/admin/project/phase/update` | POST | 更新阶段 |
|
||||||
|
| `/admin/project/phase/delete` | POST | 删除阶段 |
|
||||||
|
| `/admin/project/task/tree` | GET | 任务树(含子任务层级) |
|
||||||
|
| `/admin/project/task/page` | GET | 任务分页列表(表格视图) |
|
||||||
|
| `/admin/project/task/add` | POST | 创建任务 |
|
||||||
|
| `/admin/project/task/update` | POST | 更新任务 |
|
||||||
|
| `/admin/project/task/delete` | POST | 删除任务 |
|
||||||
|
| `/admin/project/task/ganttData` | GET | 甘特图数据(任务+阶段+依赖) |
|
||||||
|
| `/admin/project/task/ganttUpdate` | POST | 甘特图拖拽批量更新 |
|
||||||
|
| `/admin/project/task/kanban` | GET | 看板数据(按状态分组) |
|
||||||
|
| `/admin/project/task/kanbanSort` | POST | 看板拖拽排序/状态变更 |
|
||||||
|
| `/admin/project/timeLog/page` | GET | 工时记录分页 |
|
||||||
|
| `/admin/project/timeLog/add` | POST | 添加工时 |
|
||||||
|
| `/admin/project/timeLog/delete` | POST | 删除工时 |
|
||||||
|
| `/admin/project/taskDependency/add` | POST | 添加依赖 |
|
||||||
|
| `/admin/project/taskDependency/delete` | POST | 删除依赖 |
|
||||||
|
|
||||||
|
### 4.2 甘特图聚合接口 ganttData
|
||||||
|
|
||||||
|
请求:`GET /admin/project/task/ganttData?projectId=1`
|
||||||
|
|
||||||
|
响应格式(适配 DHTMLX Gantt):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{ "id": "p_1", "text": "阶段一", "start_date": "2026-04-01", "end_date": "2026-05-01", "progress": 0.5, "type": "project", "open": true },
|
||||||
|
{ "id": "t_1", "text": "官网设计", "start_date": "2026-04-01", "end_date": "2026-04-15", "progress": 0.3, "parent": "p_1", "priority": 1, "assignee": "张三" },
|
||||||
|
{ "id": "t_2", "text": "设计首页", "start_date": "2026-04-01", "end_date": "2026-04-07", "progress": 0, "parent": "t_1" }
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{ "id": 1, "source": "t_1", "target": "t_3", "type": "0" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 前端模块结构
|
||||||
|
|
||||||
|
```
|
||||||
|
packages/frontend/src/modules/project/
|
||||||
|
├── views/
|
||||||
|
│ ├── list.vue -- 项目列表页(入口)
|
||||||
|
│ ├── detail.vue -- 项目详情页(四视图容器)
|
||||||
|
│ └── components/
|
||||||
|
│ ├── gantt.vue -- 甘特图视图
|
||||||
|
│ ├── calendar.vue -- 日历视图
|
||||||
|
│ ├── table.vue -- 表格列表视图
|
||||||
|
│ ├── kanban.vue -- 看板视图
|
||||||
|
│ ├── task-drawer.vue -- 任务详情抽屉
|
||||||
|
│ ├── time-log-dialog.vue -- 工时记录弹窗
|
||||||
|
│ └── phase-manager.vue -- 阶段管理弹窗
|
||||||
|
└── config.ts -- 模块菜单配置
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.1 页面流程
|
||||||
|
|
||||||
|
```
|
||||||
|
系统管理 → 项目管理(list.vue)
|
||||||
|
├── 项目卡片列表:名称、进度条、状态、负责人、日期范围
|
||||||
|
├── 新建项目按钮
|
||||||
|
└── 点击项目 → detail.vue
|
||||||
|
├── 顶部:项目信息栏(名称、进度条、日期范围、负责人、阶段管理按钮)
|
||||||
|
└── Tab 切换:甘特图 | 日历 | 列表 | 看板
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 四种视图
|
||||||
|
|
||||||
|
**甘特图(gantt.vue):**
|
||||||
|
- DHTMLX Gantt 渲染
|
||||||
|
- 左侧:树形任务列表(阶段 → 任务 → 子任务)
|
||||||
|
- 右侧:时间轴条形图
|
||||||
|
- 任务间依赖箭头连线
|
||||||
|
- 拖拽调整开始/结束日期,拖拽完自动调用 ganttUpdate
|
||||||
|
- 双击任务 → 打开 task-drawer
|
||||||
|
|
||||||
|
**日历(calendar.vue):**
|
||||||
|
- FullCalendar 月/周切换
|
||||||
|
- 任务按日期范围显示为色块
|
||||||
|
- 点击日期 → 快速创建任务
|
||||||
|
- 点击任务 → 打开 task-drawer
|
||||||
|
|
||||||
|
**表格列表(table.vue):**
|
||||||
|
- cl-crud 标准表格
|
||||||
|
- 筛选:状态、优先级、分类、负责人、阶段
|
||||||
|
- 排序:优先级、日期、进度
|
||||||
|
- 支持批量操作(删除、修改状态)
|
||||||
|
|
||||||
|
**看板(kanban.vue):**
|
||||||
|
- 四列:待办 | 进行中 | 已完成 | 已关闭
|
||||||
|
- 任务卡片:名称、优先级标签、负责人、截止日期
|
||||||
|
- vuedraggable 拖拽跨列 → 自动更新状态
|
||||||
|
- 卡片点击 → 打开 task-drawer
|
||||||
|
|
||||||
|
### 5.3 任务详情抽屉(task-drawer.vue)
|
||||||
|
|
||||||
|
右侧滑出抽屉,包含:
|
||||||
|
- 基本信息:名称、描述(纯文本 textarea)、状态、优先级、分类
|
||||||
|
- 时间:开始/结束日期选择器、预估工时输入
|
||||||
|
- 负责人:下拉选择系统用户
|
||||||
|
- 子任务列表:展示+快速添加
|
||||||
|
- 依赖关系:前置任务选择器(下拉搜索)
|
||||||
|
- 工时记录 Tab:工时列表 + 添加工时按钮
|
||||||
|
|
||||||
|
### 5.4 数据同步策略
|
||||||
|
|
||||||
|
- 项目详情页用 Pinia store 统一管理当前项目的任务/阶段/依赖数据
|
||||||
|
- 进入详情页时一次性加载(ganttData 接口)
|
||||||
|
- 切换 Tab 不重新请求,共享同一份 store 数据
|
||||||
|
- 任何视图中的修改(拖拽、状态变更、新增/删除)同时更新 store + 调用 API
|
||||||
|
|
||||||
|
## 6. 进度自动计算规则
|
||||||
|
|
||||||
|
- **子任务** → 手动更新进度,或按工时比例(actualHours / estimatedHours * 100)
|
||||||
|
- **任务进度** = 子任务进度加权平均(权重为 estimatedHours,无预估则等权)
|
||||||
|
- **阶段进度** = 下属任务进度加权平均
|
||||||
|
- **项目进度** = 各阶段进度加权平均
|
||||||
|
- 进度计算在后端 service 层实现,每次任务更新时触发向上汇总
|
||||||
|
|
||||||
|
## 7. 权限控制
|
||||||
|
|
||||||
|
- 复用现有 RBAC 体系
|
||||||
|
- 在菜单管理中添加「项目管理」菜单节点(目录 + 两个菜单页)
|
||||||
|
- 权限点:
|
||||||
|
- `project:info:add/update/delete` — 项目增删改
|
||||||
|
- `project:task:add/update/delete` — 任务增删改
|
||||||
|
- `project:timeLog:add/delete` — 工时记录增删
|
||||||
|
- 仅管理员/项目经理角色可见
|
||||||
|
|
||||||
|
## 8. 系统参数配置
|
||||||
|
|
||||||
|
需在「系统管理 → 参数配置」中预置:
|
||||||
|
|
||||||
|
| 参数键 | 说明 | 默认值示例 |
|
||||||
|
|--------|------|-----------|
|
||||||
|
| `project_task_category` | 任务分类选项 | 前端,后端,AI,设计,运营,法务,市场 |
|
||||||
|
| `project_task_priority` | 优先级选项(可选,也可硬编码) | P0紧急,P1高,P2中,P3低 |
|
||||||
|
|
||||||
|
## 9. 明确不做的事
|
||||||
|
|
||||||
|
- 通知/提醒系统
|
||||||
|
- 文件附件上传
|
||||||
|
- 评论/讨论功能
|
||||||
|
- 自动排期算法(关键路径法)
|
||||||
|
- 多项目资源冲突检测
|
||||||
|
- 移动端适配
|
||||||
|
- 成员自助端(仅管理员使用)
|
||||||
1121
docs/superpowers/specs/2026-04-13-agent-chat-ux-overhaul-design.md
Normal file
1121
docs/superpowers/specs/2026-04-13-agent-chat-ux-overhaul-design.md
Normal file
File diff suppressed because it is too large
Load Diff
386
docs/superpowers/specs/2026-04-13-skill-evolution-design.md
Normal file
386
docs/superpowers/specs/2026-04-13-skill-evolution-design.md
Normal file
@ -0,0 +1,386 @@
|
|||||||
|
# Skill 自进化系统设计
|
||||||
|
|
||||||
|
> 日期: 2026-04-13
|
||||||
|
> 模块: netaclaw/skill-evolution
|
||||||
|
> 状态: 设计阶段
|
||||||
|
> 依赖: P0 长期记忆系统(共享 background review 模式)
|
||||||
|
|
||||||
|
## 1. 目标
|
||||||
|
|
||||||
|
为 NetaClaw Agent 添加 Skill 自进化能力:Agent 在对话中积累经验后,后台 review agent 自动提炼可复用的方法论为 Skill,或更新已有 Skill。同时提供 `skill_manage` 工具允许 Agent 在对话中主动创建/编辑 Skill。所有 Agent 创建的 Skill 经过安全扫描后才写入文件系统。
|
||||||
|
|
||||||
|
## 2. 核心决策
|
||||||
|
|
||||||
|
| 决策项 | 选择 | 理由 |
|
||||||
|
|--------|------|------|
|
||||||
|
| Review 触发方式 | 迭代计数(默认每 10 轮工具调用) | Hermes 验证过的模式,平衡频率和成本 |
|
||||||
|
| Review Agent 模型 | 复用主 Agent 同模型 | 保证提炼质量 |
|
||||||
|
| 安全校验 | 完整正则扫描(~84 条规则) | Agent 创建内容需要严格审查 |
|
||||||
|
| Skill 存储 | 文件系统 + DB 元数据 | 与现有架构一致 |
|
||||||
|
| Review 执行方式 | 异步不阻塞主对话 | 不影响用户体验 |
|
||||||
|
|
||||||
|
## 3. 系统架构
|
||||||
|
|
||||||
|
```
|
||||||
|
[Agent 对话循环 (attempt.ts)]
|
||||||
|
↓ toolCallCount 累计 >= N
|
||||||
|
[chat.ts 检测触发条件]
|
||||||
|
↓ 异步(不阻塞响应)
|
||||||
|
[spawnSkillReview()] → 独立 runAgent() 调用
|
||||||
|
↓ review agent 拥有 skill_manage + skill_list 工具
|
||||||
|
[skill_manage 工具]
|
||||||
|
↓ 写入前
|
||||||
|
[SkillGuard 安全扫描]
|
||||||
|
↓ 通过 → 原子写入
|
||||||
|
[文件系统 skills/{name}/SKILL.md] + [DB netaclaw_skill]
|
||||||
|
↓ 写入后
|
||||||
|
[SkillLoader.reloadSkill()] → 热更新内存缓存
|
||||||
|
↓ 下次对话
|
||||||
|
[system prompt 自动包含新 skill]
|
||||||
|
```
|
||||||
|
|
||||||
|
关键约束:
|
||||||
|
- Review agent 是一次性的:spawn → 分析 → 写入 → 结束
|
||||||
|
- Review agent 禁用自身的 nudge interval(防止递归 review)
|
||||||
|
- Review agent 最多 8 轮工具调用
|
||||||
|
- Skill 写入是原子的:先写临时文件,扫描通过后 rename
|
||||||
|
- Review agent 不产生用户可见输出
|
||||||
|
|
||||||
|
## 4. Skill Manager 工具
|
||||||
|
|
||||||
|
### 4.1 skill_manage 工具
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// tools/builtin/skill_manage.ts
|
||||||
|
interface SkillManageParams {
|
||||||
|
action: 'create' | 'edit' | 'patch' | 'delete' | 'write_file' | 'remove_file';
|
||||||
|
name: string; // skill 名称
|
||||||
|
content?: string; // SKILL.md 完整内容(create/edit)
|
||||||
|
old_string?: string; // patch: 要替换的文本
|
||||||
|
new_string?: string; // patch: 替换后的文本
|
||||||
|
replace_all?: boolean; // patch: 替换所有匹配
|
||||||
|
category?: string; // 可选分类目录
|
||||||
|
file_path?: string; // 支持文件路径(write_file/remove_file)
|
||||||
|
file_content?: string; // 支持文件内容(write_file)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| action | 用途 | 必需参数 |
|
||||||
|
|--------|------|---------|
|
||||||
|
| create | 创建新 skill | name, content |
|
||||||
|
| edit | 完整重写 SKILL.md | name, content |
|
||||||
|
| patch | 局部替换 | name, old_string, new_string |
|
||||||
|
| delete | 删除 skill 目录 | name |
|
||||||
|
| write_file | 写入支持文件 | name, file_path, file_content |
|
||||||
|
| remove_file | 删除支持文件 | name, file_path |
|
||||||
|
|
||||||
|
### 4.2 skill_list 工具
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 只读工具,返回已有 skill 列表
|
||||||
|
interface SkillListParams {
|
||||||
|
category?: string; // 可选:按分类过滤
|
||||||
|
}
|
||||||
|
// 返回: [{ name, description, category }]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 验证规则
|
||||||
|
|
||||||
|
| 规则 | 限制 |
|
||||||
|
|------|------|
|
||||||
|
| name | 最长 64 字符,`/^[a-z0-9][a-z0-9._-]*$/` |
|
||||||
|
| category | 单层目录,同 name 命名规则 |
|
||||||
|
| SKILL.md content | 最大 100,000 字符 |
|
||||||
|
| 支持文件 | 最大 1MB/文件 |
|
||||||
|
| 支持文件目录 | 仅 `references/`, `templates/`, `scripts/`, `assets/` |
|
||||||
|
| frontmatter | 必须包含 `name` 和 `description` 字段 |
|
||||||
|
| description | 最长 1024 字符 |
|
||||||
|
| 总文件数 | 每个 skill 最多 50 个文件 |
|
||||||
|
| 总大小 | 每个 skill 最大 1MB |
|
||||||
|
|
||||||
|
### 4.4 原子写入流程
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 验证参数 → 失败则返回错误
|
||||||
|
2. 写入临时文件 skills/{name}/.tmp_SKILL.md
|
||||||
|
3. 调用 SkillGuard.scan() 扫描临时文件
|
||||||
|
4. 如果 verdict === 'dangerous' → 删除临时文件,返回错误
|
||||||
|
5. rename 临时文件 → SKILL.md
|
||||||
|
6. 同步 DB 元数据(netaclaw_skill 表)
|
||||||
|
7. 调用 SkillLoader.reloadSkill(name) 热更新缓存
|
||||||
|
8. 清除 skill prompt 缓存
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Skill Guard 安全扫描
|
||||||
|
|
||||||
|
### 5.1 威胁模式分类
|
||||||
|
|
||||||
|
参考 Hermes skills_guard.py 的 84 条正则规则,按类别组织:
|
||||||
|
|
||||||
|
| 类别 | 规则数 | severity | 示例模式 |
|
||||||
|
|------|--------|----------|---------|
|
||||||
|
| 数据泄露 (exfiltration) | 18 | critical | `process\.env`, `\.ssh/`, `credentials`, DNS exfil |
|
||||||
|
| 提示注入 (prompt_injection) | 16 | critical | `ignore.*instructions`, `you are now`, `system prompt` |
|
||||||
|
| 破坏性操作 (destructive) | 8 | critical | `rm\s+-rf\s+/`, `mkfs`, `dd\s+if=`, `chmod\s+777` |
|
||||||
|
| 持久化 (persistence) | 10 | high | `crontab`, `authorized_keys`, `systemd`, `sudoers` |
|
||||||
|
| 网络 (network) | 9 | high | reverse shells, tunnels, hardcoded IPs |
|
||||||
|
| 混淆 (obfuscation) | 14 | high | `eval\(`, `Buffer\.from.*base64`, hex encoding |
|
||||||
|
| 代码执行 (execution) | 6 | high | `child_process`, `exec\(`, `spawn\(` |
|
||||||
|
| 路径穿越 (path_traversal) | 5 | high | `\.\.\/`, `/etc/passwd`, `/proc/` |
|
||||||
|
| 加密挖矿 (crypto_mining) | 2 | critical | `stratum+tcp`, `xmrig` |
|
||||||
|
| 供应链 (supply_chain) | 6 | critical | `curl.*\|.*bash`, unpinned deps |
|
||||||
|
| 权限提升 (privilege_escalation) | 5 | high | `sudo`, `setuid`, `NOPASSWD` |
|
||||||
|
| 凭证暴露 (credential_exposure) | 5 | critical | hardcoded secrets, private keys |
|
||||||
|
|
||||||
|
### 5.2 扫描接口
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// skill_evolution/guard.ts
|
||||||
|
interface ScanFinding {
|
||||||
|
category: string;
|
||||||
|
severity: 'critical' | 'high' | 'medium';
|
||||||
|
pattern: string;
|
||||||
|
match: string;
|
||||||
|
file: string;
|
||||||
|
line: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScanResult {
|
||||||
|
verdict: 'safe' | 'caution' | 'dangerous';
|
||||||
|
findings: ScanFinding[];
|
||||||
|
summary: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scanSkill(skillPath: string): ScanResult;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 判定逻辑
|
||||||
|
|
||||||
|
```
|
||||||
|
findings 中有 critical → verdict = 'dangerous'
|
||||||
|
findings 中有 high → verdict = 'caution'
|
||||||
|
无 findings → verdict = 'safe'
|
||||||
|
```
|
||||||
|
|
||||||
|
Agent 创建的 skill 策略:
|
||||||
|
- safe → 允许
|
||||||
|
- caution → 允许(记录日志)
|
||||||
|
- dangerous → 阻止,回滚,返回错误信息给 agent
|
||||||
|
|
||||||
|
### 5.4 二进制文件拦截
|
||||||
|
|
||||||
|
禁止的文件扩展名:`.exe`, `.dll`, `.so`, `.dylib`, `.bin`, `.dat`, `.com`, `.msi`, `.dmg`, `.app`, `.deb`, `.rpm`
|
||||||
|
|
||||||
|
## 6. Background Review 机制
|
||||||
|
|
||||||
|
### 6.1 触发条件
|
||||||
|
|
||||||
|
在 `controller/chat.ts` 中,`runAgent()` 返回后:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 从 Agent 配置读取
|
||||||
|
const evoConfig = agentEntity?.config?.skillEvolution;
|
||||||
|
const skillEvolutionEnabled = evoConfig?.enabled ?? false;
|
||||||
|
const nudgeInterval = evoConfig?.nudgeInterval ?? 10;
|
||||||
|
|
||||||
|
// 累计工具调用计数(跨同一会话的多次请求)
|
||||||
|
const totalCalls = (sessionMeta.toolCallsSinceReview ?? 0) + result.toolCallCount;
|
||||||
|
|
||||||
|
const shouldReview = (
|
||||||
|
skillEvolutionEnabled &&
|
||||||
|
totalCalls >= nudgeInterval &&
|
||||||
|
result.toolCallCount > 0
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldReview) {
|
||||||
|
sessionMeta.toolCallsSinceReview = 0;
|
||||||
|
// 异步触发,不阻塞响应
|
||||||
|
spawnSkillReview({
|
||||||
|
conversationHistory: history,
|
||||||
|
parentAgentConfig: agentConfig,
|
||||||
|
skillLoader: this.skillLoader,
|
||||||
|
linkedSkills: agentEntity?.skills ?? [],
|
||||||
|
allowOptimize: evoConfig?.allowOptimizeLinked ?? true,
|
||||||
|
allowCreateNew: evoConfig?.allowCreateNew ?? true,
|
||||||
|
}).catch(err => logger.warn('Skill review failed:', err));
|
||||||
|
} else {
|
||||||
|
sessionMeta.toolCallsSinceReview = totalCalls;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 Review Agent 配置
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// skill_evolution/review.ts
|
||||||
|
interface SkillReviewContext {
|
||||||
|
conversationHistory: LLMMessage[];
|
||||||
|
parentAgentConfig: AgentConfig;
|
||||||
|
skillLoader: SkillLoaderService;
|
||||||
|
skillRepo: Repository<NetaClawSkillEntity>; // DB 同步用
|
||||||
|
agentRepo: Repository<NetaClawAgentEntity>; // 自动关联新 skill 用
|
||||||
|
agentName: string; // 当前 Agent 名称
|
||||||
|
linkedSkills: string[]; // Agent 已勾选的 skill 名称
|
||||||
|
allowOptimize: boolean;
|
||||||
|
allowCreateNew: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function spawnSkillReview(ctx: SkillReviewContext): Promise<void> {
|
||||||
|
const reviewPrompt = buildSkillReviewPrompt(
|
||||||
|
ctx.linkedSkills, ctx.allowOptimize, ctx.allowCreateNew,
|
||||||
|
);
|
||||||
|
|
||||||
|
const reviewConfig: AgentConfig = {
|
||||||
|
...ctx.parentAgentConfig,
|
||||||
|
name: `${ctx.parentAgentConfig.name}_skill_reviewer`,
|
||||||
|
systemPrompt: SKILL_REVIEW_SYSTEM_PROMPT,
|
||||||
|
maxToolRounds: 8,
|
||||||
|
};
|
||||||
|
|
||||||
|
// skill_manage 工具接收 SkillWriter 实例(非 SkillLoader)
|
||||||
|
const writer = new SkillWriter(ctx.skillLoader.getSkillsDir(), ctx.skillRepo, ctx.skillLoader);
|
||||||
|
const tools = [
|
||||||
|
createSkillManageTool(writer, {
|
||||||
|
allowedEditSkills: ctx.allowOptimize ? ctx.linkedSkills : [],
|
||||||
|
allowCreateNew: ctx.allowCreateNew,
|
||||||
|
}),
|
||||||
|
createSkillListTool(ctx.skillLoader),
|
||||||
|
];
|
||||||
|
|
||||||
|
await runAgent({
|
||||||
|
agentConfig: reviewConfig,
|
||||||
|
tools,
|
||||||
|
userMessage: reviewPrompt,
|
||||||
|
history: ctx.conversationHistory,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 Review Prompt
|
||||||
|
|
||||||
|
Review prompt 是动态生成的,根据 Agent 的 skillEvolution 配置和已勾选 skill 列表拼接:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function buildSkillReviewPrompt(
|
||||||
|
linkedSkills: string[], // Agent 已勾选的 skill 名称列表
|
||||||
|
allowOptimize: boolean,
|
||||||
|
allowCreateNew: boolean,
|
||||||
|
): string {
|
||||||
|
let prompt = '回顾上面的对话,考虑是否需要保存或更新 skill。\n\n';
|
||||||
|
prompt += '关注:\n';
|
||||||
|
prompt += '1. 是否使用了非显而易见的方法来完成任务?\n';
|
||||||
|
prompt += '2. 是否经历了试错或因实际发现而改变了方案?\n';
|
||||||
|
prompt += '3. 用户是否期望或希望不同的方法或结果?\n\n';
|
||||||
|
|
||||||
|
if (allowOptimize && linkedSkills.length > 0) {
|
||||||
|
prompt += `当前 Agent 已关联以下 skill:${linkedSkills.join(', ')}\n`;
|
||||||
|
prompt += '请优先检查这些已有 skill 是否可以根据本次对话的经验进行优化(用 patch 更新)。\n\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowCreateNew) {
|
||||||
|
prompt += '如果发现了全新的可复用方法论且没有相关 skill,请创建新 skill。\n\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt += '操作指南:\n';
|
||||||
|
prompt += '- 先用 skill_list 查看已有 skill,避免重复创建\n';
|
||||||
|
prompt += '- 优化已有 skill 时优先用 patch(局部替换),避免 edit 全量重写\n';
|
||||||
|
prompt += '- Skill 内容应聚焦于可复用的方法论,而非具体的业务数据\n';
|
||||||
|
prompt += '- 如果没有值得保存的内容,直接说"无需保存"并停止\n';
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
### 6.4 Review System Prompt
|
||||||
|
|
||||||
|
```
|
||||||
|
你是一个 Skill 提炼专家。你的任务是分析对话历史,提取可复用的方法论并保存为 Skill。
|
||||||
|
|
||||||
|
Skill 格式要求:
|
||||||
|
- SKILL.md 必须包含 YAML frontmatter(name + description)
|
||||||
|
- 正文用 Markdown,包含:触发条件、工作流程、规则约束
|
||||||
|
- name 使用小写字母、数字、连字符(如 nginx-deploy-config)
|
||||||
|
- description 一句话描述 skill 的用途
|
||||||
|
|
||||||
|
你只有 8 轮工具调用机会,请高效行动。
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Agent 配置扩展
|
||||||
|
|
||||||
|
### 7.1 AgentEntity.config 扩展
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface AgentConfig {
|
||||||
|
// ...现有字段
|
||||||
|
skillEvolution?: {
|
||||||
|
enabled: boolean;
|
||||||
|
nudgeInterval?: number; // 触发 review 的工具调用间隔,默认 10
|
||||||
|
allowOptimizeLinked?: boolean; // 是否允许优化已勾选的 skill,默认 true
|
||||||
|
allowCreateNew?: boolean; // 是否允许创建全新 skill,默认 true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 Skill 进化范围
|
||||||
|
|
||||||
|
Agent 的 skill 进化有两种模式,均可在 Agent 编辑页面独立开关:
|
||||||
|
|
||||||
|
**模式 1:优化已勾选 Skill(allowOptimizeLinked)**
|
||||||
|
- Agent 配置中已关联的 skill(`agent.skills[]` 列表)可以被 review agent 通过 patch/edit 更新
|
||||||
|
- 适用场景:电商运营 Agent 反复执行"上架商品"skill,在实践中发现更好的标题写法或定价策略,自动优化该 skill
|
||||||
|
- Review agent 在分析对话时,优先检查已勾选 skill 是否有改进空间
|
||||||
|
|
||||||
|
**模式 2:创建全新 Skill(allowCreateNew)**
|
||||||
|
- Review agent 可以发现对话中的新方法论并创建全新 skill
|
||||||
|
- 新创建的 skill 自动关联到当前 Agent(加入 `agent.skills[]`)
|
||||||
|
- 适用场景:投流 Agent 在实践中摸索出一套新的出价策略,提炼为独立 skill
|
||||||
|
|
||||||
|
### 7.3 多 Agent 协作场景考虑
|
||||||
|
|
||||||
|
电商运营系统中多个 Agent 各司其职(上架、下架、投流、产品管理等),skill 进化需要考虑:
|
||||||
|
|
||||||
|
- **Skill 共享**:一个 Agent 优化的 skill 如果被其他 Agent 也勾选了,优化会自动生效(因为 skill 存在文件系统,所有 Agent 共享同一份)
|
||||||
|
- **写入冲突**:多个 Agent 同时优化同一个 skill 时,用文件锁(lockfile)保证原子性,后写入的 patch 基于最新内容
|
||||||
|
- **进化隔离**:如果某个 Agent 的场景特殊,不希望它的优化影响其他 Agent,可以关闭 `allowOptimizeLinked`,只允许 `allowCreateNew`(新 skill 只关联到自己)
|
||||||
|
|
||||||
|
### 7.4 Agent 编辑页面配置
|
||||||
|
|
||||||
|
在 Agent 编辑页面新增"Skill 进化"配置区域:
|
||||||
|
- 总开关:启用/禁用 Skill 进化
|
||||||
|
- 子开关:允许优化已勾选 Skill(默认开)
|
||||||
|
- 子开关:允许创建新 Skill(默认开)
|
||||||
|
- 数字输入:Review 触发间隔(高级选项,默认 10)
|
||||||
|
|
||||||
|
### 7.5 SkillLoader 扩展
|
||||||
|
|
||||||
|
新增方法:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 热更新单个 skill(skill_manage 写入后调用)
|
||||||
|
async reloadSkill(name: string): Promise<void>;
|
||||||
|
|
||||||
|
// 获取 skill 列表摘要(给 review agent 用)
|
||||||
|
getSkillSummaries(): { name: string; description: string; category?: string }[];
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. 新增文件清单
|
||||||
|
|
||||||
|
```
|
||||||
|
src/modules/netaclaw/
|
||||||
|
├── skill_evolution/
|
||||||
|
│ ├── guard.ts # SkillGuard 安全扫描(正则模式匹配)
|
||||||
|
│ ├── guard_patterns.ts # 威胁模式定义(84 条规则)
|
||||||
|
│ ├── review.ts # spawnSkillReview() 后台 review 逻辑
|
||||||
|
│ └── skill_writer.ts # 原子写入 + 验证 + DB 同步
|
||||||
|
├── tools/builtin/
|
||||||
|
│ ├── skill_manage.ts # skill_manage 工具
|
||||||
|
│ └── skill_list.ts # skill_list 工具
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. 修改文件清单
|
||||||
|
|
||||||
|
| 文件 | 修改内容 |
|
||||||
|
|------|---------|
|
||||||
|
| `controller/chat.ts` | runAgent 返回后检测触发条件,异步调用 spawnSkillReview,通过 sessionRepo 直接更新 metadata |
|
||||||
|
| `service/skill_loader.ts` | 新增 reloadSkill()、getSkillSummaries()、getSkillsDir() 方法 |
|
||||||
|
|
||||||
|
## 10. 依赖
|
||||||
|
|
||||||
|
无新增外部依赖。安全扫描用纯正则实现,skill 文件读写用 Node.js fs 模块。
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user