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,110 +1,111 @@
1
- // src/channels/index.js - 通道管理器 (注册表, config 驱动, 可更换)
2
- // 每个通道 = Channel 子类 (connect/send/disconnect); 新增通道类型 → 注册进 channelTypes 即可, 不改 manager
3
- // 使用方: new ChannelManager(agent, config.channels) → await start()
4
- import { HttpChannel } from "./http.js";
5
- import { FeishuChannel } from "./feishu.js";
6
- import { WechatWebhookChannel } from "./wechat.js";
7
- import { LogChannel } from "./log.js";
8
-
9
- // 内置通道类型注册表: name -> 通道类 (构造函数签名: (agent, configObj))
10
- export const BUILTIN_CHANNEL_TYPES = {
11
- http: HttpChannel,
12
- feishu: FeishuChannel,
13
- wechat: WechatWebhookChannel,
14
- log: LogChannel,
15
- };
16
-
17
- // 启停策略: http 默认开, 其余默认关 (config.channels.<name>.enabled)
18
- function isEnabled(name, cfg) {
19
- if (name === "http") return cfg.enabled !== false;
20
- return !!cfg.enabled;
21
- }
22
-
23
- export class ChannelManager {
24
- constructor(agent, config = {}, channelTypes = null) {
25
- this.agent = agent;
26
- this.config = config || {}; // config.channels
27
- this.channelTypes = channelTypes || BUILTIN_CHANNEL_TYPES; // 可注入自定义类型 (更换通道实现)
28
- this.channels = [];
29
- this.httpServer = null;
30
- }
31
-
32
- // 按注册表顺序启动所有启用的通道 (单个失败不影响其他)
33
- // 流程: 全部 connect → 若 HTTP 通道在跑, 给 webhook 型通道挂载路由
34
- async start() {
35
- const started = [];
36
- for (const [name, Ctor] of Object.entries(this.channelTypes)) {
37
- const cfg = { ...(this.config[name] || {}) };
38
- if (!isEnabled(name, cfg)) continue;
39
- try {
40
- const ch = new Ctor(this.agent, cfg);
41
- await ch.connect();
42
- this.channels.push(ch);
43
- if (ch.server) this.httpServer = ch.server;
44
- started.push(name);
45
- } catch (e) {
46
- console.warn(`[channels] ${name} 通道启动失败: ${e.message}`);
47
- }
48
- }
49
- // webhook 型通道挂到 HTTP server (若已启动); 传 http 通道实例供 registerWebhook 路由分发
50
- if (this.httpServer) {
51
- const httpCh = this.get("http") || null;
52
- for (const ch of this.channels) {
53
- if (ch.name !== "http" && typeof ch.mount === "function") {
54
- try { ch.mount(this.httpServer, httpCh); } catch (e) { console.warn(`[channels] ${ch.name} webhook 挂载失败: ${e.message}`); }
55
- }
56
- }
57
- }
58
- return started;
59
- }
60
-
61
- // 按 name 取已启动通道 (无则 null)
62
- get(name) {
63
- return this.channels.find((c) => c.name === name) || null;
64
- }
65
-
66
- // 列出所有可配置通道的状态 (含未启用的), 供 CLI/UI 展示
67
- list() {
68
- return Object.entries(this.channelTypes).map(([name, Ctor]) => {
69
- const cfg = this.config[name] || {};
70
- return { name, enabled: isEnabled(name, cfg), connected: !!this.get(name) };
71
- });
72
- }
73
-
74
- // 连通性测试: 用独立实例验证某通道配置 (不干扰已启动的通道)
75
- async test(name) {
76
- const Ctor = this.channelTypes[name];
77
- if (!Ctor) return { ok: false, detail: `未知通道类型: ${name}` };
78
- const cfg = { ...(this.config[name] || {}) };
79
- try {
80
- const ch = new Ctor(this.agent, cfg);
81
- const r = typeof ch.test === "function" ? await ch.test() : await ch.connect();
82
- return typeof r === "object" && r !== null ? r : { ok: true, detail: String(r) };
83
- } catch (e) {
84
- return { ok: false, detail: e.message };
85
- }
86
- }
87
-
88
- // 广播到所有已启动通道 (主动提醒/系统消息投递入口)
89
- async broadcast(text, { to = "*" } = {}) {
90
- const out = [];
91
- for (const c of this.channels) {
92
- try { out.push(await c.send(to, text)); } catch (e) { console.warn(`[channels] ${c.name} 发送失败: ${e.message}`); }
93
- }
94
- return out;
95
- }
96
-
97
- async stop() {
98
- for (const c of this.channels) {
99
- try { await c.disconnect?.(); } catch {}
100
- }
101
- this.channels = [];
102
- this.httpServer = null;
103
- }
104
- }
105
-
106
- export { Channel } from "./base.js";
107
- export { HttpChannel } from "./http.js";
108
- export { FeishuChannel } from "./feishu.js";
109
- export { WechatWebhookChannel } from "./wechat.js";
110
- export { LogChannel } from "./log.js";
1
+ // src/channels/index.js - 通道管理器 (注册表, config 驱动, 可更换)
2
+ // 每个通道 = Channel 子类 (connect/send/disconnect); 新增通道类型 → 注册进 channelTypes 即可, 不改 manager
3
+ // 使用方: new ChannelManager(agent, config.channels) → await start()
4
+ import { HttpChannel } from "./http.js";
5
+ import { FeishuChannel } from "./feishu.js";
6
+ import { WechatWebhookChannel } from "./wechat.js";
7
+ import { LogChannel } from "./log.js";
8
+ import { debug } from "../utils/logger.js";
9
+
10
+ // 内置通道类型注册表: name -> 通道类 (构造函数签名: (agent, configObj))
11
+ export const BUILTIN_CHANNEL_TYPES = {
12
+ http: HttpChannel,
13
+ feishu: FeishuChannel,
14
+ wechat: WechatWebhookChannel,
15
+ log: LogChannel,
16
+ };
17
+
18
+ // 启停策略: http 默认开, 其余默认关 (config.channels.<name>.enabled)
19
+ function isEnabled(name, cfg) {
20
+ if (name === "http") return cfg.enabled !== false;
21
+ return !!cfg.enabled;
22
+ }
23
+
24
+ export class ChannelManager {
25
+ constructor(agent, config = {}, channelTypes = null) {
26
+ this.agent = agent;
27
+ this.config = config || {}; // config.channels
28
+ this.channelTypes = channelTypes || BUILTIN_CHANNEL_TYPES; // 可注入自定义类型 (更换通道实现)
29
+ this.channels = [];
30
+ this.httpServer = null;
31
+ }
32
+
33
+ // 按注册表顺序启动所有启用的通道 (单个失败不影响其他)
34
+ // 流程: 全部 connect → 若 HTTP 通道在跑, 给 webhook 型通道挂载路由
35
+ async start() {
36
+ const started = [];
37
+ for (const [name, Ctor] of Object.entries(this.channelTypes)) {
38
+ const cfg = { ...(this.config[name] || {}) };
39
+ if (!isEnabled(name, cfg)) continue;
40
+ try {
41
+ const ch = new Ctor(this.agent, cfg);
42
+ await ch.connect();
43
+ this.channels.push(ch);
44
+ if (ch.server) this.httpServer = ch.server;
45
+ started.push(name);
46
+ } catch (e) {
47
+ console.warn(`[channels] ${name} 通道启动失败: ${e.message}`);
48
+ }
49
+ }
50
+ // webhook 型通道挂到 HTTP server (若已启动); 传 http 通道实例供 registerWebhook 路由分发
51
+ if (this.httpServer) {
52
+ const httpCh = this.get("http") || null;
53
+ for (const ch of this.channels) {
54
+ if (ch.name !== "http" && typeof ch.mount === "function") {
55
+ try { ch.mount(this.httpServer, httpCh); } catch (e) { console.warn(`[channels] ${ch.name} webhook 挂载失败: ${e.message}`); }
56
+ }
57
+ }
58
+ }
59
+ return started;
60
+ }
61
+
62
+ // 按 name 取已启动通道 (无则 null)
63
+ get(name) {
64
+ return this.channels.find((c) => c.name === name) || null;
65
+ }
66
+
67
+ // 列出所有可配置通道的状态 (含未启用的), 供 CLI/UI 展示
68
+ list() {
69
+ return Object.entries(this.channelTypes).map(([name, Ctor]) => {
70
+ const cfg = this.config[name] || {};
71
+ return { name, enabled: isEnabled(name, cfg), connected: !!this.get(name) };
72
+ });
73
+ }
74
+
75
+ // 连通性测试: 用独立实例验证某通道配置 (不干扰已启动的通道)
76
+ async test(name) {
77
+ const Ctor = this.channelTypes[name];
78
+ if (!Ctor) return { ok: false, detail: `未知通道类型: ${name}` };
79
+ const cfg = { ...(this.config[name] || {}) };
80
+ try {
81
+ const ch = new Ctor(this.agent, cfg);
82
+ const r = typeof ch.test === "function" ? await ch.test() : await ch.connect();
83
+ return typeof r === "object" && r !== null ? r : { ok: true, detail: String(r) };
84
+ } catch (e) {
85
+ return { ok: false, detail: e.message };
86
+ }
87
+ }
88
+
89
+ // 广播到所有已启动通道 (主动提醒/系统消息投递入口)
90
+ async broadcast(text, { to = "*" } = {}) {
91
+ const out = [];
92
+ for (const c of this.channels) {
93
+ try { out.push(await c.send(to, text)); } catch (e) { console.warn(`[channels] ${c.name} 发送失败: ${e.message}`); }
94
+ }
95
+ return out;
96
+ }
97
+
98
+ async stop() {
99
+ for (const c of this.channels) {
100
+ try { await c.disconnect?.(); } catch (e) { debug(`[channels/index] 已忽略异常: ${e && e.message ? e.message : e}`); }
101
+ }
102
+ this.channels = [];
103
+ this.httpServer = null;
104
+ }
105
+ }
106
+
107
+ export { Channel } from "./base.js";
108
+ export { HttpChannel } from "./http.js";
109
+ export { FeishuChannel } from "./feishu.js";
110
+ export { WechatWebhookChannel } from "./wechat.js";
111
+ export { LogChannel } from "./log.js";
@@ -1,29 +1,29 @@
1
- // src/channels/log.js - 日志通道 (dummy): 无网络依赖, 把消息打印到 stdout
2
- // 用途: 主动提醒/消息先落到本地日志验证契约, 再接真实通道 (webhook/IM) 时只需"换一个 adapter"
3
- import { Channel } from "./base.js";
4
-
5
- export class LogChannel extends Channel {
6
- constructor(agent, { target = "console" } = {}) {
7
- super("log", agent);
8
- this.target = target; // 预留: "console" | 未来文件/远程日志
9
- }
10
-
11
- async connect() {
12
- this.connected = true;
13
- return this;
14
- }
15
-
16
- async send(to, text) {
17
- const prefix = to && to !== "*" ? `[log:${to}]` : "[log]";
18
- console.log(`${prefix} ${String(text)}`);
19
- return text;
20
- }
21
-
22
- async test() {
23
- return { ok: true, detail: "日志通道无需网络, 总是可用" };
24
- }
25
-
26
- async disconnect() {
27
- this.connected = false;
28
- }
29
- }
1
+ // src/channels/log.js - 日志通道 (dummy): 无网络依赖, 把消息打印到 stdout
2
+ // 用途: 主动提醒/消息先落到本地日志验证契约, 再接真实通道 (webhook/IM) 时只需"换一个 adapter"
3
+ import { Channel } from "./base.js";
4
+
5
+ export class LogChannel extends Channel {
6
+ constructor(agent, { target = "console" } = {}) {
7
+ super("log", agent);
8
+ this.target = target; // 预留: "console" | 未来文件/远程日志
9
+ }
10
+
11
+ async connect() {
12
+ this.connected = true;
13
+ return this;
14
+ }
15
+
16
+ async send(to, text) {
17
+ const prefix = to && to !== "*" ? `[log:${to}]` : "[log]";
18
+ console.log(`${prefix} ${String(text)}`);
19
+ return text;
20
+ }
21
+
22
+ async test() {
23
+ return { ok: true, detail: "日志通道无需网络, 总是可用" };
24
+ }
25
+
26
+ async disconnect() {
27
+ this.connected = false;
28
+ }
29
+ }
@@ -1,74 +1,73 @@
1
- // src/channels/wechat-crypto.js - 微信消息加解密 (企业微信/公众号) 零依赖
2
- // 参考官方 WXBizMsgCrypt: AES-256-CBC + PKCS7 填充 + SHA1 签名校验
3
- // 用 Node 原生 crypto, 无任何 npm 依赖。真实联调需企微/公众号凭据 (token + encodingAESKey)。
4
- import crypto from "node:crypto";
5
-
6
- // 43 字符 encodingAESKey → 32 字节 AES key (base64, 补 '=' 凑满 32 字节)
7
- function keyOf(encodingAESKey) {
8
- return Buffer.from(String(encodingAESKey) + "=", "base64");
9
- }
10
-
11
- // SHA1 签名校验: sort([token, timestamp, nonce, encrypt]).join("") 的 sha1 与 signature 比对
12
- export function verifySignature(token, timestamp, nonce, encrypt, signature) {
13
- const arr = [String(token), String(timestamp), String(nonce), String(encrypt)].sort();
14
- const sha1 = crypto.createHash("sha1").update(arr.join(""), "utf8").digest("hex");
15
- return sha1 === String(signature);
16
- }
17
-
18
- // 生成签名 (加密回包时用, 与 verifySignature 同一算法)
19
- export function generateSignature(token, timestamp, nonce, encrypt) {
20
- const arr = [String(token), String(timestamp), String(nonce), String(encrypt)].sort();
21
- return crypto.createHash("sha1").update(arr.join(""), "utf8").digest("hex");
22
- }
23
-
24
- // 解密 <Encrypt> → { msg, receiveId } (receiveId 通常为 corpId)
25
- export function decryptMsg(encodingAESKey, encryptedXml) {
26
- const m = String(encryptedXml).match(/<Encrypt><!\[CDATA\[([\s\S]*?)\]\]><\/Encrypt>/);
27
- const enc = m ? m[1] : "";
28
- if (!enc) throw new Error("消息体无 <Encrypt>");
29
- const key = keyOf(encodingAESKey);
30
- const iv = key.slice(0, 16);
31
- const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv);
32
- decipher.setAutoPadding(false);
33
- const dec = Buffer.concat([decipher.update(enc, "base64"), decipher.final()]);
34
- // PKCS7 去填充
35
- const pad = dec[dec.length - 1];
36
- const plain = dec.slice(0, dec.length - pad);
37
- // 明文格式: random(16) + msgLen(4, 大端) + msg + receiveId
38
- const msgLen = plain.readUInt32BE(16);
39
- const msg = plain.slice(20, 20 + msgLen).toString("utf8");
40
- const receiveId = plain.slice(20 + msgLen).toString("utf8");
41
- return { msg, receiveId };
42
- }
43
-
44
- // 加密回复 → 返回 <Encrypt> 里的 base64 密文 (供拼回复 XML)
45
- export function encryptMsg(encodingAESKey, msg, receiveId = "") {
46
- const key = keyOf(encodingAESKey);
47
- const iv = key.slice(0, 16);
48
- const msgBuf = Buffer.from(String(msg), "utf8");
49
- const idBuf = Buffer.from(String(receiveId), "utf8");
50
- const lenBuf = Buffer.alloc(4);
51
- lenBuf.writeUInt32BE(msgBuf.length, 0);
52
- const plain = Buffer.concat([crypto.randomBytes(16), lenBuf, msgBuf, idBuf]);
53
- // PKCS7 填充到 32 字节倍数
54
- const padLen = 32 - (plain.length % 32);
55
- const padded = Buffer.concat([plain, Buffer.alloc(padLen, padLen)]);
56
- const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
57
- cipher.setAutoPadding(false);
58
- return Buffer.concat([cipher.update(padded), cipher.final()]).toString("base64");
59
- }
60
-
61
- // 加密被动回复: 生成企业微信加密模式回包 XML (含 MsgSignature/TimeStamp/Nonce)
62
- // replyXml = 明文回复 XML, receiveId = corpId (解密时拿到的)
63
- export function encryptReplyXml({ encodingAESKey, token, replyXml, receiveId = "", timestamp = null, nonce = null }) {
64
- const enc = encryptMsg(encodingAESKey, replyXml, receiveId);
65
- const ts = String(timestamp ?? Math.floor(Date.now() / 1000));
66
- const nc = nonce ?? Math.random().toString(36).slice(2, 12);
67
- const sig = generateSignature(token, ts, nc, enc);
68
- return `<xml>
69
- <Encrypt><![CDATA[${enc}]]></Encrypt>
70
- <MsgSignature><![CDATA[${sig}]]></MsgSignature>
71
- <TimeStamp>${ts}</TimeStamp>
72
- <Nonce><![CDATA[${nc}]]></Nonce>
73
- </xml>`;
74
- }
1
+ // src/channels/wechat-crypto.js - 微信消息加解密 (企业微信/公众号) 零依赖
2
+ // 参考官方 WXBizMsgCrypt: AES-256-CBC + PKCS7 填充 + SHA1 签名校验
3
+ // 用 Node 原生 crypto, 无任何 npm 依赖。真实联调需企微/公众号凭据 (token + encodingAESKey)。
4
+ import crypto from "node:crypto";
5
+
6
+ // 43 字符 encodingAESKey → 32 字节 AES key (base64, 补 '=' 凑满 32 字节)
7
+ function keyOf(encodingAESKey) {
8
+ return Buffer.from(String(encodingAESKey) + "=", "base64");
9
+ }
10
+
11
+ // SHA1 签名: sort([token, timestamp, nonce, encrypt]).join("") 的 sha1 (验签与回包签名同一算法)
12
+ // 2026-09-18 重构: 原 verifySignature / generateSignature 各抄一份算法, 现统一由本函数产出
13
+ export function generateSignature(token, timestamp, nonce, encrypt) {
14
+ const arr = [String(token), String(timestamp), String(nonce), String(encrypt)].sort();
15
+ return crypto.createHash("sha1").update(arr.join(""), "utf8").digest("hex");
16
+ }
17
+
18
+ // 签名校验 (与 generateSignature 同算法)
19
+ export function verifySignature(token, timestamp, nonce, encrypt, signature) {
20
+ return generateSignature(token, timestamp, nonce, encrypt) === String(signature);
21
+ }
22
+
23
+ // 解密 <Encrypt> → { msg, receiveId } (receiveId 通常为 corpId)
24
+ export function decryptMsg(encodingAESKey, encryptedXml) {
25
+ const m = String(encryptedXml).match(/<Encrypt><!\[CDATA\[([\s\S]*?)\]\]><\/Encrypt>/);
26
+ const enc = m ? m[1] : "";
27
+ if (!enc) throw new Error("消息体无 <Encrypt>");
28
+ const key = keyOf(encodingAESKey);
29
+ const iv = key.slice(0, 16);
30
+ const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv);
31
+ decipher.setAutoPadding(false);
32
+ const dec = Buffer.concat([decipher.update(enc, "base64"), decipher.final()]);
33
+ // PKCS7 去填充
34
+ const pad = dec[dec.length - 1];
35
+ const plain = dec.slice(0, dec.length - pad);
36
+ // 明文格式: random(16) + msgLen(4, 大端) + msg + receiveId
37
+ const msgLen = plain.readUInt32BE(16);
38
+ const msg = plain.slice(20, 20 + msgLen).toString("utf8");
39
+ const receiveId = plain.slice(20 + msgLen).toString("utf8");
40
+ return { msg, receiveId };
41
+ }
42
+
43
+ // 加密回复 → 返回 <Encrypt> 里的 base64 密文 (供拼回复 XML)
44
+ export function encryptMsg(encodingAESKey, msg, receiveId = "") {
45
+ const key = keyOf(encodingAESKey);
46
+ const iv = key.slice(0, 16);
47
+ const msgBuf = Buffer.from(String(msg), "utf8");
48
+ const idBuf = Buffer.from(String(receiveId), "utf8");
49
+ const lenBuf = Buffer.alloc(4);
50
+ lenBuf.writeUInt32BE(msgBuf.length, 0);
51
+ const plain = Buffer.concat([crypto.randomBytes(16), lenBuf, msgBuf, idBuf]);
52
+ // PKCS7 填充到 32 字节倍数
53
+ const padLen = 32 - (plain.length % 32);
54
+ const padded = Buffer.concat([plain, Buffer.alloc(padLen, padLen)]);
55
+ const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
56
+ cipher.setAutoPadding(false);
57
+ return Buffer.concat([cipher.update(padded), cipher.final()]).toString("base64");
58
+ }
59
+
60
+ // 加密被动回复: 生成企业微信加密模式回包 XML (含 MsgSignature/TimeStamp/Nonce)
61
+ // replyXml = 明文回复 XML, receiveId = corpId (解密时拿到的)
62
+ export function encryptReplyXml({ encodingAESKey, token, replyXml, receiveId = "", timestamp = null, nonce = null }) {
63
+ const enc = encryptMsg(encodingAESKey, replyXml, receiveId);
64
+ const ts = String(timestamp ?? Math.floor(Date.now() / 1000));
65
+ const nc = nonce ?? Math.random().toString(36).slice(2, 12);
66
+ const sig = generateSignature(token, ts, nc, enc);
67
+ return `<xml>
68
+ <Encrypt><![CDATA[${enc}]]></Encrypt>
69
+ <MsgSignature><![CDATA[${sig}]]></MsgSignature>
70
+ <TimeStamp>${ts}</TimeStamp>
71
+ <Nonce><![CDATA[${nc}]]></Nonce>
72
+ </xml>`;
73
+ }