ppxans-harness 2.6.0 → 3.2.1

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 (189) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +195 -195
  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 +6 -124
  11. package/config/ppx.json.example +143 -0
  12. package/package.json +16 -6
  13. package/skills/.usage.json +6 -0
  14. package/skills/agent-professional-training/SKILL.md +94 -0
  15. package/skills/agent-professional-training/trajectories/2026-09-11-dpo-pairs.jsonl +2 -0
  16. package/skills/agent-professional-training/trajectories/2026-09-11-round2-trajectory.json +15 -0
  17. package/skills/agent-professional-training/trajectories/2026-09-13-first-run-trajectory.json +17 -0
  18. package/skills/brainstorm/SKILL.md +24 -0
  19. package/skills/cupid-lover-comms/SKILL.md +37 -0
  20. package/skills/debug/SKILL.md +26 -0
  21. package/skills/plan/SKILL.md +25 -0
  22. package/skills/ponytail/SKILL.md +25 -0
  23. package/skills/ppx-memory/SKILL.md +91 -0
  24. package/skills/ppx-memory/scripts/cli.js +192 -0
  25. package/skills/ppx-memory/scripts/experience.js +133 -0
  26. package/skills/ppx-memory/scripts/fact-store.js +842 -0
  27. package/skills/ppx-memory/scripts/l0.js +52 -0
  28. package/skills/ppx-memory/scripts/l2.js +146 -0
  29. package/skills/ppx-memory/scripts/l3.js +112 -0
  30. package/skills/ppx-memory/scripts/memory-ticker.js +238 -0
  31. package/skills/ppx-memory/scripts/pii.js +42 -0
  32. package/skills/ppx-memory/scripts/schema.js +80 -0
  33. package/skills/ppx-memory/scripts/session.js +398 -0
  34. package/skills/ppx-memory/scripts/similarity.js +43 -0
  35. package/skills/ppx-memory/scripts/store.js +116 -0
  36. package/skills/ppx-memory/scripts/wal.js +38 -0
  37. package/skills/ppx-selfheal/SKILL.md +24 -0
  38. package/skills/ppx-selfheal/scripts/cli.js +80 -0
  39. package/skills/ppx-selfheal/scripts/healer.js +184 -0
  40. package/skills/ppx-selfheal/scripts/logger.js +17 -0
  41. package/skills/ppx-selfheal/scripts/store.js +116 -0
  42. package/skills/prompt-depth-kit/SKILL.md +28 -0
  43. package/skills/session-naming/SKILL.md +36 -0
  44. package/skills/verify/SKILL.md +25 -0
  45. package/src/agent/index.js +1352 -717
  46. package/src/agent/prompts.js +46 -2
  47. package/src/aml-server.js +197 -151
  48. package/src/ans/eviction.js +123 -143
  49. package/src/ans/guard.js +159 -120
  50. package/src/ans/lifecycle.js +96 -93
  51. package/src/ans/proactive.js +112 -129
  52. package/src/ans/reward.js +95 -111
  53. package/src/ans/values.js +15 -15
  54. package/src/audit/audit-chain.js +43 -7
  55. package/src/audit/verifier.js +157 -120
  56. package/src/bus/circuit-breaker.js +9 -1
  57. package/src/bus/runtime-bus.js +107 -93
  58. package/src/channels/base.js +57 -34
  59. package/src/channels/feishu.js +118 -126
  60. package/src/channels/http.js +1090 -651
  61. package/src/channels/index.js +111 -110
  62. package/src/channels/log.js +29 -29
  63. package/src/channels/wechat-crypto.js +73 -74
  64. package/src/channels/wechat.js +191 -197
  65. package/src/channels/workspace.js +94 -0
  66. package/src/channels-cli.js +126 -124
  67. package/src/cli.js +129 -120
  68. package/src/commands/index.js +142 -0
  69. package/src/config/channels.js +137 -170
  70. package/src/config/index.js +277 -224
  71. package/src/config/placeholder.js +31 -0
  72. package/src/config/providers.js +148 -182
  73. package/src/config/settings.js +156 -182
  74. package/src/core/policy.js +69 -18
  75. package/src/core/trace.js +8 -6
  76. package/src/edit/editblock.js +266 -0
  77. package/src/edit/snapshot.js +67 -0
  78. package/src/evidence/index.js +153 -0
  79. package/src/evolve/playbook.js +9 -10
  80. package/src/hooks/index.js +113 -0
  81. package/src/llm/client.js +4 -3
  82. package/src/llm/dsml.js +74 -74
  83. package/src/llm/embedder.js +41 -35
  84. package/src/llm/fence.js +52 -52
  85. package/src/llm/index.js +4 -4
  86. package/src/llm/local-embedder.js +94 -0
  87. package/src/llm/presets.js +113 -0
  88. package/src/llm/pricing.js +93 -0
  89. package/src/llm/retry.js +73 -73
  90. package/src/llm/router.js +92 -89
  91. package/src/mcp/admin.js +44 -10
  92. package/src/mcp/client.js +487 -477
  93. package/src/mcp/http.js +23 -37
  94. package/src/mcp/index.js +116 -116
  95. package/src/mcp/server.js +23 -18
  96. package/src/mcp/tasks.js +9 -14
  97. package/src/memory/asset-hub.js +6 -11
  98. package/src/memory/canvas.js +2 -5
  99. package/src/memory/compaction.js +28 -28
  100. package/src/memory/experience.js +133 -122
  101. package/src/memory/fact-store.js +914 -698
  102. package/src/memory/failure-episode.js +20 -11
  103. package/src/memory/fork.js +17 -8
  104. package/src/memory/index.js +8 -6
  105. package/src/memory/l0.js +53 -52
  106. package/src/memory/l2.js +145 -130
  107. package/src/memory/l3.js +111 -111
  108. package/src/memory/legion-board.js +71 -0
  109. package/src/memory/memory-ticker.js +239 -240
  110. package/src/memory/session.js +398 -397
  111. package/src/memory/sqlite-store.js +581 -0
  112. package/src/mode/blackboard.js +49 -49
  113. package/src/mode/graph.js +42 -41
  114. package/src/mode/index.js +64 -64
  115. package/src/mode/legion.js +54 -51
  116. package/src/mode/plan-exec.js +50 -50
  117. package/src/mode/router.js +28 -40
  118. package/src/orchestrator/agent-worker.js +69 -69
  119. package/src/orchestrator/dag.js +90 -83
  120. package/src/orchestrator/experts.js +76 -0
  121. package/src/orchestrator/index.js +1 -1
  122. package/src/orchestrator/legion.js +179 -187
  123. package/src/orchestrator/supervisor.js +6 -8
  124. package/src/permissions/index.js +378 -0
  125. package/src/persona/index.js +28 -29
  126. package/src/plugin/builtin.js +313 -215
  127. package/src/plugin/context.js +80 -79
  128. package/src/plugin/index.js +62 -62
  129. package/src/plugin/v3.js +73 -0
  130. package/src/protocol/index.js +148 -0
  131. package/src/repomap/index.js +309 -0
  132. package/src/review/index.js +393 -0
  133. package/src/seam/registry.js +3 -0
  134. package/src/seam/shell.js +55 -55
  135. package/src/security/injection.js +79 -0
  136. package/src/selfheal/evolve.js +67 -67
  137. package/src/selfheal/healer.js +184 -167
  138. package/src/selfheal/run.js +9 -9
  139. package/src/server.js +63 -60
  140. package/src/services/diagnose.js +180 -0
  141. package/src/services/learning-service.js +9 -0
  142. package/src/services/memory-health.js +34 -6
  143. package/src/services/memory-service.js +42 -9
  144. package/src/services/triage.js +138 -0
  145. package/src/session/parts.js +76 -0
  146. package/src/session/projection.js +73 -0
  147. package/src/session/rollout.js +54 -0
  148. package/src/session/turn.js +137 -0
  149. package/src/skills/lint.js +72 -0
  150. package/src/skills/loader.js +231 -150
  151. package/src/skills/search.js +58 -0
  152. package/src/skills/verify.js +95 -100
  153. package/src/tools/advanced.js +388 -352
  154. package/src/tools/builtin.js +384 -297
  155. package/src/tools/catalog.js +283 -159
  156. package/src/tools/command-guard.js +112 -112
  157. package/src/tools/custom.js +47 -47
  158. package/src/tools/delegate.js +383 -297
  159. package/src/tools/document.js +254 -253
  160. package/src/tools/git.js +151 -0
  161. package/src/tools/governance.js +47 -20
  162. package/src/tools/index.js +16 -11
  163. package/src/tools/methods.js +178 -178
  164. package/src/tools/ocr.js +59 -59
  165. package/src/tools/sandbox-worker.js +40 -0
  166. package/src/tools/sandbox.js +92 -0
  167. package/src/tools/seam.js +162 -125
  168. package/src/tools/selfmod.js +196 -176
  169. package/src/tools/v3.js +225 -0
  170. package/src/tools/vad.js +176 -0
  171. package/src/tools/voice.js +238 -0
  172. package/src/utils/async.js +14 -0
  173. package/src/utils/config-file.js +53 -0
  174. package/src/utils/crashguard.js +88 -0
  175. package/src/utils/http.js +53 -0
  176. package/src/utils/id.js +8 -0
  177. package/src/utils/json-state.js +33 -0
  178. package/src/utils/logger.js +17 -17
  179. package/src/utils/ndjson.js +25 -0
  180. package/src/utils/pii.js +42 -42
  181. package/src/utils/rate-limit.js +50 -0
  182. package/src/utils/schema.js +80 -0
  183. package/src/utils/similarity.js +43 -0
  184. package/src/utils/store.js +170 -108
  185. package/src/utils/text.js +15 -15
  186. package/src/utils/trace.js +153 -153
  187. package/src/utils/wal.js +39 -0
  188. package/src/utils/winutf8.js +16 -15
  189. package/src/wiki/index.js +170 -0
