@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.
Files changed (79) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.en.md +204 -32
  3. package/README.md +234 -63
  4. package/docs/channels/wecom.md +137 -1
  5. package/openclaw.plugin.json +694 -10
  6. package/package.json +207 -4
  7. package/scripts/wecom-agent-selfcheck.mjs +775 -0
  8. package/scripts/wecom-bot-longconn-probe.mjs +582 -0
  9. package/scripts/wecom-bot-selfcheck.mjs +952 -0
  10. package/scripts/wecom-callback-matrix.mjs +224 -0
  11. package/scripts/wecom-doctor.mjs +1407 -0
  12. package/scripts/wecom-e2e-scenario.mjs +333 -0
  13. package/scripts/wecom-migrate.mjs +261 -0
  14. package/scripts/wecom-quickstart.mjs +1824 -0
  15. package/scripts/wecom-release-check.mjs +232 -0
  16. package/scripts/wecom-remote-e2e.mjs +310 -0
  17. package/scripts/wecom-selfcheck.mjs +1255 -0
  18. package/scripts/wecom-smoke.sh +74 -0
  19. package/src/core/delivery-router.js +21 -0
  20. package/src/core.js +631 -34
  21. package/src/wecom/account-config-core.js +27 -1
  22. package/src/wecom/account-config.js +19 -2
  23. package/src/wecom/agent-dispatch-executor.js +11 -0
  24. package/src/wecom/agent-dispatch-handlers.js +61 -8
  25. package/src/wecom/agent-inbound-guards.js +63 -16
  26. package/src/wecom/agent-inbound-processor.js +34 -2
  27. package/src/wecom/agent-late-reply-runtime.js +30 -2
  28. package/src/wecom/agent-text-sender.js +2 -0
  29. package/src/wecom/api-client-core.js +27 -19
  30. package/src/wecom/api-client-media.js +16 -7
  31. package/src/wecom/api-client-send-text.js +4 -0
  32. package/src/wecom/api-client-send-typed.js +4 -1
  33. package/src/wecom/api-client-senders.js +41 -3
  34. package/src/wecom/api-client.js +1 -0
  35. package/src/wecom/bot-dispatch-fallback.js +18 -3
  36. package/src/wecom/bot-dispatch-handlers.js +47 -10
  37. package/src/wecom/bot-inbound-dispatch-runtime.js +3 -0
  38. package/src/wecom/bot-inbound-executor-helpers.js +11 -1
  39. package/src/wecom/bot-inbound-executor.js +25 -1
  40. package/src/wecom/bot-inbound-guards.js +78 -23
  41. package/src/wecom/bot-long-connection-manager.js +4 -4
  42. package/src/wecom/channel-config-schema.js +132 -0
  43. package/src/wecom/channel-plugin.js +370 -7
  44. package/src/wecom/command-handlers.js +107 -10
  45. package/src/wecom/command-status-text.js +275 -1
  46. package/src/wecom/doc-client.js +7 -1
  47. package/src/wecom/inbound-content-handler-file-video-link.js +4 -0
  48. package/src/wecom/inbound-content-handler-image-voice.js +6 -0
  49. package/src/wecom/inbound-content.js +5 -0
  50. package/src/wecom/installer-api.js +910 -0
  51. package/src/wecom/media-download.js +2 -2
  52. package/src/wecom/migration-diagnostics.js +816 -0
  53. package/src/wecom/network-config.js +91 -0
  54. package/src/wecom/observability-metrics.js +9 -3
  55. package/src/wecom/outbound-agent-delivery.js +313 -0
  56. package/src/wecom/outbound-agent-media-sender.js +37 -7
  57. package/src/wecom/outbound-agent-push.js +1 -0
  58. package/src/wecom/outbound-delivery.js +129 -12
  59. package/src/wecom/outbound-stream-msg-item.js +25 -2
  60. package/src/wecom/outbound-webhook-delivery.js +19 -0
  61. package/src/wecom/outbound-webhook-media.js +30 -6
  62. package/src/wecom/pairing.js +188 -0
  63. package/src/wecom/pending-reply-manager.js +143 -0
  64. package/src/wecom/plugin-account-policy-services.js +26 -0
  65. package/src/wecom/plugin-base-services.js +58 -0
  66. package/src/wecom/plugin-constants.js +1 -1
  67. package/src/wecom/plugin-delivery-inbound-services.js +25 -0
  68. package/src/wecom/plugin-processing-deps.js +7 -0
  69. package/src/wecom/plugin-route-runtime-deps.js +1 -0
  70. package/src/wecom/plugin-services.js +87 -0
  71. package/src/wecom/policy-resolvers.js +93 -20
  72. package/src/wecom/quickstart-metadata.js +1247 -0
  73. package/src/wecom/reasoning-visibility.js +104 -0
  74. package/src/wecom/register-runtime.js +10 -0
  75. package/src/wecom/reliable-delivery-persistence.js +138 -0
  76. package/src/wecom/reliable-delivery.js +642 -0
  77. package/src/wecom/reply-output-policy.js +171 -0
  78. package/src/wecom/text-inbound-scheduler.js +6 -1
  79. package/src/wecom/workspace-auto-sender.js +2 -0
