ppxans-harness 2.4.0 → 3.2.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.
Files changed (186) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +218 -265
  3. package/bin/ppx-channels.js +2 -2
  4. package/bin/ppx-serve.js +5 -5
  5. package/bin/ppx-setup.js +124 -0
  6. package/bin/ppx-web.js +140 -0
  7. package/bin/ppx.js +2 -2
  8. package/config/identity.md +6 -6
  9. package/config/ishiki.md +16 -16
  10. package/config/ppx.json +15 -151
  11. package/config/ppx.json.example +143 -0
  12. package/package.json +17 -10
  13. package/skills/.usage.json +6 -0
  14. package/skills/agent-professional-training/SKILL.md +94 -0
  15. package/skills/brainstorm/SKILL.md +24 -0
  16. package/skills/cupid-lover-comms/SKILL.md +37 -0
  17. package/skills/debug/SKILL.md +26 -0
  18. package/skills/plan/SKILL.md +25 -0
  19. package/skills/ponytail/SKILL.md +25 -0
  20. package/skills/ppx-memory/SKILL.md +91 -0
  21. package/skills/ppx-memory/scripts/cli.js +192 -0
  22. package/skills/ppx-memory/scripts/experience.js +133 -0
  23. package/skills/ppx-memory/scripts/fact-store.js +842 -0
  24. package/skills/ppx-memory/scripts/l0.js +52 -0
  25. package/skills/ppx-memory/scripts/l2.js +146 -0
  26. package/skills/ppx-memory/scripts/l3.js +112 -0
  27. package/skills/ppx-memory/scripts/memory-ticker.js +238 -0
  28. package/skills/ppx-memory/scripts/pii.js +42 -0
  29. package/skills/ppx-memory/scripts/schema.js +80 -0
  30. package/skills/ppx-memory/scripts/session.js +398 -0
  31. package/skills/ppx-memory/scripts/similarity.js +43 -0
  32. package/skills/ppx-memory/scripts/store.js +116 -0
  33. package/skills/ppx-memory/scripts/wal.js +38 -0
  34. package/skills/ppx-selfheal/SKILL.md +24 -0
  35. package/skills/ppx-selfheal/scripts/cli.js +80 -0
  36. package/skills/ppx-selfheal/scripts/healer.js +184 -0
  37. package/skills/ppx-selfheal/scripts/logger.js +17 -0
  38. package/skills/ppx-selfheal/scripts/store.js +116 -0
  39. package/skills/prompt-depth-kit/SKILL.md +28 -0
  40. package/skills/session-naming/SKILL.md +36 -0
  41. package/skills/verify/SKILL.md +25 -0
  42. package/src/agent/index.js +1340 -717
  43. package/src/agent/prompts.js +47 -3
  44. package/src/aml-server.js +197 -151
  45. package/src/ans/eviction.js +123 -143
  46. package/src/ans/guard.js +159 -120
  47. package/src/ans/lifecycle.js +96 -93
  48. package/src/ans/proactive.js +112 -129
  49. package/src/ans/reward.js +95 -111
  50. package/src/ans/values.js +15 -15
  51. package/src/audit/audit-chain.js +43 -7
  52. package/src/audit/verifier.js +157 -120
  53. package/src/bus/circuit-breaker.js +9 -1
  54. package/src/bus/runtime-bus.js +107 -93
  55. package/src/channels/base.js +57 -34
  56. package/src/channels/feishu.js +118 -126
  57. package/src/channels/http.js +1090 -592
  58. package/src/channels/index.js +111 -110
  59. package/src/channels/log.js +29 -29
  60. package/src/channels/wechat-crypto.js +73 -74
  61. package/src/channels/wechat.js +191 -197
  62. package/src/channels/workspace.js +94 -0
  63. package/src/channels-cli.js +126 -124
  64. package/src/cli.js +129 -120
  65. package/src/commands/index.js +142 -0
  66. package/src/config/channels.js +137 -170
  67. package/src/config/index.js +277 -224
  68. package/src/config/placeholder.js +31 -0
  69. package/src/config/providers.js +148 -188
  70. package/src/config/settings.js +156 -182
  71. package/src/core/policy.js +69 -18
  72. package/src/core/trace.js +8 -6
  73. package/src/edit/editblock.js +266 -0
  74. package/src/edit/snapshot.js +67 -0
  75. package/src/evidence/index.js +153 -0
  76. package/src/evolve/playbook.js +9 -10
  77. package/src/hooks/index.js +113 -0
  78. package/src/llm/client.js +188 -446
  79. package/src/llm/dsml.js +74 -74
  80. package/src/llm/embedder.js +41 -35
  81. package/src/llm/fence.js +52 -105
  82. package/src/llm/index.js +4 -4
  83. package/src/llm/local-embedder.js +94 -0
  84. package/src/llm/presets.js +113 -0
  85. package/src/llm/pricing.js +93 -0
  86. package/src/llm/retry.js +73 -73
  87. package/src/llm/router.js +92 -97
  88. package/src/mcp/admin.js +326 -0
  89. package/src/mcp/client.js +487 -375
  90. package/src/mcp/http.js +203 -0
  91. package/src/mcp/index.js +116 -116
  92. package/src/mcp/server.js +392 -0
  93. package/src/mcp/tasks.js +133 -0
  94. package/src/memory/asset-hub.js +6 -11
  95. package/src/memory/canvas.js +2 -5
  96. package/src/memory/compaction.js +28 -28
  97. package/src/memory/experience.js +133 -122
  98. package/src/memory/fact-store.js +914 -698
  99. package/src/memory/failure-episode.js +20 -11
  100. package/src/memory/fork.js +17 -8
  101. package/src/memory/index.js +8 -6
  102. package/src/memory/l0.js +53 -52
  103. package/src/memory/l2.js +145 -130
  104. package/src/memory/l3.js +111 -111
  105. package/src/memory/legion-board.js +71 -0
  106. package/src/memory/memory-ticker.js +239 -240
  107. package/src/memory/session.js +398 -397
  108. package/src/memory/sqlite-store.js +581 -0
  109. package/src/mode/blackboard.js +49 -49
  110. package/src/mode/graph.js +42 -41
  111. package/src/mode/index.js +64 -64
  112. package/src/mode/legion.js +54 -51
  113. package/src/mode/plan-exec.js +50 -50
  114. package/src/mode/router.js +28 -40
  115. package/src/orchestrator/agent-worker.js +69 -69
  116. package/src/orchestrator/dag.js +90 -83
  117. package/src/orchestrator/experts.js +76 -0
  118. package/src/orchestrator/index.js +1 -1
  119. package/src/orchestrator/legion.js +179 -187
  120. package/src/orchestrator/supervisor.js +6 -8
  121. package/src/permissions/index.js +378 -0
  122. package/src/persona/index.js +28 -29
  123. package/src/plugin/builtin.js +313 -212
  124. package/src/plugin/context.js +80 -79
  125. package/src/plugin/index.js +62 -62
  126. package/src/plugin/v3.js +73 -0
  127. package/src/protocol/index.js +148 -0
  128. package/src/repomap/index.js +309 -0
  129. package/src/review/index.js +393 -0
  130. package/src/seam/registry.js +3 -0
  131. package/src/seam/shell.js +55 -55
  132. package/src/security/injection.js +79 -0
  133. package/src/selfheal/evolve.js +67 -67
  134. package/src/selfheal/healer.js +184 -167
  135. package/src/selfheal/run.js +9 -9
  136. package/src/server.js +63 -60
  137. package/src/services/diagnose.js +180 -0
  138. package/src/services/learning-service.js +9 -0
  139. package/src/services/memory-health.js +34 -6
  140. package/src/services/memory-service.js +42 -9
  141. package/src/services/triage.js +138 -0
  142. package/src/session/parts.js +76 -0
  143. package/src/session/projection.js +73 -0
  144. package/src/session/rollout.js +54 -0
  145. package/src/session/turn.js +137 -0
  146. package/src/skills/lint.js +72 -0
  147. package/src/skills/loader.js +231 -150
  148. package/src/skills/search.js +58 -0
  149. package/src/skills/verify.js +95 -100
  150. package/src/tools/advanced.js +388 -352
  151. package/src/tools/builtin.js +384 -297
  152. package/src/tools/catalog.js +283 -159
  153. package/src/tools/command-guard.js +112 -112
  154. package/src/tools/custom.js +47 -47
  155. package/src/tools/delegate.js +383 -297
  156. package/src/tools/document.js +254 -253
  157. package/src/tools/git.js +151 -0
  158. package/src/tools/governance.js +47 -20
  159. package/src/tools/index.js +16 -11
  160. package/src/tools/methods.js +178 -178
  161. package/src/tools/ocr.js +59 -59
  162. package/src/tools/sandbox-worker.js +40 -0
  163. package/src/tools/sandbox.js +92 -0
  164. package/src/tools/seam.js +162 -125
  165. package/src/tools/selfmod.js +196 -176
  166. package/src/tools/v3.js +225 -0
  167. package/src/tools/vad.js +176 -0
  168. package/src/tools/voice.js +238 -0
  169. package/src/utils/async.js +14 -0
  170. package/src/utils/config-file.js +53 -0
  171. package/src/utils/crashguard.js +88 -0
  172. package/src/utils/http.js +53 -0
  173. package/src/utils/id.js +8 -0
  174. package/src/utils/json-state.js +33 -0
  175. package/src/utils/logger.js +17 -17
  176. package/src/utils/ndjson.js +25 -0
  177. package/src/utils/pii.js +42 -42
  178. package/src/utils/rate-limit.js +50 -0
  179. package/src/utils/schema.js +80 -0
  180. package/src/utils/similarity.js +43 -0
  181. package/src/utils/store.js +170 -108
  182. package/src/utils/text.js +15 -15
  183. package/src/utils/trace.js +153 -153
  184. package/src/utils/wal.js +39 -0
  185. package/src/utils/winutf8.js +16 -15
  186. package/src/wiki/index.js +170 -0