@@ -1,182 +1,156 @@
1
- // src/config/settings.js - 通用设置读写 (HTTP API 后端)
2
- // 覆盖 config/ppx.json 顶层字段: user / channels.http / security / agent 预设 (values/system_extra/citation_rule/mode)
3
- // 设计 (与 providers.js 对齐):
4
- // - 唯一事实源 = config/ppx.json
5
- // - 写盘: 备份原文件 → 原子写 (.tmp + rename) → 防配置丢失
6
- // - 读取: 深度合并默认值 (loadConfig), 保证前端总能拿到完整结构
7
- // - 热重载: 写盘后由调用方调 agent.reload() 重建内存
8
- import fs from "node:fs";
9
- import path from "node:path";
10
- import { loadConfig, DEFAULT_CONFIG } from "./index.js";
11
- import { withFileLock } from "../utils/store.js";
12
- import { warn } from "../utils/logger.js";
13
-
14
- function getConfigPath(root) {
15
- return path.join(root, "config", "ppx.json");
16
- }
17
-
18
- // 读取当前 config (磁盘原文), 缺文件返回空
19
- export function readConfigRaw(root) {
20
- const p = getConfigPath(root);
21
- if (!fs.existsSync(p)) return {};
22
- try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch (e) { warn("config/ppx.json 读取失败:", e.message); return {}; }
23
- }
24
-
25
- // 写盘: 先备份 (保留最近 3 个), 再原子写 (tmp + rename)
26
- function writeConfigAtomic(root, cfg) {
27
- const p = getConfigPath(root);
28
- if (fs.existsSync(p)) {
29
- const bak = p + ".bak-" + new Date().toISOString().replace(/[:.]/g, "-");
30
- try { fs.copyFileSync(p, bak); } catch (e) { warn("备份失败:", e.message); }
31
- try {
32
- const dir = path.dirname(p);
33
- const base = path.basename(p);
34
- const baks = fs.readdirSync(dir)
35
- .filter((f) => f.startsWith(base + ".bak-"))
36
- .map((f) => ({ f, t: fs.statSync(path.join(dir, f)).mtimeMs }));
37
- baks.sort((a, b) => b.t - a.t);
38
- for (const old of baks.slice(3)) {
39
- try { fs.unlinkSync(path.join(dir, old.f)); } catch {}
40
- }
41
- } catch {}
42
- }
43
- const tmp = p + ".tmp";
44
- fs.writeFileSync(tmp, JSON.stringify(cfg, null, 2), "utf8");
45
- fs.renameSync(tmp, p);
46
- }
47
-
48
- // 通用设置字段白名单 (GET 返回 + PUT 可改的顶层键)
49
- // 每个键对应 config 里的一个可编辑分区, 值里的字段再细分
50
- export const SETTINGS_FIELDS = {
51
- user: ["name"],
52
- http: ["port", "auth_token"],
53
- security: ["allow_all", "command_timeout_ms", "code_act"],
54
- agent: ["name", "mode", "citation_rule", "system_extra", "values"],
55
- mcp: ["servers", "auto_connect"],
56
- tools: ["disabled"],
57
- };
58
-
59
- // MCP 服务器字段白名单 (防注入任意字段)
60
- export const MCP_SERVER_KEYS = ["command", "args", "env", "prefix", "url", "headers", "timeout", "name"];
61
-
62
- // 安全视图: 抹掉敏感明文 (auth_token / api_key / mcp headers), 只暴露 set 标志
63
- function sanitizeSettings(cfg) {
64
- const out = {
65
- user: { name: cfg.user?.name || "兄弟" },
66
- http: {
67
- port: cfg.channels?.http?.port ?? 8899,
68
- auth_token_set: !!(cfg.channels?.http?.auth_token),
69
- },
70
- security: {
71
- allow_all: !!cfg.security?.allow_all,
72
- command_timeout_ms: cfg.security?.command_timeout_ms ?? 30000,
73
- code_act: !!cfg.security?.code_act,
74
- },
75
- agent: {
76
- name: cfg.agent?.name || "皮皮虾",
77
- mode: cfg.agent?.mode || "react",
78
- citation_rule: cfg.agent?.citation_rule ?? "",
79
- system_extra: cfg.agent?.system_extra ?? "",
80
- values: Array.isArray(cfg.agent?.values) ? cfg.agent.values : (DEFAULT_CONFIG.agent?.values || []),
81
- },
82
- mcp: {
83
- auto_connect: !!cfg.mcp?.auto_connect,
84
- servers: Array.isArray(cfg.mcp?.servers) ? cfg.mcp.servers.map((s) => sanitizeMcpServer(s)) : [],
85
- },
86
- tools: {
87
- disabled: Array.isArray(cfg.tools?.disabled) ? cfg.tools.disabled : [],
88
- },
89
- };
90
- return out;
91
- }
92
-
93
- // MCP 服务器安全视图: 只暴露白名单字段, headers 抹掉明文只回 set 标志
94
- function sanitizeMcpServer(s) {
95
- if (!s || typeof s !== "object") return {};
96
- const out = {};
97
- for (const k of MCP_SERVER_KEYS) {
98
- if (k === "headers") {
99
- if (s.headers && typeof s.headers === "object") out.headers_set = true;
100
- continue;
101
- }
102
- if (k === "env") {
103
- if (s.env && typeof s.env === "object") out.env_set = Object.keys(s.env).length > 0;
104
- continue;
105
- }
106
- if (s[k] !== undefined) out[k] = s[k];
107
- }
108
- if (!out.name) out.name = s.command || s.url || "";
109
- return out;
110
- }
111
-
112
- // GET /api/settings: 返回可编辑设置的安全视图
113
- export function getSettings(root) {
114
- // 用 loadConfig 深合并默认值, 保证缺字段也有结构; 但 write 需基于磁盘原文避免覆盖默认
115
- const merged = loadConfig(root);
116
- return sanitizeSettings(merged);
117
- }
118
-
119
- // PUT /api/settings: 只更新白名单字段, 返回更新后的安全视图
120
- // patch = { user?, http?, security?, agent?, mcp?, tools? }
121
- // v1.0.8: patch 顶层/分区字段都按 SETTINGS_FIELDS 白名单过滤 (防注入任意字段); 写盘在文件锁内读-改-写 (防并发丢更新)
122
- export function updateSettings(root, patch) {
123
- if (!patch || typeof patch !== "object") throw new Error("patch 必须是对象");
124
- const p = getConfigPath(root);
125
- return withFileLock(p, () => {
126
- const cfg = readConfigRaw(root); // 锁内重读: 拿最新磁盘状态再改
127
- // 只取白名单字段 (SETTINGS_FIELDS 定义每个分区可改的键)
128
- const pick = (obj, keys) => {
129
- const out = {};
130
- for (const k of keys) if (obj && typeof obj === "object" && obj[k] !== undefined) out[k] = obj[k];
131
- return out;
132
- };
133
- cfg.user = { ...(cfg.user || {}), ...pick(patch.user, SETTINGS_FIELDS.user) };
134
- cfg.channels = cfg.channels || {};
135
- cfg.channels.http = { ...(cfg.channels.http || {}), ...pick(patch.http, SETTINGS_FIELDS.http) };
136
- cfg.security = { ...(cfg.security || {}), ...pick(patch.security, SETTINGS_FIELDS.security) };
137
- cfg.agent = { ...(cfg.agent || {}), ...pick(patch.agent, SETTINGS_FIELDS.agent) };
138
- cfg.mcp = { ...(cfg.mcp || {}), ...pick(patch.mcp, SETTINGS_FIELDS.mcp) };
139
- cfg.tools = { ...(cfg.tools || {}), ...pick(patch.tools, SETTINGS_FIELDS.tools) };
140
-
141
- // 校验
142
- const port = cfg.channels.http.port;
143
- if (port != null && (!Number.isInteger(port) || port < 1 || port > 65535)) throw new Error("HTTP 端口必须是 1-65535 的整数");
144
- const timeout = cfg.security.command_timeout_ms;
145
- if (timeout != null && (!Number.isFinite(timeout) || timeout < 1000)) throw new Error("命令超时至少 1000ms");
146
- if (cfg.agent.values != null && !Array.isArray(cfg.agent.values)) throw new Error("核心价值必须是字符串数组");
147
- if (cfg.agent.values && cfg.agent.values.some((v) => typeof v !== "string")) throw new Error("核心价值必须是字符串数组");
148
- if (cfg.agent.mode != null && !/^[a-z-]{2,30}$/.test(String(cfg.agent.mode))) throw new Error("编排模式只含小写字母/横线");
149
- // mcp.servers: 必须是对象数组, 每项至少 command 或 url, 只保留白名单字段
150
- if (cfg.mcp.servers != null && !Array.isArray(cfg.mcp.servers)) throw new Error("MCP servers 必须是数组");
151
- if (Array.isArray(cfg.mcp.servers)) {
152
- const clean = [];
153
- for (const s of cfg.mcp.servers) {
154
- if (!s || typeof s !== "object") continue;
155
- if (!s.command && !s.url) throw new Error("MCP 服务器至少需要 command(stdio) 或 url(http)");
156
- const norm = {};
157
- for (const k of MCP_SERVER_KEYS) if (s[k] !== undefined) norm[k] = s[k];
158
- clean.push(norm);
159
- }
160
- cfg.mcp.servers = clean;
161
- }
162
- // tools.disabled: 必须是字符串数组
163
- if (cfg.tools.disabled != null) {
164
- if (!Array.isArray(cfg.tools.disabled)) throw new Error("tools.disabled 必须是字符串数组");
165
- if (cfg.tools.disabled.some((t) => typeof t !== "string")) throw new Error("tools.disabled 必须是字符串数组");
166
- }
167
-
168
- writeConfigAtomic(root, cfg);
169
- return sanitizeSettings(cfg);
170
- });
171
- }
172
-
173
- // 应用 tools.disabled: 返回需要禁用的工具名列表 (供启动时/热重载时禁用)
174
- export function disabledTools(root) {
175
- const { tools } = getSettings(root);
176
- return Array.isArray(tools?.disabled) ? tools.disabled : [];
177
- }
178
-
179
- // 可编辑字段元数据 (供前端渲染表单, 免前端硬编码结构)
180
- export function settingsSchema() {
181
- return SETTINGS_FIELDS;
182
- }
1
+ // src/config/settings.js - 通用设置读写 (HTTP API 后端)
2
+ // 覆盖 config/ppx.json 顶层字段: user / channels.http / security / agent 预设 (values/system_extra/citation_rule/mode)
3
+ // 设计 (与 providers.js 对齐):
4
+ // - 唯一事实源 = config/ppx.json
5
+ // - 写盘: 备份原文件 → 原子写 (.tmp + rename) → 防配置丢失
6
+ // - 读取: 深度合并默认值 (loadConfig), 保证前端总能拿到完整结构
7
+ // - 热重载: 写盘后由调用方调 agent.reload() 重建内存
8
+ import { loadConfig, DEFAULT_CONFIG } from "./index.js";
9
+ import { withFileLock } from "../utils/store.js";
10
+ import { configFilePath, readPpxConfig, writeConfigAtomic } from "../utils/config-file.js";
11
+
12
+ const getConfigPath = configFilePath;
13
+
14
+ // 读取当前 config (磁盘原文), 缺文件返回空
15
+ export function readConfigRaw(root) {
16
+ return readPpxConfig(root, {});
17
+ }
18
+
19
+ // 通用设置字段白名单 (GET 返回 + PUT 可改的顶层键)
20
+ // 每个键对应 config 里的一个可编辑分区, 值里的字段再细分
21
+ export const SETTINGS_FIELDS = {
22
+ user: ["name"],
23
+ http: ["port", "auth_token"],
24
+ security: ["allow_all", "command_timeout_ms", "code_act", "deny"],
25
+ agent: ["name", "mode", "citation_rule", "system_extra", "values"],
26
+ mcp: ["servers", "auto_connect"],
27
+ tools: ["disabled"],
28
+ };
29
+
30
+ // MCP 服务器字段白名单 (防注入任意字段)
31
+ export const MCP_SERVER_KEYS = ["command", "args", "env", "prefix", "url", "headers", "timeout", "name"];
32
+
33
+ // 安全视图: 抹掉敏感明文 (auth_token / api_key / mcp headers), 只暴露 set 标志
34
+ function sanitizeSettings(cfg) {
35
+ const out = {
36
+ user: { name: cfg.user?.name || "兄弟" },
37
+ http: {
38
+ port: cfg.channels?.http?.port ?? 8899,
39
+ auth_token_set: !!(cfg.channels?.http?.auth_token),
40
+ },
41
+ security: {
42
+ allow_all: !!cfg.security?.allow_all,
43
+ command_timeout_ms: cfg.security?.command_timeout_ms ?? 30000,
44
+ code_act: !!cfg.security?.code_act,
45
+ deny: Array.isArray(cfg.security?.deny) ? cfg.security.deny : [],
46
+ },
47
+ agent: {
48
+ name: cfg.agent?.name || "皮皮虾",
49
+ mode: cfg.agent?.mode || "react",
50
+ citation_rule: cfg.agent?.citation_rule ?? "",
51
+ system_extra: cfg.agent?.system_extra ?? "",
52
+ values: Array.isArray(cfg.agent?.values) ? cfg.agent.values : (DEFAULT_CONFIG.agent?.values || []),
53
+ },
54
+ mcp: {
55
+ auto_connect: !!cfg.mcp?.auto_connect,
56
+ servers: Array.isArray(cfg.mcp?.servers) ? cfg.mcp.servers.map((s) => sanitizeMcpServer(s)) : [],
57
+ },
58
+ tools: {
59
+ disabled: Array.isArray(cfg.tools?.disabled) ? cfg.tools.disabled : [],
60
+ },
61
+ };
62
+ return out;
63
+ }
64
+
65
+ // MCP 服务器安全视图: 只暴露白名单字段, headers 抹掉明文只回 set 标志
66
+ function sanitizeMcpServer(s) {
67
+ if (!s || typeof s !== "object") return {};
68
+ const out = {};
69
+ for (const k of MCP_SERVER_KEYS) {
70
+ if (k === "headers") {
71
+ if (s.headers && typeof s.headers === "object") out.headers_set = true;
72
+ continue;
73
+ }
74
+ if (k === "env") {
75
+ if (s.env && typeof s.env === "object") out.env_set = Object.keys(s.env).length > 0;
76
+ continue;
77
+ }
78
+ if (s[k] !== undefined) out[k] = s[k];
79
+ }
80
+ if (!out.name) out.name = s.command || s.url || "";
81
+ return out;
82
+ }
83
+
84
+ // GET /api/settings: 返回可编辑设置的安全视图
85
+ export function getSettings(root) {
86
+ // 用 loadConfig 深合并默认值, 保证缺字段也有结构; 但 write 需基于磁盘原文避免覆盖默认
87
+ const merged = loadConfig(root);
88
+ return sanitizeSettings(merged);
89
+ }
90
+
91
+ // PUT /api/settings: 只更新白名单字段, 返回更新后的安全视图
92
+ // patch = { user?, http?, security?, agent?, mcp?, tools? }
93
+ // v1.0.8: patch 顶层/分区字段都按 SETTINGS_FIELDS 白名单过滤 (防注入任意字段); 写盘在文件锁内读-改-写 (防并发丢更新)
94
+ export function updateSettings(root, patch) {
95
+ if (!patch || typeof patch !== "object") throw new Error("patch 必须是对象");
96
+ const p = getConfigPath(root);
97
+ return withFileLock(p, () => {
98
+ const cfg = readConfigRaw(root); // 锁内重读: 拿最新磁盘状态再改
99
+ // 只取白名单字段 (SETTINGS_FIELDS 定义每个分区可改的键)
100
+ const pick = (obj, keys) => {
101
+ const out = {};
102
+ for (const k of keys) if (obj && typeof obj === "object" && obj[k] !== undefined) out[k] = obj[k];
103
+ return out;
104
+ };
105
+ cfg.user = { ...(cfg.user || {}), ...pick(patch.user, SETTINGS_FIELDS.user) };
106
+ cfg.channels = cfg.channels || {};
107
+ cfg.channels.http = { ...(cfg.channels.http || {}), ...pick(patch.http, SETTINGS_FIELDS.http) };
108
+ cfg.security = { ...(cfg.security || {}), ...pick(patch.security, SETTINGS_FIELDS.security) };
109
+ cfg.agent = { ...(cfg.agent || {}), ...pick(patch.agent, SETTINGS_FIELDS.agent) };
110
+ cfg.mcp = { ...(cfg.mcp || {}), ...pick(patch.mcp, SETTINGS_FIELDS.mcp) };
111
+ cfg.tools = { ...(cfg.tools || {}), ...pick(patch.tools, SETTINGS_FIELDS.tools) };
112
+
113
+ // 校验
114
+ const port = cfg.channels.http.port;
115
+ if (port != null && (!Number.isInteger(port) || port < 1 || port > 65535)) throw new Error("HTTP 端口必须是 1-65535 的整数");
116
+ const timeout = cfg.security.command_timeout_ms;
117
+ if (timeout != null && (!Number.isFinite(timeout) || timeout < 1000)) throw new Error("命令超时至少 1000ms");
118
+ if (cfg.security.deny != null && !Array.isArray(cfg.security.deny)) throw new Error("额外拒绝规则必须是字符串数组");
119
+ if (cfg.security.deny && cfg.security.deny.some((d) => typeof d !== "string")) throw new Error("额外拒绝规则必须是字符串数组");
120
+ if (cfg.agent.values != null && !Array.isArray(cfg.agent.values)) throw new Error("核心价值必须是字符串数组");
121
+ if (cfg.agent.values && cfg.agent.values.some((v) => typeof v !== "string")) throw new Error("核心价值必须是字符串数组");
122
+ if (cfg.agent.mode != null && !/^[a-z-]{2,30}$/.test(String(cfg.agent.mode))) throw new Error("编排模式只含小写字母/横线");
123
+ // mcp.servers: 必须是对象数组, 每项至少 command 或 url, 只保留白名单字段
124
+ if (cfg.mcp.servers != null && !Array.isArray(cfg.mcp.servers)) throw new Error("MCP servers 必须是数组");
125
+ if (Array.isArray(cfg.mcp.servers)) {
126
+ const clean = [];
127
+ for (const s of cfg.mcp.servers) {
128
+ if (!s || typeof s !== "object") continue;
129
+ if (!s.command && !s.url) throw new Error("MCP 服务器至少需要 command(stdio) 或 url(http)");
130
+ const norm = {};
131
+ for (const k of MCP_SERVER_KEYS) if (s[k] !== undefined) norm[k] = s[k];
132
+ clean.push(norm);
133
+ }
134
+ cfg.mcp.servers = clean;
135
+ }
136
+ // tools.disabled: 必须是字符串数组
137
+ if (cfg.tools.disabled != null) {
138
+ if (!Array.isArray(cfg.tools.disabled)) throw new Error("tools.disabled 必须是字符串数组");
139
+ if (cfg.tools.disabled.some((t) => typeof t !== "string")) throw new Error("tools.disabled 必须是字符串数组");
140
+ }
141
+
142
+ writeConfigAtomic(root, cfg);
143
+ return sanitizeSettings(cfg);
144
+ });
145
+ }
146
+
147
+ // 应用 tools.disabled: 返回需要禁用的工具名列表 (供启动时/热重载时禁用)
148
+ export function disabledTools(root) {
149
+ const { tools } = getSettings(root);
150
+ return Array.isArray(tools?.disabled) ? tools.disabled : [];
151
+ }
152
+
153
+ // 可编辑字段元数据 (供前端渲染表单, 免前端硬编码结构)
154
+ export function settingsSchema() {
155
+ return SETTINGS_FIELDS;
156
+ }
@@ -5,7 +5,7 @@
5
5
  // 循环何时停、降档、重试、注入方向盘, 全由本模块决策。
