@dingxiang-me/openclaw-wechat 2.1.0 → 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 +72 -0
- package/README.en.md +181 -14
- package/README.md +201 -16
- package/docs/channels/wecom.md +137 -1
- package/openclaw.plugin.json +688 -6
- package/package.json +204 -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 +619 -30
- 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 +24 -0
- 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 +24 -0
- package/src/wecom/bot-inbound-guards.js +31 -1
- package/src/wecom/channel-config-schema.js +132 -0
- package/src/wecom/channel-plugin.js +348 -7
- package/src/wecom/command-handlers.js +102 -11
- package/src/wecom/command-status-text.js +206 -0
- 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/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,1824 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { createInterface } from "node:readline/promises";
|
|
8
|
+
import {
|
|
9
|
+
WECOM_QUICKSTART_APPLY_REPAIR_COMMAND,
|
|
10
|
+
WECOM_QUICKSTART_CONFIRM_REPAIR_COMMAND,
|
|
11
|
+
buildWecomQuickstartSetupPlan,
|
|
12
|
+
WECOM_QUICKSTART_FORCE_CHECKS_COMMAND,
|
|
13
|
+
listWecomQuickstartGroupProfiles,
|
|
14
|
+
WECOM_QUICKSTART_DEFAULT_GROUP_PROFILE,
|
|
15
|
+
listWecomQuickstartModes,
|
|
16
|
+
WECOM_QUICKSTART_RECOMMENDED_MODE,
|
|
17
|
+
WECOM_QUICKSTART_RUN_CHECKS_COMMAND,
|
|
18
|
+
WECOM_QUICKSTART_WIZARD_COMMAND,
|
|
19
|
+
} from "../src/wecom/quickstart-metadata.js";
|
|
20
|
+
|
|
21
|
+
const DM_MODES = new Set(["open", "allowlist", "pairing", "deny"]);
|
|
22
|
+
const DM_MODE_OPTIONS = Object.freeze([
|
|
23
|
+
{ id: "pairing", label: "pairing", summary: "首次私聊先审批" },
|
|
24
|
+
{ id: "open", label: "open", summary: "私聊直接可用" },
|
|
25
|
+
{ id: "allowlist", label: "allowlist", summary: "仅白名单可私聊" },
|
|
26
|
+
{ id: "deny", label: "deny", summary: "关闭私聊入口" },
|
|
27
|
+
]);
|
|
28
|
+
let sharedNonTtyAnswersPromise = null;
|
|
29
|
+
let sharedNonTtyAnswerIndex = 0;
|
|
30
|
+
|
|
31
|
+
function expandHome(p) {
|
|
32
|
+
if (!p) return p;
|
|
33
|
+
if (p === "~") return os.homedir();
|
|
34
|
+
if (p.startsWith("~/")) return path.join(os.homedir(), p.slice(2));
|
|
35
|
+
return p;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseArgs(argv) {
|
|
39
|
+
const out = {
|
|
40
|
+
mode: WECOM_QUICKSTART_RECOMMENDED_MODE,
|
|
41
|
+
account: "default",
|
|
42
|
+
dmMode: "pairing",
|
|
43
|
+
groupProfile: WECOM_QUICKSTART_DEFAULT_GROUP_PROFILE,
|
|
44
|
+
groupChatId: "",
|
|
45
|
+
groupAllow: "",
|
|
46
|
+
configPath: process.env.OPENCLAW_CONFIG_PATH || "~/.openclaw/openclaw.json",
|
|
47
|
+
write: false,
|
|
48
|
+
json: false,
|
|
49
|
+
wizard: false,
|
|
50
|
+
runChecks: false,
|
|
51
|
+
forceChecks: false,
|
|
52
|
+
applyRepair: false,
|
|
53
|
+
confirmRepair: false,
|
|
54
|
+
repairDir: "",
|
|
55
|
+
checkTimeoutMs: 120000,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
for (let index = 2; index < argv.length; index += 1) {
|
|
59
|
+
const arg = argv[index];
|
|
60
|
+
const next = argv[index + 1];
|
|
61
|
+
if (arg === "--mode" && next) {
|
|
62
|
+
out.mode = String(next).trim().toLowerCase();
|
|
63
|
+
index += 1;
|
|
64
|
+
} else if (arg === "--account" && next) {
|
|
65
|
+
out.account = String(next).trim().toLowerCase() || "default";
|
|
66
|
+
index += 1;
|
|
67
|
+
} else if (arg === "--dm-mode" && next) {
|
|
68
|
+
out.dmMode = String(next).trim().toLowerCase();
|
|
69
|
+
index += 1;
|
|
70
|
+
} else if (arg === "--group-profile" && next) {
|
|
71
|
+
out.groupProfile = String(next).trim().toLowerCase();
|
|
72
|
+
index += 1;
|
|
73
|
+
} else if (arg === "--group-chat-id" && next) {
|
|
74
|
+
out.groupChatId = String(next).trim();
|
|
75
|
+
index += 1;
|
|
76
|
+
} else if (arg === "--group-allow" && next) {
|
|
77
|
+
out.groupAllow = String(next).trim();
|
|
78
|
+
index += 1;
|
|
79
|
+
} else if (arg === "--config" && next) {
|
|
80
|
+
out.configPath = next;
|
|
81
|
+
index += 1;
|
|
82
|
+
} else if (arg === "--write") {
|
|
83
|
+
out.write = true;
|
|
84
|
+
} else if (arg === "--json") {
|
|
85
|
+
out.json = true;
|
|
86
|
+
} else if (arg === "--wizard") {
|
|
87
|
+
out.wizard = true;
|
|
88
|
+
} else if (arg === "--run-checks") {
|
|
89
|
+
out.runChecks = true;
|
|
90
|
+
} else if (arg === "--force-checks") {
|
|
91
|
+
out.forceChecks = true;
|
|
92
|
+
out.runChecks = true;
|
|
93
|
+
} else if (arg === "--apply-repair") {
|
|
94
|
+
out.applyRepair = true;
|
|
95
|
+
out.runChecks = true;
|
|
96
|
+
} else if (arg === "--confirm-repair") {
|
|
97
|
+
out.confirmRepair = true;
|
|
98
|
+
out.applyRepair = true;
|
|
99
|
+
out.runChecks = true;
|
|
100
|
+
} else if (arg === "--repair-dir" && next) {
|
|
101
|
+
out.repairDir = next;
|
|
102
|
+
index += 1;
|
|
103
|
+
} else if (arg === "--check-timeout-ms" && next) {
|
|
104
|
+
const parsed = Number(next);
|
|
105
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
106
|
+
out.checkTimeoutMs = Math.floor(parsed);
|
|
107
|
+
}
|
|
108
|
+
index += 1;
|
|
109
|
+
} else if (arg === "-h" || arg === "--help") {
|
|
110
|
+
printHelp();
|
|
111
|
+
process.exit(0);
|
|
112
|
+
} else {
|
|
113
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function printHelp() {
|
|
120
|
+
console.log(`OpenClaw-Wechat quickstart
|
|
121
|
+
|
|
122
|
+
Usage:
|
|
123
|
+
npm run wecom:quickstart -- [options]
|
|
124
|
+
|
|
125
|
+
Options:
|
|
126
|
+
--mode <id> quickstart mode: bot_long_connection | agent_callback | hybrid
|
|
127
|
+
--account <id> account id to scaffold (default: default)
|
|
128
|
+
--dm-mode <mode> dm policy: open | allowlist | pairing | deny (default: pairing)
|
|
129
|
+
--group-profile <id> group policy template: inherit | mention_only | open_direct | allowlist_template | deny
|
|
130
|
+
--group-chat-id <id> optional chatId used by allowlist_template
|
|
131
|
+
--group-allow <list> optional comma-separated member ids for allowlist_template
|
|
132
|
+
--config <path> target OpenClaw config path when using --write (default: ~/.openclaw/openclaw.json)
|
|
133
|
+
--wizard run an interactive setup wizard before generating the report
|
|
134
|
+
--run-checks execute recommended selfcheck commands after generating config
|
|
135
|
+
--force-checks run checks even when starter config still contains placeholders
|
|
136
|
+
--apply-repair merge the generated repair configPatch into the target config file
|
|
137
|
+
--confirm-repair preview the repair patch and ask before applying it
|
|
138
|
+
--repair-dir <path> write generated repairArtifacts files into this directory
|
|
139
|
+
--check-timeout-ms timeout for each check command (default: 120000)
|
|
140
|
+
--write merge generated starter config into the target config file
|
|
141
|
+
--json print machine-readable JSON report
|
|
142
|
+
-h, --help show this help
|
|
143
|
+
`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function normalizeYesNo(value, defaultValue = false) {
|
|
147
|
+
const normalized = String(value ?? "").trim().toLowerCase();
|
|
148
|
+
if (!normalized) return defaultValue;
|
|
149
|
+
if (["y", "yes", "true", "1"].includes(normalized)) return true;
|
|
150
|
+
if (["n", "no", "false", "0"].includes(normalized)) return false;
|
|
151
|
+
return defaultValue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function findOptionByAnswer(options, answer, fallbackId) {
|
|
155
|
+
const normalized = String(answer ?? "").trim().toLowerCase();
|
|
156
|
+
if (!normalized) return options.find((item) => item.id === fallbackId) ?? options[0];
|
|
157
|
+
const byIndex = Number.parseInt(normalized, 10);
|
|
158
|
+
if (Number.isInteger(byIndex) && byIndex >= 1 && byIndex <= options.length) {
|
|
159
|
+
return options[byIndex - 1];
|
|
160
|
+
}
|
|
161
|
+
return options.find((item) => item.id === normalized) ?? options.find((item) => item.id === fallbackId) ?? options[0];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function askQuestion(rl, message, defaultValue = "") {
|
|
165
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : "";
|
|
166
|
+
const answer = await rl.question(`${message}${suffix}: `);
|
|
167
|
+
const normalized = String(answer ?? "").trim();
|
|
168
|
+
return normalized || defaultValue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function askSelect(rl, title, options, defaultId) {
|
|
172
|
+
const fallback = options.find((item) => item.id === defaultId) ?? options[0];
|
|
173
|
+
options.forEach((item, index) => {
|
|
174
|
+
const marker = item.id === fallback?.id ? " (default)" : "";
|
|
175
|
+
const summary = item.summary ? ` - ${item.summary}` : "";
|
|
176
|
+
rl.output.write(` ${index + 1}. ${item.label}${summary}${marker}\n`);
|
|
177
|
+
});
|
|
178
|
+
const answer = await askQuestion(rl, title, fallback?.id ?? "");
|
|
179
|
+
return findOptionByAnswer(options, answer, fallback?.id);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function askBoolean(rl, message, defaultValue = false) {
|
|
183
|
+
const label = defaultValue ? "Y/n" : "y/N";
|
|
184
|
+
const answer = await rl.question(`${message} [${label}]: `);
|
|
185
|
+
return normalizeYesNo(answer, defaultValue);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function summarizeCheckResults(checks = []) {
|
|
189
|
+
const failed = checks.filter((item) => item.ok !== true).length;
|
|
190
|
+
return {
|
|
191
|
+
ok: failed === 0,
|
|
192
|
+
total: checks.length,
|
|
193
|
+
passed: checks.length - failed,
|
|
194
|
+
failed,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function dedupeRemediation(items = []) {
|
|
199
|
+
const out = [];
|
|
200
|
+
const seen = new Set();
|
|
201
|
+
for (const item of items) {
|
|
202
|
+
const id = String(item?.id ?? "").trim();
|
|
203
|
+
if (!id || seen.has(id)) continue;
|
|
204
|
+
seen.add(id);
|
|
205
|
+
out.push(item);
|
|
206
|
+
}
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function buildRemediationItem(id, title, detail, command = "") {
|
|
211
|
+
return {
|
|
212
|
+
id,
|
|
213
|
+
title,
|
|
214
|
+
detail,
|
|
215
|
+
command: String(command ?? "").trim() || undefined,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function readStdinLines() {
|
|
220
|
+
return new Promise((resolve, reject) => {
|
|
221
|
+
let raw = "";
|
|
222
|
+
process.stdin.setEncoding("utf8");
|
|
223
|
+
process.stdin.on("data", (chunk) => {
|
|
224
|
+
raw += chunk;
|
|
225
|
+
});
|
|
226
|
+
process.stdin.on("end", () => {
|
|
227
|
+
resolve(raw.split(/\r?\n/));
|
|
228
|
+
});
|
|
229
|
+
process.stdin.on("error", reject);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function createWizardPrompt() {
|
|
234
|
+
if (process.stdin.isTTY) {
|
|
235
|
+
return createInterface({
|
|
236
|
+
input: process.stdin,
|
|
237
|
+
output: process.stderr,
|
|
238
|
+
terminal: process.stderr.isTTY === true,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!sharedNonTtyAnswersPromise) {
|
|
243
|
+
sharedNonTtyAnswersPromise = readStdinLines();
|
|
244
|
+
}
|
|
245
|
+
const answers = await sharedNonTtyAnswersPromise;
|
|
246
|
+
return {
|
|
247
|
+
output: process.stderr,
|
|
248
|
+
async question(prompt) {
|
|
249
|
+
this.output.write(prompt);
|
|
250
|
+
const answer = answers[sharedNonTtyAnswerIndex] ?? "";
|
|
251
|
+
sharedNonTtyAnswerIndex += 1;
|
|
252
|
+
if (answer) {
|
|
253
|
+
this.output.write(`${answer}\n`);
|
|
254
|
+
} else {
|
|
255
|
+
this.output.write("\n");
|
|
256
|
+
}
|
|
257
|
+
return answer;
|
|
258
|
+
},
|
|
259
|
+
close() {},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function runWizard(args) {
|
|
264
|
+
const availableModes = listWecomQuickstartModes().map((item) => ({
|
|
265
|
+
id: item.id,
|
|
266
|
+
label: `${item.label} (${item.id})`,
|
|
267
|
+
summary: item.summary,
|
|
268
|
+
}));
|
|
269
|
+
const availableGroupProfiles = listWecomQuickstartGroupProfiles().map((item) => ({
|
|
270
|
+
id: item.id,
|
|
271
|
+
label: `${item.label} (${item.id})`,
|
|
272
|
+
summary: item.summary,
|
|
273
|
+
}));
|
|
274
|
+
const rl = await createWizardPrompt();
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
rl.output.write("WeCom quickstart wizard\n");
|
|
278
|
+
rl.output.write(`Use Enter to keep the default shown in brackets. You can still rerun ${WECOM_QUICKSTART_WIZARD_COMMAND} later.\n\n`);
|
|
279
|
+
|
|
280
|
+
const mode = await askSelect(rl, "Select mode", availableModes, args.mode);
|
|
281
|
+
const account = (await askQuestion(rl, "Account id", args.account || "default")).toLowerCase() || "default";
|
|
282
|
+
const dmMode = await askSelect(rl, "DM mode", DM_MODE_OPTIONS, args.dmMode);
|
|
283
|
+
const groupProfile = await askSelect(rl, "Group profile", availableGroupProfiles, args.groupProfile);
|
|
284
|
+
|
|
285
|
+
let groupChatId = args.groupChatId;
|
|
286
|
+
let groupAllow = args.groupAllow;
|
|
287
|
+
if (groupProfile.id === "allowlist_template") {
|
|
288
|
+
groupChatId = await askQuestion(rl, "Group chatId", args.groupChatId || "");
|
|
289
|
+
groupAllow = await askQuestion(
|
|
290
|
+
rl,
|
|
291
|
+
"Group allowlist (comma-separated member ids)",
|
|
292
|
+
args.groupAllow || "",
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const write = await askBoolean(rl, "Merge generated config into openclaw.json now", args.write === true);
|
|
297
|
+
let configPath = args.configPath;
|
|
298
|
+
if (write) {
|
|
299
|
+
configPath = await askQuestion(rl, "Target config path", args.configPath);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
...args,
|
|
304
|
+
wizard: true,
|
|
305
|
+
mode: mode.id,
|
|
306
|
+
account,
|
|
307
|
+
dmMode: dmMode.id,
|
|
308
|
+
groupProfile: groupProfile.id,
|
|
309
|
+
groupChatId,
|
|
310
|
+
groupAllow,
|
|
311
|
+
write,
|
|
312
|
+
configPath,
|
|
313
|
+
runChecks: args.runChecks === true,
|
|
314
|
+
forceChecks: args.forceChecks === true,
|
|
315
|
+
};
|
|
316
|
+
} finally {
|
|
317
|
+
rl.close();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function asObject(value) {
|
|
322
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function mergeDeep(base, patch) {
|
|
326
|
+
if (Array.isArray(patch)) return patch.slice();
|
|
327
|
+
if (!patch || typeof patch !== "object") return patch;
|
|
328
|
+
const out = { ...asObject(base) };
|
|
329
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
330
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
331
|
+
out[key] = mergeDeep(asObject(base?.[key]), value);
|
|
332
|
+
} else if (Array.isArray(value)) {
|
|
333
|
+
out[key] = value.slice();
|
|
334
|
+
} else {
|
|
335
|
+
out[key] = value;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return out;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function normalizeAccountId(accountId = "default") {
|
|
342
|
+
const normalized = String(accountId ?? "default").trim().toLowerCase();
|
|
343
|
+
return normalized || "default";
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function getNestedValue(value, dottedPath = "") {
|
|
347
|
+
if (!dottedPath) return value;
|
|
348
|
+
return dottedPath.split(".").reduce((current, segment) => {
|
|
349
|
+
if (!current || typeof current !== "object") return undefined;
|
|
350
|
+
return current[segment];
|
|
351
|
+
}, value);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function setNestedValue(target, dottedPath, value) {
|
|
355
|
+
const parts = String(dottedPath ?? "").split(".").filter(Boolean);
|
|
356
|
+
if (parts.length === 0) return target;
|
|
357
|
+
let cursor = target;
|
|
358
|
+
for (let index = 0; index < parts.length - 1; index += 1) {
|
|
359
|
+
const key = parts[index];
|
|
360
|
+
if (!cursor[key] || typeof cursor[key] !== "object" || Array.isArray(cursor[key])) {
|
|
361
|
+
cursor[key] = {};
|
|
362
|
+
}
|
|
363
|
+
cursor = cursor[key];
|
|
364
|
+
}
|
|
365
|
+
cursor[parts.at(-1)] = value;
|
|
366
|
+
return target;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function pickPathsFromObject(source, dottedPaths = []) {
|
|
370
|
+
const patch = {};
|
|
371
|
+
for (const dottedPath of dottedPaths) {
|
|
372
|
+
const value = getNestedValue(source, dottedPath);
|
|
373
|
+
if (value === undefined) continue;
|
|
374
|
+
setNestedValue(patch, dottedPath, value);
|
|
375
|
+
}
|
|
376
|
+
return patch;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function resolveTargetAccountConfig(starterConfig, accountId = "default") {
|
|
380
|
+
const normalizedAccountId = normalizeAccountId(accountId);
|
|
381
|
+
const channelConfig = asObject(starterConfig?.channels?.wecom);
|
|
382
|
+
if (normalizedAccountId === "default") return channelConfig;
|
|
383
|
+
return asObject(channelConfig?.accounts?.[normalizedAccountId]);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function buildScopedConfigPatch(accountId, patch) {
|
|
387
|
+
const normalizedAccountId = normalizeAccountId(accountId);
|
|
388
|
+
if (normalizedAccountId === "default") {
|
|
389
|
+
return {
|
|
390
|
+
path: "channels.wecom",
|
|
391
|
+
configPatch: {
|
|
392
|
+
channels: {
|
|
393
|
+
wecom: patch,
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
path: `channels.wecom.accounts.${normalizedAccountId}`,
|
|
400
|
+
configPatch: {
|
|
401
|
+
channels: {
|
|
402
|
+
wecom: {
|
|
403
|
+
accounts: {
|
|
404
|
+
[normalizedAccountId]: patch,
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const REPAIR_PATH_GROUPS = Object.freeze({
|
|
413
|
+
agentCore: Object.freeze([
|
|
414
|
+
"enabled",
|
|
415
|
+
"corpId",
|
|
416
|
+
"corpSecret",
|
|
417
|
+
"agentId",
|
|
418
|
+
"callbackToken",
|
|
419
|
+
"callbackAesKey",
|
|
420
|
+
"webhookPath",
|
|
421
|
+
]),
|
|
422
|
+
botWebhook: Object.freeze([
|
|
423
|
+
"enabled",
|
|
424
|
+
"bot.enabled",
|
|
425
|
+
"bot.token",
|
|
426
|
+
"bot.encodingAesKey",
|
|
427
|
+
"bot.webhookPath",
|
|
428
|
+
]),
|
|
429
|
+
botLongConnection: Object.freeze([
|
|
430
|
+
"enabled",
|
|
431
|
+
"bot.enabled",
|
|
432
|
+
"bot.longConnection.enabled",
|
|
433
|
+
"bot.longConnection.botId",
|
|
434
|
+
"bot.longConnection.secret",
|
|
435
|
+
"bot.longConnection.url",
|
|
436
|
+
]),
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
const REPAIR_VALUE_PRESERVE_PATHS = Object.freeze([
|
|
440
|
+
"corpId",
|
|
441
|
+
"corpSecret",
|
|
442
|
+
"agentId",
|
|
443
|
+
"callbackToken",
|
|
444
|
+
"callbackAesKey",
|
|
445
|
+
"webhookPath",
|
|
446
|
+
"bot.token",
|
|
447
|
+
"bot.encodingAesKey",
|
|
448
|
+
"bot.webhookPath",
|
|
449
|
+
"bot.longConnection.botId",
|
|
450
|
+
"bot.longConnection.secret",
|
|
451
|
+
"bot.longConnection.url",
|
|
452
|
+
]);
|
|
453
|
+
|
|
454
|
+
function buildBackupPath(configPath) {
|
|
455
|
+
return `${configPath}.bak-${Date.now()}`;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function loadConfig(configPath) {
|
|
459
|
+
try {
|
|
460
|
+
const raw = await readFile(configPath, "utf8");
|
|
461
|
+
return {
|
|
462
|
+
exists: true,
|
|
463
|
+
config: JSON.parse(raw),
|
|
464
|
+
};
|
|
465
|
+
} catch (err) {
|
|
466
|
+
if (err?.code === "ENOENT") {
|
|
467
|
+
return {
|
|
468
|
+
exists: false,
|
|
469
|
+
config: {},
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
throw err;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async function writeMergedConfig(configPath, patch) {
|
|
477
|
+
const resolvedPath = path.resolve(expandHome(configPath));
|
|
478
|
+
const loaded = await loadConfig(resolvedPath);
|
|
479
|
+
const backupPath = loaded.exists ? buildBackupPath(resolvedPath) : null;
|
|
480
|
+
const changedPaths = collectChangedPaths(loaded.config, patch);
|
|
481
|
+
const merged = mergeDeep(asObject(loaded.config), patch);
|
|
482
|
+
await mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
483
|
+
if (loaded.exists) {
|
|
484
|
+
await writeFile(backupPath, `${JSON.stringify(loaded.config, null, 2)}\n`, "utf8");
|
|
485
|
+
}
|
|
486
|
+
await writeFile(resolvedPath, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
|
|
487
|
+
return {
|
|
488
|
+
configPath: resolvedPath,
|
|
489
|
+
backupPath,
|
|
490
|
+
existed: loaded.exists,
|
|
491
|
+
mergedConfig: merged,
|
|
492
|
+
changedPaths,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function valuesEqual(left, right) {
|
|
497
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function collectChangedPaths(baseValue, patchValue, prefix = "", out = []) {
|
|
501
|
+
if (Array.isArray(patchValue)) {
|
|
502
|
+
if (!valuesEqual(baseValue, patchValue) && prefix) out.push(prefix);
|
|
503
|
+
return out;
|
|
504
|
+
}
|
|
505
|
+
if (!patchValue || typeof patchValue !== "object") {
|
|
506
|
+
if (!valuesEqual(baseValue, patchValue) && prefix) out.push(prefix);
|
|
507
|
+
return out;
|
|
508
|
+
}
|
|
509
|
+
for (const [key, value] of Object.entries(patchValue)) {
|
|
510
|
+
const nextPrefix = prefix ? `${prefix}.${key}` : key;
|
|
511
|
+
const nextBase = baseValue && typeof baseValue === "object" ? baseValue[key] : undefined;
|
|
512
|
+
collectChangedPaths(nextBase, value, nextPrefix, out);
|
|
513
|
+
}
|
|
514
|
+
return out;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function resolveCheckCommands(modeChecks = [], { accountId = "default", configPath = "" } = {}) {
|
|
518
|
+
const normalizedAccountId = String(accountId ?? "default").trim().toLowerCase() || "default";
|
|
519
|
+
const escapedConfigPath = JSON.stringify(String(configPath ?? ""));
|
|
520
|
+
return modeChecks.map((rawCommand) => {
|
|
521
|
+
let command = String(rawCommand ?? "").trim();
|
|
522
|
+
if (!command) return command;
|
|
523
|
+
if (/--account\s+\S+/.test(command)) {
|
|
524
|
+
command = command.replace(/--account\s+\S+/g, `--account ${normalizedAccountId}`);
|
|
525
|
+
}
|
|
526
|
+
if (/--config\s+\S+/.test(command)) {
|
|
527
|
+
command = command.replace(/--config\s+\S+/g, `--config ${escapedConfigPath}`);
|
|
528
|
+
} else if (/--\s/.test(command)) {
|
|
529
|
+
command = command.replace(/--\s/, `-- --config ${escapedConfigPath} `);
|
|
530
|
+
} else {
|
|
531
|
+
command = `${command} --config ${escapedConfigPath}`;
|
|
532
|
+
}
|
|
533
|
+
if (!/--json(?:\s|$)/.test(command)) {
|
|
534
|
+
command = `${command} --json`;
|
|
535
|
+
}
|
|
536
|
+
return command;
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function buildReport(args, currentConfig = {}) {
|
|
541
|
+
const availableModes = listWecomQuickstartModes();
|
|
542
|
+
const availableGroupProfiles = listWecomQuickstartGroupProfiles();
|
|
543
|
+
const selectedMode =
|
|
544
|
+
availableModes.find((item) => item.id === args.mode) ??
|
|
545
|
+
availableModes.find((item) => item.id === WECOM_QUICKSTART_RECOMMENDED_MODE);
|
|
546
|
+
if (!selectedMode) {
|
|
547
|
+
throw new Error("No quickstart modes available");
|
|
548
|
+
}
|
|
549
|
+
if (!DM_MODES.has(args.dmMode)) {
|
|
550
|
+
throw new Error(`Unsupported dm mode: ${args.dmMode}`);
|
|
551
|
+
}
|
|
552
|
+
const selectedGroupProfile =
|
|
553
|
+
availableGroupProfiles.find((item) => item.id === args.groupProfile) ??
|
|
554
|
+
availableGroupProfiles.find((item) => item.id === WECOM_QUICKSTART_DEFAULT_GROUP_PROFILE);
|
|
555
|
+
if (!selectedGroupProfile) {
|
|
556
|
+
throw new Error("No quickstart group profiles available");
|
|
557
|
+
}
|
|
558
|
+
const setupPlan = buildWecomQuickstartSetupPlan({
|
|
559
|
+
mode: selectedMode.id,
|
|
560
|
+
accountId: args.account,
|
|
561
|
+
dmMode: args.dmMode,
|
|
562
|
+
groupProfile: selectedGroupProfile.id,
|
|
563
|
+
groupChatId: args.groupChatId,
|
|
564
|
+
groupAllow: args.groupAllow,
|
|
565
|
+
currentConfig,
|
|
566
|
+
});
|
|
567
|
+
const preferredChecks =
|
|
568
|
+
Array.isArray(setupPlan.sourcePlaybook?.checkOrder) && setupPlan.sourcePlaybook.checkOrder.length > 0
|
|
569
|
+
? setupPlan.sourcePlaybook.checkOrder.map((item) => item.command).filter(Boolean)
|
|
570
|
+
: selectedMode.checks;
|
|
571
|
+
return {
|
|
572
|
+
mode: selectedMode,
|
|
573
|
+
checkCommands: resolveCheckCommands(preferredChecks, {
|
|
574
|
+
accountId: args.account,
|
|
575
|
+
configPath: path.resolve(expandHome(args.configPath)),
|
|
576
|
+
}),
|
|
577
|
+
groupProfile: selectedGroupProfile,
|
|
578
|
+
accountId: args.account,
|
|
579
|
+
dmMode: args.dmMode,
|
|
580
|
+
groupChatId: args.groupChatId,
|
|
581
|
+
groupAllow: String(args.groupAllow ?? "")
|
|
582
|
+
.split(/[,\n]/)
|
|
583
|
+
.map((item) => item.trim())
|
|
584
|
+
.filter(Boolean),
|
|
585
|
+
configPath: path.resolve(expandHome(args.configPath)),
|
|
586
|
+
wizard: {
|
|
587
|
+
used: args.wizard === true,
|
|
588
|
+
command: WECOM_QUICKSTART_WIZARD_COMMAND,
|
|
589
|
+
},
|
|
590
|
+
commands: setupPlan.commands,
|
|
591
|
+
installState: setupPlan.installState,
|
|
592
|
+
installStateSummary: setupPlan.installStateSummary,
|
|
593
|
+
migrationState: setupPlan.migrationState,
|
|
594
|
+
migrationStateSummary: setupPlan.migrationStateSummary,
|
|
595
|
+
migrationSource: setupPlan.migrationSource,
|
|
596
|
+
migrationSourceSummary: setupPlan.migrationSourceSummary,
|
|
597
|
+
migrationSourceSignals: setupPlan.migrationSourceSignals,
|
|
598
|
+
detectedLegacyFields: setupPlan.detectedLegacyFields,
|
|
599
|
+
migration: setupPlan.migration,
|
|
600
|
+
sourcePlaybook: setupPlan.sourcePlaybook,
|
|
601
|
+
actions: setupPlan.actions,
|
|
602
|
+
runChecks: {
|
|
603
|
+
requested: args.runChecks === true,
|
|
604
|
+
forced: args.forceChecks === true,
|
|
605
|
+
applyRepair: args.applyRepair === true,
|
|
606
|
+
confirmRepair: args.confirmRepair === true,
|
|
607
|
+
command: WECOM_QUICKSTART_RUN_CHECKS_COMMAND,
|
|
608
|
+
forceCommand: WECOM_QUICKSTART_FORCE_CHECKS_COMMAND,
|
|
609
|
+
repairDir: args.repairDir ? path.resolve(expandHome(args.repairDir)) : "",
|
|
610
|
+
timeoutMs: args.checkTimeoutMs,
|
|
611
|
+
},
|
|
612
|
+
starterConfig: setupPlan.starterConfig,
|
|
613
|
+
placeholders: setupPlan.placeholders,
|
|
614
|
+
setupChecklist: setupPlan.checklist,
|
|
615
|
+
warnings: setupPlan.warnings,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function printTextReport(report, writeResult = null) {
|
|
620
|
+
const runChecksLabel = report.runChecks?.requested
|
|
621
|
+
? `${report.runChecks.forced ? "forced" : "requested"}${report.runChecks.applyRepair ? report.runChecks.confirmRepair ? " + confirmRepair" : " + applyRepair" : ""}`
|
|
622
|
+
: "off";
|
|
623
|
+
console.log("WeCom quickstart");
|
|
624
|
+
console.log(`- mode: ${report.mode.label} (${report.mode.id})${report.mode.recommended ? " [recommended]" : ""}`);
|
|
625
|
+
console.log(`- account: ${report.accountId}`);
|
|
626
|
+
console.log(`- dmMode: ${report.dmMode}`);
|
|
627
|
+
console.log(`- groupProfile: ${report.groupProfile.label} (${report.groupProfile.id})`);
|
|
628
|
+
if (report.wizard?.used) {
|
|
629
|
+
console.log("- wizard: interactive");
|
|
630
|
+
}
|
|
631
|
+
if (report.groupChatId) {
|
|
632
|
+
console.log(`- groupChatId: ${report.groupChatId}`);
|
|
633
|
+
}
|
|
634
|
+
if (Array.isArray(report.groupAllow) && report.groupAllow.length > 0) {
|
|
635
|
+
console.log(`- groupAllow: ${report.groupAllow.join(", ")}`);
|
|
636
|
+
}
|
|
637
|
+
console.log(`- publicWebhook: ${report.mode.requiresPublicWebhook ? "required" : "not required"}`);
|
|
638
|
+
console.log(`- docsAnchor: ${report.mode.docsAnchor}`);
|
|
639
|
+
console.log(`- summary: ${report.mode.summary}`);
|
|
640
|
+
console.log(`- groupSummary: ${report.groupProfile.summary}`);
|
|
641
|
+
console.log(`- installState: ${report.installState}`);
|
|
642
|
+
console.log(`- migrationState: ${report.migrationState}`);
|
|
643
|
+
console.log(`- migrationSource: ${report.migrationSource}`);
|
|
644
|
+
console.log(`- sourceSummary: ${report.migrationSourceSummary}`);
|
|
645
|
+
if (report.sourcePlaybook?.repairDefaults) {
|
|
646
|
+
console.log(
|
|
647
|
+
`- repairDefaults: doctorFix=${report.sourcePlaybook.repairDefaults.doctorFixMode}, preserveNetworkCompatibility=${
|
|
648
|
+
report.sourcePlaybook.repairDefaults.preserveNetworkCompatibility ? "yes" : "no"
|
|
649
|
+
}, removeLegacyFieldAliases=${report.sourcePlaybook.repairDefaults.removeLegacyFieldAliases ? "yes" : "no"}`,
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
console.log(`- placeholders: ${report.placeholders.length}`);
|
|
653
|
+
console.log(`- runChecks: ${runChecksLabel}`);
|
|
654
|
+
if (report.commands?.preview) {
|
|
655
|
+
console.log(`- previewCommand: ${report.commands.preview}`);
|
|
656
|
+
}
|
|
657
|
+
if (report.runChecks?.command) {
|
|
658
|
+
console.log(`- runChecksCommand: ${report.runChecks.command}`);
|
|
659
|
+
}
|
|
660
|
+
if (report.runChecks?.forceCommand) {
|
|
661
|
+
console.log(`- forceChecksCommand: ${report.runChecks.forceCommand}`);
|
|
662
|
+
}
|
|
663
|
+
if (report.commands?.applyRepair) {
|
|
664
|
+
console.log(`- applyRepairCommand: ${report.commands.applyRepair}`);
|
|
665
|
+
}
|
|
666
|
+
if (report.commands?.confirmRepair) {
|
|
667
|
+
console.log(`- confirmRepairCommand: ${report.commands.confirmRepair}`);
|
|
668
|
+
}
|
|
669
|
+
if (report.commands?.migrate) {
|
|
670
|
+
console.log(`- migrateCommand: ${report.commands.migrate}`);
|
|
671
|
+
}
|
|
672
|
+
if (report.runChecks?.repairDir) {
|
|
673
|
+
console.log(`- repairDir: ${report.runChecks.repairDir}`);
|
|
674
|
+
}
|
|
675
|
+
if (report.wizard?.command) {
|
|
676
|
+
console.log(`- wizardCommand: ${report.wizard.command}`);
|
|
677
|
+
}
|
|
678
|
+
if (report.commands?.write) {
|
|
679
|
+
console.log(`- writeCommand: ${report.commands.write}`);
|
|
680
|
+
}
|
|
681
|
+
if (writeResult) {
|
|
682
|
+
console.log(
|
|
683
|
+
`- write: merged into ${writeResult.configPath}${writeResult.backupPath ? ` (backup: ${writeResult.backupPath})` : " (new file)"}`,
|
|
684
|
+
);
|
|
685
|
+
} else {
|
|
686
|
+
console.log("- write: not applied (use --write to merge into openclaw.json)");
|
|
687
|
+
}
|
|
688
|
+
console.log("- checks:");
|
|
689
|
+
for (const check of report.checkCommands) {
|
|
690
|
+
console.log(` - ${check}`);
|
|
691
|
+
}
|
|
692
|
+
console.log("- requiredConfigPaths:");
|
|
693
|
+
for (const item of report.mode.requiredConfigPaths) {
|
|
694
|
+
console.log(` - ${item}`);
|
|
695
|
+
}
|
|
696
|
+
if (Array.isArray(report.mode.notes) && report.mode.notes.length > 0) {
|
|
697
|
+
console.log("- notes:");
|
|
698
|
+
for (const note of report.mode.notes) {
|
|
699
|
+
console.log(` - ${note}`);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (Array.isArray(report.groupProfile.notes) && report.groupProfile.notes.length > 0) {
|
|
703
|
+
console.log("- groupNotes:");
|
|
704
|
+
for (const note of report.groupProfile.notes) {
|
|
705
|
+
console.log(` - ${note}`);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
if (Array.isArray(report.warnings) && report.warnings.length > 0) {
|
|
709
|
+
console.log("- warnings:");
|
|
710
|
+
for (const warning of report.warnings) {
|
|
711
|
+
console.log(` - ${warning}`);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
if (Array.isArray(report.sourcePlaybook?.notes) && report.sourcePlaybook.notes.length > 0) {
|
|
715
|
+
console.log("- sourceNotes:");
|
|
716
|
+
for (const note of report.sourcePlaybook.notes) {
|
|
717
|
+
console.log(` - ${note}`);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
if (Array.isArray(report.placeholders) && report.placeholders.length > 0) {
|
|
721
|
+
console.log("- placeholders:");
|
|
722
|
+
for (const placeholder of report.placeholders) {
|
|
723
|
+
console.log(` - ${placeholder.path}: ${placeholder.action}`);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
if (Array.isArray(report.sourcePlaybook?.checkOrder) && report.sourcePlaybook.checkOrder.length > 0) {
|
|
727
|
+
console.log("- preferredChecks:");
|
|
728
|
+
for (const item of report.sourcePlaybook.checkOrder) {
|
|
729
|
+
console.log(` - ${item.title}: ${item.detail}`);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
if (Array.isArray(report.setupChecklist) && report.setupChecklist.length > 0) {
|
|
733
|
+
console.log("- setupChecklist:");
|
|
734
|
+
for (const item of report.setupChecklist) {
|
|
735
|
+
console.log(` - [${item.kind}] ${item.title}: ${item.detail}`);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
if (Array.isArray(report.detectedLegacyFields) && report.detectedLegacyFields.length > 0) {
|
|
739
|
+
console.log("- detectedLegacyFields:");
|
|
740
|
+
for (const item of report.detectedLegacyFields) {
|
|
741
|
+
console.log(` - ${item.path}: ${item.detail}`);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
if (Array.isArray(report.migrationSourceSignals) && report.migrationSourceSignals.length > 0) {
|
|
745
|
+
console.log("- migrationSourceSignals:");
|
|
746
|
+
for (const item of report.migrationSourceSignals) {
|
|
747
|
+
console.log(` - [${item.source}] ${item.path}: ${item.detail}`);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
if (Array.isArray(report.actions) && report.actions.length > 0) {
|
|
751
|
+
console.log("- actions:");
|
|
752
|
+
for (const action of report.actions) {
|
|
753
|
+
const flags = [
|
|
754
|
+
action.recommended ? "recommended" : "",
|
|
755
|
+
action.blocking ? "blocking" : "",
|
|
756
|
+
].filter(Boolean);
|
|
757
|
+
console.log(` - [${action.kind}] ${action.title}: ${action.detail}${flags.length ? ` (${flags.join(", ")})` : ""}`);
|
|
758
|
+
if (action.command) {
|
|
759
|
+
console.log(` command: ${action.command}`);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
console.log("\nStarter config:");
|
|
764
|
+
console.log(JSON.stringify(report.starterConfig, null, 2));
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function printPostcheckSummary(postcheck) {
|
|
768
|
+
console.log("\nPostcheck:");
|
|
769
|
+
if (!postcheck?.requested) {
|
|
770
|
+
console.log("- status: not requested");
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
if (postcheck?.blockedByPlaceholders) {
|
|
774
|
+
console.log("- status: blocked by placeholders");
|
|
775
|
+
for (const item of postcheck.blockingPlaceholders ?? []) {
|
|
776
|
+
console.log(` - ${item}`);
|
|
777
|
+
}
|
|
778
|
+
if (Array.isArray(postcheck.remediation) && postcheck.remediation.length > 0) {
|
|
779
|
+
console.log("- remediation:");
|
|
780
|
+
for (const item of postcheck.remediation) {
|
|
781
|
+
console.log(` - ${item.title}: ${item.detail}`);
|
|
782
|
+
if (item.command) {
|
|
783
|
+
console.log(` command: ${item.command}`);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
printRepairArtifacts(postcheck.repairArtifacts);
|
|
788
|
+
printRepairPlan(postcheck.repairPlan);
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
console.log(
|
|
792
|
+
`- summary: ${postcheck.summary?.passed ?? 0}/${postcheck.summary?.total ?? 0} passed${postcheck.usedTempConfig ? " (temp config)" : ""}`,
|
|
793
|
+
);
|
|
794
|
+
for (const check of postcheck.checks ?? []) {
|
|
795
|
+
console.log(`${check.ok ? "OK " : "FAIL"} ${check.command} :: ${check.summary}`);
|
|
796
|
+
}
|
|
797
|
+
if (Array.isArray(postcheck.remediation) && postcheck.remediation.length > 0) {
|
|
798
|
+
console.log("- remediation:");
|
|
799
|
+
for (const item of postcheck.remediation) {
|
|
800
|
+
console.log(` - ${item.title}: ${item.detail}`);
|
|
801
|
+
if (item.command) {
|
|
802
|
+
console.log(` command: ${item.command}`);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
printRepairArtifacts(postcheck.repairArtifacts);
|
|
807
|
+
printRepairPlan(postcheck.repairPlan);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function printRepairArtifacts(repairArtifacts) {
|
|
811
|
+
if (!repairArtifacts) return;
|
|
812
|
+
console.log("- repairArtifacts:");
|
|
813
|
+
if (Array.isArray(repairArtifacts.groups) && repairArtifacts.groups.length > 0) {
|
|
814
|
+
console.log(` - groups: ${repairArtifacts.groups.join(", ")}`);
|
|
815
|
+
}
|
|
816
|
+
if (repairArtifacts.configPath) {
|
|
817
|
+
console.log(` - configPath: ${repairArtifacts.configPath}`);
|
|
818
|
+
}
|
|
819
|
+
if (repairArtifacts.accountPatch && Object.keys(repairArtifacts.accountPatch).length > 0) {
|
|
820
|
+
console.log(" - accountPatch:");
|
|
821
|
+
for (const line of JSON.stringify(repairArtifacts.accountPatch, null, 2).split("\n")) {
|
|
822
|
+
console.log(` ${line}`);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
if (repairArtifacts.configPatch && Object.keys(repairArtifacts.configPatch).length > 0) {
|
|
826
|
+
console.log(" - configPatch:");
|
|
827
|
+
for (const line of JSON.stringify(repairArtifacts.configPatch, null, 2).split("\n")) {
|
|
828
|
+
console.log(` ${line}`);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
if (Array.isArray(repairArtifacts.envTemplate?.lines) && repairArtifacts.envTemplate.lines.length > 0) {
|
|
832
|
+
console.log(` - envTemplate (${repairArtifacts.envTemplate.format || "dotenv"}):`);
|
|
833
|
+
for (const line of repairArtifacts.envTemplate.lines) {
|
|
834
|
+
console.log(` ${line}`);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
if (Array.isArray(repairArtifacts.notes) && repairArtifacts.notes.length > 0) {
|
|
838
|
+
console.log(" - notes:");
|
|
839
|
+
for (const note of repairArtifacts.notes) {
|
|
840
|
+
console.log(` ${note}`);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
if (repairArtifacts.files) {
|
|
844
|
+
console.log(" - files:");
|
|
845
|
+
if (repairArtifacts.files.directory) {
|
|
846
|
+
console.log(` directory: ${repairArtifacts.files.directory}`);
|
|
847
|
+
}
|
|
848
|
+
if (repairArtifacts.files.configPatchFile) {
|
|
849
|
+
console.log(` configPatchFile: ${repairArtifacts.files.configPatchFile}`);
|
|
850
|
+
}
|
|
851
|
+
if (repairArtifacts.files.accountPatchFile) {
|
|
852
|
+
console.log(` accountPatchFile: ${repairArtifacts.files.accountPatchFile}`);
|
|
853
|
+
}
|
|
854
|
+
if (repairArtifacts.files.envTemplateFile) {
|
|
855
|
+
console.log(` envTemplateFile: ${repairArtifacts.files.envTemplateFile}`);
|
|
856
|
+
}
|
|
857
|
+
if (repairArtifacts.files.notesFile) {
|
|
858
|
+
console.log(` notesFile: ${repairArtifacts.files.notesFile}`);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function printRepairPlan(repairPlan) {
|
|
864
|
+
if (!repairPlan) return;
|
|
865
|
+
console.log("- repairPlan:");
|
|
866
|
+
console.log(` - summary: ${repairPlan.summary}`);
|
|
867
|
+
console.log(` - requiresConfirmation: ${repairPlan.requiresConfirmation ? "yes" : "no"}`);
|
|
868
|
+
if (Array.isArray(repairPlan.changes) && repairPlan.changes.length > 0) {
|
|
869
|
+
console.log(" - changes:");
|
|
870
|
+
for (const change of repairPlan.changes) {
|
|
871
|
+
console.log(` ${change.path} = ${formatRepairPreviewValue(change.value)}`);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
if (Array.isArray(repairPlan.envChanges) && repairPlan.envChanges.length > 0) {
|
|
875
|
+
console.log(" - envChanges:");
|
|
876
|
+
for (const change of repairPlan.envChanges) {
|
|
877
|
+
console.log(` ${change.line}`);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
if (Array.isArray(repairPlan.fileWrites) && repairPlan.fileWrites.length > 0) {
|
|
881
|
+
console.log(" - fileWrites:");
|
|
882
|
+
for (const fileWrite of repairPlan.fileWrites) {
|
|
883
|
+
console.log(` [${fileWrite.kind}] ${fileWrite.path}`);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function getStubbedPostcheckResult(report) {
|
|
889
|
+
const raw = String(process.env.WECOM_QUICKSTART_CHECKS_STUB_JSON ?? "").trim();
|
|
890
|
+
if (!raw) return null;
|
|
891
|
+
const parsed = JSON.parse(raw);
|
|
892
|
+
const checks = Array.isArray(parsed?.checks) ? parsed.checks : [];
|
|
893
|
+
return {
|
|
894
|
+
requested: true,
|
|
895
|
+
forced: report.runChecks?.forced === true,
|
|
896
|
+
executed: checks.length,
|
|
897
|
+
skipped: 0,
|
|
898
|
+
blockedByPlaceholders: false,
|
|
899
|
+
usedTempConfig: false,
|
|
900
|
+
configPath: report.configPath,
|
|
901
|
+
checks,
|
|
902
|
+
summary: parsed?.summary ?? summarizeCheckResults(checks),
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
async function buildCheckConfigPath(args, report) {
|
|
907
|
+
if (args.write === true) {
|
|
908
|
+
return {
|
|
909
|
+
configPath: report.configPath,
|
|
910
|
+
tempDir: null,
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), "wecom-quickstart-checks-"));
|
|
914
|
+
const configPath = path.join(tempDir, "openclaw.json");
|
|
915
|
+
const loaded = await loadConfig(report.configPath);
|
|
916
|
+
const merged = mergeDeep(asObject(loaded.config), report.starterConfig);
|
|
917
|
+
await writeFile(configPath, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
|
|
918
|
+
return {
|
|
919
|
+
configPath,
|
|
920
|
+
tempDir,
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
async function runShellCommand(command, timeoutMs) {
|
|
925
|
+
return new Promise((resolve) => {
|
|
926
|
+
const child = spawn(process.env.SHELL || "/bin/sh", ["-lc", command], {
|
|
927
|
+
cwd: process.cwd(),
|
|
928
|
+
env: { ...process.env },
|
|
929
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
930
|
+
});
|
|
931
|
+
let stdout = "";
|
|
932
|
+
let stderr = "";
|
|
933
|
+
let settled = false;
|
|
934
|
+
const timer = setTimeout(() => {
|
|
935
|
+
if (settled) return;
|
|
936
|
+
settled = true;
|
|
937
|
+
child.kill("SIGTERM");
|
|
938
|
+
resolve({
|
|
939
|
+
ok: false,
|
|
940
|
+
exitCode: -1,
|
|
941
|
+
timedOut: true,
|
|
942
|
+
stdout,
|
|
943
|
+
stderr,
|
|
944
|
+
});
|
|
945
|
+
}, timeoutMs);
|
|
946
|
+
child.stdout.on("data", (chunk) => {
|
|
947
|
+
stdout += chunk.toString();
|
|
948
|
+
});
|
|
949
|
+
child.stderr.on("data", (chunk) => {
|
|
950
|
+
stderr += chunk.toString();
|
|
951
|
+
});
|
|
952
|
+
child.on("close", (code) => {
|
|
953
|
+
if (settled) return;
|
|
954
|
+
settled = true;
|
|
955
|
+
clearTimeout(timer);
|
|
956
|
+
resolve({
|
|
957
|
+
ok: code === 0,
|
|
958
|
+
exitCode: Number.isInteger(code) ? code : -1,
|
|
959
|
+
timedOut: false,
|
|
960
|
+
stdout,
|
|
961
|
+
stderr,
|
|
962
|
+
});
|
|
963
|
+
});
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function parseCheckCommandJson(stdout = "") {
|
|
968
|
+
const trimmed = String(stdout ?? "").trim();
|
|
969
|
+
if (!trimmed) return null;
|
|
970
|
+
try {
|
|
971
|
+
return JSON.parse(trimmed);
|
|
972
|
+
} catch {}
|
|
973
|
+
const lines = trimmed.split(/\r?\n/).map((item) => item.trim()).filter(Boolean);
|
|
974
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
975
|
+
try {
|
|
976
|
+
return JSON.parse(lines[index]);
|
|
977
|
+
} catch {}
|
|
978
|
+
}
|
|
979
|
+
return null;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function extractFailedChecks(parsed = null) {
|
|
983
|
+
if (!parsed || typeof parsed !== "object") return [];
|
|
984
|
+
const candidates = [];
|
|
985
|
+
if (Array.isArray(parsed.checks)) {
|
|
986
|
+
candidates.push(...parsed.checks);
|
|
987
|
+
}
|
|
988
|
+
if (Array.isArray(parsed.accounts)) {
|
|
989
|
+
for (const account of parsed.accounts) {
|
|
990
|
+
if (Array.isArray(account?.checks)) {
|
|
991
|
+
candidates.push(...account.checks);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
return candidates
|
|
996
|
+
.filter((item) => item && typeof item === "object" && item.ok === false)
|
|
997
|
+
.map((item) => ({
|
|
998
|
+
name: String(item.name ?? "").trim(),
|
|
999
|
+
detail: String(item.detail ?? "").trim(),
|
|
1000
|
+
data: item.data ?? null,
|
|
1001
|
+
}));
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function buildCheckResult(command, execution) {
|
|
1005
|
+
const parsed = parseCheckCommandJson(execution.stdout);
|
|
1006
|
+
const summaryText =
|
|
1007
|
+
typeof parsed?.summary === "string"
|
|
1008
|
+
? parsed.summary
|
|
1009
|
+
: parsed?.summary && typeof parsed.summary === "object" && Number.isFinite(parsed.summary?.total)
|
|
1010
|
+
? `${parsed.summary.passed}/${parsed.summary.total} passed`
|
|
1011
|
+
: typeof parsed?.diagnosis?.summary === "string"
|
|
1012
|
+
? parsed.diagnosis.summary
|
|
1013
|
+
: execution.timedOut
|
|
1014
|
+
? "timed out"
|
|
1015
|
+
: execution.ok
|
|
1016
|
+
? "ok"
|
|
1017
|
+
: "failed";
|
|
1018
|
+
return {
|
|
1019
|
+
command,
|
|
1020
|
+
ok: execution.ok,
|
|
1021
|
+
exitCode: execution.exitCode,
|
|
1022
|
+
timedOut: execution.timedOut,
|
|
1023
|
+
diagnosisCode: String(parsed?.diagnosis?.code ?? "").trim() || undefined,
|
|
1024
|
+
summary: summaryText,
|
|
1025
|
+
stdoutPreview: String(execution.stdout ?? "").trim().slice(0, 400),
|
|
1026
|
+
stderrPreview: String(execution.stderr ?? "").trim().slice(0, 400),
|
|
1027
|
+
failedChecks: extractFailedChecks(parsed),
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
async function runRecommendedChecks({ args, report }) {
|
|
1032
|
+
const stubbed = getStubbedPostcheckResult(report);
|
|
1033
|
+
if (stubbed) return stubbed;
|
|
1034
|
+
if (report.runChecks?.requested !== true) {
|
|
1035
|
+
return {
|
|
1036
|
+
requested: false,
|
|
1037
|
+
forced: false,
|
|
1038
|
+
executed: 0,
|
|
1039
|
+
skipped: report.checkCommands.length,
|
|
1040
|
+
blockedByPlaceholders: false,
|
|
1041
|
+
usedTempConfig: false,
|
|
1042
|
+
configPath: report.configPath,
|
|
1043
|
+
checks: [],
|
|
1044
|
+
summary: summarizeCheckResults([]),
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
if (report.placeholders.length > 0 && report.runChecks?.forced !== true) {
|
|
1048
|
+
return {
|
|
1049
|
+
requested: true,
|
|
1050
|
+
forced: false,
|
|
1051
|
+
executed: 0,
|
|
1052
|
+
skipped: report.checkCommands.length,
|
|
1053
|
+
blockedByPlaceholders: true,
|
|
1054
|
+
blockingPlaceholders: report.placeholders.map((item) => item.path),
|
|
1055
|
+
usedTempConfig: false,
|
|
1056
|
+
configPath: report.configPath,
|
|
1057
|
+
checks: [],
|
|
1058
|
+
summary: {
|
|
1059
|
+
ok: false,
|
|
1060
|
+
total: 0,
|
|
1061
|
+
passed: 0,
|
|
1062
|
+
failed: 0,
|
|
1063
|
+
},
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
const prepared = await buildCheckConfigPath(args, report);
|
|
1068
|
+
const checks = [];
|
|
1069
|
+
try {
|
|
1070
|
+
const commands = resolveCheckCommands(report.mode.checks, {
|
|
1071
|
+
accountId: report.accountId,
|
|
1072
|
+
configPath: prepared.configPath,
|
|
1073
|
+
});
|
|
1074
|
+
for (const command of commands) {
|
|
1075
|
+
// Keep execution order deterministic for easier diagnosis.
|
|
1076
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1077
|
+
const execution = await runShellCommand(command, report.runChecks?.timeoutMs || 120000);
|
|
1078
|
+
checks.push(buildCheckResult(command, execution));
|
|
1079
|
+
}
|
|
1080
|
+
} finally {
|
|
1081
|
+
if (prepared.tempDir) {
|
|
1082
|
+
await rm(prepared.tempDir, { recursive: true, force: true });
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
return {
|
|
1087
|
+
requested: true,
|
|
1088
|
+
forced: report.runChecks?.forced === true,
|
|
1089
|
+
executed: checks.length,
|
|
1090
|
+
skipped: 0,
|
|
1091
|
+
blockedByPlaceholders: false,
|
|
1092
|
+
usedTempConfig: Boolean(prepared.tempDir),
|
|
1093
|
+
configPath: prepared.configPath,
|
|
1094
|
+
checks,
|
|
1095
|
+
summary: summarizeCheckResults(checks),
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function buildPostcheckRemediation(postcheck, report) {
|
|
1100
|
+
const fixes = [];
|
|
1101
|
+
if (!postcheck?.requested) return fixes;
|
|
1102
|
+
|
|
1103
|
+
if (postcheck?.blockedByPlaceholders) {
|
|
1104
|
+
fixes.push(
|
|
1105
|
+
buildRemediationItem(
|
|
1106
|
+
"fill-placeholders",
|
|
1107
|
+
"先替换模板占位项",
|
|
1108
|
+
"当前 starter config 里还有占位字段,先补齐 CorpId / Secret / Token / AES Key 等真实值,再重新运行推荐检查。",
|
|
1109
|
+
report.runChecks?.forceCommand,
|
|
1110
|
+
),
|
|
1111
|
+
);
|
|
1112
|
+
return fixes;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
for (const check of postcheck.checks ?? []) {
|
|
1116
|
+
const command = check?.command || "";
|
|
1117
|
+
const failedChecks = Array.isArray(check?.failedChecks) ? check.failedChecks : [];
|
|
1118
|
+
for (const failed of failedChecks) {
|
|
1119
|
+
const name = String(failed?.name ?? "").trim();
|
|
1120
|
+
const detail = String(failed?.detail ?? "").trim();
|
|
1121
|
+
if (name === "config.load") {
|
|
1122
|
+
fixes.push(
|
|
1123
|
+
buildRemediationItem(
|
|
1124
|
+
"config-load",
|
|
1125
|
+
"修正配置文件路径或 JSON 语法",
|
|
1126
|
+
"quickstart 在读取 openclaw.json 时失败。确认 --config 指向正确文件,且 JSON 结构可解析。",
|
|
1127
|
+
command,
|
|
1128
|
+
),
|
|
1129
|
+
);
|
|
1130
|
+
} else if (name === "config.account") {
|
|
1131
|
+
fixes.push(
|
|
1132
|
+
buildRemediationItem(
|
|
1133
|
+
"config-account",
|
|
1134
|
+
"确认目标账号真实存在",
|
|
1135
|
+
"当前 accountId 没有在 channels.wecom 顶层或 accounts.<id> 下解析成功。确认账号 ID 和配置层级一致。",
|
|
1136
|
+
command,
|
|
1137
|
+
),
|
|
1138
|
+
);
|
|
1139
|
+
} else if (
|
|
1140
|
+
[
|
|
1141
|
+
"config.callbackToken",
|
|
1142
|
+
"config.callbackAesKey",
|
|
1143
|
+
"config.callbackAesKey.length",
|
|
1144
|
+
"config.bot.token",
|
|
1145
|
+
"config.bot.encodingAesKey",
|
|
1146
|
+
"config.bot.encodingAesKey.length",
|
|
1147
|
+
].includes(name)
|
|
1148
|
+
) {
|
|
1149
|
+
fixes.push(
|
|
1150
|
+
buildRemediationItem(
|
|
1151
|
+
"callback-secrets",
|
|
1152
|
+
"补齐企业微信回调密钥",
|
|
1153
|
+
"企业微信 Token / EncodingAESKey 仍缺失或长度不正确。请从企业微信后台复制真实回调密钥,而不是保留模板值。",
|
|
1154
|
+
command,
|
|
1155
|
+
),
|
|
1156
|
+
);
|
|
1157
|
+
} else if (["config.webhookPath", "config.bot.webhookPath"].includes(name)) {
|
|
1158
|
+
fixes.push(
|
|
1159
|
+
buildRemediationItem(
|
|
1160
|
+
"webhook-path",
|
|
1161
|
+
"确认 webhookPath 与企业微信后台一致",
|
|
1162
|
+
"当前 webhookPath 缺失或未命中。确认 openclaw.json、反向代理和企业微信后台填写的是同一路径。",
|
|
1163
|
+
command,
|
|
1164
|
+
),
|
|
1165
|
+
);
|
|
1166
|
+
} else if (name === "network.gettoken") {
|
|
1167
|
+
fixes.push(
|
|
1168
|
+
buildRemediationItem(
|
|
1169
|
+
"access-token",
|
|
1170
|
+
"检查 CorpId / Secret、代理与可信 IP",
|
|
1171
|
+
"AccessToken 获取失败。优先检查 corpId/corpSecret、出网代理,以及企业微信后台是否已放行当前出口 IP。",
|
|
1172
|
+
command,
|
|
1173
|
+
),
|
|
1174
|
+
);
|
|
1175
|
+
} else if (["local.webhook.health", "e2e.health.get", "e2e.health.get.legacyAlias"].includes(name)) {
|
|
1176
|
+
const reason = detail.toLowerCase();
|
|
1177
|
+
const title =
|
|
1178
|
+
reason.includes("gateway-auth") || reason.includes("redirect")
|
|
1179
|
+
? "移除 webhook 路径上的鉴权或跳转"
|
|
1180
|
+
: "修正 webhook 反代与路由";
|
|
1181
|
+
fixes.push(
|
|
1182
|
+
buildRemediationItem(
|
|
1183
|
+
"webhook-health",
|
|
1184
|
+
title,
|
|
1185
|
+
"回调探测没有直达 OpenClaw webhook。请检查反向代理、Zero Trust / SSO、路径重写,以及是否误回到了 WebUI HTML。",
|
|
1186
|
+
command,
|
|
1187
|
+
),
|
|
1188
|
+
);
|
|
1189
|
+
} else if (["e2e.url.verify", "e2e.url.verify.legacyAlias"].includes(name)) {
|
|
1190
|
+
fixes.push(
|
|
1191
|
+
buildRemediationItem(
|
|
1192
|
+
"url-verify",
|
|
1193
|
+
"修正 Token / AES Key 与回调 URL 验证",
|
|
1194
|
+
"企业微信 URL 验证失败。确认企业微信后台里的 Token / EncodingAESKey 与当前配置完全一致,并确保回调地址可被公网访问。",
|
|
1195
|
+
command,
|
|
1196
|
+
),
|
|
1197
|
+
);
|
|
1198
|
+
} else if (["e2e.message.post", "e2e.message.response.stream", "e2e.stream.refresh"].includes(name)) {
|
|
1199
|
+
fixes.push(
|
|
1200
|
+
buildRemediationItem(
|
|
1201
|
+
"message-delivery",
|
|
1202
|
+
"检查消息回包链路",
|
|
1203
|
+
"请求已发出但消息投递链路没有闭环。检查 gateway 是否在线、插件是否加载、Bot/Agent 回调是否注册,以及长连接或 response_url 是否可用。",
|
|
1204
|
+
command,
|
|
1205
|
+
),
|
|
1206
|
+
);
|
|
1207
|
+
} else if (name === "config.enabled" || name === "config.bot.enabled") {
|
|
1208
|
+
fixes.push(
|
|
1209
|
+
buildRemediationItem(
|
|
1210
|
+
"channel-disabled",
|
|
1211
|
+
"启用对应账号或 Bot 模式",
|
|
1212
|
+
"当前账号/机器人在配置里仍是 disabled。先启用它,再重新运行推荐检查。",
|
|
1213
|
+
command,
|
|
1214
|
+
),
|
|
1215
|
+
);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
if (check?.timedOut === true) {
|
|
1220
|
+
fixes.push(
|
|
1221
|
+
buildRemediationItem(
|
|
1222
|
+
"check-timeout",
|
|
1223
|
+
"放宽检查超时或先单独跑目标自检",
|
|
1224
|
+
"某条推荐检查超时。先确认 gateway 在线,再根据链路情况提高 --check-timeout-ms,或单独执行对应 selfcheck 看完整日志。",
|
|
1225
|
+
command,
|
|
1226
|
+
),
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
if (check?.diagnosisCode === "proxy-blocked") {
|
|
1231
|
+
fixes.push(
|
|
1232
|
+
buildRemediationItem(
|
|
1233
|
+
"proxy-blocked",
|
|
1234
|
+
"更换支持 WebSocket 的代理",
|
|
1235
|
+
"长连接探针显示代理链路拦截了 WebSocket Upgrade。为长连接禁用该代理,或改用支持 CONNECT/WebSocket 的代理。",
|
|
1236
|
+
command,
|
|
1237
|
+
),
|
|
1238
|
+
);
|
|
1239
|
+
} else if (check?.diagnosisCode === "direct-network-blocked") {
|
|
1240
|
+
fixes.push(
|
|
1241
|
+
buildRemediationItem(
|
|
1242
|
+
"direct-network",
|
|
1243
|
+
"保留代理并检查本机直连出网",
|
|
1244
|
+
"长连接探针显示直连失败但代理可用。优先保留代理,再检查本机到企业微信入口的防火墙、DNS 和直连出网策略。",
|
|
1245
|
+
command,
|
|
1246
|
+
),
|
|
1247
|
+
);
|
|
1248
|
+
} else if (check?.diagnosisCode === "endpoint-unavailable") {
|
|
1249
|
+
fixes.push(
|
|
1250
|
+
buildRemediationItem(
|
|
1251
|
+
"longconn-endpoint",
|
|
1252
|
+
"确认 Bot 长连接入口已开通",
|
|
1253
|
+
"长连接探针显示官方入口在握手阶段不可用。确认机器人已切到长连接模式,并向企业微信确认当前租户是否已开通该能力。",
|
|
1254
|
+
command,
|
|
1255
|
+
),
|
|
1256
|
+
);
|
|
1257
|
+
} else if (check?.diagnosisCode === "websocket-handshake-failed") {
|
|
1258
|
+
fixes.push(
|
|
1259
|
+
buildRemediationItem(
|
|
1260
|
+
"websocket-handshake",
|
|
1261
|
+
"检查长连接鉴权和握手参数",
|
|
1262
|
+
"长连接已到 WebSocket 阶段但未完成握手/鉴权。优先核对 BotID、Secret、代理和企业微信后台的长连接开通状态。",
|
|
1263
|
+
command,
|
|
1264
|
+
),
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
if (!postcheck.summary?.ok && fixes.length === 0) {
|
|
1270
|
+
fixes.push(
|
|
1271
|
+
buildRemediationItem(
|
|
1272
|
+
"generic-postcheck",
|
|
1273
|
+
"按失败命令逐条查看 JSON 结果",
|
|
1274
|
+
"quickstart 已捕获失败,但还没识别到特定模式。先单独运行对应 selfcheck,看完整 JSON 中的 checks/detail。",
|
|
1275
|
+
report.runChecks?.command,
|
|
1276
|
+
),
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
return dedupeRemediation(fixes);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
function buildModeRepairGroups(report) {
|
|
1284
|
+
if (report.mode?.id === "agent_callback") return ["agentCore"];
|
|
1285
|
+
if (report.mode?.id === "bot_long_connection") return ["botLongConnection"];
|
|
1286
|
+
if (report.mode?.id === "hybrid") return ["agentCore", "botLongConnection"];
|
|
1287
|
+
return [];
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function classifyCheckCommand(command = "") {
|
|
1291
|
+
const normalized = String(command ?? "");
|
|
1292
|
+
if (normalized.includes("wecom:bot:longconn:probe")) return "bot-longconn";
|
|
1293
|
+
if (normalized.includes("wecom:bot:selfcheck")) return "bot-selfcheck";
|
|
1294
|
+
if (normalized.includes("wecom:agent:selfcheck")) return "agent-selfcheck";
|
|
1295
|
+
if (normalized.includes("wecom:selfcheck")) return "shared-selfcheck";
|
|
1296
|
+
return "unknown";
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
function buildRepairGroups(postcheck, report) {
|
|
1300
|
+
const groups = new Set();
|
|
1301
|
+
if (!postcheck?.requested) return groups;
|
|
1302
|
+
if (Array.isArray(report.placeholders) && report.placeholders.length > 0) {
|
|
1303
|
+
for (const group of buildModeRepairGroups(report)) {
|
|
1304
|
+
groups.add(group);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
if (postcheck?.blockedByPlaceholders) {
|
|
1308
|
+
return groups;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
for (const check of postcheck.checks ?? []) {
|
|
1312
|
+
const command = String(check?.command ?? "");
|
|
1313
|
+
const commandKind = classifyCheckCommand(command);
|
|
1314
|
+
if (
|
|
1315
|
+
commandKind === "bot-longconn" &&
|
|
1316
|
+
(check?.ok === false || check?.timedOut === true || Boolean(check?.diagnosisCode))
|
|
1317
|
+
) {
|
|
1318
|
+
groups.add("botLongConnection");
|
|
1319
|
+
}
|
|
1320
|
+
if (check?.diagnosisCode) {
|
|
1321
|
+
groups.add("botLongConnection");
|
|
1322
|
+
}
|
|
1323
|
+
if (check?.ok === false && Array.isArray(check?.failedChecks) && check.failedChecks.length === 0) {
|
|
1324
|
+
for (const group of buildModeRepairGroups(report)) {
|
|
1325
|
+
groups.add(group);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
for (const failed of check?.failedChecks ?? []) {
|
|
1329
|
+
const name = String(failed?.name ?? "").trim();
|
|
1330
|
+
if (name === "config.account") {
|
|
1331
|
+
for (const group of buildModeRepairGroups(report)) {
|
|
1332
|
+
groups.add(group);
|
|
1333
|
+
}
|
|
1334
|
+
} else if (
|
|
1335
|
+
[
|
|
1336
|
+
"config.callbackToken",
|
|
1337
|
+
"config.callbackAesKey",
|
|
1338
|
+
"config.callbackAesKey.length",
|
|
1339
|
+
"config.webhookPath",
|
|
1340
|
+
"network.gettoken",
|
|
1341
|
+
"e2e.health.get",
|
|
1342
|
+
"e2e.health.get.legacyAlias",
|
|
1343
|
+
].includes(name)
|
|
1344
|
+
) {
|
|
1345
|
+
groups.add("agentCore");
|
|
1346
|
+
} else if (["e2e.url.verify", "e2e.url.verify.legacyAlias"].includes(name)) {
|
|
1347
|
+
groups.add(commandKind === "bot-selfcheck" ? "botWebhook" : "agentCore");
|
|
1348
|
+
} else if (
|
|
1349
|
+
[
|
|
1350
|
+
"config.bot.token",
|
|
1351
|
+
"config.bot.encodingAesKey",
|
|
1352
|
+
"config.bot.encodingAesKey.length",
|
|
1353
|
+
"config.bot.webhookPath",
|
|
1354
|
+
"local.webhook.health",
|
|
1355
|
+
"e2e.message.post",
|
|
1356
|
+
"e2e.message.response.stream",
|
|
1357
|
+
"e2e.stream.refresh",
|
|
1358
|
+
].includes(name)
|
|
1359
|
+
) {
|
|
1360
|
+
groups.add(commandKind === "bot-selfcheck" ? "botWebhook" : "agentCore");
|
|
1361
|
+
} else if (name === "config.enabled") {
|
|
1362
|
+
if (commandKind === "bot-selfcheck" || commandKind === "bot-longconn") {
|
|
1363
|
+
groups.add("botWebhook");
|
|
1364
|
+
} else {
|
|
1365
|
+
groups.add("agentCore");
|
|
1366
|
+
}
|
|
1367
|
+
} else if (name === "config.bot.enabled") {
|
|
1368
|
+
groups.add("botWebhook");
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
return groups;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
function buildAgentEnvTemplate(accountId, accountConfig) {
|
|
1376
|
+
const normalizedAccountId = normalizeAccountId(accountId);
|
|
1377
|
+
const prefix = normalizedAccountId === "default" ? "WECOM" : `WECOM_${normalizedAccountId.toUpperCase()}`;
|
|
1378
|
+
return [
|
|
1379
|
+
`${prefix}_ENABLED=${accountConfig?.enabled === false ? "false" : "true"}`,
|
|
1380
|
+
`${prefix}_CORP_ID=${String(accountConfig?.corpId ?? "ww-your-corp-id")}`,
|
|
1381
|
+
`${prefix}_CORP_SECRET=${String(accountConfig?.corpSecret ?? "your-app-secret")}`,
|
|
1382
|
+
`${prefix}_AGENT_ID=${String(accountConfig?.agentId ?? 1000002)}`,
|
|
1383
|
+
`${prefix}_CALLBACK_TOKEN=${String(accountConfig?.callbackToken ?? "your-callback-token")}`,
|
|
1384
|
+
`${prefix}_CALLBACK_AES_KEY=${String(accountConfig?.callbackAesKey ?? "your-callback-aes-key")}`,
|
|
1385
|
+
`${prefix}_WEBHOOK_PATH=${String(accountConfig?.webhookPath ?? "/wecom/callback")}`,
|
|
1386
|
+
];
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
function buildBotWebhookEnvTemplate(accountId, accountConfig) {
|
|
1390
|
+
const normalizedAccountId = normalizeAccountId(accountId);
|
|
1391
|
+
const prefix = normalizedAccountId === "default" ? "WECOM_BOT" : `WECOM_${normalizedAccountId.toUpperCase()}_BOT`;
|
|
1392
|
+
const botConfig = asObject(accountConfig?.bot);
|
|
1393
|
+
return [
|
|
1394
|
+
`${prefix}_ENABLED=${botConfig?.enabled === false ? "false" : "true"}`,
|
|
1395
|
+
`${prefix}_TOKEN=${String(botConfig?.token ?? "your-bot-token")}`,
|
|
1396
|
+
`${prefix}_ENCODING_AES_KEY=${String(botConfig?.encodingAesKey ?? "your-bot-encoding-aes-key")}`,
|
|
1397
|
+
`${prefix}_WEBHOOK_PATH=${String(botConfig?.webhookPath ?? "/wecom/bot/callback")}`,
|
|
1398
|
+
];
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
function buildBotLongConnectionEnvTemplate(accountId, accountConfig) {
|
|
1402
|
+
const normalizedAccountId = normalizeAccountId(accountId);
|
|
1403
|
+
const prefix = normalizedAccountId === "default" ? "WECOM_BOT" : `WECOM_${normalizedAccountId.toUpperCase()}_BOT`;
|
|
1404
|
+
const longConnection = asObject(accountConfig?.bot?.longConnection);
|
|
1405
|
+
return [
|
|
1406
|
+
`${prefix}_ENABLED=${accountConfig?.bot?.enabled === false ? "false" : "true"}`,
|
|
1407
|
+
`${prefix}_LONG_CONNECTION_ENABLED=${longConnection?.enabled === false ? "false" : "true"}`,
|
|
1408
|
+
`${prefix}_LONG_CONNECTION_BOT_ID=${String(longConnection?.botId ?? "your-bot-id")}`,
|
|
1409
|
+
`${prefix}_LONG_CONNECTION_SECRET=${String(longConnection?.secret ?? "your-bot-secret")}`,
|
|
1410
|
+
`${prefix}_LONG_CONNECTION_URL=${String(longConnection?.url ?? "wss://openws.work.weixin.qq.com")}`,
|
|
1411
|
+
];
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
function buildRepairArtifacts(postcheck, report, baseConfig = {}) {
|
|
1415
|
+
const existingAccountConfig = resolveTargetAccountConfig(baseConfig, report.accountId);
|
|
1416
|
+
const targetAccountConfig = mergeDeep(
|
|
1417
|
+
resolveTargetAccountConfig(report.starterConfig, report.accountId),
|
|
1418
|
+
pickPathsFromObject(existingAccountConfig, REPAIR_VALUE_PRESERVE_PATHS),
|
|
1419
|
+
);
|
|
1420
|
+
if (!targetAccountConfig || Object.keys(targetAccountConfig).length === 0) return null;
|
|
1421
|
+
|
|
1422
|
+
const groups = Array.from(buildRepairGroups(postcheck, report));
|
|
1423
|
+
if (groups.length === 0) return null;
|
|
1424
|
+
|
|
1425
|
+
const dottedPaths = Array.from(
|
|
1426
|
+
new Set(groups.flatMap((group) => REPAIR_PATH_GROUPS[group] ?? [])),
|
|
1427
|
+
);
|
|
1428
|
+
const accountPatch = pickPathsFromObject(targetAccountConfig, dottedPaths);
|
|
1429
|
+
const scopedPatch = buildScopedConfigPatch(report.accountId, accountPatch);
|
|
1430
|
+
const envLines = [];
|
|
1431
|
+
if (groups.includes("agentCore")) {
|
|
1432
|
+
envLines.push(...buildAgentEnvTemplate(report.accountId, targetAccountConfig));
|
|
1433
|
+
}
|
|
1434
|
+
if (groups.includes("botWebhook")) {
|
|
1435
|
+
envLines.push(...buildBotWebhookEnvTemplate(report.accountId, targetAccountConfig));
|
|
1436
|
+
}
|
|
1437
|
+
if (groups.includes("botLongConnection")) {
|
|
1438
|
+
envLines.push(...buildBotLongConnectionEnvTemplate(report.accountId, targetAccountConfig));
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
return {
|
|
1442
|
+
groups,
|
|
1443
|
+
configPath: scopedPatch.path,
|
|
1444
|
+
accountPatch,
|
|
1445
|
+
configPatch: scopedPatch.configPatch,
|
|
1446
|
+
envTemplate: {
|
|
1447
|
+
format: "dotenv",
|
|
1448
|
+
lines: Array.from(new Set(envLines)),
|
|
1449
|
+
},
|
|
1450
|
+
notes: [
|
|
1451
|
+
"configPatch 可直接按 JSON merge 方式合并到 openclaw.json。",
|
|
1452
|
+
"envTemplate 更适合放进 env.vars 或系统环境变量,用于覆盖 Secret / 回调参数。",
|
|
1453
|
+
],
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
function buildRepairPlan(repairArtifacts) {
|
|
1458
|
+
if (!repairArtifacts?.configPatch) return null;
|
|
1459
|
+
const changes = collectRepairPlanChanges(repairArtifacts.accountPatch, repairArtifacts.configPath);
|
|
1460
|
+
const envChanges = (repairArtifacts.envTemplate?.lines ?? []).map((line) => {
|
|
1461
|
+
const [key, ...rest] = String(line ?? "").split("=");
|
|
1462
|
+
return {
|
|
1463
|
+
key: String(key ?? "").trim(),
|
|
1464
|
+
value: rest.join("="),
|
|
1465
|
+
line,
|
|
1466
|
+
};
|
|
1467
|
+
});
|
|
1468
|
+
const fileWrites = repairArtifacts.files
|
|
1469
|
+
? [
|
|
1470
|
+
repairArtifacts.files.configPatchFile
|
|
1471
|
+
? { kind: "config_patch", path: repairArtifacts.files.configPatchFile }
|
|
1472
|
+
: null,
|
|
1473
|
+
repairArtifacts.files.accountPatchFile
|
|
1474
|
+
? { kind: "account_patch", path: repairArtifacts.files.accountPatchFile }
|
|
1475
|
+
: null,
|
|
1476
|
+
repairArtifacts.files.envTemplateFile
|
|
1477
|
+
? { kind: "env_template", path: repairArtifacts.files.envTemplateFile }
|
|
1478
|
+
: null,
|
|
1479
|
+
repairArtifacts.files.notesFile
|
|
1480
|
+
? { kind: "notes", path: repairArtifacts.files.notesFile }
|
|
1481
|
+
: null,
|
|
1482
|
+
].filter(Boolean)
|
|
1483
|
+
: [
|
|
1484
|
+
{ kind: "config_patch", path: "wecom.config-patch.json" },
|
|
1485
|
+
{ kind: "account_patch", path: "wecom.account-patch.json" },
|
|
1486
|
+
{ kind: "env_template", path: "wecom.env.template" },
|
|
1487
|
+
{ kind: "notes", path: "README.txt" },
|
|
1488
|
+
];
|
|
1489
|
+
|
|
1490
|
+
return {
|
|
1491
|
+
summary: `生成 ${repairArtifacts.groups.length} 组 repair patch,覆盖 ${changes.length} 个配置路径。`,
|
|
1492
|
+
changes,
|
|
1493
|
+
envChanges,
|
|
1494
|
+
fileWrites,
|
|
1495
|
+
requiresConfirmation: true,
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
function collectRepairPlanChanges(value, prefix = "", out = []) {
|
|
1500
|
+
if (Array.isArray(value)) {
|
|
1501
|
+
out.push({
|
|
1502
|
+
path: prefix,
|
|
1503
|
+
value: deepClone(value),
|
|
1504
|
+
});
|
|
1505
|
+
return out;
|
|
1506
|
+
}
|
|
1507
|
+
if (!value || typeof value !== "object") {
|
|
1508
|
+
out.push({
|
|
1509
|
+
path: prefix,
|
|
1510
|
+
value,
|
|
1511
|
+
});
|
|
1512
|
+
return out;
|
|
1513
|
+
}
|
|
1514
|
+
for (const [key, child] of Object.entries(value)) {
|
|
1515
|
+
const childPrefix = prefix ? `${prefix}.${key}` : key;
|
|
1516
|
+
collectRepairPlanChanges(child, childPrefix, out);
|
|
1517
|
+
}
|
|
1518
|
+
return out;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
function finalizePostcheck(postcheck, report, baseConfig = {}) {
|
|
1522
|
+
const repairArtifacts = buildRepairArtifacts(postcheck, report, baseConfig);
|
|
1523
|
+
return {
|
|
1524
|
+
...postcheck,
|
|
1525
|
+
remediation: buildPostcheckRemediation(postcheck, report),
|
|
1526
|
+
repairArtifacts,
|
|
1527
|
+
repairPlan: buildRepairPlan(repairArtifacts),
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
function buildRepairNotesText(repairArtifacts) {
|
|
1532
|
+
const lines = [
|
|
1533
|
+
"WeCom quickstart repair artifacts",
|
|
1534
|
+
"",
|
|
1535
|
+
`groups: ${(repairArtifacts?.groups ?? []).join(", ") || "none"}`,
|
|
1536
|
+
`configPath: ${repairArtifacts?.configPath || "channels.wecom"}`,
|
|
1537
|
+
"",
|
|
1538
|
+
"Files:",
|
|
1539
|
+
"- wecom.config-patch.json: JSON merge patch for openclaw.json",
|
|
1540
|
+
"- wecom.account-patch.json: account-scoped patch payload only",
|
|
1541
|
+
"- wecom.env.template: env template for secrets / callback parameters",
|
|
1542
|
+
"",
|
|
1543
|
+
"How to use:",
|
|
1544
|
+
"1. Merge wecom.config-patch.json into openclaw.json with your usual JSON merge workflow.",
|
|
1545
|
+
"2. Copy needed keys from wecom.env.template into env.vars or system env, then replace placeholder values.",
|
|
1546
|
+
];
|
|
1547
|
+
if (Array.isArray(repairArtifacts?.notes) && repairArtifacts.notes.length > 0) {
|
|
1548
|
+
lines.push("", "Notes:");
|
|
1549
|
+
for (const note of repairArtifacts.notes) {
|
|
1550
|
+
lines.push(`- ${note}`);
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
return `${lines.join("\n")}\n`;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
function formatRepairPreviewValue(value) {
|
|
1557
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
1558
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
1559
|
+
return JSON.stringify(value);
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
function collectRepairPreviewLines(value, prefix = "", out = []) {
|
|
1563
|
+
if (Array.isArray(value)) {
|
|
1564
|
+
out.push(`${prefix} = ${formatRepairPreviewValue(value)}`);
|
|
1565
|
+
return out;
|
|
1566
|
+
}
|
|
1567
|
+
if (!value || typeof value !== "object") {
|
|
1568
|
+
out.push(`${prefix} = ${formatRepairPreviewValue(value)}`);
|
|
1569
|
+
return out;
|
|
1570
|
+
}
|
|
1571
|
+
for (const [key, child] of Object.entries(value)) {
|
|
1572
|
+
const childPrefix = prefix ? `${prefix}.${key}` : key;
|
|
1573
|
+
collectRepairPreviewLines(child, childPrefix, out);
|
|
1574
|
+
}
|
|
1575
|
+
return out;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
function buildRepairPreviewLines(repairArtifacts) {
|
|
1579
|
+
if (!repairArtifacts?.accountPatch || !repairArtifacts?.configPath) return [];
|
|
1580
|
+
return collectRepairPreviewLines(repairArtifacts.accountPatch, repairArtifacts.configPath, []);
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
function buildRepairPreviewLinesFromPlan(repairPlan) {
|
|
1584
|
+
return (repairPlan?.changes ?? []).map(
|
|
1585
|
+
(change) => `${change.path} = ${formatRepairPreviewValue(change.value)}`,
|
|
1586
|
+
);
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
async function maybeWriteRepairArtifacts(postcheck, args) {
|
|
1590
|
+
if (!args?.repairDir || !postcheck?.repairArtifacts) return postcheck;
|
|
1591
|
+
const outputDir = path.resolve(expandHome(args.repairDir));
|
|
1592
|
+
await mkdir(outputDir, { recursive: true });
|
|
1593
|
+
|
|
1594
|
+
const configPatchFile = path.join(outputDir, "wecom.config-patch.json");
|
|
1595
|
+
const accountPatchFile = path.join(outputDir, "wecom.account-patch.json");
|
|
1596
|
+
const envTemplateFile = path.join(outputDir, "wecom.env.template");
|
|
1597
|
+
const notesFile = path.join(outputDir, "README.txt");
|
|
1598
|
+
|
|
1599
|
+
await writeFile(configPatchFile, `${JSON.stringify(postcheck.repairArtifacts.configPatch, null, 2)}\n`, "utf8");
|
|
1600
|
+
await writeFile(accountPatchFile, `${JSON.stringify(postcheck.repairArtifacts.accountPatch, null, 2)}\n`, "utf8");
|
|
1601
|
+
await writeFile(
|
|
1602
|
+
envTemplateFile,
|
|
1603
|
+
`${(postcheck.repairArtifacts.envTemplate?.lines ?? []).join("\n")}\n`,
|
|
1604
|
+
"utf8",
|
|
1605
|
+
);
|
|
1606
|
+
await writeFile(notesFile, buildRepairNotesText(postcheck.repairArtifacts), "utf8");
|
|
1607
|
+
|
|
1608
|
+
return {
|
|
1609
|
+
...postcheck,
|
|
1610
|
+
repairArtifacts: {
|
|
1611
|
+
...postcheck.repairArtifacts,
|
|
1612
|
+
files: {
|
|
1613
|
+
directory: outputDir,
|
|
1614
|
+
configPatchFile,
|
|
1615
|
+
accountPatchFile,
|
|
1616
|
+
envTemplateFile,
|
|
1617
|
+
notesFile,
|
|
1618
|
+
},
|
|
1619
|
+
},
|
|
1620
|
+
repairPlan: {
|
|
1621
|
+
...(postcheck.repairPlan ?? {}),
|
|
1622
|
+
fileWrites: [
|
|
1623
|
+
{ kind: "config_patch", path: configPatchFile },
|
|
1624
|
+
{ kind: "account_patch", path: accountPatchFile },
|
|
1625
|
+
{ kind: "env_template", path: envTemplateFile },
|
|
1626
|
+
{ kind: "notes", path: notesFile },
|
|
1627
|
+
],
|
|
1628
|
+
},
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
async function maybeConfirmRepairApply(args, postcheck) {
|
|
1633
|
+
if (args?.applyRepair !== true) {
|
|
1634
|
+
return {
|
|
1635
|
+
...args,
|
|
1636
|
+
repairPrompted: false,
|
|
1637
|
+
repairApproved: false,
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
if (!postcheck?.repairArtifacts?.configPatch) {
|
|
1641
|
+
return {
|
|
1642
|
+
...args,
|
|
1643
|
+
repairPrompted: false,
|
|
1644
|
+
repairApproved: false,
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
const shouldPrompt =
|
|
1648
|
+
args.confirmRepair === true ||
|
|
1649
|
+
(args.json !== true && process.stdin.isTTY === true);
|
|
1650
|
+
if (!shouldPrompt) {
|
|
1651
|
+
return {
|
|
1652
|
+
...args,
|
|
1653
|
+
repairPrompted: false,
|
|
1654
|
+
repairApproved: true,
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
const previewLines = buildRepairPreviewLinesFromPlan(postcheck.repairPlan);
|
|
1659
|
+
const rl = await createWizardPrompt();
|
|
1660
|
+
try {
|
|
1661
|
+
rl.output.write("\nRepair preview\n");
|
|
1662
|
+
for (const line of previewLines) {
|
|
1663
|
+
rl.output.write(` - ${line}\n`);
|
|
1664
|
+
}
|
|
1665
|
+
const approved = await askBoolean(
|
|
1666
|
+
rl,
|
|
1667
|
+
`Apply this repair patch to ${path.resolve(expandHome(args.configPath))} now`,
|
|
1668
|
+
false,
|
|
1669
|
+
);
|
|
1670
|
+
return {
|
|
1671
|
+
...args,
|
|
1672
|
+
repairPrompted: true,
|
|
1673
|
+
repairApproved: approved,
|
|
1674
|
+
};
|
|
1675
|
+
} finally {
|
|
1676
|
+
rl.close();
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
async function maybeApplyRepairArtifacts(postcheck, args) {
|
|
1681
|
+
if (args?.applyRepair !== true) {
|
|
1682
|
+
return {
|
|
1683
|
+
requested: false,
|
|
1684
|
+
applied: false,
|
|
1685
|
+
configPath: path.resolve(expandHome(args?.configPath || "~/.openclaw/openclaw.json")),
|
|
1686
|
+
changedPaths: [],
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
if (!postcheck?.repairArtifacts?.configPatch) {
|
|
1690
|
+
return {
|
|
1691
|
+
requested: true,
|
|
1692
|
+
prompted: args?.repairPrompted === true,
|
|
1693
|
+
confirmed: false,
|
|
1694
|
+
applied: false,
|
|
1695
|
+
configPath: path.resolve(expandHome(args?.configPath || "~/.openclaw/openclaw.json")),
|
|
1696
|
+
reason: "no repair configPatch available",
|
|
1697
|
+
changedPaths: [],
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
if (args?.repairApproved !== true) {
|
|
1701
|
+
return {
|
|
1702
|
+
requested: true,
|
|
1703
|
+
prompted: args?.repairPrompted === true,
|
|
1704
|
+
confirmed: false,
|
|
1705
|
+
applied: false,
|
|
1706
|
+
configPath: path.resolve(expandHome(args?.configPath || "~/.openclaw/openclaw.json")),
|
|
1707
|
+
reason: args?.repairPrompted === true ? "user declined repair patch" : "repair patch not approved",
|
|
1708
|
+
changedPaths: [],
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
1711
|
+
const result = await writeMergedConfig(args.configPath, postcheck.repairArtifacts.configPatch);
|
|
1712
|
+
return {
|
|
1713
|
+
requested: true,
|
|
1714
|
+
prompted: args?.repairPrompted === true,
|
|
1715
|
+
confirmed: true,
|
|
1716
|
+
applied: true,
|
|
1717
|
+
configPath: result.configPath,
|
|
1718
|
+
backupPath: result.backupPath,
|
|
1719
|
+
existed: result.existed,
|
|
1720
|
+
changedPaths: result.changedPaths,
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
function printRepairApplySummary(repairApplyResult) {
|
|
1725
|
+
if (!repairApplyResult?.requested) return;
|
|
1726
|
+
console.log("- repairApply:");
|
|
1727
|
+
if (repairApplyResult.prompted === true) {
|
|
1728
|
+
console.log(` - prompted: yes`);
|
|
1729
|
+
console.log(` - confirmed: ${repairApplyResult.confirmed === true ? "yes" : "no"}`);
|
|
1730
|
+
}
|
|
1731
|
+
if (!repairApplyResult.applied) {
|
|
1732
|
+
console.log(` - status: skipped (${repairApplyResult.reason || "no changes applied"})`);
|
|
1733
|
+
console.log(` - configPath: ${repairApplyResult.configPath}`);
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
console.log(` - status: applied`);
|
|
1737
|
+
console.log(` - configPath: ${repairApplyResult.configPath}`);
|
|
1738
|
+
if (Array.isArray(repairApplyResult.changedPaths) && repairApplyResult.changedPaths.length > 0) {
|
|
1739
|
+
console.log(` - changedPaths: ${repairApplyResult.changedPaths.join(", ")}`);
|
|
1740
|
+
}
|
|
1741
|
+
if (repairApplyResult.backupPath) {
|
|
1742
|
+
console.log(` - backupPath: ${repairApplyResult.backupPath}`);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
async function maybePromptWizardChecks(args, report) {
|
|
1747
|
+
if (args.wizard !== true || args.runChecks === true) return args;
|
|
1748
|
+
const rl = await createWizardPrompt();
|
|
1749
|
+
try {
|
|
1750
|
+
const defaultRunChecks = args.write === true && report.placeholders.length === 0;
|
|
1751
|
+
const question =
|
|
1752
|
+
report.placeholders.length > 0
|
|
1753
|
+
? "Starter config still has placeholders. Run recommended checks anyway"
|
|
1754
|
+
: "Run recommended checks now";
|
|
1755
|
+
const runChecks = await askBoolean(rl, question, defaultRunChecks);
|
|
1756
|
+
if (!runChecks) return args;
|
|
1757
|
+
let forceChecks = args.forceChecks === true;
|
|
1758
|
+
if (report.placeholders.length > 0) {
|
|
1759
|
+
forceChecks = true;
|
|
1760
|
+
}
|
|
1761
|
+
return {
|
|
1762
|
+
...args,
|
|
1763
|
+
runChecks: true,
|
|
1764
|
+
forceChecks,
|
|
1765
|
+
};
|
|
1766
|
+
} finally {
|
|
1767
|
+
rl.close();
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
async function main() {
|
|
1772
|
+
const parsedArgs = parseArgs(process.argv);
|
|
1773
|
+
const initialArgs = parsedArgs.wizard ? await runWizard(parsedArgs) : parsedArgs;
|
|
1774
|
+
const initialConfig = await loadConfig(initialArgs.configPath);
|
|
1775
|
+
const promptedArgs = await maybePromptWizardChecks(initialArgs, buildReport(initialArgs, initialConfig.config));
|
|
1776
|
+
const args = promptedArgs;
|
|
1777
|
+
const loadedConfig = await loadConfig(args.configPath);
|
|
1778
|
+
const report = buildReport(args, loadedConfig.config);
|
|
1779
|
+
const writeResult = args.write ? await writeMergedConfig(args.configPath, report.starterConfig) : null;
|
|
1780
|
+
const baseConfig = writeResult ? writeResult.mergedConfig : loadedConfig.config;
|
|
1781
|
+
const postcheck = await maybeWriteRepairArtifacts(
|
|
1782
|
+
finalizePostcheck(await runRecommendedChecks({ args, report }), report, baseConfig),
|
|
1783
|
+
args,
|
|
1784
|
+
);
|
|
1785
|
+
const confirmedArgs = await maybeConfirmRepairApply(args, postcheck);
|
|
1786
|
+
const repairApply = await maybeApplyRepairArtifacts(postcheck, confirmedArgs);
|
|
1787
|
+
|
|
1788
|
+
if (args.json) {
|
|
1789
|
+
console.log(
|
|
1790
|
+
JSON.stringify(
|
|
1791
|
+
{
|
|
1792
|
+
...report,
|
|
1793
|
+
postcheck,
|
|
1794
|
+
repairApply,
|
|
1795
|
+
write: writeResult
|
|
1796
|
+
? {
|
|
1797
|
+
applied: true,
|
|
1798
|
+
configPath: writeResult.configPath,
|
|
1799
|
+
backupPath: writeResult.backupPath,
|
|
1800
|
+
existed: writeResult.existed,
|
|
1801
|
+
changedPaths: writeResult.changedPaths,
|
|
1802
|
+
}
|
|
1803
|
+
: {
|
|
1804
|
+
applied: false,
|
|
1805
|
+
configPath: report.configPath,
|
|
1806
|
+
changedPaths: [],
|
|
1807
|
+
},
|
|
1808
|
+
},
|
|
1809
|
+
null,
|
|
1810
|
+
2,
|
|
1811
|
+
),
|
|
1812
|
+
);
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
printTextReport(report, writeResult);
|
|
1817
|
+
printPostcheckSummary(postcheck);
|
|
1818
|
+
printRepairApplySummary(repairApply);
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
main().catch((err) => {
|
|
1822
|
+
console.error(`WeCom quickstart failed: ${String(err?.message || err)}`);
|
|
1823
|
+
process.exit(1);
|
|
1824
|
+
});
|