ppxans-harness 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +265 -0
- package/bin/ppx-channels.js +3 -0
- package/bin/ppx-serve.js +6 -0
- package/bin/ppx.js +3 -0
- package/config/identity.md +6 -0
- package/config/ishiki.md +16 -0
- package/config/ppx.json +151 -0
- package/package.json +69 -0
- package/src/agent/context.js +179 -0
- package/src/agent/index.js +717 -0
- package/src/agent/prompts.js +107 -0
- package/src/aml-server.js +151 -0
- package/src/ans/eviction.js +144 -0
- package/src/ans/guard.js +120 -0
- package/src/ans/lifecycle.js +93 -0
- package/src/ans/proactive.js +129 -0
- package/src/ans/reward.js +112 -0
- package/src/ans/values.js +15 -0
- package/src/audit/audit-chain.js +167 -0
- package/src/audit/verifier.js +120 -0
- package/src/bus/circuit-breaker.js +115 -0
- package/src/bus/runtime-bus.js +94 -0
- package/src/channels/base.js +35 -0
- package/src/channels/feishu.js +127 -0
- package/src/channels/http.js +592 -0
- package/src/channels/index.js +110 -0
- package/src/channels/log.js +29 -0
- package/src/channels/wechat-crypto.js +74 -0
- package/src/channels/wechat.js +197 -0
- package/src/channels-cli.js +124 -0
- package/src/cli.js +120 -0
- package/src/config/channels.js +170 -0
- package/src/config/index.js +224 -0
- package/src/config/providers.js +189 -0
- package/src/config/settings.js +182 -0
- package/src/core/policy.js +272 -0
- package/src/core/trace.js +89 -0
- package/src/evolve/playbook.js +194 -0
- package/src/llm/client.js +446 -0
- package/src/llm/dsml.js +74 -0
- package/src/llm/embedder.js +35 -0
- package/src/llm/fence.js +105 -0
- package/src/llm/index.js +4 -0
- package/src/llm/retry.js +73 -0
- package/src/llm/router.js +98 -0
- package/src/mcp/client.js +375 -0
- package/src/mcp/index.js +116 -0
- package/src/memory/asset-hub.js +131 -0
- package/src/memory/canvas.js +131 -0
- package/src/memory/compaction.js +28 -0
- package/src/memory/experience.js +122 -0
- package/src/memory/fact-store.js +699 -0
- package/src/memory/failure-episode.js +99 -0
- package/src/memory/fork.js +83 -0
- package/src/memory/index.js +7 -0
- package/src/memory/l0.js +52 -0
- package/src/memory/l2.js +131 -0
- package/src/memory/l3.js +112 -0
- package/src/memory/memory-ticker.js +240 -0
- package/src/memory/session.js +398 -0
- package/src/mode/blackboard.js +49 -0
- package/src/mode/graph.js +41 -0
- package/src/mode/index.js +64 -0
- package/src/mode/legion.js +51 -0
- package/src/mode/plan-exec.js +50 -0
- package/src/mode/router.js +40 -0
- package/src/orchestrator/agent-worker.js +70 -0
- package/src/orchestrator/dag.js +83 -0
- package/src/orchestrator/index.js +2 -0
- package/src/orchestrator/legion.js +188 -0
- package/src/orchestrator/supervisor.js +177 -0
- package/src/persona/index.js +29 -0
- package/src/plugin/builtin.js +212 -0
- package/src/plugin/context.js +79 -0
- package/src/plugin/index.js +62 -0
- package/src/seam/registry.js +98 -0
- package/src/seam/shell.js +55 -0
- package/src/selfheal/evolve.js +68 -0
- package/src/selfheal/healer.js +167 -0
- package/src/selfheal/run.js +9 -0
- package/src/server.js +60 -0
- package/src/services/learning-service.js +177 -0
- package/src/services/memory-health.js +99 -0
- package/src/services/memory-service.js +160 -0
- package/src/skills/loader.js +150 -0
- package/src/skills/verify.js +100 -0
- package/src/tools/advanced.js +353 -0
- package/src/tools/builtin.js +298 -0
- package/src/tools/catalog.js +159 -0
- package/src/tools/command-guard.js +112 -0
- package/src/tools/custom.js +47 -0
- package/src/tools/delegate.js +297 -0
- package/src/tools/document.js +253 -0
- package/src/tools/governance.js +260 -0
- package/src/tools/index.js +11 -0
- package/src/tools/methods.js +178 -0
- package/src/tools/ocr.js +59 -0
- package/src/tools/seam.js +125 -0
- package/src/tools/selfmod.js +176 -0
- package/src/utils/logger.js +17 -0
- package/src/utils/pii.js +42 -0
- package/src/utils/store.js +108 -0
- package/src/utils/text.js +16 -0
- package/src/utils/trace.js +153 -0
- package/src/utils/winutf8.js +15 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// src/tools/methods.js - 方法型 Skill (借鉴 7 大神级 Skill)
|
|
2
|
+
// 零依赖: 全部通过 LLM 多阶段调用实现, 不引入外部包
|
|
3
|
+
// 1. humanize <- Humanizer-zh : 去 AI 味
|
|
4
|
+
// 2. write_article <- writing-agent : 分阶段写作
|
|
5
|
+
// 3. clarify <- Superpowers : 需求澄清 (信息不足先问)
|
|
6
|
+
|
|
7
|
+
// 内部: 用 agent 的 LLM 做一次无工具对话
|
|
8
|
+
async function llmChat(agent, system, user) {
|
|
9
|
+
if (!agent || !agent.llm) throw new Error("未配置 LLM provider, 方法型 Skill 不可用");
|
|
10
|
+
const r = await agent.llm.chat([
|
|
11
|
+
{ role: "system", content: system },
|
|
12
|
+
{ role: "user", content: user },
|
|
13
|
+
]);
|
|
14
|
+
return r.content;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function textOf(v, fallback) {
|
|
18
|
+
return String(v ?? fallback).trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function registerMethodTools(catalog) {
|
|
22
|
+
// ---------- 1. humanize: 去 AI 味 (Humanizer-zh) ----------
|
|
23
|
+
catalog.register({
|
|
24
|
+
name: "humanize",
|
|
25
|
+
description: "去除文本的 AI 模板腔。检查宣传腔、过度排比、模糊归因、连接词过多、句式重复、空话套话, 返回改写的自然版本。适合公众号稿、汇报、产品介绍。",
|
|
26
|
+
parameters: {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {
|
|
29
|
+
text: { type: "string", description: "要检查改写的文本" },
|
|
30
|
+
mode: { type: "string", enum: ["check", "rewrite"], description: "check=只列问题, rewrite=直接改写(默认)" },
|
|
31
|
+
},
|
|
32
|
+
required: ["text"],
|
|
33
|
+
},
|
|
34
|
+
execute: async (args, ctx) => {
|
|
35
|
+
const text = textOf(args.text, "");
|
|
36
|
+
if (!text) return "[工具错误] humanize: 缺少 text";
|
|
37
|
+
const mode = args.mode === "check" ? "check" : "rewrite";
|
|
38
|
+
const system = "你是文本去AI味专家。检查并消除这些痕迹: ①宣传腔/夸大空话 ②过度排比(句式重复堆叠) ③模糊归因(Experts say/行业报告称 无出处) ④连接词过多(首先/其次/总之/因此 连用) ⑤破折号狂魔 ⑥空洞收尾(未来可期/前景光明) ⑦奉承腔(Great question!/说得太对了)。输出自然、有真人质感的中文。不解释, 直接给结果。";
|
|
39
|
+
const user = mode === "rewrite"
|
|
40
|
+
? `请改写下面文本, 保留原意但去掉所有AI腔:\n\n${text}`
|
|
41
|
+
: `请逐项检查下面文本的AI痕迹, 用列表列出问题(每项: 位置+问题+修改建议):\n\n${text}`;
|
|
42
|
+
try {
|
|
43
|
+
const out = await llmChat(ctx.agent, system, user);
|
|
44
|
+
return out || "(无输出)";
|
|
45
|
+
} catch (e) {
|
|
46
|
+
return `[工具错误] humanize: ${e.message}`;
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// ---------- 2. write_article: 分阶段写作 (writing-agent) ----------
|
|
52
|
+
catalog.register({
|
|
53
|
+
name: "write_article",
|
|
54
|
+
description: "分阶段写长文: 选题→结构→初稿→审稿→修改→导出。适合公众号文章、产品介绍、课程内容、需要反复修改的长文。",
|
|
55
|
+
parameters: {
|
|
56
|
+
type: "object",
|
|
57
|
+
properties: {
|
|
58
|
+
topic: { type: "string", description: "主题" },
|
|
59
|
+
audience: { type: "string", description: "读者是谁 (可选)" },
|
|
60
|
+
length: { type: "string", description: "字数要求 (可选)" },
|
|
61
|
+
tone: { type: "string", description: "语气风格 (可选)" },
|
|
62
|
+
},
|
|
63
|
+
required: ["topic"],
|
|
64
|
+
},
|
|
65
|
+
execute: async (args, ctx) => {
|
|
66
|
+
const topic = textOf(args.topic, "");
|
|
67
|
+
if (!topic) return "[工具错误] write_article: 缺少 topic";
|
|
68
|
+
const audience = textOf(args.audience, "undefined");
|
|
69
|
+
const length = textOf(args.length, "undefined");
|
|
70
|
+
const tone = textOf(args.tone, "undefined");
|
|
71
|
+
const system = "你是资深内容创作总编。写长文必须走完整流程, 先规划再动笔, 每步都交代清楚再进下一步。";
|
|
72
|
+
const user = `写一篇关于「${topic}」的文章。\n读者: ${audience}\n字数: ${length}\n语气: ${tone}\n\n请按流程输出:\n【1.选题确认】一句话说清本文核心观点和读者收益\n【2.结构】列出大纲(标题+各段要点)\n【3.初稿】按结构写出完整正文\n【4.审稿】列出初稿的问题(事实/逻辑/语气)\n【5.修改稿】根据审稿优化后的最终版本\n【6.导出】给出可用标题(3个备选)+文章定稿`;
|
|
73
|
+
try {
|
|
74
|
+
const out = await llmChat(ctx.agent, system, user);
|
|
75
|
+
return out || "(无输出)";
|
|
76
|
+
} catch (e) {
|
|
77
|
+
return `[工具错误] write_article: ${e.message}`;
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ---------- 3. clarify: 需求澄清 (Superpowers) ----------
|
|
83
|
+
catalog.register({
|
|
84
|
+
name: "clarify",
|
|
85
|
+
description: "需求澄清: 面对模糊任务先问清需求再动手, 避免返工。传入任务描述, 返回需要澄清的问题清单; 若信息足够则直接给出执行方案。适合改代码/做项目前使用。",
|
|
86
|
+
parameters: {
|
|
87
|
+
type: "object",
|
|
88
|
+
properties: {
|
|
89
|
+
task: { type: "string", description: "任务描述" },
|
|
90
|
+
context: { type: "string", description: "已知背景/已了解的信息 (可选)" },
|
|
91
|
+
},
|
|
92
|
+
required: ["task"],
|
|
93
|
+
},
|
|
94
|
+
execute: async (args, ctx) => {
|
|
95
|
+
const task = textOf(args.task, "");
|
|
96
|
+
if (!task) return "[工具错误] clarify: 缺少 task";
|
|
97
|
+
const context = textOf(args.context, "无额外背景");
|
|
98
|
+
const system = "你是需求澄清专家。面对模糊任务, 先判断信息是否足够执行。若不足, 列出必须澄清的关键问题(≤5个, 只问真正影响执行的问题, 不啰嗦); 若已足够, 给出简明执行方案(步骤+风险+受影响的文件/模块)。不编造, 不确定就列问题。";
|
|
99
|
+
const user = `任务: ${task}\n已知背景: ${context}\n\n请判断信息是否足够, 不足则问关键问题, 足够则给执行方案。`;
|
|
100
|
+
try {
|
|
101
|
+
const out = await llmChat(ctx.agent, system, user);
|
|
102
|
+
return out || "(无输出)";
|
|
103
|
+
} catch (e) {
|
|
104
|
+
return `[工具错误] clarify: ${e.message}`;
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ---------- 场景系统: 类似灵魂文件的场景设定 ----------
|
|
110
|
+
catalog.register({
|
|
111
|
+
name: "scene_create",
|
|
112
|
+
description: "创建/更新一个场景(人设)。每个场景定义智能体在这个情境下能帮用户干什么, 类似灵魂文件。可手动设定名称/介绍/能力。",
|
|
113
|
+
parameters: {
|
|
114
|
+
type: "object",
|
|
115
|
+
properties: {
|
|
116
|
+
name: { type: "string", description: "场景名称, 如: A股交易助手" },
|
|
117
|
+
description: { type: "string", description: "场景介绍, 说明这个场景是干嘛的" },
|
|
118
|
+
canHelp: { type: "string", description: "这个场景能帮用户干什么(能力清单)" },
|
|
119
|
+
keywords: { type: "array", items: { type: "string" }, description: "触发关键词(可选)" },
|
|
120
|
+
},
|
|
121
|
+
required: ["name", "description", "canHelp"],
|
|
122
|
+
},
|
|
123
|
+
execute: async (args, ctx) => {
|
|
124
|
+
if (!ctx.agent) return "[工具错误] scene_create: 缺少 agent 上下文";
|
|
125
|
+
const s = ctx.agent.scenes.create({
|
|
126
|
+
name: args.name, description: args.description, canHelp: args.canHelp, keywords: args.keywords,
|
|
127
|
+
});
|
|
128
|
+
return JSON.stringify({ ok: true, id: s.id, name: s.name, mode: "manual" });
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
catalog.register({
|
|
133
|
+
name: "scene_list",
|
|
134
|
+
description: "列出所有场景及其介绍/能力。",
|
|
135
|
+
parameters: { type: "object", properties: {} },
|
|
136
|
+
execute: async (args, ctx) => {
|
|
137
|
+
if (!ctx.agent) return "[工具错误] scene_list: 缺少 agent 上下文";
|
|
138
|
+
const list = ctx.agent.scenes.listWithDesc();
|
|
139
|
+
return list.length ? list.map((s) => `- [${s.mode}] ${s.name}: ${s.description} | 能帮: ${s.canHelp} | ${s.facts}条记忆`).join("\n") : "(暂无场景)";
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
catalog.register({
|
|
144
|
+
name: "scene_describe",
|
|
145
|
+
description: "用 LLM 从历史对话提炼场景介绍和能力。给定场景名, 自动总结该场景的用途和能帮用户干什么。",
|
|
146
|
+
parameters: {
|
|
147
|
+
type: "object",
|
|
148
|
+
properties: { name: { type: "string", description: "要提炼的场景名" } },
|
|
149
|
+
required: ["name"],
|
|
150
|
+
},
|
|
151
|
+
execute: async (args, ctx) => {
|
|
152
|
+
const name = textOf(args.name, "");
|
|
153
|
+
if (!name) return "[工具错误] scene_describe: 缺少 name";
|
|
154
|
+
if (!ctx.agent) return "[工具错误] scene_describe: 缺少 agent";
|
|
155
|
+
// 找场景
|
|
156
|
+
const scene = ctx.agent.scenes.scenes.find((i) => i.name === name || i.name.includes(name));
|
|
157
|
+
if (!scene) return `[工具错误] scene_describe: 未找到场景 ${name}`;
|
|
158
|
+
const facts = (scene.facts || []).map((f) => f.content).slice(-10).join("\n");
|
|
159
|
+
const system = "你是场景分析器。根据场景的历史对话, 提炼出: ①场景简介(一句话) ②这个场景能帮用户干什么(能力清单, 3-5项)。直接给结果, 格式: 简介:xxx\\n能力: - xxx\\n - xxx";
|
|
160
|
+
const user = `场景: ${scene.name}\\n历史对话:\\n${facts || "(无)"}`;
|
|
161
|
+
try {
|
|
162
|
+
const out = await llmChat(ctx.agent, system, user);
|
|
163
|
+
// v1.0.9: LLM 输出未含"能力"段时保留旧值 (原 split("能力")[0] 会拿整段污染 description)
|
|
164
|
+
if (out.includes("能力")) {
|
|
165
|
+
scene.description = out.split("能力")[0].replace("简介:", "").trim().slice(0, 300) || scene.description;
|
|
166
|
+
scene.canHelp = out.split("能力")[1]?.slice(0, 300) || scene.canHelp;
|
|
167
|
+
}
|
|
168
|
+
scene.mode = "manual";
|
|
169
|
+
ctx.agent.scenes._save();
|
|
170
|
+
return out || "(无输出)";
|
|
171
|
+
} catch (e) {
|
|
172
|
+
return `[工具错误] scene_describe: ${e.message}`;
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return catalog;
|
|
178
|
+
}
|
package/src/tools/ocr.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// src/tools/ocr.js - OCR 光学字符识别 (零依赖, 可插拔)
|
|
2
|
+
// 主通道: 本地 tesseract 二进制 (零 key 零网络, 需系统安装)
|
|
3
|
+
// 回退: 百度 OCR 云 API (需 BAIDU_OCR_API_KEY / BAIDU_OCR_SECRET_KEY)
|
|
4
|
+
// 用于: 扫描件 PDF / 图片里的文字识别 (read_image 读图后无法理解文字时)
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
|
|
9
|
+
const execFileP = promisify(execFile);
|
|
10
|
+
|
|
11
|
+
// 检测 tesseract 是否可用 (注入 _exec 便于测试)
|
|
12
|
+
export async function tesseractAvailable(bin = "tesseract", _exec = execFileP) {
|
|
13
|
+
try {
|
|
14
|
+
await _exec(bin, ["--version"], { timeout: 5000, windowsHide: true });
|
|
15
|
+
return true;
|
|
16
|
+
} catch { return false; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// tesseract 识别图片 → 文字 (stdout 直接输出识别结果)
|
|
20
|
+
export async function ocrWithTesseract(filePath, { bin = "tesseract", lang = "chi_sim", timeoutMs = 30000, _exec = execFileP } = {}) {
|
|
21
|
+
const { stdout, stderr } = await _exec(bin, [filePath, "stdout", "-l", lang], {
|
|
22
|
+
timeout: timeoutMs, maxBuffer: 10 * 1024 * 1024, windowsHide: true,
|
|
23
|
+
});
|
|
24
|
+
const text = String(stdout || "").trim();
|
|
25
|
+
if (!text && stderr) throw new Error("tesseract 未识别出文字: " + String(stderr).slice(0, 200));
|
|
26
|
+
return text;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 百度 OCR: 取 access_token → 通用文字识别
|
|
30
|
+
async function ocrWithBaidu(filePath, { apiKey, secretKey }) {
|
|
31
|
+
const tok = await fetch(
|
|
32
|
+
`https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=${encodeURIComponent(apiKey)}&client_secret=${encodeURIComponent(secretKey)}`,
|
|
33
|
+
{ method: "POST", signal: AbortSignal.timeout(15000) },
|
|
34
|
+
).then((r) => r.json());
|
|
35
|
+
if (!tok.access_token) throw new Error("百度 OCR token 获取失败: " + (tok.error_description || tok.error || "未知"));
|
|
36
|
+
const img = fs.readFileSync(filePath).toString("base64");
|
|
37
|
+
const body = new URLSearchParams({ image: img, language_type: "CHN_ENG" });
|
|
38
|
+
const r = await fetch(`https://aip.baidubce.com/rest/2.0/ocr/v1/general_basic?access_token=${tok.access_token}`, {
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
41
|
+
body,
|
|
42
|
+
signal: AbortSignal.timeout(20000),
|
|
43
|
+
});
|
|
44
|
+
const j = await r.json();
|
|
45
|
+
if (j.error_code) throw new Error("百度 OCR 失败: " + (j.error_msg || j.error_code));
|
|
46
|
+
return (j.words_result || []).map((w) => w.words).join("\n");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// OCR 主入口: tesseract 优先, 云 OCR 回退, 都不可用抛中文引导
|
|
50
|
+
export async function ocrImage(filePath, { tesseract = "tesseract", lang = "chi_sim", cloud = null, _exec = execFileP } = {}) {
|
|
51
|
+
if (!fs.existsSync(filePath)) throw new Error("文件不存在: " + filePath);
|
|
52
|
+
if (await tesseractAvailable(tesseract, _exec)) {
|
|
53
|
+
return ocrWithTesseract(filePath, { bin: tesseract, lang, _exec });
|
|
54
|
+
}
|
|
55
|
+
if (cloud && cloud.apiKey && cloud.secretKey) {
|
|
56
|
+
return ocrWithBaidu(filePath, { apiKey: cloud.apiKey, secretKey: cloud.secretKey });
|
|
57
|
+
}
|
|
58
|
+
throw new Error("OCR 不可用: 请安装 tesseract (含中文语言包) 或配置 config.ocr 的云 OCR key");
|
|
59
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// src/tools/seam.js - 能力缝(Capability Seam)辅助层
|
|
2
|
+
// 参考 deepseek-harness 的 Capability Seam 三分法:
|
|
3
|
+
// Service Definition(声明/元数据) / Service Provider(execute 实现) / Consumer(runWithPolicy 统一策略入口)
|
|
4
|
+
// 零依赖, 纯 Node 原生。保留皮皮虾原有错误语义, 追加超时门禁/禁用门禁/追踪回调。
|
|
5
|
+
|
|
6
|
+
export const TOOL_ERROR_PREFIX = "[工具错误]";
|
|
7
|
+
|
|
8
|
+
// power 权限级: user < agent < super
|
|
9
|
+
export const POWER_LEVEL = { user: 0, agent: 1, super: 2 };
|
|
10
|
+
|
|
11
|
+
// ---- Definition 层: 元数据归一化 + 校验 ----
|
|
12
|
+
export function normalizeMeta(def = {}) {
|
|
13
|
+
if (!def || typeof def.name !== "string" || !def.name) {
|
|
14
|
+
throw new Error("能力缝 Definition 失败: 需 name");
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
name: def.name,
|
|
18
|
+
description: def.description || "",
|
|
19
|
+
parameters: def.parameters || { type: "object", properties: {}, required: [] },
|
|
20
|
+
// 能力缝新增元数据
|
|
21
|
+
category: def.category || "misc", // 能力域: file/net/system/memory/selfmod/...
|
|
22
|
+
power: def.power || "user", // 权限级: user/agent(0栓塞)
|
|
23
|
+
timeoutMs: Number(def.timeoutMs) || 0, // 0 = 不限时
|
|
24
|
+
idempotent: !!def.idempotent, // 是否可安全重试
|
|
25
|
+
enabled: def.enabled !== false, // 默认启用
|
|
26
|
+
execute: def.execute,
|
|
27
|
+
// 工具钩子链 (吸收 OpenClaw before/after 钩子):
|
|
28
|
+
// before(args, ctx) -> undefined 继续 | 字符串短路 | throw 拒绝
|
|
29
|
+
// after(args, result, ctx) -> 后处理(结果审计/清理), 错误不阻塞
|
|
30
|
+
before: typeof def.before === "function" ? def.before : null,
|
|
31
|
+
after: typeof def.after === "function" ? def.after : null,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---- Consumer 层: 统一策略执行 ----
|
|
36
|
+
// 统一处理: 禁用门禁 / 实现缺失 / 超时门禁 / 标准错误语义 / 追踪回调
|
|
37
|
+
export async function runWithPolicy(meta, args, ctx = {}) {
|
|
38
|
+
if (meta.enabled === false) {
|
|
39
|
+
return `${TOOL_ERROR_PREFIX} ${meta.name}: 能力已禁用`;
|
|
40
|
+
}
|
|
41
|
+
// power 权限门禁: 仅当 ctx.power 明确提供时生效(向后兼容, 无 ctx.power 默认放行)
|
|
42
|
+
if (ctx && ctx.power) {
|
|
43
|
+
const need = POWER_LEVEL[meta.power] ?? 0;
|
|
44
|
+
const have = POWER_LEVEL[ctx.power] ?? 0;
|
|
45
|
+
if (have < need) {
|
|
46
|
+
return `${TOOL_ERROR_PREFIX} ${meta.name}: 权限不足(需要 ${meta.power}, 当前 ${ctx.power})`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const fn = meta.execute;
|
|
50
|
+
if (typeof fn !== "function") {
|
|
51
|
+
return `${TOOL_ERROR_PREFIX} ${meta.name}: 无实现(Provider 缺失)`;
|
|
52
|
+
}
|
|
53
|
+
// before 钩子: 返回非 undefined 则短路(不执行), throw 则拒绝
|
|
54
|
+
if (meta.before) {
|
|
55
|
+
try {
|
|
56
|
+
const shortCircuit = await meta.before(args, ctx);
|
|
57
|
+
if (shortCircuit !== undefined && shortCircuit !== null) {
|
|
58
|
+
return typeof shortCircuit === "string" ? shortCircuit : JSON.stringify(shortCircuit);
|
|
59
|
+
}
|
|
60
|
+
} catch (e) {
|
|
61
|
+
return `${TOOL_ERROR_PREFIX} ${meta.name}: before 钩子拒绝: ${e.message}`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
let timer = null;
|
|
65
|
+
let timedOut = false;
|
|
66
|
+
const ctrl = new AbortController();
|
|
67
|
+
// 超时预算: 工具级声明优先 (meta.timeoutMs > 0), 否则全局默认 (ctx.timeoutMs, agent 从 config.agent.tool_timeout_ms 传入)
|
|
68
|
+
// v1.6.0 (第四刀): 无声明工具不再永不超时 — 有保守全局默认兑底, 避免单个慢工具卡死整个对话
|
|
69
|
+
const effectiveTimeout = meta.timeoutMs > 0 ? meta.timeoutMs : (Number(ctx.timeoutMs) || 0);
|
|
70
|
+
if (effectiveTimeout > 0) {
|
|
71
|
+
timer = setTimeout(() => { timedOut = true; ctrl.abort(); }, effectiveTimeout);
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
// v1.6.0 (第四刀): 双层超时兜底 —
|
|
75
|
+
// 1) signal 传给 execute: 配合的工具提前终止释放资源 (资源超时)
|
|
76
|
+
// 2) Promise.race 强制超时返回: 不响应 signal 的工具也不至于永远挂住对话 (语义超时兜底)
|
|
77
|
+
// 这是文档指出的灰色地带: 光靠 abort 信号, 不配合的工具会无限期挂着。
|
|
78
|
+
const run = () => (fn.length >= 2 ? fn(args, { ...ctx, signal: ctrl.signal }) : fn(args));
|
|
79
|
+
let result;
|
|
80
|
+
if (effectiveTimeout > 0) {
|
|
81
|
+
let raceTimer = null;
|
|
82
|
+
const timeoutGuard = new Promise((_, reject) => {
|
|
83
|
+
raceTimer = setTimeout(() => reject(Object.assign(new Error("timeout"), { timedOut: true })), effectiveTimeout);
|
|
84
|
+
});
|
|
85
|
+
const pRun = Promise.resolve().then(run);
|
|
86
|
+
// 哨兵: 超时赢时工具方后到的 reject 忽略, 防 unhandledRejection 崩进程
|
|
87
|
+
pRun.catch(() => {});
|
|
88
|
+
try {
|
|
89
|
+
result = await Promise.race([pRun, timeoutGuard]);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
if (e && e.timedOut) timedOut = true;
|
|
92
|
+
throw e;
|
|
93
|
+
} finally {
|
|
94
|
+
if (raceTimer) clearTimeout(raceTimer);
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
result = await run();
|
|
98
|
+
}
|
|
99
|
+
if (timedOut) return `${TOOL_ERROR_PREFIX} ${meta.name}: 超时`;
|
|
100
|
+
if (meta.after) {
|
|
101
|
+
try { await meta.after(args, result, ctx); } catch { /* after 钩子错误不阻塞 */ }
|
|
102
|
+
}
|
|
103
|
+
if (typeof ctx.onResult === "function") ctx.onResult(meta.name, "ok", null);
|
|
104
|
+
return typeof result === "string" ? result : JSON.stringify(result);
|
|
105
|
+
} catch (e) {
|
|
106
|
+
if (typeof ctx.onResult === "function") ctx.onResult(meta.name, "error", e.message);
|
|
107
|
+
return `${TOOL_ERROR_PREFIX} ${meta.name}: ${timedOut ? "超时" : e.message}`;
|
|
108
|
+
} finally {
|
|
109
|
+
if (timer) clearTimeout(timer);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 供 selfmod 工具 / 追踪用的纯净元数据(不含 execute 实现)
|
|
114
|
+
export function toDescriptor(meta) {
|
|
115
|
+
return {
|
|
116
|
+
name: meta.name,
|
|
117
|
+
description: meta.description,
|
|
118
|
+
parameters: meta.parameters,
|
|
119
|
+
category: meta.category,
|
|
120
|
+
power: meta.power,
|
|
121
|
+
timeoutMs: meta.timeoutMs,
|
|
122
|
+
idempotent: meta.idempotent,
|
|
123
|
+
enabled: meta.enabled,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// src/tools/selfmod.js - Self-modification 工具
|
|
2
|
+
// 参考 deepseek-harness 的 self-modification: agent 能检查/挂载/卸载自己的运行时能力
|
|
3
|
+
// 这里落地为"能力级自修改": 枚举能力(工具+技能) / 启用 / 禁用 / 加载技能, 不破坏零依赖内核
|
|
4
|
+
import { SkillLoader } from "../skills/loader.js";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
function capErr(name, msg) {
|
|
9
|
+
return `[工具错误] ${name}: ${msg}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function registerSelfmodTools(catalog, { skillsDir }) {
|
|
13
|
+
const loader = new SkillLoader(skillsDir);
|
|
14
|
+
|
|
15
|
+
// 1. 枚举全部能力: 工具 + 技能
|
|
16
|
+
catalog.register({
|
|
17
|
+
name: "list_capabilities",
|
|
18
|
+
description: "枚举当前所有可用的工具能力(category/power/enabled) 和已安装技能。用于 agent 了解自己能干啥。",
|
|
19
|
+
parameters: { type: "object", properties: { kind: { type: "string", enum: ["tool", "skill", "all"], description: "all=工具+技能(默认)" } }, required: [] },
|
|
20
|
+
category: "selfmod",
|
|
21
|
+
power: "agent",
|
|
22
|
+
idempotent: true,
|
|
23
|
+
execute: async (args) => {
|
|
24
|
+
const kind = args && args.kind ? args.kind : "all";
|
|
25
|
+
const lines = [];
|
|
26
|
+
if (kind === "all" || kind === "tool") {
|
|
27
|
+
lines.push("— 工具 —");
|
|
28
|
+
for (const t of catalog.listDetailed()) {
|
|
29
|
+
lines.push(`[${t.enabled ? "ON" : "OFF"}] ${t.name} (${t.category}/${t.power})${t.timeoutMs ? ` 超时${t.timeoutMs}ms` : ""}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (kind === "all" || kind === "skill") {
|
|
33
|
+
lines.push("— 技能 —");
|
|
34
|
+
for (const s of loader.list()) {
|
|
35
|
+
lines.push(`${s.id}: ${s.name} — ${s.description}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return lines.join("\n");
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// 2. 启用能力
|
|
43
|
+
catalog.register({
|
|
44
|
+
name: "enable_capability",
|
|
45
|
+
description: "启用一个已注册但被禁用的工具能力。name 为工具名。",
|
|
46
|
+
parameters: { type: "object", properties: { name: { type: "string" } }, required: ["name"] },
|
|
47
|
+
category: "selfmod",
|
|
48
|
+
power: "agent",
|
|
49
|
+
execute: async (args) => {
|
|
50
|
+
if (!catalog.enable(args.name)) return capErr("enable_capability", `未知工具: ${args.name}`);
|
|
51
|
+
return `已启用: ${args.name}`;
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// 3. 禁用能力
|
|
56
|
+
catalog.register({
|
|
57
|
+
name: "disable_capability",
|
|
58
|
+
description: "禁用工具能力(不卸载, 可随时启用)。name 为工具名。禁用后该工具不再出现在 LLM schema 且调用被拒。",
|
|
59
|
+
parameters: { type: "object", properties: { name: { type: "string" } }, required: ["name"] },
|
|
60
|
+
category: "selfmod",
|
|
61
|
+
power: "agent",
|
|
62
|
+
execute: async (args) => {
|
|
63
|
+
if (!catalog.disable(args.name)) return capErr("disable_capability", `未知工具: ${args.name}`);
|
|
64
|
+
return `已禁用: ${args.name}`;
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// 4. 加载技能
|
|
69
|
+
catalog.register({
|
|
70
|
+
name: "load_skill",
|
|
71
|
+
description: "读取一个已安装技能的 SKILL.md 全文, 供 agent 按需加载使用。id 为技能名。",
|
|
72
|
+
parameters: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
|
|
73
|
+
category: "selfmod",
|
|
74
|
+
power: "user",
|
|
75
|
+
idempotent: true,
|
|
76
|
+
execute: async (args) => {
|
|
77
|
+
const content = loader.read(args.id);
|
|
78
|
+
if (content === null) return capErr("load_skill", `未知技能: ${args.id}`);
|
|
79
|
+
|
|
80
|
+
if (loader && typeof loader.trackUse === "function") { try { loader.trackUse(args.id); } catch {} }
|
|
81
|
+
return `# ${args.id}\n\n${content}`;
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// 5. 创建新技能 (L5 auto-skill: 复杂任务后沉淀为可复用 Skill)
|
|
86
|
+
catalog.register({
|
|
87
|
+
name: "create_skill",
|
|
88
|
+
description: "把一次成功的方法/流程沉淀为可复用的 Agent Skill。name=技能名(字母数字横线), description=一句话说明, content=SKILL.md 正文。正文推荐含三个段落(参考 addyosmani/agent-skills): ①「## 流程」逐步工作流+检查点 ②「## 反合理化」常见偷懒借口+反驳 ③「## 验证」完成后必须提供的证据。写进 skills/ 目录后自动被 loader 发现。",
|
|
89
|
+
parameters: {
|
|
90
|
+
type: "object",
|
|
91
|
+
properties: {
|
|
92
|
+
name: { type: "string", description: "技能名, 仅字母/数字/横线" },
|
|
93
|
+
description: { type: "string", description: "技能一句话说明" },
|
|
94
|
+
content: { type: "string", description: "SKILL.md 正文 (建议含: 流程/反合理化/验证 三段)" },
|
|
95
|
+
},
|
|
96
|
+
required: ["name", "description", "content"],
|
|
97
|
+
},
|
|
98
|
+
category: "selfmod",
|
|
99
|
+
power: "agent",
|
|
100
|
+
execute: async (args) => {
|
|
101
|
+
const name = String(args.name || "").trim();
|
|
102
|
+
if (!/^[a-zA-Z0-9-]+$/.test(name)) return "[工具错误] create_skill: 技能名仅允许字母/数字/横线: " + name;
|
|
103
|
+
const desc = String(args.description || "").trim();
|
|
104
|
+
const content = String(args.content || "").trim();
|
|
105
|
+
if (!desc || !content) return "[工具错误] create_skill: 需 description + content";
|
|
106
|
+
// v1.0.8: 长度上限, 防写超大文件/垃圾内容
|
|
107
|
+
if (desc.length > 300) return "[工具错误] create_skill: description 超长 (最大 300 字符)";
|
|
108
|
+
if (content.length > 50000) return "[工具错误] create_skill: content 超长 (最大 50000 字符)";
|
|
109
|
+
const dir = path.join(skillsDir, name);
|
|
110
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
111
|
+
const frontmatter = "---" + "\n" + "name: " + name + "\n" + "description: " + desc + "\n" + "---" + "\n" + "\n";
|
|
112
|
+
fs.writeFileSync(path.join(dir, "SKILL.md"), frontmatter + content, "utf8");
|
|
113
|
+
return "已创建技能: " + name + " (skills/" + name + "/SKILL.md)";
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// 6b. 自我进化: 从失败轨迹自动提炼经验 (refine 的上半场, 补齐「失败→经验」闭环)
|
|
118
|
+
catalog.register({
|
|
119
|
+
name: "refine",
|
|
120
|
+
description: "从最近的失败工具调用轨迹自动提炼一条可复用经验教训 (自我进化闭环)。失败轨迹足够(≥2条)时, 用 LLM 提炼成一句话经验存进经验库, 后续任务自动注入上下文。",
|
|
121
|
+
parameters: { type: "object", properties: { limit: { type: "number", description: "回看轨迹条数, 默认 20" } }, required: [] },
|
|
122
|
+
category: "selfmod",
|
|
123
|
+
power: "agent",
|
|
124
|
+
execute: async (args, ctx) => {
|
|
125
|
+
const agent = ctx && ctx.agent;
|
|
126
|
+
if (!agent || typeof agent.refine !== "function") return capErr("refine", "无 agent 上下文");
|
|
127
|
+
const r = await agent.refine({ limit: Number(args && args.limit) || 20 });
|
|
128
|
+
return JSON.stringify(r);
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// 7. 自我进化: 从成功轨迹自动提炼可复用 Skill (refine 的下半场)
|
|
133
|
+
catalog.register({
|
|
134
|
+
name: "refine_skill",
|
|
135
|
+
description: "从最近成功的工具调用轨迹自动提炼一个可复用 Skill (自我进化闭环)。成功轨迹足够且高频工具重复出现时, 用 LLM 提炼成 skills/<name>/SKILL.md。",
|
|
136
|
+
parameters: { type: "object", properties: { limit: { type: "number", description: "回看轨迹条数, 默认 50" } }, required: [] },
|
|
137
|
+
category: "selfmod",
|
|
138
|
+
power: "agent",
|
|
139
|
+
execute: async (args, ctx) => {
|
|
140
|
+
const agent = ctx && ctx.agent;
|
|
141
|
+
if (!agent || typeof agent.refineSkill !== "function") return capErr("refine_skill", "无 agent 上下文");
|
|
142
|
+
const r = await agent.refineSkill({ limit: Number(args && args.limit) || 50 });
|
|
143
|
+
return JSON.stringify(r);
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// 6. Session Replay: 从原始日志恢复会话历史 (跨天/崩溃续跑)
|
|
148
|
+
catalog.register({
|
|
149
|
+
name: "replay_session",
|
|
150
|
+
description: "从原始对话日志恢复某会话的历史(跨天/崩溃后续跑)。sessionKey=会话名(默认default), days=回溯天数(默认7), limit=返回条数(默认40)。",
|
|
151
|
+
parameters: {
|
|
152
|
+
type: "object",
|
|
153
|
+
properties: {
|
|
154
|
+
sessionKey: { type: "string", description: "会话名, 默认 default" },
|
|
155
|
+
days: { type: "number", description: "回溯天数, 默认 7" },
|
|
156
|
+
limit: { type: "number", description: "返回条数, 默认 40" },
|
|
157
|
+
},
|
|
158
|
+
required: [],
|
|
159
|
+
},
|
|
160
|
+
category: "selfmod",
|
|
161
|
+
power: "user",
|
|
162
|
+
idempotent: true,
|
|
163
|
+
execute: async (args, ctx) => {
|
|
164
|
+
const agent = ctx && ctx.agent;
|
|
165
|
+
if (!agent || !agent.l0) return capErr("replay_session", "无 l0 记录器");
|
|
166
|
+
const msgs = agent.replaySession((args && args.sessionKey) || "default", {
|
|
167
|
+
days: Number(args && args.days) || 7,
|
|
168
|
+
limit: Number(args && args.limit) || 40,
|
|
169
|
+
});
|
|
170
|
+
if (!msgs.length) return "(该会话无历史记录)";
|
|
171
|
+
return msgs.map(m => `${m.role}: ${m.content}`).join("\n");
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
return catalog;
|
|
176
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// src/utils/logger.js - 分级日志
|
|
2
|
+
const LEVELS = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
3
|
+
|
|
4
|
+
let minLevel = LEVELS.info;
|
|
5
|
+
|
|
6
|
+
export function setLevel(lv) {
|
|
7
|
+
if (lv in LEVELS) minLevel = LEVELS[lv];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function ts() {
|
|
11
|
+
return new Date().toISOString();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function debug(...a) { if (minLevel <= LEVELS.debug) console.log(`[${ts()}] [debug]`, ...a); }
|
|
15
|
+
export function info(...a) { if (minLevel <= LEVELS.info) console.log(`[${ts()}] [info]`, ...a); }
|
|
16
|
+
export function warn(...a) { if (minLevel <= LEVELS.warn) console.log(`[${ts()}] [warn]`, ...a); }
|
|
17
|
+
export function error(...a) { if (minLevel <= LEVELS.error) console.error(`[${ts()}] [error]`, ...a); }
|
package/src/utils/pii.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// src/utils/pii.js - PII / 凭证检测与脱敏 (架构参考 openhanako pii-guard)
|
|
2
|
+
const HARD_PATTERNS = [
|
|
3
|
+
{ name: "api_key", regex: /\b(sk-[a-zA-Z0-9]{20,}|AKIA[A-Z0-9]{16}|gsk_[a-zA-Z0-9]{20,}|ghp_[a-zA-Z0-9]{36}|glpat-[a-zA-Z0-9_-]{20,}|xoxb-[a-zA-Z0-9-]+)\b/g },
|
|
4
|
+
// v1.0.8: 放宽 inline_secret 值域 (含 :/# 等) + 8 位起, 短密钥不漏检 (原 16+ 位且不含 :#)
|
|
5
|
+
// P0 (2026-09-15): key 后允许可选闭合引号 —— 原版只匹配 key=value / key: value,
|
|
6
|
+
// JSON 序列化形式 "key":"value" (key 后先有闭合引号) 完全漏检, 已修。
|
|
7
|
+
{ name: "inline_secret", regex: /\b(api[_-]?key|secret[_-]?key|access[_-]?token|auth[_-]?token|password|bearer)["']?\s*[:=]\s*["']?([a-zA-Z0-9_/+=\-.:#]{8,})["']?/gi },
|
|
8
|
+
{ name: "private_key", regex: /-----BEGIN\s+(RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g },
|
|
9
|
+
{ name: "credit_card", regex: /\b(?:\d{4}[- ]?){3}\d{4}\b/g },
|
|
10
|
+
{ name: "id_card", regex: /\b\d{6}(?:19|20)\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])\d{3}[\dXx]\b/g },
|
|
11
|
+
{ name: "ssn", regex: /\b\d{3}-\d{2}-\d{4}\b/g },
|
|
12
|
+
// v1.0.8: 邮箱 + 中国大陆手机号 (11 位, 1[3-9] 开头)
|
|
13
|
+
{ name: "email", regex: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g },
|
|
14
|
+
{ name: "phone", regex: /\b1[3-9]\d{9}\b/g },
|
|
15
|
+
// v1.6.0-dev: URL query 里的敏感参数值 (token/key/secret/sign 等) — 最常见的凭证泄漏渠道, 此前未覆盖
|
|
16
|
+
// 仅匹配参数名白名单, 避免误伤合法 URL 参数; 保留参数名, 只替换值
|
|
17
|
+
{ name: "url_secret", regex: /([?&](?:token|key|secret|api[_-]?key|access[_-]?token|auth|sign|sig|password)=)[^&#\s"']*/gi, redact: (m, pre) => pre + "[REDACTED]" },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export function scrubPII(text) {
|
|
21
|
+
if (!text) return { cleaned: text, detected: [] };
|
|
22
|
+
const detected = [];
|
|
23
|
+
let cleaned = text;
|
|
24
|
+
for (const { name, regex, redact } of HARD_PATTERNS) {
|
|
25
|
+
regex.lastIndex = 0;
|
|
26
|
+
if (regex.test(cleaned)) {
|
|
27
|
+
detected.push(name);
|
|
28
|
+
regex.lastIndex = 0;
|
|
29
|
+
cleaned = cleaned.replace(regex, redact ? redact : "[REDACTED]");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { cleaned, detected };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function hasPII(text) {
|
|
36
|
+
if (!text) return false;
|
|
37
|
+
for (const { regex } of HARD_PATTERNS) {
|
|
38
|
+
regex.lastIndex = 0;
|
|
39
|
+
if (regex.test(text)) return true;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|