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.
- package/LICENSE +201 -201
- package/README.md +218 -265
- package/bin/ppx-channels.js +2 -2
- package/bin/ppx-serve.js +5 -5
- package/bin/ppx-setup.js +124 -0
- package/bin/ppx-web.js +140 -0
- package/bin/ppx.js +2 -2
- package/config/identity.md +6 -6
- package/config/ishiki.md +16 -16
- package/config/ppx.json +15 -151
- package/config/ppx.json.example +143 -0
- package/package.json +17 -10
- package/skills/.usage.json +6 -0
- package/skills/agent-professional-training/SKILL.md +94 -0
- package/skills/brainstorm/SKILL.md +24 -0
- package/skills/cupid-lover-comms/SKILL.md +37 -0
- package/skills/debug/SKILL.md +26 -0
- package/skills/plan/SKILL.md +25 -0
- package/skills/ponytail/SKILL.md +25 -0
- package/skills/ppx-memory/SKILL.md +91 -0
- package/skills/ppx-memory/scripts/cli.js +192 -0
- package/skills/ppx-memory/scripts/experience.js +133 -0
- package/skills/ppx-memory/scripts/fact-store.js +842 -0
- package/skills/ppx-memory/scripts/l0.js +52 -0
- package/skills/ppx-memory/scripts/l2.js +146 -0
- package/skills/ppx-memory/scripts/l3.js +112 -0
- package/skills/ppx-memory/scripts/memory-ticker.js +238 -0
- package/skills/ppx-memory/scripts/pii.js +42 -0
- package/skills/ppx-memory/scripts/schema.js +80 -0
- package/skills/ppx-memory/scripts/session.js +398 -0
- package/skills/ppx-memory/scripts/similarity.js +43 -0
- package/skills/ppx-memory/scripts/store.js +116 -0
- package/skills/ppx-memory/scripts/wal.js +38 -0
- package/skills/ppx-selfheal/SKILL.md +24 -0
- package/skills/ppx-selfheal/scripts/cli.js +80 -0
- package/skills/ppx-selfheal/scripts/healer.js +184 -0
- package/skills/ppx-selfheal/scripts/logger.js +17 -0
- package/skills/ppx-selfheal/scripts/store.js +116 -0
- package/skills/prompt-depth-kit/SKILL.md +28 -0
- package/skills/session-naming/SKILL.md +36 -0
- package/skills/verify/SKILL.md +25 -0
- package/src/agent/index.js +1340 -717
- package/src/agent/prompts.js +47 -3
- package/src/aml-server.js +197 -151
- package/src/ans/eviction.js +123 -143
- package/src/ans/guard.js +159 -120
- package/src/ans/lifecycle.js +96 -93
- package/src/ans/proactive.js +112 -129
- package/src/ans/reward.js +95 -111
- package/src/ans/values.js +15 -15
- package/src/audit/audit-chain.js +43 -7
- package/src/audit/verifier.js +157 -120
- package/src/bus/circuit-breaker.js +9 -1
- package/src/bus/runtime-bus.js +107 -93
- package/src/channels/base.js +57 -34
- package/src/channels/feishu.js +118 -126
- package/src/channels/http.js +1090 -592
- package/src/channels/index.js +111 -110
- package/src/channels/log.js +29 -29
- package/src/channels/wechat-crypto.js +73 -74
- package/src/channels/wechat.js +191 -197
- package/src/channels/workspace.js +94 -0
- package/src/channels-cli.js +126 -124
- package/src/cli.js +129 -120
- package/src/commands/index.js +142 -0
- package/src/config/channels.js +137 -170
- package/src/config/index.js +277 -224
- package/src/config/placeholder.js +31 -0
- package/src/config/providers.js +148 -188
- package/src/config/settings.js +156 -182
- package/src/core/policy.js +69 -18
- package/src/core/trace.js +8 -6
- package/src/edit/editblock.js +266 -0
- package/src/edit/snapshot.js +67 -0
- package/src/evidence/index.js +153 -0
- package/src/evolve/playbook.js +9 -10
- package/src/hooks/index.js +113 -0
- package/src/llm/client.js +188 -446
- package/src/llm/dsml.js +74 -74
- package/src/llm/embedder.js +41 -35
- package/src/llm/fence.js +52 -105
- package/src/llm/index.js +4 -4
- package/src/llm/local-embedder.js +94 -0
- package/src/llm/presets.js +113 -0
- package/src/llm/pricing.js +93 -0
- package/src/llm/retry.js +73 -73
- package/src/llm/router.js +92 -97
- package/src/mcp/admin.js +326 -0
- package/src/mcp/client.js +487 -375
- package/src/mcp/http.js +203 -0
- package/src/mcp/index.js +116 -116
- package/src/mcp/server.js +392 -0
- package/src/mcp/tasks.js +133 -0
- package/src/memory/asset-hub.js +6 -11
- package/src/memory/canvas.js +2 -5
- package/src/memory/compaction.js +28 -28
- package/src/memory/experience.js +133 -122
- package/src/memory/fact-store.js +914 -698
- package/src/memory/failure-episode.js +20 -11
- package/src/memory/fork.js +17 -8
- package/src/memory/index.js +8 -6
- package/src/memory/l0.js +53 -52
- package/src/memory/l2.js +145 -130
- package/src/memory/l3.js +111 -111
- package/src/memory/legion-board.js +71 -0
- package/src/memory/memory-ticker.js +239 -240
- package/src/memory/session.js +398 -397
- package/src/memory/sqlite-store.js +581 -0
- package/src/mode/blackboard.js +49 -49
- package/src/mode/graph.js +42 -41
- package/src/mode/index.js +64 -64
- package/src/mode/legion.js +54 -51
- package/src/mode/plan-exec.js +50 -50
- package/src/mode/router.js +28 -40
- package/src/orchestrator/agent-worker.js +69 -69
- package/src/orchestrator/dag.js +90 -83
- package/src/orchestrator/experts.js +76 -0
- package/src/orchestrator/index.js +1 -1
- package/src/orchestrator/legion.js +179 -187
- package/src/orchestrator/supervisor.js +6 -8
- package/src/permissions/index.js +378 -0
- package/src/persona/index.js +28 -29
- package/src/plugin/builtin.js +313 -212
- package/src/plugin/context.js +80 -79
- package/src/plugin/index.js +62 -62
- package/src/plugin/v3.js +73 -0
- package/src/protocol/index.js +148 -0
- package/src/repomap/index.js +309 -0
- package/src/review/index.js +393 -0
- package/src/seam/registry.js +3 -0
- package/src/seam/shell.js +55 -55
- package/src/security/injection.js +79 -0
- package/src/selfheal/evolve.js +67 -67
- package/src/selfheal/healer.js +184 -167
- package/src/selfheal/run.js +9 -9
- package/src/server.js +63 -60
- package/src/services/diagnose.js +180 -0
- package/src/services/learning-service.js +9 -0
- package/src/services/memory-health.js +34 -6
- package/src/services/memory-service.js +42 -9
- package/src/services/triage.js +138 -0
- package/src/session/parts.js +76 -0
- package/src/session/projection.js +73 -0
- package/src/session/rollout.js +54 -0
- package/src/session/turn.js +137 -0
- package/src/skills/lint.js +72 -0
- package/src/skills/loader.js +231 -150
- package/src/skills/search.js +58 -0
- package/src/skills/verify.js +95 -100
- package/src/tools/advanced.js +388 -352
- package/src/tools/builtin.js +384 -297
- package/src/tools/catalog.js +283 -159
- package/src/tools/command-guard.js +112 -112
- package/src/tools/custom.js +47 -47
- package/src/tools/delegate.js +383 -297
- package/src/tools/document.js +254 -253
- package/src/tools/git.js +151 -0
- package/src/tools/governance.js +47 -20
- package/src/tools/index.js +16 -11
- package/src/tools/methods.js +178 -178
- package/src/tools/ocr.js +59 -59
- package/src/tools/sandbox-worker.js +40 -0
- package/src/tools/sandbox.js +92 -0
- package/src/tools/seam.js +162 -125
- package/src/tools/selfmod.js +196 -176
- package/src/tools/v3.js +225 -0
- package/src/tools/vad.js +176 -0
- package/src/tools/voice.js +238 -0
- package/src/utils/async.js +14 -0
- package/src/utils/config-file.js +53 -0
- package/src/utils/crashguard.js +88 -0
- package/src/utils/http.js +53 -0
- package/src/utils/id.js +8 -0
- package/src/utils/json-state.js +33 -0
- package/src/utils/logger.js +17 -17
- package/src/utils/ndjson.js +25 -0
- package/src/utils/pii.js +42 -42
- package/src/utils/rate-limit.js +50 -0
- package/src/utils/schema.js +80 -0
- package/src/utils/similarity.js +43 -0
- package/src/utils/store.js +170 -108
- package/src/utils/text.js +15 -15
- package/src/utils/trace.js +153 -153
- package/src/utils/wal.js +39 -0
- package/src/utils/winutf8.js +16 -15
- package/src/wiki/index.js +170 -0
package/src/channels/index.js
CHANGED
|
@@ -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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
this.
|
|
27
|
-
this.
|
|
28
|
-
this.
|
|
29
|
-
this.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
this.
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
export {
|
|
108
|
-
export {
|
|
109
|
-
export {
|
|
110
|
-
export {
|
|
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";
|
package/src/channels/log.js
CHANGED
|
@@ -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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
return sha1
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
export function
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
decipher.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
cipher.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
<
|
|
70
|
-
<
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
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
|
+
}
|