@@ -0,0 +1,333 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "node:child_process";
4
+
5
+ function pickFirstEnv(...names) {
6
+ for (const name of names) {
7
+ const value = String(process.env[name] ?? "").trim();
8
+ if (value) return value;
9
+ }
10
+ return "";
11
+ }
12
+
13
+ function joinBaseUrl(baseUrl, path) {
14
+ const safeBase = String(baseUrl ?? "").trim().replace(/\/+$/, "");
15
+ const safePath = String(path ?? "").trim();
16
+ if (!safeBase || !safePath) return "";
17
+ return `${safeBase}${safePath.startsWith("/") ? safePath : `/${safePath}`}`;
18
+ }
19
+
20
+ function parseArgs(argv) {
21
+ const out = {
22
+ scenario: "full-smoke",
23
+ botUrl:
24
+ pickFirstEnv("WECOM_E2E_BOT_URL") ||
25
+ joinBaseUrl(pickFirstEnv("WECOM_E2E_BASE_URL"), pickFirstEnv("WECOM_E2E_BOT_PATH")) ||
26
+ joinBaseUrl(pickFirstEnv("E2E_WECOM_BASE_URL"), pickFirstEnv("E2E_WECOM_WEBHOOK_PATH") || "/wecom/bot/callback"),
27
+ agentUrl:
28
+ pickFirstEnv("WECOM_E2E_AGENT_URL") ||
29
+ joinBaseUrl(pickFirstEnv("WECOM_E2E_BASE_URL"), pickFirstEnv("WECOM_E2E_AGENT_PATH")) ||
30
+ joinBaseUrl(pickFirstEnv("E2E_WECOM_BASE_URL"), pickFirstEnv("E2E_WECOM_AGENT_WEBHOOK_PATH") || "/wecom/callback"),
31
+ botLegacyUrl: pickFirstEnv("WECOM_E2E_BOT_LEGACY_URL"),
32
+ agentLegacyUrl: pickFirstEnv("WECOM_E2E_AGENT_LEGACY_URL"),
33
+ configPath: pickFirstEnv("WECOM_E2E_CONFIG", "OPENCLAW_CONFIG_PATH"),
34
+ account: "default",
35
+ fromUser: pickFirstEnv("WECOM_E2E_FROM_USER", "E2E_WECOM_TEST_USER"),
36
+ timeoutMs: Number(pickFirstEnv("WECOM_E2E_TIMEOUT_MS", "E2E_WECOM_STREAM_TIMEOUT_MS")) || 12000,
37
+ pollCount: Number(pickFirstEnv("WECOM_E2E_POLL_COUNT")) || 15,
38
+ pollIntervalMs: Number(pickFirstEnv("WECOM_E2E_POLL_INTERVAL_MS", "E2E_WECOM_POLL_INTERVAL_MS")) || 800,
39
+ prepareBrowser: false,
40
+ collectPdf: false,
41
+ browserPrepareMode: pickFirstEnv("E2E_BROWSER_PREPARE_MODE"),
42
+ browserRequireReady: pickFirstEnv("E2E_BROWSER_REQUIRE_READY") === "1",
43
+ };
44
+
45
+ for (let i = 2; i < argv.length; i += 1) {
46
+ const arg = argv[i];
47
+ const next = argv[i + 1];
48
+ if (arg === "--scenario" && next) {
49
+ out.scenario = String(next).trim().toLowerCase();
50
+ i += 1;
51
+ } else if (arg === "--bot-url" && next) {
52
+ out.botUrl = next;
53
+ i += 1;
54
+ } else if (arg === "--agent-url" && next) {
55
+ out.agentUrl = next;
56
+ i += 1;
57
+ } else if (arg === "--bot-legacy-url" && next) {
58
+ out.botLegacyUrl = next;
59
+ i += 1;
60
+ } else if (arg === "--agent-legacy-url" && next) {
61
+ out.agentLegacyUrl = next;
62
+ i += 1;
63
+ } else if (arg === "--config" && next) {
64
+ out.configPath = next;
65
+ i += 1;
66
+ } else if (arg === "--account" && next) {
67
+ out.account = next;
68
+ i += 1;
69
+ } else if (arg === "--from-user" && next) {
70
+ out.fromUser = next;
71
+ i += 1;
72
+ } else if (arg === "--timeout-ms" && next) {
73
+ const n = Number(next);
74
+ if (Number.isFinite(n) && n > 0) out.timeoutMs = Math.floor(n);
75
+ i += 1;
76
+ } else if (arg === "--poll-count" && next) {
77
+ const n = Number(next);
78
+ if (Number.isFinite(n) && n > 0) out.pollCount = Math.floor(n);
79
+ i += 1;
80
+ } else if (arg === "--poll-interval-ms" && next) {
81
+ const n = Number(next);
82
+ if (Number.isFinite(n) && n > 0) out.pollIntervalMs = Math.floor(n);
83
+ i += 1;
84
+ } else if (arg === "--prepare-browser") {
85
+ out.prepareBrowser = true;
86
+ } else if (arg === "--collect-pdf") {
87
+ out.collectPdf = true;
88
+ } else if (arg === "--browser-prepare-mode" && next) {
89
+ out.browserPrepareMode = String(next).trim().toLowerCase();
90
+ i += 1;
91
+ } else if (arg === "--browser-require-ready") {
92
+ out.browserRequireReady = true;
93
+ } else if (arg === "-h" || arg === "--help") {
94
+ printHelp();
95
+ process.exit(0);
96
+ } else {
97
+ throw new Error(`Unknown argument: ${arg}`);
98
+ }
99
+ }
100
+
101
+ const scenario = out.scenario;
102
+ const valid = new Set(["bot-smoke", "agent-smoke", "full-smoke", "bot-queue", "compat-smoke", "matrix-smoke"]);
103
+ valid.add("callback-matrix");
104
+ if (!valid.has(scenario)) {
105
+ throw new Error(`Invalid --scenario, expected one of: ${Array.from(valid).join(" | ")}`);
106
+ }
107
+ if (out.browserPrepareMode && !["check", "install", "off"].includes(out.browserPrepareMode)) {
108
+ throw new Error("Invalid --browser-prepare-mode, expected one of: check | install | off");
109
+ }
110
+ if ((scenario === "bot-smoke" || scenario === "full-smoke" || scenario === "bot-queue") && !String(out.botUrl).trim()) {
111
+ throw new Error("Missing required argument: --bot-url <https://.../wecom/bot/callback>");
112
+ }
113
+ if (scenario === "matrix-smoke" && !String(out.botUrl).trim()) {
114
+ throw new Error("Missing required argument: --bot-url <https://.../wecom/bot/callback>");
115
+ }
116
+ if ((scenario === "agent-smoke" || scenario === "full-smoke") && !String(out.agentUrl).trim()) {
117
+ throw new Error("Missing required argument: --agent-url <https://.../wecom/callback>");
118
+ }
119
+ if (scenario === "compat-smoke") {
120
+ const hasBotPair = String(out.botUrl).trim() && String(out.botLegacyUrl).trim();
121
+ const hasAgentPair = String(out.agentUrl).trim() && String(out.agentLegacyUrl).trim();
122
+ if (!hasBotPair && !hasAgentPair) {
123
+ throw new Error(
124
+ "compat-smoke requires at least one pair: (--bot-url + --bot-legacy-url) or (--agent-url + --agent-legacy-url)",
125
+ );
126
+ }
127
+ }
128
+
129
+ return out;
130
+ }
131
+
132
+ function printHelp() {
133
+ console.log(`OpenClaw-Wechat scenario E2E
134
+
135
+ Usage:
136
+ npm run wecom:e2e:scenario -- --scenario <bot-smoke|agent-smoke|full-smoke|bot-queue|compat-smoke|matrix-smoke|callback-matrix> [options]
137
+
138
+ Scenarios:
139
+ bot-smoke Run remote bot E2E once
140
+ agent-smoke Run remote agent E2E once
141
+ full-smoke Run remote all-in-one E2E (agent + bot + account selfcheck)
142
+ bot-queue Run bot E2E twice with same sender to validate queue/stream recovery
143
+ compat-smoke Run compatibility matrix on new + legacy webhook URLs
144
+ matrix-smoke Run remote bot protocol matrix E2E test suite
145
+ callback-matrix Probe public callback health for Agent/Bot (+ optional legacy alias)
146
+
147
+ Options:
148
+ --bot-url <url> Bot callback URL (required for bot/full/bot-queue)
149
+ --agent-url <url> Agent callback URL (required for agent/full)
150
+ --bot-legacy-url <url> Legacy Bot callback URL (used by compat-smoke)
151
+ --agent-legacy-url <url> Legacy Agent callback URL (used by compat-smoke)
152
+ --config <path> Optional OpenClaw config path
153
+ --account <id> Agent account id (default: default)
154
+ --from-user <userid> Fixed sender id for scenario checks
155
+ --timeout-ms <ms> HTTP timeout (default: 12000)
156
+ --poll-count <n> Bot stream-refresh polls (default: 15)
157
+ --poll-interval-ms <ms> Bot poll interval (default: 800)
158
+ --prepare-browser Run remote browser sandbox prepare before E2E
159
+ --collect-pdf Collect browser-generated PDFs after E2E
160
+ --browser-prepare-mode check | install | off
161
+ --browser-require-ready Fail when browser sandbox is not ready
162
+ -h, --help Show help
163
+
164
+ Env shortcuts:
165
+ WECOM_E2E_BOT_URL / WECOM_E2E_AGENT_URL / WECOM_E2E_BASE_URL + *_PATH
166
+ WECOM_E2E_TIMEOUT_MS / WECOM_E2E_POLL_* / WECOM_E2E_FROM_USER
167
+ WECOM_E2E_BOT_LEGACY_URL / WECOM_E2E_AGENT_LEGACY_URL
168
+ Legacy: E2E_WECOM_BASE_URL / E2E_WECOM_WEBHOOK_PATH / E2E_WECOM_*
169
+ `);
170
+ }
171
+
172
+ async function runNodeScript(script, args = [], extraEnv = {}) {
173
+ await new Promise((resolve, reject) => {
174
+ const child = spawn(process.execPath, [script, ...args], {
175
+ stdio: "inherit",
176
+ env: {
177
+ ...process.env,
178
+ ...extraEnv,
179
+ },
180
+ });
181
+ child.on("error", reject);
182
+ child.on("exit", (code) => {
183
+ if (code === 0) resolve();
184
+ else reject(new Error(`${script} exited with code ${code}`));
185
+ });
186
+ });
187
+ }
188
+
189
+ function buildRemoteE2eArgs({ mode, options, content }) {
190
+ const args = [
191
+ "--mode",
192
+ mode,
193
+ "--timeout-ms",
194
+ String(options.timeoutMs),
195
+ "--poll-count",
196
+ String(options.pollCount),
197
+ "--poll-interval-ms",
198
+ String(options.pollIntervalMs),
199
+ "--content",
200
+ content,
201
+ "--account",
202
+ options.account,
203
+ ];
204
+ if (options.botUrl) args.push("--bot-url", options.botUrl);
205
+ if (options.agentUrl) args.push("--agent-url", options.agentUrl);
206
+ if (options.botLegacyUrl) args.push("--bot-legacy-url", options.botLegacyUrl);
207
+ if (options.agentLegacyUrl) args.push("--agent-legacy-url", options.agentLegacyUrl);
208
+ if (options.configPath) args.push("--config", options.configPath);
209
+ if (options.fromUser) args.push("--from-user", options.fromUser);
210
+ if (options.prepareBrowser) args.push("--prepare-browser");
211
+ if (options.collectPdf) args.push("--collect-pdf");
212
+ if (options.browserPrepareMode) args.push("--browser-prepare-mode", options.browserPrepareMode);
213
+ if (options.browserRequireReady) args.push("--browser-require-ready");
214
+ return args;
215
+ }
216
+
217
+ async function main() {
218
+ const args = parseArgs(process.argv);
219
+ const steps = [];
220
+
221
+ if (args.scenario === "bot-smoke") {
222
+ steps.push({
223
+ label: "Bot smoke E2E",
224
+ script: "./scripts/wecom-remote-e2e.mjs",
225
+ args: buildRemoteE2eArgs({ mode: "bot", options: args, content: "/status" }),
226
+ });
227
+ } else if (args.scenario === "agent-smoke") {
228
+ steps.push({
229
+ label: "Agent smoke E2E",
230
+ script: "./scripts/wecom-remote-e2e.mjs",
231
+ args: buildRemoteE2eArgs({ mode: "agent", options: args, content: "/status" }),
232
+ });
233
+ } else if (args.scenario === "full-smoke") {
234
+ steps.push({
235
+ label: "Full smoke E2E (agent+bot)",
236
+ script: "./scripts/wecom-remote-e2e.mjs",
237
+ args: buildRemoteE2eArgs({ mode: "all", options: args, content: "/status" }),
238
+ });
239
+ } else if (args.scenario === "bot-queue") {
240
+ const queueUser = args.fromUser || `e2e-queue-${Date.now().toString(36).slice(-6)}`;
241
+ const queueOptions = { ...args, fromUser: queueUser };
242
+ steps.push({
243
+ label: "Bot queue scenario: first message",
244
+ script: "./scripts/wecom-remote-e2e.mjs",
245
+ args: buildRemoteE2eArgs({ mode: "bot", options: queueOptions, content: "第一条队列消息 /status" }),
246
+ });
247
+ steps.push({
248
+ label: "Bot queue scenario: second message",
249
+ script: "./scripts/wecom-remote-e2e.mjs",
250
+ args: buildRemoteE2eArgs({ mode: "bot", options: queueOptions, content: "第二条队列消息 /status" }),
251
+ });
252
+ } else if (args.scenario === "compat-smoke") {
253
+ if (args.agentUrl && args.agentLegacyUrl) {
254
+ steps.push({
255
+ label: "Compat smoke: Agent new URL",
256
+ script: "./scripts/wecom-remote-e2e.mjs",
257
+ args: buildRemoteE2eArgs({ mode: "agent", options: args, content: "/status" }),
258
+ });
259
+ const legacyAgentOptions = { ...args, agentUrl: args.agentLegacyUrl };
260
+ steps.push({
261
+ label: "Compat smoke: Agent legacy URL",
262
+ script: "./scripts/wecom-remote-e2e.mjs",
263
+ args: buildRemoteE2eArgs({ mode: "agent", options: legacyAgentOptions, content: "/status" }),
264
+ });
265
+ }
266
+ if (args.botUrl && args.botLegacyUrl) {
267
+ steps.push({
268
+ label: "Compat smoke: Bot new URL",
269
+ script: "./scripts/wecom-remote-e2e.mjs",
270
+ args: buildRemoteE2eArgs({ mode: "bot", options: args, content: "/status" }),
271
+ });
272
+ const legacyBotOptions = { ...args, botUrl: args.botLegacyUrl };
273
+ steps.push({
274
+ label: "Compat smoke: Bot legacy URL",
275
+ script: "./scripts/wecom-remote-e2e.mjs",
276
+ args: buildRemoteE2eArgs({ mode: "bot", options: legacyBotOptions, content: "/status" }),
277
+ });
278
+ }
279
+ } else if (args.scenario === "matrix-smoke") {
280
+ const matrixToken = pickFirstEnv("WECOM_BOT_TOKEN", "WECOM_E2E_TOKEN", "E2E_WECOM_TOKEN");
281
+ const matrixAesKey = pickFirstEnv(
282
+ "WECOM_BOT_ENCODING_AES_KEY",
283
+ "WECOM_E2E_ENCODING_AES_KEY",
284
+ "E2E_WECOM_ENCODING_AES_KEY",
285
+ );
286
+ if (!matrixToken || !matrixAesKey) {
287
+ throw new Error(
288
+ "matrix-smoke requires bot crypto env: WECOM_BOT_TOKEN and WECOM_BOT_ENCODING_AES_KEY (or compatible E2E_WECOM_* vars)",
289
+ );
290
+ }
291
+ steps.push({
292
+ label: "Matrix smoke: Bot protocol matrix",
293
+ script: "--test",
294
+ args: ["tests/e2e/remote-wecom.matrix.test.mjs"],
295
+ env: {
296
+ WECOM_E2E_MATRIX_ENABLE: "1",
297
+ WECOM_E2E_BOT_URL: args.botUrl,
298
+ WECOM_BOT_TOKEN: matrixToken,
299
+ WECOM_BOT_ENCODING_AES_KEY: matrixAesKey,
300
+ WECOM_E2E_MATRIX_TIMEOUT_MS: String(args.timeoutMs),
301
+ WECOM_E2E_MATRIX_POLL_COUNT: String(args.pollCount),
302
+ WECOM_E2E_MATRIX_POLL_INTERVAL_MS: String(args.pollIntervalMs),
303
+ ...(args.fromUser ? { WECOM_E2E_FROM_USER: args.fromUser } : {}),
304
+ },
305
+ });
306
+ } else if (args.scenario === "callback-matrix") {
307
+ const matrixArgs = ["--timeout-ms", String(args.timeoutMs)];
308
+ if (args.agentUrl) matrixArgs.push("--agent-url", args.agentUrl);
309
+ if (args.botUrl) matrixArgs.push("--bot-url", args.botUrl);
310
+ if (args.agentLegacyUrl) matrixArgs.push("--agent-legacy-url", args.agentLegacyUrl);
311
+ if (args.botLegacyUrl) matrixArgs.push("--bot-legacy-url", args.botLegacyUrl);
312
+ steps.push({
313
+ label: "Callback matrix",
314
+ script: "./scripts/wecom-callback-matrix.mjs",
315
+ args: matrixArgs,
316
+ });
317
+ }
318
+
319
+ let index = 0;
320
+ const total = steps.length;
321
+ for (const step of steps) {
322
+ index += 1;
323
+ console.log(`[${index}/${total}] ${step.label}`);
324
+ // eslint-disable-next-line no-await-in-loop
325
+ await runNodeScript(step.script, step.args, step.env || {});
326
+ }
327
+ console.log(`Scenario completed: ${args.scenario}`);
328
+ }
329
+
330
+ main().catch((err) => {
331
+ console.error(`Scenario E2E failed: ${String(err?.message || err)}`);
332
+ process.exit(1);
333
+ });
@@ -0,0 +1,261 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import {
7
+ collectWecomMigrationDiagnostics,
8
+ WECOM_MIGRATION_COMMAND,
9
+ } from "../src/wecom/migration-diagnostics.js";
10
+
11
+ function expandHome(p) {
12
+ if (!p) return p;
13
+ if (p === "~") return os.homedir();
14
+ if (p.startsWith("~/")) return path.join(os.homedir(), p.slice(2));
15
+ return p;
16
+ }
17
+
18
+ function asObject(value) {
19
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
20
+ }
21
+
22
+ function mergeDeep(base, patch) {
23
+ if (Array.isArray(patch)) return patch.slice();
24
+ if (!patch || typeof patch !== "object") return patch;
25
+ const out = { ...asObject(base) };
26
+ for (const [key, value] of Object.entries(patch)) {
27
+ if (value && typeof value === "object" && !Array.isArray(value)) {
28
+ out[key] = mergeDeep(asObject(base?.[key]), value);
29
+ } else if (Array.isArray(value)) {
30
+ out[key] = value.slice();
31
+ } else {
32
+ out[key] = value;
33
+ }
34
+ }
35
+ return out;
36
+ }
37
+
38
+ function valuesEqual(left, right) {
39
+ return JSON.stringify(left) === JSON.stringify(right);
40
+ }
41
+
42
+ function collectChangedPaths(baseValue, patchValue, prefix = "", out = []) {
43
+ if (Array.isArray(patchValue)) {
44
+ if (!valuesEqual(baseValue, patchValue) && prefix) out.push(prefix);
45
+ return out;
46
+ }
47
+ if (!patchValue || typeof patchValue !== "object") {
48
+ if (!valuesEqual(baseValue, patchValue) && prefix) out.push(prefix);
49
+ return out;
50
+ }
51
+ for (const [key, value] of Object.entries(patchValue)) {
52
+ const nextPrefix = prefix ? `${prefix}.${key}` : key;
53
+ const nextBase = baseValue && typeof baseValue === "object" ? baseValue[key] : undefined;
54
+ collectChangedPaths(nextBase, value, nextPrefix, out);
55
+ }
56
+ return out;
57
+ }
58
+
59
+ function buildBackupPath(configPath) {
60
+ return `${configPath}.bak-${Date.now()}`;
61
+ }
62
+
63
+ async function loadConfig(configPath) {
64
+ const resolvedPath = path.resolve(expandHome(configPath));
65
+ try {
66
+ const raw = await readFile(resolvedPath, "utf8");
67
+ return {
68
+ exists: true,
69
+ configPath: resolvedPath,
70
+ config: JSON.parse(raw),
71
+ };
72
+ } catch (err) {
73
+ if (err?.code === "ENOENT") {
74
+ return {
75
+ exists: false,
76
+ configPath: resolvedPath,
77
+ config: {},
78
+ };
79
+ }
80
+ throw err;
81
+ }
82
+ }
83
+
84
+ async function writeMergedConfig(configPath, patch) {
85
+ const loaded = await loadConfig(configPath);
86
+ const changedPaths = collectChangedPaths(loaded.config, patch);
87
+ const merged = mergeDeep(loaded.config, patch);
88
+ const backupPath = loaded.exists ? buildBackupPath(loaded.configPath) : null;
89
+ await mkdir(path.dirname(loaded.configPath), { recursive: true });
90
+ if (loaded.exists) {
91
+ await writeFile(backupPath, `${JSON.stringify(loaded.config, null, 2)}\n`, "utf8");
92
+ }
93
+ await writeFile(loaded.configPath, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
94
+ return {
95
+ applied: true,
96
+ configPath: loaded.configPath,
97
+ backupPath,
98
+ existed: loaded.exists,
99
+ changedPaths,
100
+ };
101
+ }
102
+
103
+ function parseArgs(argv) {
104
+ const out = {
105
+ account: "default",
106
+ configPath: process.env.OPENCLAW_CONFIG_PATH || "~/.openclaw/openclaw.json",
107
+ json: false,
108
+ write: false,
109
+ };
110
+
111
+ for (let index = 2; index < argv.length; index += 1) {
112
+ const arg = argv[index];
113
+ const next = argv[index + 1];
114
+ if (arg === "--account" && next) {
115
+ out.account = String(next).trim().toLowerCase() || "default";
116
+ index += 1;
117
+ } else if (arg === "--config" && next) {
118
+ out.configPath = next;
119
+ index += 1;
120
+ } else if (arg === "--write") {
121
+ out.write = true;
122
+ } else if (arg === "--json") {
123
+ out.json = true;
124
+ } else if (arg === "-h" || arg === "--help") {
125
+ printHelp();
126
+ process.exit(0);
127
+ } else {
128
+ throw new Error(`Unknown argument: ${arg}`);
129
+ }
130
+ }
131
+
132
+ return out;
133
+ }
134
+
135
+ function printHelp() {
136
+ console.log(`OpenClaw-Wechat migrate
137
+
138
+ Usage:
139
+ npm run wecom:migrate -- [options]
140
+
141
+ Options:
142
+ --account <id> account id used for scoped migration hints (default: default)
143
+ --config <path> target openclaw.json path (default: ~/.openclaw/openclaw.json)
144
+ --write merge generated configPatch into the target config file
145
+ --json print machine-readable JSON report
146
+ -h, --help show this help
147
+ `);
148
+ }
149
+
150
+ function printTextReport(report) {
151
+ console.log("WeCom migrate");
152
+ console.log(`- config: ${report.configPath}`);
153
+ console.log(`- installState: ${report.installState}`);
154
+ console.log(`- migrationState: ${report.migrationState}`);
155
+ console.log(`- migrationSource: ${report.migrationSource}`);
156
+ console.log(`- summary: ${report.installStateSummary}`);
157
+ console.log(`- migrationSummary: ${report.migrationStateSummary}`);
158
+ console.log(`- sourceSummary: ${report.migrationSourceSummary}`);
159
+ if (report.installedVersion) {
160
+ console.log(`- installedVersion: ${report.installedVersion}`);
161
+ }
162
+ console.log(`- migrationCommand: ${report.migrationCommand}`);
163
+
164
+ if (Array.isArray(report.migrationSourceSignals) && report.migrationSourceSignals.length > 0) {
165
+ console.log("- migrationSourceSignals:");
166
+ for (const item of report.migrationSourceSignals) {
167
+ console.log(` - [${item.source}] ${item.path}: ${item.detail}`);
168
+ }
169
+ }
170
+
171
+ if (Array.isArray(report.detectedLegacyFields) && report.detectedLegacyFields.length > 0) {
172
+ console.log("- detectedLegacyFields:");
173
+ for (const item of report.detectedLegacyFields) {
174
+ console.log(` - ${item.path}: ${item.detail}`);
175
+ }
176
+ }
177
+
178
+ if (Array.isArray(report.recommendedActions) && report.recommendedActions.length > 0) {
179
+ console.log("- recommendedActions:");
180
+ for (const action of report.recommendedActions) {
181
+ console.log(` - [${action.kind}] ${action.title}: ${action.detail}`);
182
+ if (action.command) console.log(` command: ${action.command}`);
183
+ }
184
+ }
185
+
186
+ if (report.configPatch) {
187
+ console.log("- configPatch:");
188
+ console.log(JSON.stringify(report.configPatch, null, 2));
189
+ }
190
+ if (Array.isArray(report.envTemplate?.lines) && report.envTemplate.lines.length > 0) {
191
+ console.log("- envTemplate:");
192
+ for (const line of report.envTemplate.lines) {
193
+ console.log(` - ${line}`);
194
+ }
195
+ }
196
+ if (report.write?.requested) {
197
+ console.log("- write:");
198
+ console.log(` - applied: ${report.write.applied ? "yes" : "no"}`);
199
+ console.log(` - configPath: ${report.write.configPath}`);
200
+ if (report.write.backupPath) console.log(` - backupPath: ${report.write.backupPath}`);
201
+ if (Array.isArray(report.write.changedPaths) && report.write.changedPaths.length > 0) {
202
+ console.log(` - changedPaths: ${report.write.changedPaths.join(", ")}`);
203
+ }
204
+ }
205
+ }
206
+
207
+ async function main() {
208
+ const args = parseArgs(process.argv);
209
+ const loaded = await loadConfig(args.configPath);
210
+ const diagnostics = collectWecomMigrationDiagnostics({
211
+ config: loaded.config,
212
+ accountId: args.account,
213
+ });
214
+
215
+ const writeResult =
216
+ args.write && diagnostics.configPatch
217
+ ? await writeMergedConfig(loaded.configPath, diagnostics.configPatch)
218
+ : {
219
+ requested: args.write === true,
220
+ applied: false,
221
+ configPath: loaded.configPath,
222
+ changedPaths: [],
223
+ reason: diagnostics.configPatch ? "write not requested" : "no configPatch available",
224
+ };
225
+
226
+ const report = {
227
+ accountId: args.account,
228
+ configPath: loaded.configPath,
229
+ installState: diagnostics.installState,
230
+ installStateSummary: diagnostics.installStateSummary,
231
+ migrationState: diagnostics.migrationState,
232
+ migrationStateSummary: diagnostics.migrationStateSummary,
233
+ migrationSource: diagnostics.migrationSource,
234
+ migrationSourceSummary: diagnostics.migrationSourceSummary,
235
+ migrationSourceSignals: diagnostics.migrationSourceSignals,
236
+ installedVersion: diagnostics.installedVersion,
237
+ expectedVersion: diagnostics.expectedVersion,
238
+ stalePackage: diagnostics.stalePackage,
239
+ detectedLegacyFields: diagnostics.detectedLegacyFields,
240
+ recommendedActions: diagnostics.recommendedActions,
241
+ configPatch: diagnostics.configPatch,
242
+ envTemplate: diagnostics.envTemplate,
243
+ migrationCommand: WECOM_MIGRATION_COMMAND,
244
+ write: {
245
+ requested: args.write === true,
246
+ ...writeResult,
247
+ },
248
+ };
249
+
250
+ if (args.json) {
251
+ console.log(JSON.stringify(report, null, 2));
252
+ return;
253
+ }
254
+
255
+ printTextReport(report);
256
+ }
257
+
258
+ main().catch((err) => {
259
+ console.error(`WeCom migrate failed: ${String(err?.message || err)}`);
260
+ process.exit(1);
261
+ });