@dingxiang-me/openclaw-wechat 2.0.1 → 2.3.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/CHANGELOG.md +85 -0
- package/README.en.md +204 -32
- package/README.md +234 -63
- package/docs/channels/wecom.md +137 -1
- package/openclaw.plugin.json +694 -10
- package/package.json +207 -4
- package/scripts/wecom-agent-selfcheck.mjs +775 -0
- package/scripts/wecom-bot-longconn-probe.mjs +582 -0
- package/scripts/wecom-bot-selfcheck.mjs +952 -0
- package/scripts/wecom-callback-matrix.mjs +224 -0
- package/scripts/wecom-doctor.mjs +1407 -0
- package/scripts/wecom-e2e-scenario.mjs +333 -0
- package/scripts/wecom-migrate.mjs +261 -0
- package/scripts/wecom-quickstart.mjs +1824 -0
- package/scripts/wecom-release-check.mjs +232 -0
- package/scripts/wecom-remote-e2e.mjs +310 -0
- package/scripts/wecom-selfcheck.mjs +1255 -0
- package/scripts/wecom-smoke.sh +74 -0
- package/src/core/delivery-router.js +21 -0
- package/src/core.js +631 -34
- package/src/wecom/account-config-core.js +27 -1
- package/src/wecom/account-config.js +19 -2
- package/src/wecom/agent-dispatch-executor.js +11 -0
- package/src/wecom/agent-dispatch-handlers.js +61 -8
- package/src/wecom/agent-inbound-guards.js +63 -16
- package/src/wecom/agent-inbound-processor.js +34 -2
- package/src/wecom/agent-late-reply-runtime.js +30 -2
- package/src/wecom/agent-text-sender.js +2 -0
- package/src/wecom/api-client-core.js +27 -19
- package/src/wecom/api-client-media.js +16 -7
- package/src/wecom/api-client-send-text.js +4 -0
- package/src/wecom/api-client-send-typed.js +4 -1
- package/src/wecom/api-client-senders.js +41 -3
- package/src/wecom/api-client.js +1 -0
- package/src/wecom/bot-dispatch-fallback.js +18 -3
- package/src/wecom/bot-dispatch-handlers.js +47 -10
- package/src/wecom/bot-inbound-dispatch-runtime.js +3 -0
- package/src/wecom/bot-inbound-executor-helpers.js +11 -1
- package/src/wecom/bot-inbound-executor.js +25 -1
- package/src/wecom/bot-inbound-guards.js +78 -23
- package/src/wecom/bot-long-connection-manager.js +4 -4
- package/src/wecom/channel-config-schema.js +132 -0
- package/src/wecom/channel-plugin.js +370 -7
- package/src/wecom/command-handlers.js +107 -10
- package/src/wecom/command-status-text.js +275 -1
- package/src/wecom/doc-client.js +7 -1
- package/src/wecom/inbound-content-handler-file-video-link.js +4 -0
- package/src/wecom/inbound-content-handler-image-voice.js +6 -0
- package/src/wecom/inbound-content.js +5 -0
- package/src/wecom/installer-api.js +910 -0
- package/src/wecom/media-download.js +2 -2
- package/src/wecom/migration-diagnostics.js +816 -0
- package/src/wecom/network-config.js +91 -0
- package/src/wecom/observability-metrics.js +9 -3
- package/src/wecom/outbound-agent-delivery.js +313 -0
- package/src/wecom/outbound-agent-media-sender.js +37 -7
- package/src/wecom/outbound-agent-push.js +1 -0
- package/src/wecom/outbound-delivery.js +129 -12
- package/src/wecom/outbound-stream-msg-item.js +25 -2
- package/src/wecom/outbound-webhook-delivery.js +19 -0
- package/src/wecom/outbound-webhook-media.js +30 -6
- package/src/wecom/pairing.js +188 -0
- package/src/wecom/pending-reply-manager.js +143 -0
- package/src/wecom/plugin-account-policy-services.js +26 -0
- package/src/wecom/plugin-base-services.js +58 -0
- package/src/wecom/plugin-constants.js +1 -1
- package/src/wecom/plugin-delivery-inbound-services.js +25 -0
- package/src/wecom/plugin-processing-deps.js +7 -0
- package/src/wecom/plugin-route-runtime-deps.js +1 -0
- package/src/wecom/plugin-services.js +87 -0
- package/src/wecom/policy-resolvers.js +93 -20
- package/src/wecom/quickstart-metadata.js +1247 -0
- package/src/wecom/reasoning-visibility.js +104 -0
- package/src/wecom/register-runtime.js +10 -0
- package/src/wecom/reliable-delivery-persistence.js +138 -0
- package/src/wecom/reliable-delivery.js +642 -0
- package/src/wecom/reply-output-policy.js +171 -0
- package/src/wecom/text-inbound-scheduler.js +6 -1
- package/src/wecom/workspace-auto-sender.js +2 -0
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import {
|
|
8
|
+
collectWecomEnvAccountIds as collectSharedWecomEnvAccountIds,
|
|
9
|
+
createRequireEnv as createSharedRequireEnv,
|
|
10
|
+
normalizeAccountConfig as normalizeSharedAccountConfig,
|
|
11
|
+
readAccountConfigFromEnv as readSharedAccountConfigFromEnv,
|
|
12
|
+
} from "../src/wecom/account-config-core.js";
|
|
13
|
+
import { resolveWecomGroupChatConfig } from "../src/core.js";
|
|
14
|
+
import { buildDefaultAgentWebhookPath, buildLegacyAgentWebhookPath } from "../src/wecom/account-paths.js";
|
|
15
|
+
import { diagnoseWecomCallbackHealth } from "../src/wecom/callback-health-diagnostics.js";
|
|
16
|
+
|
|
17
|
+
const LEGACY_INLINE_ACCOUNT_RESERVED_KEYS = new Set([
|
|
18
|
+
"name",
|
|
19
|
+
"enabled",
|
|
20
|
+
"botId",
|
|
21
|
+
"botid",
|
|
22
|
+
"secret",
|
|
23
|
+
"corpId",
|
|
24
|
+
"corpSecret",
|
|
25
|
+
"agentId",
|
|
26
|
+
"callbackToken",
|
|
27
|
+
"token",
|
|
28
|
+
"callbackAesKey",
|
|
29
|
+
"encodingAesKey",
|
|
30
|
+
"webhookPath",
|
|
31
|
+
"outboundProxy",
|
|
32
|
+
"proxyUrl",
|
|
33
|
+
"proxy",
|
|
34
|
+
"network",
|
|
35
|
+
"apiBaseUrl",
|
|
36
|
+
"webhooks",
|
|
37
|
+
"allowFrom",
|
|
38
|
+
"allowFromRejectMessage",
|
|
39
|
+
"groupPolicy",
|
|
40
|
+
"groupAllowFrom",
|
|
41
|
+
"groupAllowFromRejectMessage",
|
|
42
|
+
"rejectUnauthorizedMessage",
|
|
43
|
+
"adminUsers",
|
|
44
|
+
"commandAllowlist",
|
|
45
|
+
"commandBlockMessage",
|
|
46
|
+
"commands",
|
|
47
|
+
"workspaceTemplate",
|
|
48
|
+
"groupChat",
|
|
49
|
+
"groups",
|
|
50
|
+
"dynamicAgent",
|
|
51
|
+
"dynamicAgents",
|
|
52
|
+
"dm",
|
|
53
|
+
"debounce",
|
|
54
|
+
"streaming",
|
|
55
|
+
"bot",
|
|
56
|
+
"delivery",
|
|
57
|
+
"webhookBot",
|
|
58
|
+
"stream",
|
|
59
|
+
"observability",
|
|
60
|
+
"voiceTranscription",
|
|
61
|
+
"defaultAccount",
|
|
62
|
+
"tools",
|
|
63
|
+
"accounts",
|
|
64
|
+
"agent",
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
function parseArgs(argv) {
|
|
68
|
+
const out = {
|
|
69
|
+
configPath: process.env.OPENCLAW_CONFIG_PATH || "~/.openclaw/openclaw.json",
|
|
70
|
+
account: "default",
|
|
71
|
+
allAccounts: false,
|
|
72
|
+
url: "",
|
|
73
|
+
fromUser: "",
|
|
74
|
+
content: "/status",
|
|
75
|
+
timeoutMs: 8000,
|
|
76
|
+
json: false,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
for (let i = 2; i < argv.length; i += 1) {
|
|
80
|
+
const arg = argv[i];
|
|
81
|
+
const next = argv[i + 1];
|
|
82
|
+
if (arg === "--config" && next) {
|
|
83
|
+
out.configPath = next;
|
|
84
|
+
i += 1;
|
|
85
|
+
} else if (arg === "--account" && next) {
|
|
86
|
+
out.account = next;
|
|
87
|
+
i += 1;
|
|
88
|
+
} else if (arg === "--all-accounts") {
|
|
89
|
+
out.allAccounts = true;
|
|
90
|
+
} else if (arg === "--url" && next) {
|
|
91
|
+
out.url = next;
|
|
92
|
+
i += 1;
|
|
93
|
+
} else if (arg === "--from-user" && next) {
|
|
94
|
+
out.fromUser = next;
|
|
95
|
+
i += 1;
|
|
96
|
+
} else if (arg === "--content" && next) {
|
|
97
|
+
out.content = next;
|
|
98
|
+
i += 1;
|
|
99
|
+
} else if (arg === "--timeout-ms" && next) {
|
|
100
|
+
const n = Number(next);
|
|
101
|
+
if (Number.isFinite(n) && n > 0) out.timeoutMs = Math.floor(n);
|
|
102
|
+
i += 1;
|
|
103
|
+
} else if (arg === "--json") {
|
|
104
|
+
out.json = true;
|
|
105
|
+
} else if (arg === "-h" || arg === "--help") {
|
|
106
|
+
printHelp();
|
|
107
|
+
process.exit(0);
|
|
108
|
+
} else {
|
|
109
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function printHelp() {
|
|
116
|
+
console.log(`OpenClaw-Wechat Agent selfcheck (URL verify + encrypted POST)
|
|
117
|
+
|
|
118
|
+
Usage:
|
|
119
|
+
npm run wecom:agent:selfcheck -- [options]
|
|
120
|
+
|
|
121
|
+
Options:
|
|
122
|
+
--config <path> OpenClaw config path (default: ~/.openclaw/openclaw.json)
|
|
123
|
+
--account <id> account id (default: default)
|
|
124
|
+
--all-accounts run checks for all discovered Agent accounts
|
|
125
|
+
--url <http-url> override callback URL
|
|
126
|
+
--from-user <userid> simulated sender
|
|
127
|
+
--content <text> inbound text content (default: /status)
|
|
128
|
+
--timeout-ms <ms> HTTP timeout (default: 8000)
|
|
129
|
+
--json print JSON report
|
|
130
|
+
-h, --help show this help
|
|
131
|
+
`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function expandHome(p) {
|
|
135
|
+
if (!p) return p;
|
|
136
|
+
if (p === "~") return os.homedir();
|
|
137
|
+
if (p.startsWith("~/")) return path.join(os.homedir(), p.slice(2));
|
|
138
|
+
return p;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function normalizeAccountId(accountId) {
|
|
142
|
+
const normalized = String(accountId ?? "default").trim().toLowerCase();
|
|
143
|
+
return normalized || "default";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function asNumber(value, fallback = null) {
|
|
147
|
+
const n = Number(value);
|
|
148
|
+
return Number.isFinite(n) ? n : fallback;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function pickFirstNonEmptyString(...values) {
|
|
152
|
+
for (const value of values) {
|
|
153
|
+
const text = String(value ?? "").trim();
|
|
154
|
+
if (text) return text;
|
|
155
|
+
}
|
|
156
|
+
return "";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isFalseLike(value) {
|
|
160
|
+
return ["0", "false", "off", "no"].includes(String(value ?? "").trim().toLowerCase());
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function decodeAesKey(aesKey) {
|
|
164
|
+
const keyBase64 = String(aesKey ?? "").endsWith("=") ? aesKey : `${aesKey}=`;
|
|
165
|
+
const key = Buffer.from(keyBase64, "base64");
|
|
166
|
+
if (key.length !== 32) {
|
|
167
|
+
throw new Error(`invalid callbackAesKey length: decoded ${key.length} bytes, expected 32`);
|
|
168
|
+
}
|
|
169
|
+
return key;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function formatGroupSourceLabel(source = "") {
|
|
173
|
+
const normalized = String(source ?? "").trim();
|
|
174
|
+
if (!normalized) return "none";
|
|
175
|
+
if (normalized === "default") return "default";
|
|
176
|
+
if (normalized === "inferred") return "inferred";
|
|
177
|
+
if (normalized.startsWith("env.")) return "env";
|
|
178
|
+
if (normalized.startsWith("account.group")) return "account-group";
|
|
179
|
+
if (normalized.startsWith("account.groupChat")) return "account-group-default";
|
|
180
|
+
if (normalized.startsWith("account.root")) return "account-root";
|
|
181
|
+
if (normalized.startsWith("channel.group")) return "channel-group";
|
|
182
|
+
if (normalized.startsWith("channel.groupChat")) return "channel-group-default";
|
|
183
|
+
if (normalized.startsWith("channel.root")) return "channel-root";
|
|
184
|
+
return normalized;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function pkcs7Pad(buf, blockSize = 32) {
|
|
188
|
+
const amountToPad = blockSize - (buf.length % blockSize || blockSize);
|
|
189
|
+
const pad = Buffer.alloc(amountToPad === 0 ? blockSize : amountToPad, amountToPad === 0 ? blockSize : amountToPad);
|
|
190
|
+
return Buffer.concat([buf, pad]);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function encryptWecom({ aesKey, plainText, corpId = "" }) {
|
|
194
|
+
const key = decodeAesKey(aesKey);
|
|
195
|
+
const iv = key.subarray(0, 16);
|
|
196
|
+
const random16 = crypto.randomBytes(16);
|
|
197
|
+
const msgBuffer = Buffer.from(String(plainText ?? ""), "utf8");
|
|
198
|
+
const lenBuffer = Buffer.alloc(4);
|
|
199
|
+
lenBuffer.writeUInt32BE(msgBuffer.length, 0);
|
|
200
|
+
const corpBuffer = Buffer.from(String(corpId ?? ""), "utf8");
|
|
201
|
+
const raw = Buffer.concat([random16, lenBuffer, msgBuffer, corpBuffer]);
|
|
202
|
+
const padded = pkcs7Pad(raw, 32);
|
|
203
|
+
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
|
|
204
|
+
cipher.setAutoPadding(false);
|
|
205
|
+
const encrypted = Buffer.concat([cipher.update(padded), cipher.final()]);
|
|
206
|
+
return encrypted.toString("base64");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function computeMsgSignature({ token, timestamp, nonce, encrypt }) {
|
|
210
|
+
const arr = [token, timestamp, nonce, encrypt].map(String).sort();
|
|
211
|
+
return crypto.createHash("sha1").update(arr.join("")).digest("hex");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function buildTextInboundXml({ fromUser, content, msgId }) {
|
|
215
|
+
const safeFromUser = String(fromUser ?? "").trim();
|
|
216
|
+
const safeContent = String(content ?? "").trim();
|
|
217
|
+
const safeMsgId = String(msgId ?? "").trim();
|
|
218
|
+
const nowTs = Math.floor(Date.now() / 1000);
|
|
219
|
+
return `<xml>
|
|
220
|
+
<ToUserName><![CDATA[openclaw-selfcheck]]></ToUserName>
|
|
221
|
+
<FromUserName><![CDATA[${safeFromUser}]]></FromUserName>
|
|
222
|
+
<CreateTime>${nowTs}</CreateTime>
|
|
223
|
+
<MsgType><![CDATA[text]]></MsgType>
|
|
224
|
+
<Content><![CDATA[${safeContent}]]></Content>
|
|
225
|
+
<MsgId>${safeMsgId}</MsgId>
|
|
226
|
+
</xml>`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function buildEncryptedPostBody(encrypt) {
|
|
230
|
+
return `<xml>
|
|
231
|
+
<ToUserName><![CDATA[openclaw-selfcheck]]></ToUserName>
|
|
232
|
+
<Encrypt><![CDATA[${encrypt}]]></Encrypt>
|
|
233
|
+
</xml>`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function fetchWithTimeout(url, options = {}, timeoutMs = 8000) {
|
|
237
|
+
const timeout = Math.max(1000, Number(timeoutMs) || 8000);
|
|
238
|
+
const requestOptions = {
|
|
239
|
+
...options,
|
|
240
|
+
signal: AbortSignal.timeout(timeout),
|
|
241
|
+
};
|
|
242
|
+
return fetch(url, requestOptions);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function makeCheck(name, ok, detail, data = null) {
|
|
246
|
+
return { name, ok: Boolean(ok), detail: String(detail ?? ""), data };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function summarize(checks) {
|
|
250
|
+
const failed = checks.filter((c) => !c.ok).length;
|
|
251
|
+
return {
|
|
252
|
+
ok: failed === 0,
|
|
253
|
+
total: checks.length,
|
|
254
|
+
passed: checks.length - failed,
|
|
255
|
+
failed,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function summarizeAccountReports(accountReports = []) {
|
|
260
|
+
const checks = accountReports.flatMap((report) => (Array.isArray(report?.checks) ? report.checks : []));
|
|
261
|
+
const accountsFailed = accountReports.filter((report) => report?.summary?.ok !== true).length;
|
|
262
|
+
return {
|
|
263
|
+
...summarize(checks),
|
|
264
|
+
accountsTotal: accountReports.length,
|
|
265
|
+
accountsPassed: accountReports.length - accountsFailed,
|
|
266
|
+
accountsFailed,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function buildAgentOverview({ config, account } = {}) {
|
|
271
|
+
const bindingsCount = Array.isArray(config?.bindings) ? config.bindings.length : 0;
|
|
272
|
+
const dynamicAgentEnabled =
|
|
273
|
+
config?.channels?.wecom?.dynamicAgent?.enabled === true || config?.channels?.wecom?.dynamicAgents?.enabled === true;
|
|
274
|
+
const deliveryConfig = config?.channels?.wecom?.delivery ?? {};
|
|
275
|
+
const pendingReplyConfig = deliveryConfig?.pendingReply ?? {};
|
|
276
|
+
const reasoningConfig = deliveryConfig?.reasoning ?? {};
|
|
277
|
+
const pendingReplyEnabled = pendingReplyConfig?.enabled !== false;
|
|
278
|
+
const pendingReplyPersist = pendingReplyEnabled && pendingReplyConfig?.persist !== false;
|
|
279
|
+
const pendingReplyStoreFile = pickFirstNonEmptyString(pendingReplyConfig?.storeFile) || null;
|
|
280
|
+
const quotaTrackingEnabled = deliveryConfig?.quotaTracking?.enabled !== false;
|
|
281
|
+
const reasoningMode = (() => {
|
|
282
|
+
const explicit = pickFirstNonEmptyString(reasoningConfig?.mode).toLowerCase();
|
|
283
|
+
if (explicit === "append" || explicit === "hidden" || explicit === "separate") return explicit;
|
|
284
|
+
if (reasoningConfig?.includeInFinalAnswer === true) return "append";
|
|
285
|
+
if (reasoningConfig?.sendThinkingMessage === false) return "hidden";
|
|
286
|
+
return "separate";
|
|
287
|
+
})();
|
|
288
|
+
const canReceive = Boolean(account?.callbackToken && account?.callbackAesKey && account?.webhookPath);
|
|
289
|
+
const canReply = Boolean(account?.corpId && account?.corpSecret && account?.agentId);
|
|
290
|
+
const groupPolicy = resolveWecomGroupChatConfig({
|
|
291
|
+
channelConfig: config?.channels?.wecom ?? {},
|
|
292
|
+
accountConfig: account ?? {},
|
|
293
|
+
envVars: config?.env?.vars ?? {},
|
|
294
|
+
processEnv: process.env,
|
|
295
|
+
accountId: account?.accountId || "default",
|
|
296
|
+
});
|
|
297
|
+
const allowFrom = Array.isArray(groupPolicy?.allowFrom) ? groupPolicy.allowFrom : [];
|
|
298
|
+
return {
|
|
299
|
+
canReceive,
|
|
300
|
+
canReply,
|
|
301
|
+
canSend: canReply,
|
|
302
|
+
bindingsCount,
|
|
303
|
+
dynamicAgentEnabled,
|
|
304
|
+
pendingReplyEnabled,
|
|
305
|
+
pendingReplyPersist,
|
|
306
|
+
pendingReplyStoreFile,
|
|
307
|
+
quotaTrackingEnabled,
|
|
308
|
+
reasoningMode,
|
|
309
|
+
reasoningTitle: pickFirstNonEmptyString(reasoningConfig?.title, "思考过程"),
|
|
310
|
+
reasoningMaxChars: Math.max(64, asNumber(reasoningConfig?.maxChars, 1200) || 1200),
|
|
311
|
+
groupPolicy: {
|
|
312
|
+
mode: String(groupPolicy?.policyMode ?? "open"),
|
|
313
|
+
trigger: String(groupPolicy?.triggerMode ?? "direct"),
|
|
314
|
+
allowSummary:
|
|
315
|
+
groupPolicy?.policyMode === "allowlist"
|
|
316
|
+
? String(allowFrom.length)
|
|
317
|
+
: allowFrom.length > 0 && !allowFrom.includes("*")
|
|
318
|
+
? `${allowFrom.length}(inactive)`
|
|
319
|
+
: "open",
|
|
320
|
+
groups: Number(groupPolicy?.configuredGroupCount || 0),
|
|
321
|
+
source: formatGroupSourceLabel(groupPolicy?.policySource),
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function listLegacyInlineAccountIds(channelConfig) {
|
|
327
|
+
if (!channelConfig || typeof channelConfig !== "object") return [];
|
|
328
|
+
const ids = [];
|
|
329
|
+
for (const [rawKey, value] of Object.entries(channelConfig)) {
|
|
330
|
+
const normalizedKey = normalizeAccountId(rawKey);
|
|
331
|
+
if (!normalizedKey || LEGACY_INLINE_ACCOUNT_RESERVED_KEYS.has(normalizedKey)) continue;
|
|
332
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
333
|
+
ids.push(normalizedKey);
|
|
334
|
+
}
|
|
335
|
+
return Array.from(new Set(ids));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function resolveAccountFromConfig(config, accountId) {
|
|
339
|
+
const normalizedId = normalizeAccountId(accountId);
|
|
340
|
+
const channelConfig = config?.channels?.wecom ?? {};
|
|
341
|
+
const envVars = config?.env?.vars ?? {};
|
|
342
|
+
const requireEnv = createSharedRequireEnv(process.env);
|
|
343
|
+
|
|
344
|
+
const pickFromRaw = (raw, resolvedId, source) => {
|
|
345
|
+
const normalized = normalizeSharedAccountConfig({
|
|
346
|
+
raw,
|
|
347
|
+
accountId: resolvedId,
|
|
348
|
+
normalizeWecomWebhookTargetMap: () => ({}),
|
|
349
|
+
});
|
|
350
|
+
if (!normalized) return null;
|
|
351
|
+
return {
|
|
352
|
+
...normalized,
|
|
353
|
+
source,
|
|
354
|
+
};
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const readEnv = (targetAccountId) => {
|
|
358
|
+
const normalized = readSharedAccountConfigFromEnv({
|
|
359
|
+
envVars,
|
|
360
|
+
accountId: targetAccountId,
|
|
361
|
+
requireEnv,
|
|
362
|
+
normalizeWecomWebhookTargetMap: () => ({}),
|
|
363
|
+
});
|
|
364
|
+
if (!normalized) return null;
|
|
365
|
+
return {
|
|
366
|
+
...normalized,
|
|
367
|
+
source: "env",
|
|
368
|
+
};
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
if (normalizedId === "default") {
|
|
372
|
+
return (
|
|
373
|
+
pickFromRaw(channelConfig, "default", "channels.wecom") ||
|
|
374
|
+
pickFromRaw(channelConfig?.accounts?.default, "default", "channels.wecom.accounts.default") ||
|
|
375
|
+
readEnv("default")
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return (
|
|
380
|
+
pickFromRaw(channelConfig?.accounts?.[normalizedId], normalizedId, `channels.wecom.accounts.${normalizedId}`) ||
|
|
381
|
+
pickFromRaw(channelConfig?.[normalizedId], normalizedId, `channels.wecom.${normalizedId}`) ||
|
|
382
|
+
readEnv(normalizedId) ||
|
|
383
|
+
pickFromRaw(channelConfig, "default", "channels.wecom") ||
|
|
384
|
+
pickFromRaw(channelConfig?.accounts?.default, "default", "channels.wecom.accounts.default") ||
|
|
385
|
+
readEnv("default")
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function discoverAgentAccountIds(config) {
|
|
390
|
+
const ids = new Set();
|
|
391
|
+
const channelConfig = config?.channels?.wecom;
|
|
392
|
+
const envVars = config?.env?.vars ?? {};
|
|
393
|
+
|
|
394
|
+
if (resolveAccountFromConfig(config, "default")) ids.add("default");
|
|
395
|
+
|
|
396
|
+
const accountEntries = channelConfig?.accounts;
|
|
397
|
+
if (accountEntries && typeof accountEntries === "object") {
|
|
398
|
+
for (const key of Object.keys(accountEntries)) {
|
|
399
|
+
ids.add(normalizeAccountId(key));
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
for (const key of listLegacyInlineAccountIds(channelConfig)) {
|
|
403
|
+
ids.add(key);
|
|
404
|
+
}
|
|
405
|
+
for (const key of collectSharedWecomEnvAccountIds({ envVars, processEnv: process.env })) {
|
|
406
|
+
ids.add(normalizeAccountId(key));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (ids.size === 0) ids.add("default");
|
|
410
|
+
|
|
411
|
+
const ordered = Array.from(ids);
|
|
412
|
+
ordered.sort((a, b) => {
|
|
413
|
+
if (a === "default" && b !== "default") return -1;
|
|
414
|
+
if (a !== "default" && b === "default") return 1;
|
|
415
|
+
return a.localeCompare(b);
|
|
416
|
+
});
|
|
417
|
+
return ordered;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function buildCallbackUrl({ args, account, config }) {
|
|
421
|
+
if (String(args.url ?? "").trim()) return String(args.url).trim();
|
|
422
|
+
const gatewayPort = asNumber(config?.gateway?.port, 8885);
|
|
423
|
+
const webhookPath = String(account?.webhookPath ?? "/wecom/callback").trim() || "/wecom/callback";
|
|
424
|
+
const normalizedPath = webhookPath.startsWith("/") ? webhookPath : `/${webhookPath}`;
|
|
425
|
+
return `http://127.0.0.1:${gatewayPort}${normalizedPath}`;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function normalizeHttpPath(pathname) {
|
|
429
|
+
const text = String(pathname ?? "").trim();
|
|
430
|
+
if (!text) return "/";
|
|
431
|
+
const prefixed = text.startsWith("/") ? text : `/${text}`;
|
|
432
|
+
if (prefixed.length > 1 && prefixed.endsWith("/")) return prefixed.slice(0, -1);
|
|
433
|
+
return prefixed;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function buildLegacyAliasUrl(endpoint, accountId) {
|
|
437
|
+
let parsed;
|
|
438
|
+
try {
|
|
439
|
+
parsed = new URL(endpoint);
|
|
440
|
+
} catch {
|
|
441
|
+
return "";
|
|
442
|
+
}
|
|
443
|
+
const normalizedAccountId = normalizeAccountId(accountId);
|
|
444
|
+
const currentPath = normalizeHttpPath(parsed.pathname);
|
|
445
|
+
const defaultPath = normalizeHttpPath(buildDefaultAgentWebhookPath(normalizedAccountId));
|
|
446
|
+
if (currentPath !== defaultPath) return "";
|
|
447
|
+
const legacyPath = normalizeHttpPath(buildLegacyAgentWebhookPath(normalizedAccountId));
|
|
448
|
+
if (!legacyPath || legacyPath === currentPath) return "";
|
|
449
|
+
parsed.pathname = legacyPath;
|
|
450
|
+
parsed.search = "";
|
|
451
|
+
return parsed.toString();
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function buildSignedUrl(endpoint, token, encrypt) {
|
|
455
|
+
const timestamp = String(Math.floor(Date.now() / 1000));
|
|
456
|
+
const nonce = crypto.randomBytes(8).toString("hex");
|
|
457
|
+
const msgSignature = computeMsgSignature({
|
|
458
|
+
token,
|
|
459
|
+
timestamp,
|
|
460
|
+
nonce,
|
|
461
|
+
encrypt,
|
|
462
|
+
});
|
|
463
|
+
const url = new URL(endpoint);
|
|
464
|
+
url.searchParams.set("msg_signature", msgSignature);
|
|
465
|
+
url.searchParams.set("timestamp", timestamp);
|
|
466
|
+
url.searchParams.set("nonce", nonce);
|
|
467
|
+
return url;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function reportAndExit(report, asJson = false) {
|
|
471
|
+
if (asJson) {
|
|
472
|
+
console.log(JSON.stringify(report, null, 2));
|
|
473
|
+
process.exit(report.summary.ok ? 0 : 1);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const accountReports =
|
|
477
|
+
Array.isArray(report?.accounts) && report.accounts.length > 0 ? report.accounts : [report];
|
|
478
|
+
console.log("WeCom Agent selfcheck");
|
|
479
|
+
console.log(`- config: ${report.configPath}`);
|
|
480
|
+
console.log(`- mode: ${report.args?.allAccounts ? "all-accounts" : `single-account (${report.args?.account || "default"})`}`);
|
|
481
|
+
for (const accountReport of accountReports) {
|
|
482
|
+
console.log(`\nAccount: ${accountReport.accountId}`);
|
|
483
|
+
console.log(`- endpoint: ${accountReport.endpoint}`);
|
|
484
|
+
const overview = buildAgentOverview({ config: report.config, account: accountReport.account });
|
|
485
|
+
console.log(
|
|
486
|
+
`- readiness: receive=${overview.canReceive ? "yes" : "no"} reply=${overview.canReply ? "yes" : "no"} send=${overview.canSend ? "yes" : "no"}`,
|
|
487
|
+
);
|
|
488
|
+
console.log(
|
|
489
|
+
`- routing: bindings=${overview.bindingsCount} dynamicAgent=${overview.dynamicAgentEnabled ? "on" : "off"}`,
|
|
490
|
+
);
|
|
491
|
+
console.log(
|
|
492
|
+
`- reliable-delivery: pendingReply=${overview.pendingReplyEnabled ? "on" : "off"} persist=${overview.pendingReplyPersist ? "on" : "off"} quotaTracking=${overview.quotaTrackingEnabled ? "on" : "off"} store=${overview.pendingReplyPersist ? overview.pendingReplyStoreFile || "(auto)" : "(disabled)"}`,
|
|
493
|
+
);
|
|
494
|
+
console.log(
|
|
495
|
+
`- group-policy: mode=${overview.groupPolicy.mode} trigger=${overview.groupPolicy.trigger} allowFrom=${overview.groupPolicy.allowSummary} groups=${overview.groupPolicy.groups} source=${overview.groupPolicy.source}`,
|
|
496
|
+
);
|
|
497
|
+
console.log(`- reasoning: mode=${overview.reasoningMode} title=${overview.reasoningTitle} maxChars=${overview.reasoningMaxChars}`);
|
|
498
|
+
for (const check of accountReport.checks) {
|
|
499
|
+
console.log(`${check.ok ? "OK " : "FAIL"} ${check.name} :: ${check.detail}`);
|
|
500
|
+
}
|
|
501
|
+
console.log(`Account summary: ${accountReport.summary.passed}/${accountReport.summary.total} passed`);
|
|
502
|
+
}
|
|
503
|
+
if (report.summary?.accountsTotal != null) {
|
|
504
|
+
console.log(
|
|
505
|
+
`\nSummary: accounts ${report.summary.accountsPassed}/${report.summary.accountsTotal} passed, checks ${report.summary.passed}/${report.summary.total} passed`,
|
|
506
|
+
);
|
|
507
|
+
} else {
|
|
508
|
+
console.log(`\nSummary: ${report.summary.passed}/${report.summary.total} passed`);
|
|
509
|
+
}
|
|
510
|
+
process.exit(report.summary.ok ? 0 : 1);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async function runAgentE2E({ config, args, configPath, accountId }) {
|
|
514
|
+
const checks = [];
|
|
515
|
+
const account = resolveAccountFromConfig(config, accountId);
|
|
516
|
+
const endpoint = buildCallbackUrl({ args, account, config });
|
|
517
|
+
const legacyAliasEndpoint = buildLegacyAliasUrl(endpoint, account?.accountId ?? accountId);
|
|
518
|
+
const generatedSuffix = `${normalizeAccountId(accountId).replace(/[^a-z0-9]/gi, "").slice(0, 8)}${Date.now().toString(36).slice(-6)}`;
|
|
519
|
+
const fromUser = String(args.fromUser ?? "").trim() || `DxAgentSelfCheck${generatedSuffix}`;
|
|
520
|
+
const content = String(args.content ?? "").trim() || "/status";
|
|
521
|
+
const msgId = `${Date.now()}${Math.floor(Math.random() * 1000)}`;
|
|
522
|
+
|
|
523
|
+
checks.push(makeCheck("config.account", Boolean(account), account ? `source=${account.source}` : "missing"));
|
|
524
|
+
if (!account) {
|
|
525
|
+
return {
|
|
526
|
+
configPath,
|
|
527
|
+
endpoint,
|
|
528
|
+
accountId: normalizeAccountId(accountId),
|
|
529
|
+
account: null,
|
|
530
|
+
checks,
|
|
531
|
+
summary: summarize(checks),
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
checks.push(makeCheck("config.enabled", account.enabled !== false, account.enabled === false ? "disabled" : "enabled"));
|
|
536
|
+
checks.push(makeCheck("config.callbackToken", Boolean(account.callbackToken), account.callbackToken ? "present" : "missing"));
|
|
537
|
+
checks.push(
|
|
538
|
+
makeCheck("config.callbackAesKey", Boolean(account.callbackAesKey), account.callbackAesKey ? "present" : "missing"),
|
|
539
|
+
);
|
|
540
|
+
checks.push(makeCheck("config.webhookPath", Boolean(account.webhookPath), `path=${account.webhookPath || ""}`));
|
|
541
|
+
|
|
542
|
+
let aesValid = false;
|
|
543
|
+
if (account.callbackAesKey) {
|
|
544
|
+
try {
|
|
545
|
+
decodeAesKey(account.callbackAesKey);
|
|
546
|
+
aesValid = true;
|
|
547
|
+
checks.push(makeCheck("config.callbackAesKey.length", true, "decoded-bytes=32"));
|
|
548
|
+
} catch (err) {
|
|
549
|
+
checks.push(makeCheck("config.callbackAesKey.length", false, String(err?.message || err)));
|
|
550
|
+
}
|
|
551
|
+
} else {
|
|
552
|
+
checks.push(makeCheck("config.callbackAesKey.length", false, "missing"));
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (account.enabled === false || !account.callbackToken || !account.callbackAesKey || !aesValid) {
|
|
556
|
+
return {
|
|
557
|
+
configPath,
|
|
558
|
+
endpoint,
|
|
559
|
+
accountId: account.accountId,
|
|
560
|
+
account,
|
|
561
|
+
overview: buildAgentOverview({ config, account }),
|
|
562
|
+
checks,
|
|
563
|
+
summary: summarize(checks),
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
try {
|
|
568
|
+
const healthResponse = await fetchWithTimeout(
|
|
569
|
+
endpoint,
|
|
570
|
+
{ method: "GET", redirect: "manual" },
|
|
571
|
+
Math.min(args.timeoutMs, 4000),
|
|
572
|
+
);
|
|
573
|
+
const healthBody = await healthResponse.text();
|
|
574
|
+
const diagnosis = diagnoseWecomCallbackHealth({
|
|
575
|
+
status: healthResponse.status,
|
|
576
|
+
body: healthBody,
|
|
577
|
+
mode: "agent",
|
|
578
|
+
endpoint,
|
|
579
|
+
webhookPath: new URL(endpoint).pathname,
|
|
580
|
+
location: healthResponse.headers.get("location") || "",
|
|
581
|
+
});
|
|
582
|
+
checks.push(
|
|
583
|
+
makeCheck(
|
|
584
|
+
"e2e.health.get",
|
|
585
|
+
diagnosis.ok,
|
|
586
|
+
diagnosis.detail,
|
|
587
|
+
diagnosis.data,
|
|
588
|
+
),
|
|
589
|
+
);
|
|
590
|
+
} catch (err) {
|
|
591
|
+
checks.push(makeCheck("e2e.health.get", false, `request failed: ${String(err?.message || err)}`));
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (legacyAliasEndpoint) {
|
|
595
|
+
try {
|
|
596
|
+
const healthResponse = await fetchWithTimeout(
|
|
597
|
+
legacyAliasEndpoint,
|
|
598
|
+
{ method: "GET", redirect: "manual" },
|
|
599
|
+
Math.min(args.timeoutMs, 4000),
|
|
600
|
+
);
|
|
601
|
+
const healthBody = await healthResponse.text();
|
|
602
|
+
const diagnosis = diagnoseWecomCallbackHealth({
|
|
603
|
+
status: healthResponse.status,
|
|
604
|
+
body: healthBody,
|
|
605
|
+
mode: "agent",
|
|
606
|
+
endpoint: legacyAliasEndpoint,
|
|
607
|
+
webhookPath: new URL(legacyAliasEndpoint).pathname,
|
|
608
|
+
location: healthResponse.headers.get("location") || "",
|
|
609
|
+
});
|
|
610
|
+
checks.push(
|
|
611
|
+
makeCheck(
|
|
612
|
+
"e2e.health.get.legacyAlias",
|
|
613
|
+
diagnosis.ok,
|
|
614
|
+
diagnosis.detail,
|
|
615
|
+
diagnosis.data,
|
|
616
|
+
),
|
|
617
|
+
);
|
|
618
|
+
} catch (err) {
|
|
619
|
+
checks.push(makeCheck("e2e.health.get.legacyAlias", false, `request failed: ${String(err?.message || err)}`));
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
try {
|
|
624
|
+
const plainEchostr = `agent-echostr-${Date.now().toString(36)}`;
|
|
625
|
+
const encryptedEchostr = encryptWecom({
|
|
626
|
+
aesKey: account.callbackAesKey,
|
|
627
|
+
plainText: plainEchostr,
|
|
628
|
+
corpId: account.corpId,
|
|
629
|
+
});
|
|
630
|
+
const verifyUrl = buildSignedUrl(endpoint, account.callbackToken, encryptedEchostr);
|
|
631
|
+
verifyUrl.searchParams.set("echostr", encryptedEchostr);
|
|
632
|
+
const verifyResponse = await fetchWithTimeout(verifyUrl.toString(), { method: "GET" }, args.timeoutMs);
|
|
633
|
+
const verifyBody = await verifyResponse.text();
|
|
634
|
+
const matched = verifyResponse.status === 200 && verifyBody === plainEchostr;
|
|
635
|
+
checks.push(
|
|
636
|
+
makeCheck(
|
|
637
|
+
"e2e.url.verify",
|
|
638
|
+
matched,
|
|
639
|
+
`status=${verifyResponse.status} bodyMatched=${matched}`,
|
|
640
|
+
),
|
|
641
|
+
);
|
|
642
|
+
} catch (err) {
|
|
643
|
+
checks.push(makeCheck("e2e.url.verify", false, `request failed: ${String(err?.message || err)}`));
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (legacyAliasEndpoint) {
|
|
647
|
+
try {
|
|
648
|
+
const plainEchostr = `agent-legacy-echostr-${Date.now().toString(36)}`;
|
|
649
|
+
const encryptedEchostr = encryptWecom({
|
|
650
|
+
aesKey: account.callbackAesKey,
|
|
651
|
+
plainText: plainEchostr,
|
|
652
|
+
corpId: account.corpId,
|
|
653
|
+
});
|
|
654
|
+
const verifyUrl = buildSignedUrl(legacyAliasEndpoint, account.callbackToken, encryptedEchostr);
|
|
655
|
+
verifyUrl.searchParams.set("echostr", encryptedEchostr);
|
|
656
|
+
const verifyResponse = await fetchWithTimeout(verifyUrl.toString(), { method: "GET" }, args.timeoutMs);
|
|
657
|
+
const verifyBody = await verifyResponse.text();
|
|
658
|
+
const matched = verifyResponse.status === 200 && verifyBody === plainEchostr;
|
|
659
|
+
checks.push(
|
|
660
|
+
makeCheck(
|
|
661
|
+
"e2e.url.verify.legacyAlias",
|
|
662
|
+
matched,
|
|
663
|
+
`status=${verifyResponse.status} bodyMatched=${matched}`,
|
|
664
|
+
),
|
|
665
|
+
);
|
|
666
|
+
} catch (err) {
|
|
667
|
+
checks.push(makeCheck("e2e.url.verify.legacyAlias", false, `request failed: ${String(err?.message || err)}`));
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
try {
|
|
672
|
+
const plaintextXml = buildTextInboundXml({
|
|
673
|
+
fromUser,
|
|
674
|
+
content,
|
|
675
|
+
msgId,
|
|
676
|
+
});
|
|
677
|
+
const encryptedXml = encryptWecom({
|
|
678
|
+
aesKey: account.callbackAesKey,
|
|
679
|
+
plainText: plaintextXml,
|
|
680
|
+
corpId: account.corpId,
|
|
681
|
+
});
|
|
682
|
+
const postUrl = buildSignedUrl(endpoint, account.callbackToken, encryptedXml).toString();
|
|
683
|
+
const postBody = buildEncryptedPostBody(encryptedXml);
|
|
684
|
+
const postResponse = await fetchWithTimeout(
|
|
685
|
+
postUrl,
|
|
686
|
+
{
|
|
687
|
+
method: "POST",
|
|
688
|
+
headers: {
|
|
689
|
+
"Content-Type": "application/xml",
|
|
690
|
+
},
|
|
691
|
+
body: postBody,
|
|
692
|
+
},
|
|
693
|
+
args.timeoutMs,
|
|
694
|
+
);
|
|
695
|
+
const postText = await postResponse.text();
|
|
696
|
+
const accepted = postResponse.status === 200 && String(postText ?? "").trim().toLowerCase() === "success";
|
|
697
|
+
checks.push(
|
|
698
|
+
makeCheck(
|
|
699
|
+
"e2e.message.post",
|
|
700
|
+
accepted,
|
|
701
|
+
`status=${postResponse.status} body=${String(postText ?? "").slice(0, 120)}`,
|
|
702
|
+
),
|
|
703
|
+
);
|
|
704
|
+
} catch (err) {
|
|
705
|
+
checks.push(makeCheck("e2e.message.post", false, `request failed: ${String(err?.message || err)}`));
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
return {
|
|
709
|
+
configPath,
|
|
710
|
+
endpoint,
|
|
711
|
+
accountId: account.accountId,
|
|
712
|
+
account,
|
|
713
|
+
overview: buildAgentOverview({ config, account }),
|
|
714
|
+
checks,
|
|
715
|
+
summary: summarize(checks),
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
async function main() {
|
|
720
|
+
const args = parseArgs(process.argv);
|
|
721
|
+
const configPath = path.resolve(expandHome(args.configPath));
|
|
722
|
+
if (args.allAccounts && String(args.url ?? "").trim()) {
|
|
723
|
+
throw new Error("--url cannot be used with --all-accounts (each account has its own webhookPath)");
|
|
724
|
+
}
|
|
725
|
+
let config;
|
|
726
|
+
try {
|
|
727
|
+
const raw = await readFile(configPath, "utf8");
|
|
728
|
+
config = JSON.parse(raw);
|
|
729
|
+
} catch (err) {
|
|
730
|
+
const report = {
|
|
731
|
+
args,
|
|
732
|
+
configPath,
|
|
733
|
+
config: null,
|
|
734
|
+
accounts: [
|
|
735
|
+
{
|
|
736
|
+
configPath,
|
|
737
|
+
endpoint: "",
|
|
738
|
+
accountId: normalizeAccountId(args.account),
|
|
739
|
+
checks: [
|
|
740
|
+
makeCheck("config.load", false, `failed to load ${configPath}: ${String(err?.message || err)}`),
|
|
741
|
+
],
|
|
742
|
+
},
|
|
743
|
+
],
|
|
744
|
+
};
|
|
745
|
+
report.accounts[0].summary = summarize(report.accounts[0].checks);
|
|
746
|
+
report.summary = summarizeAccountReports(report.accounts);
|
|
747
|
+
reportAndExit(report, args.json);
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const targetAccounts = args.allAccounts ? discoverAgentAccountIds(config) : [normalizeAccountId(args.account)];
|
|
752
|
+
const accountReports = [];
|
|
753
|
+
for (const accountId of targetAccounts) {
|
|
754
|
+
// Keep output deterministic and easier to scan.
|
|
755
|
+
// eslint-disable-next-line no-await-in-loop
|
|
756
|
+
const report = await runAgentE2E({ config, args, configPath, accountId });
|
|
757
|
+
report.checks.unshift(makeCheck("config.load", true, `loaded ${configPath}`));
|
|
758
|
+
report.summary = summarize(report.checks);
|
|
759
|
+
accountReports.push(report);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const report = {
|
|
763
|
+
args,
|
|
764
|
+
configPath,
|
|
765
|
+
config,
|
|
766
|
+
accounts: accountReports,
|
|
767
|
+
summary: summarizeAccountReports(accountReports),
|
|
768
|
+
};
|
|
769
|
+
reportAndExit(report, args.json);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
main().catch((err) => {
|
|
773
|
+
console.error(`Agent selfcheck failed: ${String(err?.message || err)}`);
|
|
774
|
+
process.exit(1);
|
|
775
|
+
});
|