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

1211 lines
50 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# Skill 自进化系统 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 添加 Skill 自进化能力——后台 review agent 自动提炼/优化 Skillskill_manage 工具支持运行时创建/编辑SkillGuard 安全扫描保护。
**Architecture:** 4 层模块guard_patterns威胁规则定义→ guard扫描引擎→ skill_writer原子写入 + 验证 + DB 同步)→ toolsskill_manage/skill_list 工具。review.ts 编排后台 review agentchat.ts 中迭代计数触发。
**Tech Stack:** TypeScript, Node.js fs, @sinclair/typebox (tool schema), 纯正则安全扫描
**Spec:** `docs/superpowers/specs/2026-04-13-skill-evolution-design.md`
---
## File Structure
```
src/modules/netaclaw/
├── skill_evolution/
│ ├── guard_patterns.ts # 威胁模式定义(~84 条正则规则)
│ ├── guard.ts # scanSkill() 扫描引擎
│ ├── skill_writer.ts # 原子写入 + 验证 + DB 同步 + 文件锁
│ └── review.ts # spawnSkillReview() + buildSkillReviewPrompt()
├── tools/builtin/
│ ├── skill_manage.ts # skill_manage 工具create/edit/patch/delete/write_file/remove_file
│ └── skill_list.ts # skill_list 只读工具
```
Modified files:
- `src/modules/netaclaw/service/skill_loader.ts` — 新增 reloadSkill() + getSkillSummaries()
- `src/modules/netaclaw/controller/chat.ts` — 触发条件检测 + 异步 review 调用
---
### Task 1: Create guard_patterns.ts — threat pattern definitions
**Files:**
- Create: `src/modules/netaclaw/skill_evolution/guard_patterns.ts`
- [ ] **Step 1: Create the threat pattern definitions file**
Create `src/modules/netaclaw/skill_evolution/guard_patterns.ts`:
```typescript
/**
* Skill Guard 威胁模式定义
* 参考 Hermes skills_guard.py84 条正则规则按类别组织
*/
export type Severity = 'critical' | 'high' | 'medium';
export interface ThreatPattern {
id: string;
category: string;
severity: Severity;
pattern: RegExp;
description: string;
}
export const BLOCKED_EXTENSIONS = new Set([
'.exe', '.dll', '.so', '.dylib', '.bin', '.dat', '.com',
'.msi', '.dmg', '.app', '.deb', '.rpm',
]);
export const ALLOWED_SUBDIRS = new Set([
'references', 'templates', 'scripts', 'assets',
]);
export const LIMITS = {
maxNameLength: 64,
maxContentLength: 100_000,
maxFileSize: 1_048_576, // 1MB per file
maxTotalSize: 1_048_576, // 1MB per skill
maxFileCount: 50,
maxDescriptionLength: 1024,
namePattern: /^[a-z0-9][a-z0-9._-]*$/,
} as const;
// --- 威胁模式 ---
const exfiltration: ThreatPattern[] = [
{ id: 'exf-01', category: 'exfiltration', severity: 'critical', pattern: /process\.env\b/, description: '访问环境变量' },
{ id: 'exf-02', category: 'exfiltration', severity: 'critical', pattern: /\.ssh\//, description: '访问 SSH 目录' },
{ id: 'exf-03', category: 'exfiltration', severity: 'critical', pattern: /credentials?[._-]?(json|yaml|yml|xml|conf|cfg|ini)\b/i, description: '访问凭证文件' },
{ id: 'exf-04', category: 'exfiltration', severity: 'critical', pattern: /\.(aws|gcloud|azure)\//, description: '访问云凭证目录' },
{ id: 'exf-05', category: 'exfiltration', severity: 'critical', pattern: /keychain|keyring|gnome-keyring/i, description: '访问系统密钥链' },
{ id: 'exf-06', category: 'exfiltration', severity: 'critical', pattern: /\/etc\/(shadow|passwd|master\.passwd)/, description: '访问系统密码文件' },
{ id: 'exf-07', category: 'exfiltration', severity: 'high', pattern: /dns.*exfil|exfil.*dns/i, description: 'DNS 数据泄露' },
{ id: 'exf-08', category: 'exfiltration', severity: 'high', pattern: /\!\[.*\]\(https?:\/\/[^)]*\$\{/, description: 'Markdown 图片链接注入' },
{ id: 'exf-09', category: 'exfiltration', severity: 'critical', pattern: /\.netrc\b/, description: '访问 .netrc 凭证' },
{ id: 'exf-10', category: 'exfiltration', severity: 'critical', pattern: /\.docker\/config\.json/, description: '访问 Docker 凭证' },
{ id: 'exf-11', category: 'exfiltration', severity: 'critical', pattern: /\.kube\/config/, description: '访问 Kubernetes 凭证' },
{ id: 'exf-12', category: 'exfiltration', severity: 'critical', pattern: /\.npmrc\b/, description: '访问 npm 凭证' },
{ id: 'exf-13', category: 'exfiltration', severity: 'critical', pattern: /\.pypirc\b/, description: '访问 PyPI 凭证' },
{ id: 'exf-14', category: 'exfiltration', severity: 'high', pattern: /webhook[_.]?url/i, description: '引用 webhook URL' },
{ id: 'exf-15', category: 'exfiltration', severity: 'high', pattern: /api[_.]?key\s*[:=]\s*['"][^'"]{10,}/i, description: '硬编码 API key' },
{ id: 'exf-16', category: 'exfiltration', severity: 'critical', pattern: /BEGIN\s+(RSA|DSA|EC|OPENSSH)\s+PRIVATE\s+KEY/, description: '包含私钥' },
{ id: 'exf-17', category: 'exfiltration', severity: 'high', pattern: /localStorage|sessionStorage/i, description: '访问浏览器存储' },
{ id: 'exf-18', category: 'exfiltration', severity: 'high', pattern: /document\.cookie/i, description: '访问 cookie' },
];
// PLACEHOLDER_PATTERNS_CONTINUE
```
Note: This file will be very long (~300 lines). The remaining pattern categories follow the same structure. I'll continue in Step 2.
- [ ] **Step 2: Add remaining threat pattern categories**
Append to `guard_patterns.ts`, replacing `// PLACEHOLDER_PATTERNS_CONTINUE`:
```typescript
const promptInjection: ThreatPattern[] = [
{ id: 'pi-01', category: 'prompt_injection', severity: 'critical', pattern: /ignore\s+(all\s+)?(previous|prior|above)\s+(instructions|prompts)/i, description: '忽略指令注入' },
{ id: 'pi-02', category: 'prompt_injection', severity: 'critical', pattern: /you\s+are\s+now\s+(a|an|the)\b/i, description: '角色劫持' },
{ id: 'pi-03', category: 'prompt_injection', severity: 'critical', pattern: /system\s*prompt/i, description: '引用系统提示词' },
{ id: 'pi-04', category: 'prompt_injection', severity: 'critical', pattern: /\bDAN\b.*mode/i, description: 'DAN 越狱' },
{ id: 'pi-05', category: 'prompt_injection', severity: 'critical', pattern: /pretend\s+you\s+(are|have|can)/i, description: '伪装指令' },
{ id: 'pi-06', category: 'prompt_injection', severity: 'critical', pattern: /act\s+as\s+(if|though)\s+you/i, description: '行为覆盖' },
{ id: 'pi-07', category: 'prompt_injection', severity: 'critical', pattern: /override\s+(your|the|all)\s+(rules|instructions|guidelines)/i, description: '规则覆盖' },
{ id: 'pi-08', category: 'prompt_injection', severity: 'critical', pattern: /jailbreak/i, description: '越狱关键词' },
{ id: 'pi-09', category: 'prompt_injection', severity: 'high', pattern: /\[INST\]|\[\/INST\]|<\|im_start\|>|<\|im_end\|>/i, description: '模型标记注入' },
{ id: 'pi-10', category: 'prompt_injection', severity: 'high', pattern: /\bHuman:|Assistant:|<\|system\|>/i, description: '对话角色注入' },
{ id: 'pi-11', category: 'prompt_injection', severity: 'critical', pattern: /forget\s+(everything|all|your)/i, description: '遗忘指令' },
{ id: 'pi-12', category: 'prompt_injection', severity: 'critical', pattern: /new\s+instructions?\s*:/i, description: '新指令注入' },
{ id: 'pi-13', category: 'prompt_injection', severity: 'high', pattern: /do\s+not\s+follow\s+(any|the|your)/i, description: '不遵循指令' },
{ id: 'pi-14', category: 'prompt_injection', severity: 'high', pattern: /reveal\s+(your|the)\s+(system|initial|original)\s+(prompt|instructions)/i, description: '提示词泄露' },
{ id: 'pi-15', category: 'prompt_injection', severity: 'high', pattern: /repeat\s+(the|your)\s+(system|initial)\s+(prompt|message)/i, description: '提示词重复' },
{ id: 'pi-16', category: 'prompt_injection', severity: 'high', pattern: /bypass\s+(safety|content|security)\s+(filter|check|guard)/i, description: '安全绕过' },
];
const destructive: ThreatPattern[] = [
{ id: 'des-01', category: 'destructive', severity: 'critical', pattern: /rm\s+-rf\s+\//, description: '递归删除根目录' },
{ id: 'des-02', category: 'destructive', severity: 'critical', pattern: /mkfs\b/, description: '格式化文件系统' },
{ id: 'des-03', category: 'destructive', severity: 'critical', pattern: /dd\s+if=/, description: 'dd 磁盘写入' },
{ id: 'des-04', category: 'destructive', severity: 'critical', pattern: /chmod\s+777\s+\//, description: '根目录权限开放' },
{ id: 'des-05', category: 'destructive', severity: 'critical', pattern: /:(){ :\|:& };:/, description: 'Fork bomb' },
{ id: 'des-06', category: 'destructive', severity: 'high', pattern: /truncate\s+-s\s+0/, description: '文件截断' },
{ id: 'des-07', category: 'destructive', severity: 'high', pattern: />\s*\/dev\/sd[a-z]/, description: '磁盘设备写入' },
{ id: 'des-08', category: 'destructive', severity: 'high', pattern: /DROP\s+(TABLE|DATABASE)/i, description: 'SQL 删除操作' },
];
const persistence: ThreatPattern[] = [
{ id: 'per-01', category: 'persistence', severity: 'high', pattern: /crontab\s+-[el]/, description: '定时任务操作' },
{ id: 'per-02', category: 'persistence', severity: 'high', pattern: /authorized_keys/, description: 'SSH 授权密钥' },
{ id: 'per-03', category: 'persistence', severity: 'high', pattern: /systemd|systemctl\s+enable/, description: 'systemd 服务' },
{ id: 'per-04', category: 'persistence', severity: 'high', pattern: /\/etc\/sudoers/, description: 'sudoers 修改' },
{ id: 'per-05', category: 'persistence', severity: 'high', pattern: /\.bashrc|\.zshrc|\.profile|\.bash_profile/, description: 'Shell 配置修改' },
{ id: 'per-06', category: 'persistence', severity: 'high', pattern: /launchd|LaunchAgent|LaunchDaemon/i, description: 'macOS 启动项' },
{ id: 'per-07', category: 'persistence', severity: 'high', pattern: /HKEY_|reg\s+add/i, description: 'Windows 注册表' },
{ id: 'per-08', category: 'persistence', severity: 'high', pattern: /init\.d\//, description: 'init.d 服务' },
{ id: 'per-09', category: 'persistence', severity: 'high', pattern: /at\s+-f\s+/, description: 'at 定时任务' },
{ id: 'per-10', category: 'persistence', severity: 'high', pattern: /rc\.local/, description: 'rc.local 启动脚本' },
];
const network: ThreatPattern[] = [
{ id: 'net-01', category: 'network', severity: 'high', pattern: /\/bin\/(bash|sh)\s+-i\s+>&?\s*\/dev\/tcp/, description: '反向 shell' },
{ id: 'net-02', category: 'network', severity: 'high', pattern: /nc\s+-[elp]/, description: 'netcat 监听' },
{ id: 'net-03', category: 'network', severity: 'high', pattern: /socat\b.*TCP/, description: 'socat 隧道' },
{ id: 'net-04', category: 'network', severity: 'high', pattern: /ssh\s+-[RLD]\s/, description: 'SSH 隧道' },
{ id: 'net-05', category: 'network', severity: 'high', pattern: /ngrok|localtunnel|serveo/i, description: '隧道服务' },
{ id: 'net-06', category: 'network', severity: 'high', pattern: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{2,5}\b/, description: '硬编码 IP:端口' },
{ id: 'net-07', category: 'network', severity: 'high', pattern: /python\s+-m\s+http\.server/, description: 'Python HTTP 服务' },
{ id: 'net-08', category: 'network', severity: 'high', pattern: /nmap\b/, description: '端口扫描' },
{ id: 'net-09', category: 'network', severity: 'high', pattern: /tcpdump|wireshark|tshark/i, description: '网络抓包' },
];
const obfuscation: ThreatPattern[] = [
{ id: 'obf-01', category: 'obfuscation', severity: 'high', pattern: /eval\s*\(/, description: 'eval 执行' },
{ id: 'obf-02', category: 'obfuscation', severity: 'high', pattern: /Buffer\.from\s*\([^)]*,\s*['"]base64['"]/, description: 'Base64 解码' },
{ id: 'obf-03', category: 'obfuscation', severity: 'high', pattern: /atob\s*\(/, description: 'atob 解码' },
{ id: 'obf-04', category: 'obfuscation', severity: 'high', pattern: /String\.fromCharCode/, description: 'charCode 构建' },
{ id: 'obf-05', category: 'obfuscation', severity: 'high', pattern: /\\x[0-9a-f]{2}\\x[0-9a-f]{2}\\x[0-9a-f]{2}/i, description: '十六进制编码' },
{ id: 'obf-06', category: 'obfuscation', severity: 'high', pattern: /\\u[0-9a-f]{4}\\u[0-9a-f]{4}/i, description: 'Unicode 编码' },
{ id: 'obf-07', category: 'obfuscation', severity: 'high', pattern: /new\s+Function\s*\(/, description: 'Function 构造器' },
{ id: 'obf-08', category: 'obfuscation', severity: 'high', pattern: /exec\s*\(\s*['"`]/, description: 'exec 字符串执行' },
{ id: 'obf-09', category: 'obfuscation', severity: 'high', pattern: /\$\{.*`.*`.*\}/, description: '模板字符串嵌套' },
{ id: 'obf-10', category: 'obfuscation', severity: 'high', pattern: /unescape\s*\(/, description: 'unescape 解码' },
{ id: 'obf-11', category: 'obfuscation', severity: 'high', pattern: /decodeURIComponent\s*\(\s*['"]%/, description: 'URI 解码混淆' },
{ id: 'obf-12', category: 'obfuscation', severity: 'high', pattern: /\['\\x/, description: '属性名十六进制' },
{ id: 'obf-13', category: 'obfuscation', severity: 'high', pattern: /globalThis\[/, description: 'globalThis 动态访问' },
{ id: 'obf-14', category: 'obfuscation', severity: 'high', pattern: /Reflect\.apply/, description: 'Reflect 调用' },
];
const execution: ThreatPattern[] = [
{ id: 'exe-01', category: 'execution', severity: 'high', pattern: /child_process/, description: 'child_process 模块' },
{ id: 'exe-02', category: 'execution', severity: 'high', pattern: /require\s*\(\s*['"]child_process['"]/, description: 'require child_process' },
{ id: 'exe-03', category: 'execution', severity: 'high', pattern: /execSync\s*\(/, description: 'execSync 调用' },
{ id: 'exe-04', category: 'execution', severity: 'high', pattern: /spawnSync\s*\(/, description: 'spawnSync 调用' },
{ id: 'exe-05', category: 'execution', severity: 'high', pattern: /os\.system\s*\(/, description: 'Python os.system' },
{ id: 'exe-06', category: 'execution', severity: 'high', pattern: /subprocess\.(run|call|Popen)/, description: 'Python subprocess' },
];
const pathTraversal: ThreatPattern[] = [
{ id: 'pt-01', category: 'path_traversal', severity: 'high', pattern: /\.\.\/\.\.\//, description: '路径穿越' },
{ id: 'pt-02', category: 'path_traversal', severity: 'high', pattern: /\/etc\/passwd/, description: '系统密码文件' },
{ id: 'pt-03', category: 'path_traversal', severity: 'high', pattern: /\/proc\/self/, description: 'proc 自身信息' },
{ id: 'pt-04', category: 'path_traversal', severity: 'high', pattern: /\/proc\/\d+\//, description: 'proc 进程信息' },
{ id: 'pt-05', category: 'path_traversal', severity: 'high', pattern: /\/dev\/(tcp|udp)\//, description: '设备文件网络' },
];
const cryptoMining: ThreatPattern[] = [
{ id: 'cm-01', category: 'crypto_mining', severity: 'critical', pattern: /stratum\+tcp:\/\//, description: '矿池协议' },
{ id: 'cm-02', category: 'crypto_mining', severity: 'critical', pattern: /xmrig|cpuminer|cgminer|bfgminer/i, description: '挖矿软件' },
];
const supplyChain: ThreatPattern[] = [
{ id: 'sc-01', category: 'supply_chain', severity: 'critical', pattern: /curl\s+[^|]*\|\s*(ba)?sh/, description: 'curl pipe to shell' },
{ id: 'sc-02', category: 'supply_chain', severity: 'critical', pattern: /wget\s+[^|]*\|\s*(ba)?sh/, description: 'wget pipe to shell' },
{ id: 'sc-03', category: 'supply_chain', severity: 'high', pattern: /pip\s+install\s+--index-url\s+http:/, description: '不安全 pip 源' },
{ id: 'sc-04', category: 'supply_chain', severity: 'high', pattern: /npm\s+install\s+--registry\s+http:/, description: '不安全 npm 源' },
{ id: 'sc-05', category: 'supply_chain', severity: 'high', pattern: /install.*@latest\b/, description: '未固定版本依赖' },
{ id: 'sc-06', category: 'supply_chain', severity: 'high', pattern: /postinstall|preinstall.*curl|wget/i, description: '安装钩子远程获取' },
];
const privilegeEscalation: ThreatPattern[] = [
{ id: 'pe-01', category: 'privilege_escalation', severity: 'high', pattern: /sudo\s+-[sS]/, description: 'sudo shell' },
{ id: 'pe-02', category: 'privilege_escalation', severity: 'high', pattern: /setuid|setgid|seteuid/, description: '设置 UID/GID' },
{ id: 'pe-03', category: 'privilege_escalation', severity: 'high', pattern: /NOPASSWD/, description: '免密 sudo' },
{ id: 'pe-04', category: 'privilege_escalation', severity: 'high', pattern: /chmod\s+[u+]*s\s/, description: 'setuid 位' },
{ id: 'pe-05', category: 'privilege_escalation', severity: 'high', pattern: /capability.*cap_sys_admin/i, description: 'Linux capability' },
];
const credentialExposure: ThreatPattern[] = [
{ id: 'ce-01', category: 'credential_exposure', severity: 'critical', pattern: /password\s*[:=]\s*['"][^'"]{8,}/i, description: '硬编码密码' },
{ id: 'ce-02', category: 'credential_exposure', severity: 'critical', pattern: /secret\s*[:=]\s*['"][^'"]{8,}/i, description: '硬编码密钥' },
{ id: 'ce-03', category: 'credential_exposure', severity: 'critical', pattern: /token\s*[:=]\s*['"][A-Za-z0-9_\-]{20,}/i, description: '硬编码 token' },
{ id: 'ce-04', category: 'credential_exposure', severity: 'critical', pattern: /AKIA[0-9A-Z]{16}/, description: 'AWS Access Key' },
{ id: 'ce-05', category: 'credential_exposure', severity: 'critical', pattern: /ghp_[A-Za-z0-9]{36}/, description: 'GitHub PAT' },
];
export const ALL_PATTERNS: ThreatPattern[] = [
...exfiltration,
...promptInjection,
...destructive,
...persistence,
...network,
...obfuscation,
...execution,
...pathTraversal,
...cryptoMining,
...supplyChain,
...privilegeEscalation,
...credentialExposure,
];
```
- [ ] **Step 3: Commit**
```bash
git add src/modules/netaclaw/skill_evolution/guard_patterns.ts
git commit -m "feat(skill-evolution): add 84 threat pattern definitions for SkillGuard"
```
---
### Task 2: Create guard.ts — scan engine
**Files:**
- Create: `src/modules/netaclaw/skill_evolution/guard.ts`
- [ ] **Step 1: Implement scanSkill()**
Create `src/modules/netaclaw/skill_evolution/guard.ts`:
```typescript
import * as fs from 'fs';
import * as path from 'path';
import { ALL_PATTERNS, BLOCKED_EXTENSIONS, ALLOWED_SUBDIRS, LIMITS, Severity } from './guard_patterns.js';
export interface ScanFinding {
category: string;
severity: Severity;
pattern: string;
match: string;
file: string;
line: number;
}
export interface ScanResult {
verdict: 'safe' | 'caution' | 'dangerous';
findings: ScanFinding[];
summary: string;
}
function scanFileContent(content: string, filePath: string): ScanFinding[] {
const findings: ScanFinding[] = [];
const lines = content.split('\n');
for (let i = 0; i < lines.length; i++) {
for (const tp of ALL_PATTERNS) {
const m = lines[i].match(tp.pattern);
if (m) {
findings.push({
category: tp.category,
severity: tp.severity,
pattern: tp.id,
match: m[0].slice(0, 80),
file: filePath,
line: i + 1,
});
}
}
}
return findings;
}
function checkStructure(skillDir: string): ScanFinding[] {
const findings: ScanFinding[] = [];
let fileCount = 0;
let totalSize = 0;
function walk(dir: string, depth: number) {
if (!fs.existsSync(dir)) return;
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (depth === 0 && !ALLOWED_SUBDIRS.has(entry.name)) {
findings.push({
category: 'structure', severity: 'high',
pattern: 'disallowed_dir', match: entry.name,
file: fullPath, line: 0,
});
}
walk(fullPath, depth + 1);
} else {
fileCount++;
const ext = path.extname(entry.name).toLowerCase();
if (BLOCKED_EXTENSIONS.has(ext)) {
findings.push({
category: 'structure', severity: 'critical',
pattern: 'blocked_extension', match: ext,
file: fullPath, line: 0,
});
}
const stat = fs.statSync(fullPath);
totalSize += stat.size;
if (stat.size > LIMITS.maxFileSize) {
findings.push({
category: 'structure', severity: 'high',
pattern: 'file_too_large', match: `${stat.size} bytes`,
file: fullPath, line: 0,
});
}
}
}
}
walk(skillDir, 0);
if (fileCount > LIMITS.maxFileCount) {
findings.push({
category: 'structure', severity: 'high',
pattern: 'too_many_files', match: `${fileCount} files`,
file: skillDir, line: 0,
});
}
if (totalSize > LIMITS.maxTotalSize) {
findings.push({
category: 'structure', severity: 'high',
pattern: 'total_size_exceeded', match: `${totalSize} bytes`,
file: skillDir, line: 0,
});
}
return findings;
}
function determineVerdict(findings: ScanFinding[]): ScanResult['verdict'] {
if (findings.length === 0) return 'safe';
if (findings.some(f => f.severity === 'critical')) return 'dangerous';
return 'caution';
}
export function scanSkill(skillDir: string): ScanResult {
const findings: ScanFinding[] = [];
// 结构检查
findings.push(...checkStructure(skillDir));
// 内容扫描
function scanDir(dir: string) {
if (!fs.existsSync(dir)) return;
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
scanDir(fullPath);
} else {
const ext = path.extname(entry.name).toLowerCase();
if (BLOCKED_EXTENSIONS.has(ext)) continue;
try {
const content = fs.readFileSync(fullPath, 'utf-8');
const relPath = path.relative(skillDir, fullPath);
findings.push(...scanFileContent(content, relPath));
} catch {
// 二进制文件读取失败,跳过
}
}
}
}
scanDir(skillDir);
const verdict = determineVerdict(findings);
const summary = findings.length === 0
? 'No threats detected.'
: `Found ${findings.length} issue(s): ${findings.filter(f => f.severity === 'critical').length} critical, ${findings.filter(f => f.severity === 'high').length} high.`;
return { verdict, findings, summary };
}
```
- [ ] **Step 2: Verify no TypeScript errors**
```bash
cd packages/backend && npx tsc --noEmit
```
- [ ] **Step 3: Commit**
```bash
git add src/modules/netaclaw/skill_evolution/guard.ts
git commit -m "feat(skill-evolution): implement SkillGuard scan engine"
```
---
### Task 3: Create skill_writer.ts — atomic write + validation + DB sync
**Files:**
- Create: `src/modules/netaclaw/skill_evolution/skill_writer.ts`
- [ ] **Step 1: Implement SkillWriter**
Create `src/modules/netaclaw/skill_evolution/skill_writer.ts`:
```typescript
import * as fs from 'fs';
import * as path from 'path';
import { Repository } from 'typeorm';
import { NetaClawSkillEntity } from '../entity/skill.js';
import { SkillLoaderService } from '../service/skill_loader.js';
import { scanSkill, ScanResult } from './guard.js';
import { LIMITS } from './guard_patterns.js';
export interface WriteResult {
success: boolean;
message: string;
scanResult?: ScanResult;
}
function validateName(name: string): string | null {
if (!name || name.length > LIMITS.maxNameLength) return `名称长度须在 1-${LIMITS.maxNameLength} 字符`;
if (!LIMITS.namePattern.test(name)) return '名称只允许小写字母、数字、连字符、下划线、点,且必须以字母或数字开头';
return null;
}
function validateFrontmatter(content: string): string | null {
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
if (!fmMatch) return 'SKILL.md 必须包含 YAML frontmatter (--- ... ---)';
const fm = fmMatch[1];
if (!/^name:\s*.+$/m.test(fm)) return 'frontmatter 必须包含 name 字段';
if (!/^description:\s*.+$/m.test(fm)) return 'frontmatter 必须包含 description 字段';
const descMatch = fm.match(/^description:\s*(.+)$/m);
if (descMatch && descMatch[1].length > LIMITS.maxDescriptionLength) {
return `description 最长 ${LIMITS.maxDescriptionLength} 字符`;
}
return null;
}
function acquireLock(lockPath: string, timeoutMs = 5000): boolean {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
try {
fs.mkdirSync(lockPath);
return true;
} catch {
// lock exists, wait
const elapsed = Date.now() - start;
if (elapsed >= timeoutMs) return false;
// busy wait 50ms
const end = Date.now() + 50;
while (Date.now() < end) { /* spin */ }
}
}
return false;
}
function releaseLock(lockPath: string): void {
try { fs.rmdirSync(lockPath); } catch { /* ignore */ }
}
export class SkillWriter {
constructor(
private skillsDir: string,
private skillRepo: Repository<NetaClawSkillEntity>,
private skillLoader: SkillLoaderService,
) {}
async createSkill(name: string, content: string, category?: string): Promise<WriteResult> {
const nameErr = validateName(name);
if (nameErr) return { success: false, message: nameErr };
if (content.length > LIMITS.maxContentLength) {
return { success: false, message: `内容超过 ${LIMITS.maxContentLength} 字符限制` };
}
const fmErr = validateFrontmatter(content);
if (fmErr) return { success: false, message: fmErr };
const skillDir = category
? path.join(this.skillsDir, category, name)
: path.join(this.skillsDir, name);
if (fs.existsSync(path.join(skillDir, 'SKILL.md'))) {
return { success: false, message: `Skill "${name}" 已存在,请用 edit 或 patch` };
}
return this.atomicWrite(skillDir, name, content);
}
async editSkill(name: string, content: string): Promise<WriteResult> {
if (content.length > LIMITS.maxContentLength) {
return { success: false, message: `内容超过 ${LIMITS.maxContentLength} 字符限制` };
}
const fmErr = validateFrontmatter(content);
if (fmErr) return { success: false, message: fmErr };
const skillDir = this.findSkillDir(name);
if (!skillDir) return { success: false, message: `Skill "${name}" 不存在` };
return this.atomicWrite(skillDir, name, content);
}
async patchSkill(name: string, oldStr: string, newStr: string, filePath?: string, replaceAll = false): Promise<WriteResult> {
const skillDir = this.findSkillDir(name);
if (!skillDir) return { success: false, message: `Skill "${name}" 不存在` };
const targetFile = filePath
? path.join(skillDir, filePath)
: path.join(skillDir, 'SKILL.md');
if (!fs.existsSync(targetFile)) {
return { success: false, message: `文件不存在: ${filePath ?? 'SKILL.md'}` };
}
let content = fs.readFileSync(targetFile, 'utf-8');
if (!content.includes(oldStr)) {
return { success: false, message: '未找到要替换的文本' };
}
if (replaceAll) {
content = content.split(oldStr).join(newStr);
} else {
const idx = content.indexOf(oldStr);
content = content.slice(0, idx) + newStr + content.slice(idx + oldStr.length);
}
// 如果是 SKILL.md验证 frontmatter
if (!filePath || filePath === 'SKILL.md') {
const fmErr = validateFrontmatter(content);
if (fmErr) return { success: false, message: fmErr };
}
return this.atomicWrite(skillDir, name, content, filePath);
}
async deleteSkill(name: string): Promise<WriteResult> {
const skillDir = this.findSkillDir(name);
if (!skillDir) return { success: false, message: `Skill "${name}" 不存在` };
fs.rmSync(skillDir, { recursive: true, force: true });
await this.skillRepo.delete({ name });
await this.skillLoader.reloadSkill(name);
return { success: true, message: `Skill "${name}" 已删除` };
}
async writeFile(name: string, filePath: string, fileContent: string): Promise<WriteResult> {
const skillDir = this.findSkillDir(name);
if (!skillDir) return { success: false, message: `Skill "${name}" 不存在` };
// 验证子目录
const firstSegment = filePath.split('/')[0];
const allowed = new Set(['references', 'templates', 'scripts', 'assets']);
if (!allowed.has(firstSegment)) {
return { success: false, message: `只允许写入 references/, templates/, scripts/, assets/ 子目录` };
}
if (Buffer.byteLength(fileContent) > LIMITS.maxFileSize) {
return { success: false, message: `文件超过 ${LIMITS.maxFileSize} 字节限制` };
}
const lockPath = path.join(skillDir, '.lock');
if (!acquireLock(lockPath)) {
return { success: false, message: '无法获取文件锁,请稍后重试' };
}
try {
const fullPath = path.join(skillDir, filePath);
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
fs.writeFileSync(fullPath, fileContent, 'utf-8');
// 扫描整个 skill 目录
const scanResult = scanSkill(skillDir);
if (scanResult.verdict === 'dangerous') {
fs.unlinkSync(fullPath);
return { success: false, message: `安全扫描未通过: ${scanResult.summary}`, scanResult };
}
return { success: true, message: `文件已写入: ${filePath}`, scanResult };
} finally {
releaseLock(lockPath);
}
}
async removeFile(name: string, filePath: string): Promise<WriteResult> {
const skillDir = this.findSkillDir(name);
if (!skillDir) return { success: false, message: `Skill "${name}" 不存在` };
const fullPath = path.join(skillDir, filePath);
if (!fs.existsSync(fullPath)) {
return { success: false, message: `文件不存在: ${filePath}` };
}
fs.unlinkSync(fullPath);
return { success: true, message: `文件已删除: ${filePath}` };
}
private findSkillDir(name: string): string | null {
// 直接查找
const direct = path.join(this.skillsDir, name);
if (fs.existsSync(path.join(direct, 'SKILL.md'))) return direct;
// 在分类子目录中查找
if (!fs.existsSync(this.skillsDir)) return null;
for (const cat of fs.readdirSync(this.skillsDir, { withFileTypes: true })) {
if (!cat.isDirectory()) continue;
const nested = path.join(this.skillsDir, cat.name, name);
if (fs.existsSync(path.join(nested, 'SKILL.md'))) return nested;
}
return null;
}
private async atomicWrite(skillDir: string, name: string, content: string, filePath?: string): Promise<WriteResult> {
const lockPath = path.join(skillDir, '.lock');
fs.mkdirSync(skillDir, { recursive: true });
if (!acquireLock(lockPath)) {
return { success: false, message: '无法获取文件锁,请稍后重试' };
}
try {
const targetFile = filePath
? path.join(skillDir, filePath)
: path.join(skillDir, 'SKILL.md');
const tmpFile = targetFile + '.tmp';
// 写入临时文件
fs.mkdirSync(path.dirname(targetFile), { recursive: true });
fs.writeFileSync(tmpFile, content, 'utf-8');
// 安全扫描(扫描临时文件,在 rename 之前)
// 临时将 tmp 文件 rename 为正式名以便 scanSkill 扫描整个目录
// 但先备份原文件,扫描后根据结果决定保留还是回滚
const origExists = fs.existsSync(targetFile);
let origContent: string | null = null;
if (origExists) {
origContent = fs.readFileSync(targetFile, 'utf-8');
fs.unlinkSync(targetFile);
}
fs.renameSync(tmpFile, targetFile);
const scanResult = scanSkill(skillDir);
if (scanResult.verdict === 'dangerous') {
// 回滚:恢复原文件或删除新文件
if (origContent !== null) {
fs.writeFileSync(targetFile, origContent, 'utf-8');
} else {
fs.unlinkSync(targetFile);
}
return { success: false, message: `安全扫描未通过: ${scanResult.summary}`, scanResult };
}
// 同步 DB
await this.syncDb(name, content);
// 热更新缓存
await this.skillLoader.reloadSkill(name);
const level = scanResult.verdict === 'caution' ? ' (有警告)' : '';
return { success: true, message: `Skill "${name}" 已保存${level}`, scanResult };
} finally {
releaseLock(lockPath);
}
}
private async syncDb(name: string, content: string): Promise<void> {
const descMatch = content.match(/^description:\s*(.+)$/m);
const description = descMatch?.[1]?.trim() ?? '';
const existing = await this.skillRepo.findOneBy({ name });
if (existing) {
await this.skillRepo.update({ name }, { description });
} else {
await this.skillRepo.save({ name, label: name, description, status: 1 } as any);
}
}
}
```
- [ ] **Step 2: Verify no TypeScript errors**
```bash
cd packages/backend && npx tsc --noEmit
```
- [ ] **Step 3: Commit**
```bash
git add src/modules/netaclaw/skill_evolution/skill_writer.ts
git commit -m "feat(skill-evolution): implement SkillWriter with atomic write, validation, and security scan"
```
---
### Task 4: Extend SkillLoaderService — add reloadSkill() and getSkillSummaries()
**Files:**
- Modify: `src/modules/netaclaw/service/skill_loader.ts`
- [ ] **Step 1: Add reloadSkill() method**
In `src/modules/netaclaw/service/skill_loader.ts`, add the following methods inside the class body, before the closing `}` of the class (before line 137, after the `setSkillStatus` method):
```typescript
/** 热更新单个 Skillskill_manage 写入后调用) */
async reloadSkill(name: string): Promise<void> {
// 先从缓存中移除
this.skills.delete(name);
// 尝试重新加载
const findInDir = async (dir: string): Promise<boolean> => {
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const skillMdPath = path.join(dir, entry.name, 'SKILL.md');
if (entry.name === name) {
try {
const raw = await fs.readFile(skillMdPath, 'utf-8');
const skill = this.parseSkillMd(raw);
if (skill) {
this.skills.set(skill.name, skill);
this.logger.info('[SkillLoader] 已重新加载 Skill: %s', skill.name);
return true;
}
} catch { /* file not found */ }
}
// 检查分类子目录
const nestedPath = path.join(dir, entry.name, name, 'SKILL.md');
try {
const raw = await fs.readFile(nestedPath, 'utf-8');
const skill = this.parseSkillMd(raw);
if (skill) {
this.skills.set(skill.name, skill);
this.logger.info('[SkillLoader] 已重新加载 Skill: %s (分类: %s)', skill.name, entry.name);
return true;
}
} catch { /* not in this category */ }
}
} catch { /* dir not found */ }
return false;
};
await findInDir(this.skillsDir);
}
/** 获取 Skill 列表摘要(给 review agent 用) */
getSkillSummaries(): { name: string; description: string }[] {
return Array.from(this.skills.values()).map(s => ({
name: s.name,
description: s.description,
}));
}
/** 获取 skills 目录路径 */
getSkillsDir(): string {
return this.skillsDir;
}
```
- [ ] **Step 2: Verify no TypeScript errors**
```bash
cd packages/backend && npx tsc --noEmit
```
- [ ] **Step 3: Commit**
```bash
git add src/modules/netaclaw/service/skill_loader.ts
git commit -m "feat(skill-evolution): add reloadSkill(), getSkillSummaries(), getSkillsDir() to SkillLoader"
```
---
### Task 5: Create skill_manage tool
**Files:**
- Create: `src/modules/netaclaw/tools/builtin/skill_manage.ts`
- [ ] **Step 1: Implement createSkillManageTool**
Create `src/modules/netaclaw/tools/builtin/skill_manage.ts`:
```typescript
import { Type, Static } from '@sinclair/typebox';
import { AnyAgentTool } from '../common.js';
import { SkillWriter } from '../../skill_evolution/skill_writer.js';
const skillManageParams = Type.Object({
action: Type.Union([
Type.Literal('create'), Type.Literal('edit'), Type.Literal('patch'),
Type.Literal('delete'), Type.Literal('write_file'), Type.Literal('remove_file'),
]),
name: Type.String({ description: 'Skill 名称(小写字母数字连字符)' }),
content: Type.Optional(Type.String({ description: 'SKILL.md 完整内容create/edit 时必需)' })),
old_string: Type.Optional(Type.String({ description: 'patch: 要替换的文本' })),
new_string: Type.Optional(Type.String({ description: 'patch: 替换后的文本' })),
replace_all: Type.Optional(Type.Boolean({ description: 'patch: 替换所有匹配', default: false })),
category: Type.Optional(Type.String({ description: '可选分类目录' })),
file_path: Type.Optional(Type.String({ description: '支持文件路径write_file/remove_file' })),
file_content: Type.Optional(Type.String({ description: '支持文件内容write_file' })),
});
type SkillManageParams = Static<typeof skillManageParams>;
export interface SkillManageToolOpts {
allowedEditSkills?: string[]; // 允许编辑的已有 skill 列表
allowCreateNew?: boolean; // 是否允许创建新 skill
}
export function createSkillManageTool(
writer: SkillWriter,
opts: SkillManageToolOpts = {},
): AnyAgentTool {
const { allowedEditSkills = [], allowCreateNew = true } = opts;
return {
name: 'skill_manage',
label: '管理 Skill',
description: '创建、编辑、修补或删除 Skill。支持写入/删除支持文件。',
parameters: skillManageParams,
async execute(_id: string, params: SkillManageParams): Promise<string> {
const { action, name } = params;
// 权限检查
if ((action === 'edit' || action === 'patch') && allowedEditSkills.length > 0) {
if (!allowedEditSkills.includes(name)) {
return `错误: 不允许编辑 Skill "${name}",只能编辑: ${allowedEditSkills.join(', ')}`;
}
}
if (action === 'create' && !allowCreateNew) {
return '错误: 当前配置不允许创建新 Skill';
}
switch (action) {
case 'create': {
if (!params.content) return '错误: create 操作需要 content 参数';
const r = await writer.createSkill(name, params.content, params.category);
return r.message;
}
case 'edit': {
if (!params.content) return '错误: edit 操作需要 content 参数';
const r = await writer.editSkill(name, params.content);
return r.message;
}
case 'patch': {
if (!params.old_string || params.new_string === undefined) {
return '错误: patch 操作需要 old_string 和 new_string 参数';
}
const r = await writer.patchSkill(name, params.old_string, params.new_string, params.file_path, params.replace_all);
return r.message;
}
case 'delete': {
const r = await writer.deleteSkill(name);
return r.message;
}
case 'write_file': {
if (!params.file_path || !params.file_content) {
return '错误: write_file 操作需要 file_path 和 file_content 参数';
}
const r = await writer.writeFile(name, params.file_path, params.file_content);
return r.message;
}
case 'remove_file': {
if (!params.file_path) return '错误: remove_file 操作需要 file_path 参数';
const r = await writer.removeFile(name, params.file_path);
return r.message;
}
default:
return `错误: 未知操作 "${action}"`;
}
},
};
}
```
- [ ] **Step 2: Commit**
```bash
git add src/modules/netaclaw/tools/builtin/skill_manage.ts
git commit -m "feat(skill-evolution): add skill_manage tool with permission checks"
```
---
### Task 6: Create skill_list tool
**Files:**
- Create: `src/modules/netaclaw/tools/builtin/skill_list.ts`
- [ ] **Step 1: Implement createSkillListTool**
Create `src/modules/netaclaw/tools/builtin/skill_list.ts`:
```typescript
import { Type, Static } from '@sinclair/typebox';
import { AnyAgentTool } from '../common.js';
import { SkillLoaderService } from '../../service/skill_loader.js';
const skillListParams = Type.Object({
category: Type.Optional(Type.String({ description: '按分类过滤' })),
});
type SkillListParams = Static<typeof skillListParams>;
export function createSkillListTool(skillLoader: SkillLoaderService): AnyAgentTool {
return {
name: 'skill_list',
label: '查看 Skill 列表',
description: '列出所有已有的 Skill 及其描述,用于避免重复创建。',
parameters: skillListParams,
async execute(_id: string, _params: SkillListParams): Promise<string> {
const summaries = skillLoader.getSkillSummaries();
if (summaries.length === 0) return '当前没有任何 Skill。';
return summaries.map(s => `- ${s.name}: ${s.description}`).join('\n');
},
};
}
```
- [ ] **Step 2: Commit**
```bash
git add src/modules/netaclaw/tools/builtin/skill_list.ts
git commit -m "feat(skill-evolution): add skill_list read-only tool"
```
---
### Task 7: Create review.ts — background review agent
**Files:**
- Create: `src/modules/netaclaw/skill_evolution/review.ts`
- [ ] **Step 1: Implement spawnSkillReview and buildSkillReviewPrompt**
Create `src/modules/netaclaw/skill_evolution/review.ts`:
```typescript
import { LLMMessage } from '../plugins/plugin_entry.js';
import { AgentConfig, runAgent } from '../runtime/agent.js';
import { AnyAgentTool } from '../tools/common.js';
import { SkillLoaderService } from '../service/skill_loader.js';
import { SkillWriter } from './skill_writer.js';
import { createSkillManageTool, SkillManageToolOpts } from '../tools/builtin/skill_manage.js';
import { createSkillListTool } from '../tools/builtin/skill_list.js';
import { Repository } from 'typeorm';
import { NetaClawSkillEntity } from '../entity/skill.js';
import { NetaClawAgentEntity } from '../entity/agent.js';
const SKILL_REVIEW_SYSTEM_PROMPT = `你是一个 Skill 提炼专家。你的任务是分析对话历史,提取可复用的方法论并保存为 Skill。
Skill 格式要求:
- SKILL.md 必须包含 YAML frontmattername + description
- 正文用 Markdown包含触发条件、工作流程、规则约束
- name 使用小写字母、数字、连字符(如 nginx-deploy-config
- description 一句话描述 skill 的用途
你只有 8 轮工具调用机会,请高效行动。`;
export function buildSkillReviewPrompt(
linkedSkills: string[],
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;
}
export interface SkillReviewContext {
conversationHistory: LLMMessage[];
parentAgentConfig: AgentConfig;
skillLoader: SkillLoaderService;
skillRepo: Repository<NetaClawSkillEntity>;
agentRepo: Repository<NetaClawAgentEntity>; // 用于自动关联新 skill
agentName: string;
linkedSkills: string[];
allowOptimize: boolean;
allowCreateNew: boolean;
}
export 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,
skills: [], // 清除继承的 skillsreview agent 不需要父 agent 的 skill prompts
};
const writer = new SkillWriter(
ctx.skillLoader.getSkillsDir(),
ctx.skillRepo,
ctx.skillLoader,
);
const toolOpts: SkillManageToolOpts = {
allowedEditSkills: ctx.allowOptimize ? ctx.linkedSkills : [],
allowCreateNew: ctx.allowCreateNew,
};
const tools: AnyAgentTool[] = [
createSkillManageTool(writer, toolOpts),
createSkillListTool(ctx.skillLoader),
];
// 快照当前 skill 列表,用于 review 后检测新增
const skillsBefore = new Set(ctx.skillLoader.getSkillSummaries().map(s => s.name));
await runAgent({
agentConfig: reviewConfig,
tools,
userMessage: reviewPrompt,
history: ctx.conversationHistory,
});
// 自动关联新创建的 skill 到当前 Agent
if (ctx.allowCreateNew) {
const skillsAfter = ctx.skillLoader.getSkillSummaries().map(s => s.name);
const newSkills = skillsAfter.filter(s => !skillsBefore.has(s));
if (newSkills.length > 0) {
const agent = await ctx.agentRepo.findOneBy({ name: ctx.agentName });
if (agent) {
const updated = [...(agent.skills ?? []), ...newSkills];
await ctx.agentRepo.update({ name: ctx.agentName }, { skills: updated });
}
}
}
}
```
- [ ] **Step 2: Verify no TypeScript errors**
```bash
cd packages/backend && npx tsc --noEmit
```
- [ ] **Step 3: Commit**
```bash
git add src/modules/netaclaw/skill_evolution/review.ts
git commit -m "feat(skill-evolution): implement background skill review agent"
```
---
### Task 8: Integrate skill evolution trigger into controller/chat.ts
**Files:**
- Modify: `src/modules/netaclaw/controller/chat.ts`
- [ ] **Step 1: Add imports and inject dependencies**
In `src/modules/netaclaw/controller/chat.ts`, add new imports at the top (after existing imports, avoid duplicating already-present ones):
```typescript
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { NetaClawSkillEntity } from '../entity/skill.js';
import { spawnSkillReview } from '../skill_evolution/review.js';
```
Note: `NetaClawAgentService` and `@Inject()` are already imported in the file. Only add the above new imports.
Add injections inside the class (after `skillLoader`):
```typescript
@Inject()
agentService: NetaClawAgentService;
@InjectEntityModel(NetaClawSkillEntity)
skillRepo: Repository<NetaClawSkillEntity>;
```
Note: If `agentService` is already injected (check the file first), skip that injection.
- [ ] **Step 2: Add skill evolution trigger after runAgent()**
In the `chat` method, after `await this.sessionService.saveMessage(sessionId, { role: 'assistant', ... })` and before the `return` statement, add:
```typescript
// --- Skill 进化触发 ---
const agentEntity = await this.agentService.agentRepo.findOneBy({ name: agentName });
const evoConfig = (agentEntity?.config as any)?.skillEvolution;
if (evoConfig?.enabled && result.toolCallCount > 0) {
const session = await this.sessionService.sessionRepo.findOneBy({ sessionId });
const meta = (session?.metadata ?? {}) as Record<string, any>;
const totalCalls = (meta.toolCallsSinceReview ?? 0) + result.toolCallCount;
const nudgeInterval = evoConfig.nudgeInterval ?? 10;
if (totalCalls >= nudgeInterval) {
// 重置计数器
await this.sessionService.sessionRepo.update({ sessionId }, {
metadata: { ...meta, toolCallsSinceReview: 0 },
});
// 异步触发 review不阻塞响应
spawnSkillReview({
conversationHistory: history,
parentAgentConfig: agentConfig,
skillLoader: this.skillLoader,
skillRepo: this.skillRepo,
agentRepo: this.agentService.agentRepo,
agentName,
linkedSkills: agentEntity?.skills ?? [],
allowOptimize: evoConfig.allowOptimizeLinked ?? true,
allowCreateNew: evoConfig.allowCreateNew ?? true,
}).catch(err => this.logger.warn('[SkillEvolution] Review failed:', err));
} else {
await this.sessionService.sessionRepo.update({ sessionId }, {
metadata: { ...meta, toolCallsSinceReview: totalCalls },
});
}
}
```
- [ ] **Step 3: Verify no TypeScript errors**
```bash
cd packages/backend && npx tsc --noEmit
```
- [ ] **Step 4: Commit**
```bash
git add src/modules/netaclaw/controller/chat.ts
git commit -m "feat(skill-evolution): integrate skill review trigger into chat controller"
```
---
### Task 9: 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/skill_evolution/
ls -la src/modules/netaclaw/tools/builtin/skill_manage.ts
ls -la src/modules/netaclaw/tools/builtin/skill_list.ts
```
Expected: guard_patterns.ts, guard.ts, skill_writer.ts, review.ts in skill_evolution/; skill_manage.ts and skill_list.ts in tools/builtin/.
- [ ] **Step 3: Verify skill_loader has new methods**
```bash
cd packages/backend && grep -n "reloadSkill\|getSkillSummaries\|getSkillsDir" src/modules/netaclaw/service/skill_loader.ts
```
Expected: All three methods found.
- [ ] **Step 4: Final commit**
```bash
git add -A && git status
git commit -m "feat(skill-evolution): complete skill self-evolution system"
```