6
6
  // 依赖全部注入 (llm/tools/runTool/shrinkMessages/...), 无 agent 引用, 可独立测试。
7
7
  import { TOOL_ERROR_PREFIX } from "../tools/index.js";
8
- import { warn } from "../utils/logger.js";
8
+ import { warn, debug } from "../utils/logger.js";
9
9
 
10
10
  // ---- 阈值默认值 (config.agent.* 可覆盖) ----
11
11
  export const DEFAULT_MAX_TOOL_ROUNDS = 8;
@@ -78,7 +78,7 @@ export async function callWithTimeoutRetry({
78
78
  budgetMs = null, // 工具超时预算 (toolTimeoutOf 注入, 事件采集用)
79
79
  onEvent = null,
80
80
  }) {
81
- const ev = (type, payload) => { if (onEvent) { try { onEvent(type, payload); } catch {} } };
81
+ const ev = (type, payload) => { if (onEvent) { try { onEvent(type, payload); } catch (e) { debug(`[core/policy] 已忽略异常: ${e && e.message ? e.message : e}`); } } };
82
82
  const t0 = Date.now();
83
83
  let result = await runTool(name, args);
84
84
  let elapsedMs = Date.now() - t0;
@@ -112,6 +112,8 @@ export class ToolLoopPolicy {
112
112
  this.exploreBreak = Number(c.explore_break_limit) || DEFAULT_EXPLORE_BREAK;
113
113
  this.repeatFlag = Number(c.repeat_flag_limit) || DEFAULT_REPEAT_FLAG;
114
114
  this.overflowShrinkMax = DEFAULT_OVERFLOW_SHRINK_MAX;
115
+ // 同轮独立工具调用并发执行 (2026-10-01 优化): 默认开, agent.parallel_tool_calls=false 回退串行
116
+ this.parallelToolCalls = c.parallel_tool_calls !== false;
115
117
  // 运行时状态 (每轮循环实例持有, 重启归零)
116
118
  this.errorRetries = 0;
117
119
  this.exploreStreak = 0;
@@ -142,7 +144,7 @@ export class ToolLoopPolicy {
142
144
  let repeatHit = false;
143
145
  for (const tc of called) {
144
146
  let a = {};
145
- try { a = JSON.parse(tc.function.arguments || "{}"); } catch {}
147
+ try { a = JSON.parse(tc.function.arguments || "{}"); } catch (e) { debug(`[core/policy] 已忽略异常: ${e && e.message ? e.message : e}`); }
146
148
  const sig = (tc.function?.name || "") + "::" + JSON.stringify(a).slice(0, 120);
147
149
  this.seenSig.set(sig, (this.seenSig.get(sig) || 0) + 1);
148
150
  if (this.seenSig.get(sig) >= this.repeatFlag) repeatHit = true;
@@ -166,6 +168,24 @@ export class ToolLoopPolicy {
166
168
  this.errorRetries++;
167
169
  return true;
168
170
  }
171
+
172
+ // ---- 自省裁决 (Reflective 内核): 对工具失败做语义分类, 决定重试策略 ----
173
+ // 在机械次数重试之上加一道闸门: 硬拒绝类错误 (黑名单/审批拒/权限/deny/DENY_HINT)
174
+ // 不许盲目改写命令绕过, 直接拦停; 可修正类错误才走次数重试。
175
+ // 返回 { action: "stop"|"retry", reason } — 纯方法, 可独立测试。
176
+ selfReviewError(errors) {
177
+ if (!errors || !errors.length) return null;
178
+ const text = errors.join("\n");
179
+ // 硬拒绝特征: 命中命令守卫拦截 / 审批被拒 / 权限拒绝 / 黑名单
180
+ if (/命中后不要重试|改造命令绕过|硬黑名单|审批被拒绝|审批拒绝|权限.*拒|deny|DENY|拦截/i.test(text)) {
181
+ return {
182
+ action: "stop",
183
+ reason: "这是硬性拒绝类错误, 盲目重试或改写命令会绕过安全闸门 — 停下不重试, 说明原因或请用户调整配置。",
184
+ };
185
+ }
186
+ // 可修正类错误 (命令不存在/文件缺失/参数错等): 走次数重试道
187
+ return { action: "retry", reason: "可修正错误, 喂回模型重试 (仍受次数上限约束)" };
188
+ }
169
189
  }
170
190
 
171
191
  // ---- 工具循环主驱动 (原 PPXAgent._llmWithTools) ----
@@ -198,11 +218,11 @@ export async function runToolLoop({
198
218
  }) {
199
219
  const policy = new ToolLoopPolicy(config.agent || config);
200
220
  let messages = [...seedMessages];
201
- const ev = (type, payload) => { if (onEvent) { try { onEvent(type, payload); } catch {} } };
221
+ const ev = (type, payload) => { if (onEvent) { try { onEvent(type, payload); } catch (e) { debug(`[core/policy] 已忽略异常: ${e && e.message ? e.message : e}`); } } };
202
222
 
203
223
  for (let round = 0; round < policy.maxRounds; round++) {
204
224
  if (isInterrupted()) return "[皮皮虾] 任务已被中断 (operator cancelled).";
205
- if (onStep) { try { onStep({ type: "step", round, maxRounds: policy.maxRounds, ts: Date.now() }); } catch {} }
225
+ if (onStep) { try { onStep({ type: "step", round, maxRounds: policy.maxRounds, ts: Date.now() }); } catch (e) { debug(`[core/policy] 已忽略异常: ${e && e.message ? e.message : e}`); } }
206
226
 
207
227
  let resp;
208
228
  try {
@@ -232,25 +252,56 @@ export async function runToolLoop({
232
252
  }
233
253
 
234
254
  // 工具错误重试: 若本轮有工具失败, 汇总错误喂回模型修正后重试 (最多 maxErrorRetry 次)
235
- const errors = [];
255
+ // 并发执行 (2026-10-01 优化, 审计 P1 遗留项): 同一轮的 tool_calls 相互独立
256
+ // (OpenAI 语义: 数组内无依赖), 串行会让 N 个独立调用的延迟线性叠加。
257
+ // Promise.all 保序: messages 回传顺序与 errors 汇总顺序仍与 tool_calls 一致,
258
+ // 下游 (recordTurn 重复检测 / 错误喂回) 语义不变。agent.parallel_tool_calls=false 回退串行。
259
+ const callable = [];
236
260
  for (const tc of toolCalls) {
237
261
  if (tc.type === "function" && tc.function) {
238
262
  let args = {};
239
- try { args = JSON.parse(tc.function.arguments || "{}"); } catch {}
240
- // v1.6.0 第四刀: 超时检测 + 幂等重试一次 (tool/timeout 事件采集 P50/P95/P99 数据基础)
241
- const { result } = await callWithTimeoutRetry({
242
- name: tc.function.name,
243
- args,
244
- runTool,
245
- isIdempotent: isIdempotentTool(tc.function.name),
246
- budgetMs: toolTimeoutOf(tc.function.name),
247
- onEvent,
248
- });
249
- messages.push({ role: "tool", tool_call_id: tc.id, content: toToolContent(result, policy.resultBudget) });
250
- if (result.startsWith(TOOL_ERROR_PREFIX)) errors.push(result);
263
+ try { args = JSON.parse(tc.function.arguments || "{}"); } catch (e) { debug(`[core/policy] 已忽略异常: ${e && e.message ? e.message : e}`); }
264
+ callable.push({ tc, args });
265
+ }
266
+ }
267
+ // v1.6.0 第四刀语义保留: 超时检测 + 幂等重试一次 (tool/timeout 事件采集 P50/P95/P99 数据基础)
268
+ const execOne = ({ tc, args }) => callWithTimeoutRetry({
269
+ name: tc.function.name,
270
+ args,
271
+ runTool,
272
+ isIdempotent: isIdempotentTool(tc.function.name),
273
+ budgetMs: toolTimeoutOf(tc.function.name),
274
+ onEvent,
275
+ }).then((r) => ({ tc, ...r }));
276
+ const errors = [];
277
+ const collect = (tc, result) => {
278
+ // 2026-10-03 修复 (P1): 原 tool 消息只有非标 `_id`, 缺 OpenAI 规范要求的 tool_call_id
279
+ // → 严格校验后端 (OpenAI 官方 / vLLM) 第二轮必 400。补上标准字段; `_id` 保留兼容内部消费方
280
+ messages.push({ role: "tool", tool_call_id: tc.id, _id: tc.id, content: toToolContent(result, policy.resultBudget) });
281
+ if (result.startsWith(TOOL_ERROR_PREFIX)) errors.push(result);
282
+ };
283
+ if (policy.parallelToolCalls) {
284
+ const settled = await Promise.all(callable.map(execOne));
285
+ for (const { tc, result } of settled) collect(tc, result);
286
+ } else {
287
+ // 串行回退路径 (旧行为): 逐个执行 + 逐个回传
288
+ for (const item of callable) {
289
+ const { result } = await execOne(item);
290
+ collect(item.tc, result);
251
291
  }
252
292
  }
253
293
  if (policy.shouldRetryErrors(errors)) {
294
+ // 自省裁决 (Reflective 内核): 硬拒绝类错误直接拦停, 不得盲目改写命令绕过
295
+ // 语义闸门在次数闸门之前: 即使重试次数未满, 硬拒绝也不重试 (安全红线)
296
+ const verdict = policy.selfReviewError(errors);
297
+ if (verdict && verdict.action === "stop") {
298
+ ev("tool/self_review_stop", { round, reason: verdict.reason, retries: policy.errorRetries });
299
+ messages.push({
300
+ role: "user",
301
+ content: "自省裁决: " + verdict.reason + "\n失败详情:\n" + errors.join("\n") + "\n请停止重试, 直接基于已有信息给出结论, 或向用户说明原因。",
302
+ });
303
+ continue;
304
+ }
254
305
  ev("tool/error_retry", { round, errors: errors.length, retries: policy.errorRetries, max: policy.maxErrorRetry });
255
306
  messages.push({
256
307
  role: "user",
package/src/core/trace.js CHANGED
@@ -11,14 +11,15 @@
11
11
  import { AsyncLocalStorage } from "node:async_hooks";
12
12
  import fs from "node:fs";
13
13
  import path from "node:path";
14
- import { ensureDir, logicalDay } from "../utils/store.js";
14
+ import { logsDir, logicalDay } from "../utils/store.js";
15
+ import { shortId } from "../utils/id.js";
15
16
  import { scrubPII } from "../utils/pii.js";
16
17
 
17
18
  const als = new AsyncLocalStorage();
18
19
  const MAX_PAYLOAD = 2000; // 单条事件载荷上限, 防爆文件
19
20
 
20
21
  export function genTraceId() {
21
- return "t_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
22
+ return shortId("t_", 8);
22
23
  }
23
24
 
24
25
  // 入口包装: 生成新 traceId, 所有异步子调用自动继承
@@ -41,9 +42,8 @@ export function hasTrace() {
41
42
  // ---- 事件流写入器 (agent 构造时实例化一个, 全局复用) ----
42
43
  export class EventTracer {
43
44
  constructor(dataDir) {
44
- this.dir = path.join(dataDir, "logs", "traces");
45
- ensureDir(this.dir);
46
- this.sessionId = "s_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
45
+ this.dir = logsDir(dataDir); // 与 utils/trace.js 同一目录 (logs/traces)
46
+ this.sessionId = shortId("s_", 6);
47
47
  this.count = 0;
48
48
  }
49
49
 
@@ -61,8 +61,10 @@ export class EventTracer {
61
61
  sessionId: this.sessionId,
62
62
  traceId: store?.traceId || null,
63
63
  seq: this.count,
64
- type,
64
+ // ...safe 在 type 之前 (2026-09-18 修复): payload 自带 type 键时不得覆盖埋点事件类型,
65
+ // 否则 events-*.jsonl 中 type 与实际语义不符, 基于 type 的检索/统计失真
65
66
  ...safe,
67
+ type,
66
68
  };
67
69
  if (opts.durationMs != null) entry.durationMs = Math.round(opts.durationMs);
68
70
  if (opts.error != null) entry.error = String(opts.error).slice(0, 500);