@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,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
|
+
});
|