初始化提交

This commit is contained in:
陈二狗 2026-05-20 21:39:12 +08:00
commit cc660d19d1
10092 changed files with 1374415 additions and 0 deletions

8
.env.example Normal file
View 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
View 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
View 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
View 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 PlusAI 引擎采用自研 NetaClawReAct 循环 + 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
View 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
View 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 报告中标记需要用户确认的内容

View 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

View 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]]

View 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]] — 前端架构

View 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]]

View 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 编排上下文来源

View 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 循环
ReActReasoning + 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]] — 流式推送通道

View 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]]

View 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 供给稳定形态”一起纳入运行时模型。

View 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]]

View 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 协议

View 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 routeOperations 层只负责“被允许执行后,具体由哪个后端完成 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 看到的工具 schemaprompt 和参数兼容旧工具。
- Operations 不负责 Session Tree 写入;工具结果仍由 [[agent-runtime]] / [[session-tree-runtime]] 记录。
- Operations 不包含 `memory_*``delegate_*``clarify``todo` 等不直接做文件或进程操作的工具。
## 相关页面
- [[tool-system]]
- [[tool-governance]]
- [[tool-runtime-policy]]
- [[subagent-session]]
- [[agent-runtime]]

View 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]]

View 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 模块下

View 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 双 Agentreply agent 委托 desktop agent 调 [[desktop-op-module]] 发送 |
## ClawBot 私聊流程
```
创建渠道 → 绑定 Agent
→ createWeixinQr() 生成二维码
→ 用户扫码 → pollWeixinQr() 轮询状态
→ 状态流转wait → scaned_but_redirect → confirmed
→ 获取 credentialaccountId, 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 agentreply agent 用 `delegate_task` 把发送动作委托给 desktop agentdesktop agent 调用 [[tool-system]] 的 `weixin_send_text`,再由 [[desktop-op-module]] 操作 PC 微信窗口。
## Clarify 纯文本降级
微信渠道不支持 WebSocket 交互,[[clarify-tool]] 采用纯文本 + 数字映射方案:
1. 构造文本消息:`❓ 问题\n1. 选项1\n2. 选项2\n请回复数字或直接输入`
2. 通过微信 API 发送,存入 `pendingClarify` Mapkey: `${channelId}:${senderId}`
3. 用户回复数字 → 映射到 `choices[num-1]`;回复文本 → 直接使用
4. 解决 PromiseAgent 继续执行
## 相关页面
- [[netaclaw-module]] — 所属模块
- [[agent-runtime]] — Agent 执行器
- [[clarify-tool]] — Clarify 澄清工具(微信降级实现)
- [[desktop-op-module]] — weixin-db 自动回复的桌面发送执行后端
- [[tool-system]] — `weixin_send_text``delegate_task` 工具链路
- [[websocket-gateway]] — 前端管理页面通过 HTTP API 操作

View 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 边界稳定输出的一部分契约。

View 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]] — 数据源管理菜单和配置页

View 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` | 生成 requestIdPromise 阻塞,转发 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. 解决 PromiseAgent 继续
## 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]] — 工具系统总览

View 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 代理

View 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` | 集群 APICRUD + saveCanvas/publish |
| `controller/admin/crew_trigger.ts` | 触发 APIstart/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

View 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 表速查

View 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]]

View 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]]

View 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]]

View 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 编排中的模型配置解析

View 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 为空或 `*` 时走 listsearch 失败时 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` 管理入口

View 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]]

View 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]]

View 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`JSONapiUrl/apiKey/modelId/contextWindow
- `config`JSONmemory/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 / allprefix 仅兼容)
- `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`JSONallowedTables、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 操作运行时

View 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 运行时执行

View 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 个业务表

View 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 唯一性校验、群聊管理和微信自动回复配置区块。

View 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 APICRUD + 安装/卸载/更新/上传/检查更新 |
## 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

View 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` 会长期保存完整过程。

View 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';
}
```
## TodoStoreruntime/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 事件推送

View 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]] — 所属模块

View 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]] 看成工具执行后端维度:治理层决定某个工具是否进入 runtimeOperations 决定这个工具执行时用本地、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]]

View 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]]

View 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]]

View 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
View 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
View 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、前端架构等摘要。

View File

@ -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 的长期知识条目。

View 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` 一期版本
- 满足内部使用的安装或启动方式说明
- 第一期功能清单
- 第一期验收清单和验收场景
- 基础使用说明
- 上线问题跟踪与修复记录

View 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 '关联审核订单IDaudit_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` = '管理');

View 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 全部通过

View 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 替代)

View File

@ -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\"}"
}
]
}
]
}

View File

@ -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}
```
````

File diff suppressed because it is too large Load Diff

View 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 ngramSqliteMemoryProvider 用 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"
```

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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): 端到端验证修复"
```

File diff suppressed because it is too large Load Diff

View 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"
```

View 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 进行文件编辑(但不加 clarifyCrew 无用户交互通道)。
```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"
```

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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 entryentry 通过 `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` entrytool 状态作为 `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。

View File

@ -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 设计系统
这些子系统不应塞进同一个实施计划。每个子系统都需要单独计划、单独测试、单独提交。
## 计划 1Agent 运行时内核
**计划文件:** `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 事件可以流式推送到前端,并持久化用于回放。
## 计划 4Skill / 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`
**依赖:** 计划 1subagent 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`

View 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
```
预期PASS2 个测试文件全部通过。
- [ ] **步骤 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` 或数据目录。

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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 messagescontent 保持纯净。前端附件功能拆分为 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_recognizeresolve 阶段排除未配置工具)
**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_recognizeresolve 阶段排除未配置)`
---
### 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 inputemit `@select(files: File[])`
- [ ] **Step 2:** 创建 ChatAttachmentPreview.vue — 横向滚动预览条,缩略图/文件图标/删除/首尾帧标记/上传进度
- [ ] **Step 3:** 改造 ChatComposer.vue
- 集成 ChatAttachmentButtontextarea 左侧)和 ChatAttachmentPreviewtextarea 上方)
- 支持拖拽(@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): 消息气泡附件展示`

View 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"
```

View 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"
```

View 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"
```

View 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"
```

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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 SchemaAgentTool 参数定义)
@anthropic-ai/sdk # Anthropic Claude API
node-systray # 系统托盘图标
open # 自动打开浏览器
pkg # 打包为 .exedevDependency
```

View 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 连接。

View File

@ -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 逻辑

View File

@ -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 自动重连替代) |
**通信方式改造:**
- 移除 SSEfetch 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 等事件正确推送和渲染

View 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 | 租户IDBaseEntity |
### 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 | 父任务 IDnull 为顶级任务) |
| 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. 明确不做的事
- 通知/提醒系统
- 文件附件上传
- 评论/讨论功能
- 自动排期算法(关键路径法)
- 多项目资源冲突检测
- 移动端适配
- 成员自助端(仅管理员使用)

File diff suppressed because it is too large Load Diff

View 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 frontmattername + 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优化已勾选 SkillallowOptimizeLinked**
- Agent 配置中已关联的 skill`agent.skills[]` 列表)可以被 review agent 通过 patch/edit 更新
- 适用场景:电商运营 Agent 反复执行"上架商品"skill在实践中发现更好的标题写法或定价策略自动优化该 skill
- Review agent 在分析对话时,优先检查已勾选 skill 是否有改进空间
**模式 2创建全新 SkillallowCreateNew**
- 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
// 热更新单个 skillskill_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