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/wechat.js
CHANGED
|
@@ -1,197 +1,191 @@
|
|
|
1
|
-
// src/channels/wechat.js - 微信通道适配器 (企业微信/公众号 webhook)
|
|
2
|
-
// 已实现:
|
|
3
|
-
// - 加密消息解密 (AES-256-CBC) + 验签 (wechat-crypto.js)
|
|
4
|
-
// - 明文/加密模式被动回复 (加密模式回包 encryptReplyXml)
|
|
5
|
-
// - 主动推送 send(): 企业微信应用消息 API (corp_id + corp_secret + agent_id)
|
|
6
|
-
// 凭据: 接收回调用 token + encodingAESKey; 主动推送用 corp_id + corp_secret + agent_id
|
|
7
|
-
// (config.channels.wechat 或环境变量 WECHAT_*)
|
|
8
|
-
import { Channel } from "./base.js";
|
|
9
|
-
import { decryptMsg, verifySignature, encryptReplyXml } from "./wechat-crypto.js";
|
|
10
|
-
|
|
11
|
-
export class WechatWebhookChannel extends Channel {
|
|
12
|
-
constructor(agent, {
|
|
13
|
-
path = "/wechat/webhook",
|
|
14
|
-
token = "",
|
|
15
|
-
encodingAESKey = "",
|
|
16
|
-
corpId = "",
|
|
17
|
-
corpSecret = "",
|
|
18
|
-
agentId = "",
|
|
19
|
-
} = {}) {
|
|
20
|
-
super("wechat", agent);
|
|
21
|
-
this.path = path;
|
|
22
|
-
this.token = token || process.env.WECHAT_WEBHOOK_TOKEN || "";
|
|
23
|
-
this.encodingAESKey = encodingAESKey || process.env.WECHAT_ENCODING_AES_KEY || "";
|
|
24
|
-
// 主动推送凭据 (企业微信应用消息)
|
|
25
|
-
this.corpId = corpId || process.env.WECHAT_CORP_ID || "";
|
|
26
|
-
this.corpSecret = corpSecret || process.env.WECHAT_CORP_SECRET || "";
|
|
27
|
-
this.agentId = agentId || process.env.WECHAT_AGENT_ID || "";
|
|
28
|
-
this.accessToken = null;
|
|
29
|
-
this.tokenExpire = 0;
|
|
30
|
-
this.lastUser = null; // 最近收到消息的用户 ID, 供主动提醒(广播)回发
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async connect() {
|
|
34
|
-
this.connected = true;
|
|
35
|
-
return this;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// 连通性测试: 配置了主动推送凭据则验证 access_token; 仅回调模式返回提示
|
|
39
|
-
async test() {
|
|
40
|
-
if (!this.corpId || !this.corpSecret) {
|
|
41
|
-
return { ok: true, detail: "回调模式配置完整 (回调无法离线探测, 需公网/平台侧验证); 未配置主动推送凭据 (corp_id/corp_secret/agent_id)" };
|
|
42
|
-
}
|
|
43
|
-
try {
|
|
44
|
-
const token = await this._getAccessToken();
|
|
45
|
-
return { ok: true, detail: `企业微信推送通道通 (access_token 获取成功, ${String(token).slice(0, 6)}...)` };
|
|
46
|
-
} catch (e) {
|
|
47
|
-
return { ok: false, detail: e.message };
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// 把 /wechat/webhook 挂到 HTTP server (webhook 型通道, 支持明文/加密回包 + GET echostr URL 验证)
|
|
52
|
-
// v1.0.8: 注册到 HttpChannel 的 webhook 路由 (单一 request handler 分发); 支持 GET echostr
|
|
53
|
-
mount(server, httpChannel = null) {
|
|
54
|
-
const register =
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
});
|
|
193
|
-
const data = await r.json();
|
|
194
|
-
if (data.errcode !== 0) throw new Error(`微信发送失败: ${data.errmsg} (errcode=${data.errcode})`);
|
|
195
|
-
return data;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
1
|
+
// src/channels/wechat.js - 微信通道适配器 (企业微信/公众号 webhook)
|
|
2
|
+
// 已实现:
|
|
3
|
+
// - 加密消息解密 (AES-256-CBC) + 验签 (wechat-crypto.js)
|
|
4
|
+
// - 明文/加密模式被动回复 (加密模式回包 encryptReplyXml)
|
|
5
|
+
// - 主动推送 send(): 企业微信应用消息 API (corp_id + corp_secret + agent_id)
|
|
6
|
+
// 凭据: 接收回调用 token + encodingAESKey; 主动推送用 corp_id + corp_secret + agent_id
|
|
7
|
+
// (config.channels.wechat 或环境变量 WECHAT_*)
|
|
8
|
+
import { Channel } from "./base.js";
|
|
9
|
+
import { decryptMsg, verifySignature, encryptReplyXml } from "./wechat-crypto.js";
|
|
10
|
+
|
|
11
|
+
export class WechatWebhookChannel extends Channel {
|
|
12
|
+
constructor(agent, {
|
|
13
|
+
path = "/wechat/webhook",
|
|
14
|
+
token = "",
|
|
15
|
+
encodingAESKey = "",
|
|
16
|
+
corpId = "",
|
|
17
|
+
corpSecret = "",
|
|
18
|
+
agentId = "",
|
|
19
|
+
} = {}) {
|
|
20
|
+
super("wechat", agent);
|
|
21
|
+
this.path = path;
|
|
22
|
+
this.token = token || process.env.WECHAT_WEBHOOK_TOKEN || "";
|
|
23
|
+
this.encodingAESKey = encodingAESKey || process.env.WECHAT_ENCODING_AES_KEY || "";
|
|
24
|
+
// 主动推送凭据 (企业微信应用消息)
|
|
25
|
+
this.corpId = corpId || process.env.WECHAT_CORP_ID || "";
|
|
26
|
+
this.corpSecret = corpSecret || process.env.WECHAT_CORP_SECRET || "";
|
|
27
|
+
this.agentId = agentId || process.env.WECHAT_AGENT_ID || "";
|
|
28
|
+
this.accessToken = null;
|
|
29
|
+
this.tokenExpire = 0;
|
|
30
|
+
this.lastUser = null; // 最近收到消息的用户 ID, 供主动提醒(广播)回发
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async connect() {
|
|
34
|
+
this.connected = true;
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 连通性测试: 配置了主动推送凭据则验证 access_token; 仅回调模式返回提示
|
|
39
|
+
async test() {
|
|
40
|
+
if (!this.corpId || !this.corpSecret) {
|
|
41
|
+
return { ok: true, detail: "回调模式配置完整 (回调无法离线探测, 需公网/平台侧验证); 未配置主动推送凭据 (corp_id/corp_secret/agent_id)" };
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const token = await this._getAccessToken();
|
|
45
|
+
return { ok: true, detail: `企业微信推送通道通 (access_token 获取成功, ${String(token).slice(0, 6)}...)` };
|
|
46
|
+
} catch (e) {
|
|
47
|
+
return { ok: false, detail: e.message };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 把 /wechat/webhook 挂到 HTTP server (webhook 型通道, 支持明文/加密回包 + GET echostr URL 验证)
|
|
52
|
+
// v1.0.8: 注册到 HttpChannel 的 webhook 路由 (单一 request handler 分发); 支持 GET echostr
|
|
53
|
+
mount(server, httpChannel = null) {
|
|
54
|
+
const register = this._registrar(server, httpChannel);
|
|
55
|
+
register(this.path, async (req, res) => {
|
|
56
|
+
const u = new URL(req.url || "/", "http://localhost");
|
|
57
|
+
const body = req.method === "POST" ? await this._readBody(req) : "";
|
|
58
|
+
const query = {
|
|
59
|
+
msg_signature: u.searchParams.get("msg_signature") || "",
|
|
60
|
+
timestamp: u.searchParams.get("timestamp") || "",
|
|
61
|
+
nonce: u.searchParams.get("nonce") || "",
|
|
62
|
+
echostr: u.searchParams.get("echostr") || "",
|
|
63
|
+
};
|
|
64
|
+
try {
|
|
65
|
+
const out = await this.handleWebhook(body, query);
|
|
66
|
+
if (out.xml) { res.writeHead(200, { "Content-Type": "text/xml" }); res.end(out.xml); }
|
|
67
|
+
else if (typeof out === "string") { res.writeHead(200, { "Content-Type": "text/plain" }); res.end(out); } // echostr 明文回显 (企业微信 URL 验证要求)
|
|
68
|
+
else this._sendJson(res, 200, out);
|
|
69
|
+
} catch (e) {
|
|
70
|
+
this._sendJson(res, 500, { error: e.message });
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 企业微信回调: URL query 带 msg_signature/timestamp/nonce; body 为 XML
|
|
76
|
+
// 加密模式: 外层 XML 含 <Encrypt>, 解密得内层明文消息
|
|
77
|
+
// v1.0.8 安全加固: 配置了 token 时所有模式都必须验签 (明文/加密/echostr), 防伪造消息驱动 agent
|
|
78
|
+
async handleWebhook(body, query = {}) {
|
|
79
|
+
const raw = typeof body === "string" ? body : JSON.stringify(body);
|
|
80
|
+
const { msg_signature, timestamp, nonce } = query;
|
|
81
|
+
|
|
82
|
+
// URL 验证 (GET echostr, 明文模式验签后直接回显)
|
|
83
|
+
if (query.echostr) {
|
|
84
|
+
if (this.token) {
|
|
85
|
+
if (!msg_signature) return { error: "缺少 msg_signature" };
|
|
86
|
+
const ok = verifySignature(this.token, timestamp || "", nonce || "", query.echostr, msg_signature);
|
|
87
|
+
if (!ok) return { error: "签名校验失败" };
|
|
88
|
+
}
|
|
89
|
+
return query.echostr;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 加密消息: 验签 + 解密 + 加密回包
|
|
93
|
+
if (/<Encrypt>/.test(raw)) {
|
|
94
|
+
const enc = (raw.match(/<Encrypt><!\[CDATA\[([\s\S]*?)\]\]><\/Encrypt>/) || [])[1] || "";
|
|
95
|
+
// 配置了 token 就必须验签 (防无 token 时跳过)
|
|
96
|
+
if (this.token) {
|
|
97
|
+
if (!msg_signature) return { error: "缺少 msg_signature" };
|
|
98
|
+
const ok = verifySignature(this.token, timestamp || "", nonce || "", enc, msg_signature);
|
|
99
|
+
if (!ok) return { error: "签名校验失败" };
|
|
100
|
+
}
|
|
101
|
+
if (!this.encodingAESKey) return { error: "未配置 encodingAESKey, 无法解密" };
|
|
102
|
+
const { msg, receiveId } = decryptMsg(this.encodingAESKey, raw);
|
|
103
|
+
const reply = await this._replyFromXml(msg);
|
|
104
|
+
// 加密模式: 把明文回复 XML 加密成回包 (含签名/时间戳/随机数)
|
|
105
|
+
if (reply.xml) {
|
|
106
|
+
return { xml: encryptReplyXml({
|
|
107
|
+
encodingAESKey: this.encodingAESKey,
|
|
108
|
+
token: this.token,
|
|
109
|
+
replyXml: reply.xml,
|
|
110
|
+
receiveId,
|
|
111
|
+
timestamp: timestamp || null,
|
|
112
|
+
nonce: nonce || null,
|
|
113
|
+
}) };
|
|
114
|
+
}
|
|
115
|
+
return reply;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 明文 XML 模式: 配置了 token 就必须验签 (防伪造消息)
|
|
119
|
+
if (/<Content>/.test(raw)) {
|
|
120
|
+
if (this.token) {
|
|
121
|
+
if (!msg_signature) return { error: "缺少 msg_signature" };
|
|
122
|
+
const ok = verifySignature(this.token, timestamp || "", nonce || "", raw, msg_signature);
|
|
123
|
+
if (!ok) return { error: "签名校验失败" };
|
|
124
|
+
}
|
|
125
|
+
return this._replyFromXml(raw);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { error: "未识别的微信消息" };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 从明文 XML 提取文本并生成被动回复
|
|
132
|
+
async _replyFromXml(xml) {
|
|
133
|
+
const g = (tag) => ((String(xml).match(new RegExp(`<${tag}><!\\[CDATA\\[([\\s\\S]*?)\\]\\]></${tag}>`)) || [])[1] || "").trim();
|
|
134
|
+
const text = g("Content");
|
|
135
|
+
if (!text) return { error: "无文本内容" };
|
|
136
|
+
const reply = await this.agent.chat(text);
|
|
137
|
+
const toUser = g("FromUserName");
|
|
138
|
+
if (toUser) this.lastUser = toUser; // 记录最近联系人, 供主动提醒回发
|
|
139
|
+
const fromUser = g("ToUserName");
|
|
140
|
+
return {
|
|
141
|
+
xml: `<xml>
|
|
142
|
+
<ToUserName><![CDATA[${toUser}]]></ToUserName>
|
|
143
|
+
<FromUserName><![CDATA[${fromUser}]]></FromUserName>
|
|
144
|
+
<CreateTime>${Math.floor(Date.now() / 1000)}</CreateTime>
|
|
145
|
+
<MsgType><![CDATA[text]]></MsgType>
|
|
146
|
+
<Content><![CDATA[${reply}]]></Content>
|
|
147
|
+
</xml>`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 获取企业微信 access_token (主动推送用), 带缓存
|
|
152
|
+
async _getAccessToken() {
|
|
153
|
+
if (this.accessToken && Date.now() < this.tokenExpire) return this.accessToken;
|
|
154
|
+
if (!this.corpId || !this.corpSecret) {
|
|
155
|
+
throw new Error("微信主动推送未配置: 需 corp_id + corp_secret (config.channels.wechat 或环境变量 WECHAT_CORP_ID/WECHAT_CORP_SECRET)");
|
|
156
|
+
}
|
|
157
|
+
const r = await fetch(
|
|
158
|
+
`https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=${encodeURIComponent(this.corpId)}&corpsecret=${encodeURIComponent(this.corpSecret)}`,
|
|
159
|
+
{ signal: AbortSignal.timeout(15000) },
|
|
160
|
+
);
|
|
161
|
+
const data = await r.json();
|
|
162
|
+
if (data.errcode !== 0) throw new Error(`微信 access_token 失败: ${data.errmsg} (errcode=${data.errcode})`);
|
|
163
|
+
this.accessToken = data.access_token;
|
|
164
|
+
this.tokenExpire = Date.now() + (data.expires_in - 60) * 1000;
|
|
165
|
+
return this.accessToken;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 主动推送 (企业微信应用消息): 需 corp_id + corp_secret + agent_id; to="*" 时回最近联系人
|
|
169
|
+
async send(to, text) {
|
|
170
|
+
const token = await this._getAccessToken();
|
|
171
|
+
if (!this.agentId) {
|
|
172
|
+
throw new Error("微信主动推送未配置 agent_id (config.channels.wechat.agent_id 或环境变量 WECHAT_AGENT_ID)");
|
|
173
|
+
}
|
|
174
|
+
const target = !to || to === "*" ? this.lastUser : to;
|
|
175
|
+
if (!target) throw new Error("微信主动推送缺接收人: 先收到过一条用户消息记录用户 ID, 或用 send(userId, ...)");
|
|
176
|
+
const r = await fetch(`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${encodeURIComponent(token)}`, {
|
|
177
|
+
method: "POST",
|
|
178
|
+
headers: { "Content-Type": "application/json" },
|
|
179
|
+
body: JSON.stringify({
|
|
180
|
+
touser: String(target),
|
|
181
|
+
msgtype: "text",
|
|
182
|
+
agentid: Number(this.agentId),
|
|
183
|
+
text: { content: String(text) },
|
|
184
|
+
}),
|
|
185
|
+
signal: AbortSignal.timeout(15000),
|
|
186
|
+
});
|
|
187
|
+
const data = await r.json();
|
|
188
|
+
if (data.errcode !== 0) throw new Error(`微信发送失败: ${data.errmsg} (errcode=${data.errcode})`);
|
|
189
|
+
return data;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// src/channels/workspace.js - 工作区文件树/文件读取 (从 channels/http.js 抽出, 2026-09-18 重构)
|
|
2
|
+
// 职责单一: 给定工作区根目录, 产出「带深度上限与越界防护」的目录树 / 文件内容。
|
|
3
|
+
// 供 HTTP 通道的 /api/workspace/tree 与 /api/workspace/read 使用; 无状态纯函数, 便于单测。
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
// 目录树默认跳过项 (重目录/产物/数据)
|
|
8
|
+
const SKIP_DIRS = new Set([".git", "node_modules", "dist", ".next", ".cache", ".tmp", "data"]);
|
|
9
|
+
const MAX_NODES = 2000; // 树节点上限 (防超大仓库拖垮响应)
|
|
10
|
+
const MAX_READ = 256 * 1024; // 单文件读取上限 256KB
|
|
11
|
+
|
|
12
|
+
// 解析并校验「工作区内的相对路径」, 越界抛错。返回绝对路径。
|
|
13
|
+
// 2026-09-18 修复 (P2): 原实现只做字符串前缀判断, 工作区内指向外部的 symlink 可越界读。
|
|
14
|
+
// 现补 realpath 校验 (与 tools/builtin.js 的 safePath 同一策略): 存在的路径解析真实路径,
|
|
15
|
+
// 不存在的路径用最近已存在祖先目录的真实路径 + 剩余部分。
|
|
16
|
+
export function resolveInside(wsRoot, rel) {
|
|
17
|
+
const rootAbs = path.resolve(wsRoot);
|
|
18
|
+
const cleaned = String(rel || "").replace(/^\/+|\/+$/g, "");
|
|
19
|
+
const abs = cleaned ? path.resolve(rootAbs, cleaned) : rootAbs;
|
|
20
|
+
if (abs !== rootAbs && !abs.startsWith(rootAbs + path.sep)) throw new Error("路径越界");
|
|
21
|
+
try {
|
|
22
|
+
const realRoot = fs.realpathSync(rootAbs);
|
|
23
|
+
let target = abs;
|
|
24
|
+
if (fs.existsSync(target)) {
|
|
25
|
+
target = fs.realpathSync(target);
|
|
26
|
+
} else {
|
|
27
|
+
let dir = path.dirname(target);
|
|
28
|
+
while (dir !== rootAbs && dir !== path.dirname(dir) && !fs.existsSync(dir)) dir = path.dirname(dir);
|
|
29
|
+
target = path.join(fs.realpathSync(fs.existsSync(dir) ? dir : rootAbs), path.relative(dir, target));
|
|
30
|
+
}
|
|
31
|
+
if (target !== realRoot && !target.startsWith(realRoot + path.sep)) {
|
|
32
|
+
throw new Error("路径越界 (符号链接)");
|
|
33
|
+
}
|
|
34
|
+
} catch (e) {
|
|
35
|
+
if (e && e.message && e.message.includes("路径越界")) throw e;
|
|
36
|
+
// root 不存在等边缘: 退回前缀检查 (已通过)
|
|
37
|
+
}
|
|
38
|
+
return abs;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 构建目录树。返回 { tree, truncated }。maxDepth 1~8。
|
|
42
|
+
export function buildTree(wsRoot, { root: rootArg = "", maxDepth = 3 } = {}) {
|
|
43
|
+
const wsRootAbs = path.resolve(wsRoot);
|
|
44
|
+
const depth = Math.min(Math.max(Number(maxDepth) || 3, 1), 8);
|
|
45
|
+
const baseDir = resolveInside(wsRootAbs, rootArg);
|
|
46
|
+
let count = 0;
|
|
47
|
+
|
|
48
|
+
const walk = (dir, rel, d) => {
|
|
49
|
+
if (count >= MAX_NODES) return null;
|
|
50
|
+
let st;
|
|
51
|
+
try { st = fs.statSync(dir, { throwIfNoEntry: false }); } catch { return null; }
|
|
52
|
+
if (!st) return null;
|
|
53
|
+
const node = { name: path.basename(dir) || "/", path: rel, type: "dir", children: [] };
|
|
54
|
+
if (d >= depth) return node;
|
|
55
|
+
let entries = [];
|
|
56
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return node; }
|
|
57
|
+
entries.sort((a, b) => {
|
|
58
|
+
if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
|
|
59
|
+
return a.name.localeCompare(b.name);
|
|
60
|
+
});
|
|
61
|
+
for (const ent of entries) {
|
|
62
|
+
if (count >= MAX_NODES) break;
|
|
63
|
+
const childRel = rel ? rel + "/" + ent.name : ent.name;
|
|
64
|
+
const childPath = path.join(dir, ent.name);
|
|
65
|
+
try {
|
|
66
|
+
if (ent.isDirectory()) {
|
|
67
|
+
if (SKIP_DIRS.has(ent.name) || ent.name.startsWith(".")) continue;
|
|
68
|
+
count++;
|
|
69
|
+
const child = walk(childPath, childRel, d + 1);
|
|
70
|
+
if (child) node.children.push(child);
|
|
71
|
+
} else if (ent.isFile()) {
|
|
72
|
+
if (ent.name.startsWith(".")) continue;
|
|
73
|
+
node.children.push({ name: ent.name, path: childRel, type: "file", size: fs.statSync(childPath).size });
|
|
74
|
+
count++;
|
|
75
|
+
}
|
|
76
|
+
} catch { /* 单个节点失败忽略, 不影响整体树 */ }
|
|
77
|
+
}
|
|
78
|
+
return node;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const tree = walk(baseDir, String(rootArg || "").replace(/^\/+|\/+$/g, ""), 0);
|
|
82
|
+
return { tree, truncated: count >= MAX_NODES, maxDepth: depth, baseDir };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 读取工作区内单个文件 (超限截断)。返回 { path, size, truncated, content }
|
|
86
|
+
export function readWorkspaceFile(wsRoot, rel) {
|
|
87
|
+
const file = resolveInside(wsRoot, rel);
|
|
88
|
+
const st = fs.statSync(file, { throwIfNoEntry: false });
|
|
89
|
+
if (!st || !st.isFile()) throw new Error("文件不存在");
|
|
90
|
+
let buf = fs.readFileSync(file);
|
|
91
|
+
const truncated = buf.length > MAX_READ;
|
|
92
|
+
if (truncated) buf = buf.subarray(0, MAX_READ);
|
|
93
|
+
return { path: String(rel || ""), size: st.size, truncated, content: buf.toString("utf8") };
|
|
94
|
+
}
|