@@ -1,178 +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
- }
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 CHANGED
@@ -1,59 +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
- }
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,40 @@
1
+ // src/tools/sandbox-worker.js - 沙箱工作线程 (配合 sandbox.js)
2
+ // worker 隔离 = 第一层边界 (独立线程, 可强杀); node:vm 新上下文 = 第二层 (裁剪全局)。
3
+ // 沙箱内**没有** require/import/process/fetch/fs —— 纯计算用途。
4
+ import { parentPort, workerData } from "node:worker_threads";
5
+ import vm from "node:vm";
6
+
7
+ const logs = [];
8
+ const fmt = (v) => {
9
+ if (typeof v === "string") return v;
10
+ try { return JSON.stringify(v) ?? String(v); } catch { return String(v); }
11
+ };
12
+ const push = (level) => (...args) => logs.push(`[${level}] ` + args.map(fmt).join(" "));
13
+
14
+ const sandboxConsole = { log: push("log"), info: push("info"), warn: push("warn"), error: push("error") };
15
+
16
+ // 裁剪过的全局: 纯计算可用, 无 IO / 网络 / 时器 / 模块加载
17
+ const sandbox = {
18
+ console: sandboxConsole,
19
+ Math, JSON, Date, RegExp, Error, TypeError, RangeError, SyntaxError,
20
+ Array, Object, String, Number, Boolean, Symbol, BigInt, Map, Set, WeakMap, WeakSet,
21
+ Promise, isNaN, isFinite, parseInt, parseFloat, encodeURIComponent, decodeURIComponent,
22
+ structuredClone, NaN, Infinity, undefined,
23
+ };
24
+ sandbox.globalThis = sandbox;
25
+
26
+ const timeoutMs = Math.min(Number(workerData?.timeoutMs) || 3000, 10000);
27
+ try {
28
+ const result = vm.runInNewContext(String(workerData?.code || ""), sandbox, {
29
+ timeout: timeoutMs, // 同步代码超时 (vm 层)
30
+ displayErrors: true,
31
+ });
32
+ let out;
33
+ if (typeof result === "bigint") out = String(result);
34
+ else if (typeof result === "function" || (typeof result === "object" && result !== null)) {
35
+ try { out = JSON.parse(JSON.stringify(result)); } catch { out = String(result); }
36
+ } else out = result;
37
+ parentPort.postMessage({ ok: true, result: out, logs });
38
+ } catch (e) {
39
+ parentPort.postMessage({ ok: false, error: `${e.name}: ${e.message}`, logs });
40
+ }
@@ -0,0 +1,92 @@
1
+ // src/tools/sandbox.js - 内置 JS 沙箱执行器 (零依赖)
2
+ //
3
+ // 用途: 让 agent 能安全地跑一段 JS 做计算/数据变换/格式转换 (CodeAct 能力落地):
4
+ // - 不用 shell 也不落盘, 单轮纯计算, 结果回灌工具循环。
5
+ // - 双层隔离: worker_threads 独立线程 (超时强杀, 死循环也不挂主进程)
6
+ // + node:vm 裁剪全局上下文 (无 require/process/fetch/fs, 纯计算)。
7
+ // - 这不是安全边界意义上的"防恶意"沙箱 (Node 无原生强隔离), 面向的是"防失误":
8
+ // 防死循环、防误写文件、防意外网络请求。对不受信代码仍应走 run_command + 沙箱策略审批链。
9
+ import path from "node:path";
10
+ import { Worker } from "node:worker_threads";
11
+ import { TOOL_ERROR_PREFIX } from "./seam.js";
12
+ import { debug } from "../utils/logger.js";
13
+
14
+ const WORKER_URL = new URL("./sandbox-worker.js", import.meta.url);
15
+
16
+ function err(name, msg) {
17
+ return `${TOOL_ERROR_PREFIX} ${name}: ${msg}`;
18
+ }
19
+
20
+ // 跑一段代码: { ok, result, logs, durationMs } 或 { ok:false, error, logs }
21
+ export function runInSandbox(code, { timeoutMs = 3000 } = {}) {
22
+ return new Promise((resolve) => {
23
+ const t0 = Date.now();
24
+ const cap = Math.min(Number(timeoutMs) || 3000, 10000);
25
+ let worker;
26
+ try {
27
+ worker = new Worker(WORKER_URL, { workerData: { code: String(code || ""), timeoutMs: cap } });
28
+ } catch (e) {
29
+ return resolve({ ok: false, error: `worker 启动失败: ${e.message}`, logs: [], durationMs: 0 });
30
+ }
31
+ const timer = setTimeout(() => {
32
+ worker.terminate().catch((e) => debug(`[sandbox] terminate 异常: ${e.message}`));
33
+ // terminate 后 message 不会再来, 立即裁决; 哨兵标志防双 resolve
34
+ settled = true;
35
+ resolve({ ok: false, error: `执行超时 (${cap}ms), 线程已强杀`, logs: [], durationMs: Date.now() - t0 });
36
+ }, cap + 500); // vm 层超时通常先触发; 这层兜底异步死循环
37
+ let settled = false;
38
+ worker.on("message", (msg) => {
39
+ if (settled) return;
40
+ settled = true;
41
+ clearTimeout(timer);
42
+ resolve({ ...msg, durationMs: Date.now() - t0 });
43
+ });
44
+ worker.on("error", (e) => {
45
+ if (settled) return;
46
+ settled = true;
47
+ clearTimeout(timer);
48
+ resolve({ ok: false, error: `worker 异常: ${e.message}`, logs: [], durationMs: Date.now() - t0 });
49
+ });
50
+ worker.on("exit", (code) => {
51
+ if (settled) return;
52
+ settled = true;
53
+ clearTimeout(timer);
54
+ resolve({ ok: false, error: `worker 提前退出 (code=${code})`, logs: [], durationMs: Date.now() - t0 });
55
+ });
56
+ });
57
+ }
58
+
59
+ export function registerSandboxTools(catalog, { rootDir = process.cwd() } = {}) {
60
+ void rootDir;
61
+ catalog.register({
62
+ name: "code_run",
63
+ description: "在内置 JS 沙箱里执行一段 JavaScript 并返回结果 (纯计算: 数学/字符串/数组变换/JSON 处理)。沙箱无网络/文件/进程访问, 限时 10s。适合精确计算、数据转换、格式化 —— 比手算可靠, 比 shell 干净。",
64
+ parameters: {
65
+ type: "object",
66
+ properties: {
67
+ code: { type: "string", description: "JS 代码 (表达式或语句)。最后一条表达式的值或显式变量为返回值; 用 console.log 输出中间信息" },
68
+ timeout_ms: { type: "number", description: "超时毫秒 (默认 3000, 上限 10000)" },
69
+ },
70
+ required: ["code"],
71
+ },
72
+ category: "compute",
73
+ power: "user",
74
+ idempotent: true,
75
+ capability: { readOnly: true, riskLevel: "low", sideEffect: "none" },
76
+ execute: async (args) => {
77
+ const code = String(args.code || "").trim();
78
+ if (!code) return err("code_run", "code 不能为空");
79
+ const r = await runInSandbox(code, { timeoutMs: Number(args.timeout_ms) || 3000 });
80
+ if (!r.ok) {
81
+ return err("code_run", `${r.error}${r.logs?.length ? ` | 输出: ${r.logs.join(" / ").slice(0, 500)}` : ""}`);
82
+ }
83
+ return JSON.stringify({
84
+ ok: true,
85
+ result: r.result,
86
+ logs: (r.logs || []).slice(0, 50),
87
+ durationMs: r.durationMs,
88
+ });
89
+ },
90
+ });
91
+ return catalog;
92
+ }