@lark-apaas/openclaw-scripts-diagnose-cli 0.1.13 → 0.1.14-alpha.1
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/dist/index.cjs +605 -3
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -52,7 +52,7 @@ node_assert = __toESM(node_assert);
|
|
|
52
52
|
* it terse and parseable.
|
|
53
53
|
*/
|
|
54
54
|
function getVersion() {
|
|
55
|
-
return "0.1.
|
|
55
|
+
return "0.1.14-alpha.1";
|
|
56
56
|
}
|
|
57
57
|
//#endregion
|
|
58
58
|
//#region src/rule-engine/base.ts
|
|
@@ -3347,7 +3347,6 @@ function resolveUpgradeDirection(installed, ocCur, recommendedOc, isLegacy) {
|
|
|
3347
3347
|
/** 提取公共前置上下文;任何前置条件不满足时返回 null(规则 pass)。 */
|
|
3348
3348
|
function resolveCompatContext(ctx) {
|
|
3349
3349
|
const recommendedOc = ctx.vars.recommendedOpenclawTag;
|
|
3350
|
-
if (!recommendedOc) return null;
|
|
3351
3350
|
const ocCur = getOcVersion();
|
|
3352
3351
|
if (!ocCur) return null;
|
|
3353
3352
|
const installed = getInstalledPlugin(ctx);
|
|
@@ -3368,6 +3367,7 @@ let FeishuPluginOpenclawUpgradeRule = class FeishuPluginOpenclawUpgradeRule exte
|
|
|
3368
3367
|
validate(ctx) {
|
|
3369
3368
|
const cc = resolveCompatContext(ctx);
|
|
3370
3369
|
if (!cc) return { pass: true };
|
|
3370
|
+
if (!cc.recommendedOc) return { pass: true };
|
|
3371
3371
|
const { ocCur, recommendedOc, installed, isLegacy } = cc;
|
|
3372
3372
|
if (isForkPlugin(installed)) return validateForkPlugin(installed, ocCur, recommendedOc);
|
|
3373
3373
|
if (resolveUpgradeDirection(installed, ocCur, recommendedOc, isLegacy) !== "openclaw") return { pass: true };
|
|
@@ -3397,6 +3397,14 @@ let FeishuPluginLarkUpgradeRule = class FeishuPluginLarkUpgradeRule extends Diag
|
|
|
3397
3397
|
if (!cc) return { pass: true };
|
|
3398
3398
|
const { ocCur, recommendedOc, installed, isLegacy } = cc;
|
|
3399
3399
|
if (isForkPlugin(installed)) return { pass: true };
|
|
3400
|
+
if (!recommendedOc) {
|
|
3401
|
+
if (isLegacy || !isVersionCompatible(installed, ocCur)) return {
|
|
3402
|
+
pass: false,
|
|
3403
|
+
action: "upgrade_lark",
|
|
3404
|
+
message: `${buildCompatPrefix(installed, ocCur, isLegacy)};建议升级飞书插件至兼容版本`
|
|
3405
|
+
};
|
|
3406
|
+
return { pass: true };
|
|
3407
|
+
}
|
|
3400
3408
|
if (resolveUpgradeDirection(installed, ocCur, recommendedOc, isLegacy) !== "lark") return { pass: true };
|
|
3401
3409
|
return {
|
|
3402
3410
|
pass: false,
|
|
@@ -4113,6 +4121,9 @@ const PROVIDER_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-provider-
|
|
|
4113
4121
|
const SECRETS_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-openclaw-secrets.json";
|
|
4114
4122
|
/** Absolute path to the openclaw config JSON. */
|
|
4115
4123
|
const CONFIG_PATH = `${WORKSPACE_DIR}/openclaw.json`;
|
|
4124
|
+
function upgradeLarkLogFile(runId) {
|
|
4125
|
+
return `${DIAGNOSE_DIR}/upgrade-lark-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/:/g, "-")}-${runId.slice(0, 8)}.log`;
|
|
4126
|
+
}
|
|
4116
4127
|
//#endregion
|
|
4117
4128
|
//#region src/run-log.ts
|
|
4118
4129
|
let currentRunContext;
|
|
@@ -6225,6 +6236,23 @@ function reportError(params) {
|
|
|
6225
6236
|
} catch {}
|
|
6226
6237
|
}
|
|
6227
6238
|
//#endregion
|
|
6239
|
+
//#region ../../openclaw-slardar/lib/report-log.js
|
|
6240
|
+
function reportLog(params) {
|
|
6241
|
+
try {
|
|
6242
|
+
const extra = mergeLogExtra(buildTelemetryFields(params), {
|
|
6243
|
+
event: params.event,
|
|
6244
|
+
phase: params.phase,
|
|
6245
|
+
status: params.status,
|
|
6246
|
+
...params.extra
|
|
6247
|
+
});
|
|
6248
|
+
Slardar.sendLog?.({
|
|
6249
|
+
content: params.message,
|
|
6250
|
+
level: params.level ?? "info",
|
|
6251
|
+
extra
|
|
6252
|
+
});
|
|
6253
|
+
} catch {}
|
|
6254
|
+
}
|
|
6255
|
+
//#endregion
|
|
6228
6256
|
//#region src/install-cli.ts
|
|
6229
6257
|
const LARK_CLI_NAME = "lark-cli";
|
|
6230
6258
|
const AGENT_SKILLS_NAME = "agent-skills";
|
|
@@ -10121,6 +10149,109 @@ function finalize(results, aborted) {
|
|
|
10121
10149
|
};
|
|
10122
10150
|
}
|
|
10123
10151
|
//#endregion
|
|
10152
|
+
//#region src/channels-probe.ts
|
|
10153
|
+
const CHANNEL_LINE_RE = /^-\s+Feishu\s+([^:]+):\s+(.+)$/;
|
|
10154
|
+
/**
|
|
10155
|
+
* Port of Python `_account_is_working` from the feishu-channel-success-rate skill.
|
|
10156
|
+
*
|
|
10157
|
+
* Strips colon-prefixed key:value bits (dm:, bot:, in:, out:, token:, allow:,
|
|
10158
|
+
* intents:, groups:, health:) and evaluates the canonical health formula.
|
|
10159
|
+
*/
|
|
10160
|
+
function accountIsWorking(bits) {
|
|
10161
|
+
const bitTokens = /* @__PURE__ */ new Set();
|
|
10162
|
+
let hasError = false;
|
|
10163
|
+
let hasProbeFailed = false;
|
|
10164
|
+
for (const raw of bits) {
|
|
10165
|
+
const b = raw.trim();
|
|
10166
|
+
if (!b) continue;
|
|
10167
|
+
if (b.startsWith("error:")) {
|
|
10168
|
+
hasError = true;
|
|
10169
|
+
continue;
|
|
10170
|
+
}
|
|
10171
|
+
if (b === "probe failed") {
|
|
10172
|
+
hasProbeFailed = true;
|
|
10173
|
+
continue;
|
|
10174
|
+
}
|
|
10175
|
+
bitTokens.add(b.split(":")[0]);
|
|
10176
|
+
}
|
|
10177
|
+
if (!bitTokens.has("enabled") || !bitTokens.has("configured")) return false;
|
|
10178
|
+
if (bitTokens.has("works")) return true;
|
|
10179
|
+
if (bitTokens.has("running") && !hasError && !hasProbeFailed) return true;
|
|
10180
|
+
return false;
|
|
10181
|
+
}
|
|
10182
|
+
/**
|
|
10183
|
+
* Parse the raw stdout of `openclaw channels status --probe`.
|
|
10184
|
+
* Port of Python `extract_channels_probe` from the feishu-channel-success-rate skill.
|
|
10185
|
+
*/
|
|
10186
|
+
function parseChannelsProbeOutput(text) {
|
|
10187
|
+
const gatewayReachable = text.includes("Gateway reachable");
|
|
10188
|
+
const accounts = [];
|
|
10189
|
+
let anyAccountWorking = false;
|
|
10190
|
+
for (const line of text.split("\n")) {
|
|
10191
|
+
const m = CHANNEL_LINE_RE.exec(line.trim());
|
|
10192
|
+
if (!m) continue;
|
|
10193
|
+
const [, acct, rest] = m;
|
|
10194
|
+
const bits = rest.split(",").map((b) => b.trim());
|
|
10195
|
+
const isWorking = accountIsWorking(bits);
|
|
10196
|
+
if (isWorking) anyAccountWorking = true;
|
|
10197
|
+
accounts.push({
|
|
10198
|
+
id: acct.trim(),
|
|
10199
|
+
bits,
|
|
10200
|
+
isWorking,
|
|
10201
|
+
raw: line.trim()
|
|
10202
|
+
});
|
|
10203
|
+
}
|
|
10204
|
+
return {
|
|
10205
|
+
gatewayReachable,
|
|
10206
|
+
accounts,
|
|
10207
|
+
anyAccountWorking
|
|
10208
|
+
};
|
|
10209
|
+
}
|
|
10210
|
+
/**
|
|
10211
|
+
* Run `openclaw channels status --probe` and return a structured result.
|
|
10212
|
+
*
|
|
10213
|
+
* The command may exit non-zero when some bot accounts fail their probe — that
|
|
10214
|
+
* is still useful output. We therefore try to parse stdout even when the
|
|
10215
|
+
* process exits with a non-zero code, falling back to an unavailable result
|
|
10216
|
+
* only when there is genuinely no output to parse.
|
|
10217
|
+
*
|
|
10218
|
+
* @param timeoutMs Maximum wait time. Default is 60 s because v2026.4.x
|
|
10219
|
+
* lacks a per-request HTTP timeout and can block indefinitely.
|
|
10220
|
+
*/
|
|
10221
|
+
function runChannelsProbe(timeoutMs = 6e4) {
|
|
10222
|
+
let stdout = "";
|
|
10223
|
+
let execError;
|
|
10224
|
+
try {
|
|
10225
|
+
stdout = (0, node_child_process.execSync)("openclaw channels status --probe", {
|
|
10226
|
+
encoding: "utf-8",
|
|
10227
|
+
timeout: timeoutMs,
|
|
10228
|
+
stdio: [
|
|
10229
|
+
"ignore",
|
|
10230
|
+
"pipe",
|
|
10231
|
+
"pipe"
|
|
10232
|
+
]
|
|
10233
|
+
});
|
|
10234
|
+
} catch (e) {
|
|
10235
|
+
const err = e;
|
|
10236
|
+
stdout = err.stdout ?? "";
|
|
10237
|
+
execError = err.message;
|
|
10238
|
+
const stderrRaw = err.stderr;
|
|
10239
|
+
const stderr = (typeof stderrRaw === "string" ? stderrRaw : stderrRaw?.toString("utf-8") ?? "").trim();
|
|
10240
|
+
if (stderr) console.error(`channels-probe: stderr from CLI: ${stderr}`);
|
|
10241
|
+
}
|
|
10242
|
+
if (stdout.trim()) return {
|
|
10243
|
+
available: true,
|
|
10244
|
+
...parseChannelsProbeOutput(stdout)
|
|
10245
|
+
};
|
|
10246
|
+
return {
|
|
10247
|
+
available: false,
|
|
10248
|
+
gatewayReachable: false,
|
|
10249
|
+
accounts: [],
|
|
10250
|
+
anyAccountWorking: false,
|
|
10251
|
+
error: execError ?? "no output from openclaw channels status --probe"
|
|
10252
|
+
};
|
|
10253
|
+
}
|
|
10254
|
+
//#endregion
|
|
10124
10255
|
//#region src/innerapi/reportCliRun.ts
|
|
10125
10256
|
/**
|
|
10126
10257
|
* CLI-side client for studio_server's `openclaw.report_cli_run` inner
|
|
@@ -10200,7 +10331,7 @@ async function reportCliRun(opts) {
|
|
|
10200
10331
|
//#region src/help.ts
|
|
10201
10332
|
const BIN = "mclaw-diagnose";
|
|
10202
10333
|
function versionBanner() {
|
|
10203
|
-
return `v0.1.
|
|
10334
|
+
return `v0.1.14-alpha.1`;
|
|
10204
10335
|
}
|
|
10205
10336
|
const COMMANDS = [
|
|
10206
10337
|
{
|
|
@@ -10498,6 +10629,46 @@ OPTIONS
|
|
|
10498
10629
|
EXIT CODES
|
|
10499
10630
|
0 Success or skipped (prerequisites not met).
|
|
10500
10631
|
1 Secret/path unresolvable, lark-cli failed, or config unreadable.
|
|
10632
|
+
`
|
|
10633
|
+
},
|
|
10634
|
+
{
|
|
10635
|
+
name: "upgrade-lark",
|
|
10636
|
+
hidden: false,
|
|
10637
|
+
summary: "Upgrade the Feishu/Lark plugin via @larksuite/openclaw-lark-tools",
|
|
10638
|
+
help: `USAGE
|
|
10639
|
+
${BIN} upgrade-lark [--scene=<scene>] [--caller=<n>] [--trace-id=<id>]
|
|
10640
|
+
|
|
10641
|
+
DESCRIPTION
|
|
10642
|
+
Upgrades the Feishu/Lark plugin by running:
|
|
10643
|
+
npx -y @larksuite/openclaw-lark-tools update --use-existing
|
|
10644
|
+
|
|
10645
|
+
Before the upgrade, the following files are backed up:
|
|
10646
|
+
- openclaw.json
|
|
10647
|
+
- extensions/openclaw-lark/ (if present)
|
|
10648
|
+
- extensions/feishu-openclaw-plugin/ (if present)
|
|
10649
|
+
After the upgrade, the result is validated:
|
|
10650
|
+
- feishu.accounts bot count must not decrease
|
|
10651
|
+
- gateway config structure must remain valid (port/mode/bind/auth/trustedProxies)
|
|
10652
|
+
If the upgrade command fails, or validation fails, the backed-up files are
|
|
10653
|
+
restored to roll back the changes.
|
|
10654
|
+
|
|
10655
|
+
Execution is logged to /tmp/openclaw-diagnose/upgrade-lark-<runId>.log.
|
|
10656
|
+
|
|
10657
|
+
Output is a single JSON object on stdout:
|
|
10658
|
+
{ "ok": true, "stdout": "...", "stderr": "...", "logFile": "..." }
|
|
10659
|
+
{ "ok": false, "error": "...", "stderr": "...", "exitCode": 1,
|
|
10660
|
+
"rollbackOk": true, "validationError": "...", "logFile": "..." }
|
|
10661
|
+
|
|
10662
|
+
OPTIONS
|
|
10663
|
+
--scene=<scene> Telemetry label forwarded to Slardar only.
|
|
10664
|
+
Known values: PageUpgradeLark, etc. Custom strings accepted.
|
|
10665
|
+
--caller=<name> Optional metadata passed to innerapi.
|
|
10666
|
+
--trace-id=<id> Optional log-correlation id.
|
|
10667
|
+
|
|
10668
|
+
EXIT CODES
|
|
10669
|
+
0 Success: upgrade ran and all validations passed.
|
|
10670
|
+
1 Failure: npx error, validation failed, or git commit failed.
|
|
10671
|
+
File rollback was attempted (see rollbackOk in the JSON output).
|
|
10501
10672
|
`
|
|
10502
10673
|
},
|
|
10503
10674
|
{
|
|
@@ -10531,6 +10702,41 @@ EXAMPLES
|
|
|
10531
10702
|
${BIN} rules # all rules
|
|
10532
10703
|
${BIN} rules --rule=gateway # single rule
|
|
10533
10704
|
${BIN} rules --rule=gateway --rule=feishu_channel # multiple rules
|
|
10705
|
+
`
|
|
10706
|
+
},
|
|
10707
|
+
{
|
|
10708
|
+
name: "channels-probe",
|
|
10709
|
+
hidden: true,
|
|
10710
|
+
summary: "Check feishu channel health via openclaw channels status --probe",
|
|
10711
|
+
help: `USAGE
|
|
10712
|
+
${BIN} channels-probe [--timeout=<ms>]
|
|
10713
|
+
|
|
10714
|
+
DESCRIPTION
|
|
10715
|
+
Runs \`openclaw channels status --probe\` and returns a structured JSON
|
|
10716
|
+
summary of whether the current environment's feishu channels are
|
|
10717
|
+
configured and working correctly.
|
|
10718
|
+
|
|
10719
|
+
Output:
|
|
10720
|
+
{
|
|
10721
|
+
"available": true,
|
|
10722
|
+
"gatewayReachable": true,
|
|
10723
|
+
"accounts": [
|
|
10724
|
+
{ "id": "default", "bits": ["enabled","configured","running","works"],
|
|
10725
|
+
"isWorking": true, "raw": "- Feishu default: ..." }
|
|
10726
|
+
],
|
|
10727
|
+
"anyAccountWorking": true
|
|
10728
|
+
}
|
|
10729
|
+
|
|
10730
|
+
An account is considered working when:
|
|
10731
|
+
enabled ∧ configured ∧ ( works ∨ ( running ∧ no error: ∧ no probe failed ) )
|
|
10732
|
+
|
|
10733
|
+
"available": false means the CLI invocation itself failed (openclaw not
|
|
10734
|
+
found, gateway unreachable, or no parseable output returned).
|
|
10735
|
+
|
|
10736
|
+
OPTIONS
|
|
10737
|
+
--timeout=<ms> Max wait in milliseconds (default: 60000). The probe
|
|
10738
|
+
can hang indefinitely on openclaw v2026.4.x due to a
|
|
10739
|
+
missing per-request HTTP timeout — set this accordingly.
|
|
10534
10740
|
`
|
|
10535
10741
|
},
|
|
10536
10742
|
{
|
|
@@ -10706,6 +10912,358 @@ function reportDoctorRunToSlardar(opts) {
|
|
|
10706
10912
|
}
|
|
10707
10913
|
});
|
|
10708
10914
|
}
|
|
10915
|
+
/** Read the tail of a file, up to maxBytes. Returns empty string on any error. */
|
|
10916
|
+
function readFileTail(filePath, maxBytes = 4e3) {
|
|
10917
|
+
try {
|
|
10918
|
+
const content = node_fs.default.readFileSync(filePath, "utf-8");
|
|
10919
|
+
if (content.length <= maxBytes) return content;
|
|
10920
|
+
return `...(truncated — showing last ${maxBytes} chars)...\n` + content.slice(-maxBytes);
|
|
10921
|
+
} catch {
|
|
10922
|
+
return "";
|
|
10923
|
+
}
|
|
10924
|
+
}
|
|
10925
|
+
function reportUpgradeLarkToSlardar(opts) {
|
|
10926
|
+
console.error(`[slardar] upgrade-lark reportTask scene=${opts.scene ?? ""} success=${opts.success} exitCode=${opts.exitCode ?? ""} rollbackOk=${opts.rollbackOk ?? ""}`);
|
|
10927
|
+
reportTask({
|
|
10928
|
+
eventName: "upgrade_lark_run",
|
|
10929
|
+
durationMs: opts.durationMs,
|
|
10930
|
+
status: opts.success ? "success" : "failed",
|
|
10931
|
+
extraCategories: {
|
|
10932
|
+
scene: opts.scene ?? "",
|
|
10933
|
+
exit_code: String(opts.exitCode ?? ""),
|
|
10934
|
+
rollback_ok: opts.rollbackOk != null ? String(opts.rollbackOk) : "",
|
|
10935
|
+
validation_error: (opts.validationError ?? "").slice(0, 200),
|
|
10936
|
+
error_msg: (opts.error ?? "").slice(0, 200)
|
|
10937
|
+
}
|
|
10938
|
+
});
|
|
10939
|
+
const logContent = readFileTail(opts.logFile);
|
|
10940
|
+
console.error(`[slardar] upgrade_lark_detail logFile=${opts.logFile} contentLength=${logContent.length} sendLogAvailable=${typeof Slardar.sendLog}`);
|
|
10941
|
+
reportLog({
|
|
10942
|
+
event: "upgrade_lark_detail",
|
|
10943
|
+
message: logContent || "(no log content)",
|
|
10944
|
+
level: opts.success ? "info" : "error",
|
|
10945
|
+
extra: {
|
|
10946
|
+
log_file: opts.logFile,
|
|
10947
|
+
scene: opts.scene ?? "",
|
|
10948
|
+
success: String(opts.success),
|
|
10949
|
+
exit_code: String(opts.exitCode ?? ""),
|
|
10950
|
+
rollback_ok: opts.rollbackOk != null ? String(opts.rollbackOk) : ""
|
|
10951
|
+
}
|
|
10952
|
+
});
|
|
10953
|
+
}
|
|
10954
|
+
//#endregion
|
|
10955
|
+
//#region src/upgrade-lark.ts
|
|
10956
|
+
/** Plugin directories under extensions/ that are backed up before upgrade */
|
|
10957
|
+
const FEISHU_PLUGIN_DIRS = ["openclaw-lark", "feishu-openclaw-plugin"];
|
|
10958
|
+
/** Version compat rule keys checked in the doctor output after install */
|
|
10959
|
+
const VERSION_COMPAT_RULE_KEYS = ["feishu_plugin_version_compat_lark", "feishu_plugin_version_compat_openclaw"];
|
|
10960
|
+
function backupFiles(opts) {
|
|
10961
|
+
const { workspaceDir, configPath, backupDir, log } = opts;
|
|
10962
|
+
try {
|
|
10963
|
+
node_fs.default.mkdirSync(backupDir, { recursive: true });
|
|
10964
|
+
log(`backup dir: ${backupDir}`);
|
|
10965
|
+
if (node_fs.default.existsSync(configPath)) {
|
|
10966
|
+
const stat = node_fs.default.statSync(configPath);
|
|
10967
|
+
node_fs.default.copyFileSync(configPath, node_path.default.join(backupDir, "openclaw.json"));
|
|
10968
|
+
log(` backed up: openclaw.json (${stat.size} bytes)`);
|
|
10969
|
+
} else log(` skipped: openclaw.json (not found)`);
|
|
10970
|
+
const extSrc = node_path.default.join(workspaceDir, "extensions");
|
|
10971
|
+
for (const pluginDir of FEISHU_PLUGIN_DIRS) {
|
|
10972
|
+
const src = node_path.default.join(extSrc, pluginDir);
|
|
10973
|
+
if (node_fs.default.existsSync(src)) {
|
|
10974
|
+
const dst = node_path.default.join(backupDir, "extensions", pluginDir);
|
|
10975
|
+
node_fs.default.cpSync(src, dst, { recursive: true });
|
|
10976
|
+
const version = readPkgVersion(node_path.default.join(src, "package.json"));
|
|
10977
|
+
log(` backed up: extensions/${pluginDir}${version ? ` (version: ${version})` : ""}`);
|
|
10978
|
+
} else log(` skipped: extensions/${pluginDir} (not found)`);
|
|
10979
|
+
}
|
|
10980
|
+
return { ok: true };
|
|
10981
|
+
} catch (e) {
|
|
10982
|
+
return {
|
|
10983
|
+
ok: false,
|
|
10984
|
+
error: `backup failed: ${e.message}`
|
|
10985
|
+
};
|
|
10986
|
+
}
|
|
10987
|
+
}
|
|
10988
|
+
function restoreFiles(opts) {
|
|
10989
|
+
const { workspaceDir, configPath, backupDir, log } = opts;
|
|
10990
|
+
try {
|
|
10991
|
+
const configBackup = node_path.default.join(backupDir, "openclaw.json");
|
|
10992
|
+
if (node_fs.default.existsSync(configBackup)) {
|
|
10993
|
+
node_fs.default.copyFileSync(configBackup, configPath);
|
|
10994
|
+
log(` restored: openclaw.json`);
|
|
10995
|
+
}
|
|
10996
|
+
const extDst = node_path.default.join(workspaceDir, "extensions");
|
|
10997
|
+
for (const pluginDir of FEISHU_PLUGIN_DIRS) {
|
|
10998
|
+
const backupSrc = node_path.default.join(backupDir, "extensions", pluginDir);
|
|
10999
|
+
if (node_fs.default.existsSync(backupSrc)) {
|
|
11000
|
+
const dst = node_path.default.join(extDst, pluginDir);
|
|
11001
|
+
if (node_fs.default.existsSync(dst)) node_fs.default.rmSync(dst, {
|
|
11002
|
+
recursive: true,
|
|
11003
|
+
force: true
|
|
11004
|
+
});
|
|
11005
|
+
node_fs.default.cpSync(backupSrc, dst, { recursive: true });
|
|
11006
|
+
log(` restored: extensions/${pluginDir}`);
|
|
11007
|
+
}
|
|
11008
|
+
}
|
|
11009
|
+
return true;
|
|
11010
|
+
} catch (e) {
|
|
11011
|
+
log(` restore error: ${e.message}`);
|
|
11012
|
+
return false;
|
|
11013
|
+
}
|
|
11014
|
+
}
|
|
11015
|
+
function readPkgVersion(pkgPath) {
|
|
11016
|
+
try {
|
|
11017
|
+
const pkg = JSON.parse(node_fs.default.readFileSync(pkgPath, "utf-8"));
|
|
11018
|
+
return typeof pkg.version === "string" ? pkg.version : null;
|
|
11019
|
+
} catch {
|
|
11020
|
+
return null;
|
|
11021
|
+
}
|
|
11022
|
+
}
|
|
11023
|
+
function snapshotVersions(cwd, log) {
|
|
11024
|
+
const ocResult = (0, node_child_process.spawnSync)("openclaw", ["--version"], {
|
|
11025
|
+
cwd,
|
|
11026
|
+
encoding: "utf-8",
|
|
11027
|
+
stdio: [
|
|
11028
|
+
"ignore",
|
|
11029
|
+
"pipe",
|
|
11030
|
+
"pipe"
|
|
11031
|
+
],
|
|
11032
|
+
timeout: 5e3
|
|
11033
|
+
});
|
|
11034
|
+
const ocRaw = (ocResult.stdout ?? "").trim() || (ocResult.stderr ?? "").trim();
|
|
11035
|
+
const extDir = node_path.default.join(cwd, "extensions");
|
|
11036
|
+
const larkPkg = node_path.default.join(extDir, "openclaw-lark", "package.json");
|
|
11037
|
+
const feishuPkg = node_path.default.join(extDir, "feishu-openclaw-plugin", "package.json");
|
|
11038
|
+
log(` version-check paths: ${larkPkg} [${node_fs.default.existsSync(larkPkg) ? "exists" : "missing"}]`);
|
|
11039
|
+
log(` version-check paths: ${feishuPkg} [${node_fs.default.existsSync(feishuPkg) ? "exists" : "missing"}]`);
|
|
11040
|
+
return {
|
|
11041
|
+
openclaw: ocRaw || null,
|
|
11042
|
+
openclawLark: readPkgVersion(larkPkg),
|
|
11043
|
+
feishuOpenclawPlugin: readPkgVersion(feishuPkg)
|
|
11044
|
+
};
|
|
11045
|
+
}
|
|
11046
|
+
function logVersionSnapshot(label, v, log) {
|
|
11047
|
+
log(`${label}: openclaw=${v.openclaw ?? "n/a"} openclaw-lark=${v.openclawLark ?? "n/a"} feishu-openclaw-plugin=${v.feishuOpenclawPlugin ?? "n/a"}`);
|
|
11048
|
+
}
|
|
11049
|
+
function countFeishuBots(configPath) {
|
|
11050
|
+
try {
|
|
11051
|
+
const raw = node_fs.default.readFileSync(configPath, "utf-8");
|
|
11052
|
+
const config = loadJSON5().parse(raw);
|
|
11053
|
+
const accounts = getNestedMap(config, "channels", "feishu", "accounts");
|
|
11054
|
+
if (accounts) return Object.keys(accounts).length;
|
|
11055
|
+
const feishu = getNestedMap(config, "channels", "feishu");
|
|
11056
|
+
return typeof feishu?.appId === "string" && feishu.appId ? 1 : 0;
|
|
11057
|
+
} catch {
|
|
11058
|
+
return 0;
|
|
11059
|
+
}
|
|
11060
|
+
}
|
|
11061
|
+
/**
|
|
11062
|
+
* Parse doctor stdout (first JSON line) and return an error string if any
|
|
11063
|
+
* version compat rule failed. Returns null on parse failure so a broken doctor
|
|
11064
|
+
* output does not block the install.
|
|
11065
|
+
*/
|
|
11066
|
+
function checkVersionCompatFromDoctorOutput(stdout, log) {
|
|
11067
|
+
const firstLine = stdout.split("\n")[0]?.trim();
|
|
11068
|
+
if (!firstLine) {
|
|
11069
|
+
log(" doctor(compat): empty output, skipping version compat check");
|
|
11070
|
+
return null;
|
|
11071
|
+
}
|
|
11072
|
+
try {
|
|
11073
|
+
const report = JSON.parse(firstLine);
|
|
11074
|
+
for (const outcome of report.results) if (VERSION_COMPAT_RULE_KEYS.includes(outcome.rule)) {
|
|
11075
|
+
if (outcome.status === "failed" || outcome.status === "still-broken" || outcome.status === "error") return `version compat rule ${outcome.rule} ${outcome.status}: ${outcome.message ?? "(no message)"}`;
|
|
11076
|
+
}
|
|
11077
|
+
return null;
|
|
11078
|
+
} catch (e) {
|
|
11079
|
+
log(` doctor(compat): failed to parse output — ${e.message}`);
|
|
11080
|
+
return null;
|
|
11081
|
+
}
|
|
11082
|
+
}
|
|
11083
|
+
/** Run channels probe, log results, and return the result. Never throws. */
|
|
11084
|
+
function probeChannels(label, log, timeoutMs) {
|
|
11085
|
+
try {
|
|
11086
|
+
const r = runChannelsProbe(timeoutMs);
|
|
11087
|
+
log(` ${label} available=${r.available} anyAccountWorking=${r.anyAccountWorking}`);
|
|
11088
|
+
if (r.error) log(` ${label} error: ${r.error}`);
|
|
11089
|
+
if (r.gatewayReachable != null) log(` ${label} gatewayReachable: ${r.gatewayReachable}`);
|
|
11090
|
+
for (const acct of r.accounts ?? []) log(` ${label} account ${acct.id}: isWorking=${acct.isWorking} bits=[${acct.bits.join(",")}]`);
|
|
11091
|
+
return r;
|
|
11092
|
+
} catch (e) {
|
|
11093
|
+
log(` ${label} channels probe threw: ${e.message}`);
|
|
11094
|
+
return {
|
|
11095
|
+
available: false,
|
|
11096
|
+
accounts: [],
|
|
11097
|
+
anyAccountWorking: false
|
|
11098
|
+
};
|
|
11099
|
+
}
|
|
11100
|
+
}
|
|
11101
|
+
function runUpgradeLark(opts) {
|
|
11102
|
+
const cwd = opts.cwd ?? "/home/gem/workspace/agent";
|
|
11103
|
+
const configPath = opts.configPath ?? CONFIG_PATH;
|
|
11104
|
+
const logFile = upgradeLarkLogFile(opts.runId);
|
|
11105
|
+
const log = makeLogger(logFile);
|
|
11106
|
+
const fsOpts = {
|
|
11107
|
+
workspaceDir: cwd,
|
|
11108
|
+
configPath,
|
|
11109
|
+
backupDir: node_path.default.join(opts.backupBaseDir ?? "/tmp/openclaw-diagnose", `upgrade-lark-backup-${opts.runId}`),
|
|
11110
|
+
log
|
|
11111
|
+
};
|
|
11112
|
+
const cliScript = opts.cliScript ?? process.argv[1];
|
|
11113
|
+
const statusCheckDelayMs = opts.statusCheckDelayMs ?? 5e3;
|
|
11114
|
+
log(`${"=".repeat(60)}`);
|
|
11115
|
+
log(`upgrade-lark started runId=${opts.runId}`);
|
|
11116
|
+
log(` cwd : ${cwd}`);
|
|
11117
|
+
log(` configPath : ${configPath}`);
|
|
11118
|
+
log(`${"=".repeat(60)}`);
|
|
11119
|
+
log("");
|
|
11120
|
+
log("── [1/8] 升级前状态快照 ──────────────────────────────────");
|
|
11121
|
+
log(`before-state: botCount=${countFeishuBots(configPath)}`);
|
|
11122
|
+
log("");
|
|
11123
|
+
log("── [2/8] 文件备份 ────────────────────────────────────────");
|
|
11124
|
+
const backup = backupFiles(fsOpts);
|
|
11125
|
+
if (!backup.ok) {
|
|
11126
|
+
log(`ERROR: ${backup.error}`);
|
|
11127
|
+
return {
|
|
11128
|
+
ok: false,
|
|
11129
|
+
error: backup.error,
|
|
11130
|
+
logFile
|
|
11131
|
+
};
|
|
11132
|
+
}
|
|
11133
|
+
log("backup: ok");
|
|
11134
|
+
logVersionSnapshot("before-versions", snapshotVersions(cwd, log), log);
|
|
11135
|
+
log("");
|
|
11136
|
+
log("── [3/8] channels probe(升级前)────────────────────────");
|
|
11137
|
+
const beforeChannels = probeChannels("before", log, 3e4);
|
|
11138
|
+
log("");
|
|
11139
|
+
log("── [4/8] 清理本地 openclaw shim ─────────────────────────");
|
|
11140
|
+
const localOpenclawBin = node_path.default.join(cwd, "node_modules", ".bin", "openclaw");
|
|
11141
|
+
if (node_fs.default.existsSync(localOpenclawBin)) try {
|
|
11142
|
+
node_fs.default.rmSync(localOpenclawBin);
|
|
11143
|
+
log(` removed: ${localOpenclawBin}`);
|
|
11144
|
+
} catch (e) {
|
|
11145
|
+
log(` WARN: failed to remove ${localOpenclawBin}: ${e.message}`);
|
|
11146
|
+
}
|
|
11147
|
+
else log(` skipped: ${localOpenclawBin} (not found)`);
|
|
11148
|
+
log("");
|
|
11149
|
+
log("── [5/8] npx install (@larksuite/openclaw-lark-tools update) ──");
|
|
11150
|
+
const npxResult = (0, node_child_process.spawnSync)("npx", [
|
|
11151
|
+
"-y",
|
|
11152
|
+
"@larksuite/openclaw-lark-tools",
|
|
11153
|
+
"update"
|
|
11154
|
+
], {
|
|
11155
|
+
cwd,
|
|
11156
|
+
encoding: "utf-8",
|
|
11157
|
+
stdio: [
|
|
11158
|
+
"ignore",
|
|
11159
|
+
"pipe",
|
|
11160
|
+
"pipe"
|
|
11161
|
+
],
|
|
11162
|
+
timeout: 12e4
|
|
11163
|
+
});
|
|
11164
|
+
const npxStdout = npxResult.stdout?.trim() ?? "";
|
|
11165
|
+
const npxStderr = npxResult.stderr?.trim() ?? "";
|
|
11166
|
+
const npxExitCode = npxResult.status ?? 1;
|
|
11167
|
+
if (npxStdout) log(`npx stdout:\n${npxStdout}`);
|
|
11168
|
+
if (npxStderr) log(`npx stderr:\n${npxStderr}`);
|
|
11169
|
+
log(`npx exit: ${npxExitCode}${npxResult.error ? ` error: ${npxResult.error.message}` : ""}`);
|
|
11170
|
+
if (statusCheckDelayMs > 0) {
|
|
11171
|
+
log("");
|
|
11172
|
+
log(`── 等待 ${statusCheckDelayMs / 1e3}s(让 openclaw 服务完成重启) ─────────────`);
|
|
11173
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, statusCheckDelayMs);
|
|
11174
|
+
log("wait done");
|
|
11175
|
+
}
|
|
11176
|
+
const doRollback = (reason) => {
|
|
11177
|
+
log(`ERROR: ${reason}`);
|
|
11178
|
+
const rollbackOk = restoreFiles(fsOpts);
|
|
11179
|
+
log(`rollback: ${rollbackOk ? "ok" : "FAILED"}`);
|
|
11180
|
+
return {
|
|
11181
|
+
ok: false,
|
|
11182
|
+
error: reason,
|
|
11183
|
+
validationError: reason,
|
|
11184
|
+
stdout: npxStdout,
|
|
11185
|
+
stderr: npxStderr,
|
|
11186
|
+
exitCode: npxExitCode,
|
|
11187
|
+
rollbackOk,
|
|
11188
|
+
logFile
|
|
11189
|
+
};
|
|
11190
|
+
};
|
|
11191
|
+
log("");
|
|
11192
|
+
log("── [6/8] 插件安装检查 + 版本兼容校验 ───────────────────");
|
|
11193
|
+
const larkExtDir = node_path.default.join(cwd, "extensions", "openclaw-lark");
|
|
11194
|
+
const larkVersion = readPkgVersion(node_path.default.join(larkExtDir, "package.json"));
|
|
11195
|
+
log(` extensions/openclaw-lark: ${node_fs.default.existsSync(larkExtDir) ? "exists" : "missing"}, version=${larkVersion ?? "n/a"}`);
|
|
11196
|
+
if (!node_fs.default.existsSync(larkExtDir)) return doRollback("extensions/openclaw-lark not found after install");
|
|
11197
|
+
if (!larkVersion) return doRollback("extensions/openclaw-lark/package.json has no valid version after install");
|
|
11198
|
+
log(" running doctor version compat check...");
|
|
11199
|
+
const compatArgs = ["doctor"];
|
|
11200
|
+
if (opts.scene) compatArgs.push(`--scene=${opts.scene}`);
|
|
11201
|
+
const compatResult = (0, node_child_process.spawnSync)(process.execPath, [cliScript, ...compatArgs], {
|
|
11202
|
+
cwd,
|
|
11203
|
+
encoding: "utf-8",
|
|
11204
|
+
stdio: [
|
|
11205
|
+
"ignore",
|
|
11206
|
+
"pipe",
|
|
11207
|
+
"pipe"
|
|
11208
|
+
],
|
|
11209
|
+
timeout: 6e4,
|
|
11210
|
+
env: process.env
|
|
11211
|
+
});
|
|
11212
|
+
if (compatResult.stdout?.trim()) log(`doctor(compat) stdout:\n${compatResult.stdout.trim()}`);
|
|
11213
|
+
if (compatResult.stderr?.trim()) log(`doctor(compat) stderr:\n${compatResult.stderr.trim()}`);
|
|
11214
|
+
log(`doctor(compat) exit: ${compatResult.status ?? "null"}${compatResult.error ? ` error: ${compatResult.error.message}` : ""}`);
|
|
11215
|
+
const compatError = checkVersionCompatFromDoctorOutput(compatResult.stdout?.trim() ?? "", log);
|
|
11216
|
+
if (compatError) return doRollback(compatError);
|
|
11217
|
+
log(" version compat: ok");
|
|
11218
|
+
logVersionSnapshot("after-versions", snapshotVersions(cwd, log), log);
|
|
11219
|
+
log("");
|
|
11220
|
+
log("── [7/8] channels probe(升级后)────────────────────────");
|
|
11221
|
+
if (!probeChannels("after", log, 3e4).anyAccountWorking) {
|
|
11222
|
+
if (!beforeChannels.anyAccountWorking) {
|
|
11223
|
+
log(" channels: not working before or after install — pre-existing issue, skipping rollback");
|
|
11224
|
+
return {
|
|
11225
|
+
ok: false,
|
|
11226
|
+
error: "channels probe: no working account (pre-existing issue, not caused by install)",
|
|
11227
|
+
validationError: "channels probe: no working account (pre-existing)",
|
|
11228
|
+
stdout: npxStdout,
|
|
11229
|
+
stderr: npxStderr,
|
|
11230
|
+
exitCode: npxExitCode,
|
|
11231
|
+
logFile
|
|
11232
|
+
};
|
|
11233
|
+
}
|
|
11234
|
+
return doRollback("channels probe: no working account after install (was working before)");
|
|
11235
|
+
}
|
|
11236
|
+
log(" channels: ok");
|
|
11237
|
+
log("");
|
|
11238
|
+
log("── [8/8] doctor --fix ────────────────────────────────────");
|
|
11239
|
+
const fixArgs = ["doctor", "--fix"];
|
|
11240
|
+
if (opts.scene) fixArgs.push(`--scene=${opts.scene}`);
|
|
11241
|
+
const fixResult = (0, node_child_process.spawnSync)(process.execPath, [cliScript, ...fixArgs], {
|
|
11242
|
+
cwd,
|
|
11243
|
+
encoding: "utf-8",
|
|
11244
|
+
stdio: [
|
|
11245
|
+
"ignore",
|
|
11246
|
+
"pipe",
|
|
11247
|
+
"pipe"
|
|
11248
|
+
],
|
|
11249
|
+
timeout: 6e4,
|
|
11250
|
+
env: process.env
|
|
11251
|
+
});
|
|
11252
|
+
if (fixResult.stdout?.trim()) log(`doctor(fix) stdout:\n${fixResult.stdout.trim()}`);
|
|
11253
|
+
if (fixResult.stderr?.trim()) log(`doctor(fix) stderr:\n${fixResult.stderr.trim()}`);
|
|
11254
|
+
log(`doctor(fix) exit: ${fixResult.status ?? "null"}${fixResult.error ? ` error: ${fixResult.error.message}` : ""}`);
|
|
11255
|
+
log("");
|
|
11256
|
+
log(`${"=".repeat(60)}`);
|
|
11257
|
+
log("upgrade-lark completed successfully");
|
|
11258
|
+
log(`${"=".repeat(60)}`);
|
|
11259
|
+
return {
|
|
11260
|
+
ok: true,
|
|
11261
|
+
stdout: npxStdout,
|
|
11262
|
+
stderr: npxStderr,
|
|
11263
|
+
exitCode: npxExitCode,
|
|
11264
|
+
logFile
|
|
11265
|
+
};
|
|
11266
|
+
}
|
|
10709
11267
|
//#endregion
|
|
10710
11268
|
//#region src/index.ts
|
|
10711
11269
|
const args = node_process.default.argv.slice(2);
|
|
@@ -11223,6 +11781,50 @@ async function main() {
|
|
|
11223
11781
|
if (!result.ok) node_process.default.exit(1);
|
|
11224
11782
|
break;
|
|
11225
11783
|
}
|
|
11784
|
+
case "upgrade-lark": {
|
|
11785
|
+
const result = runUpgradeLark({
|
|
11786
|
+
runId: rc.runId,
|
|
11787
|
+
scene
|
|
11788
|
+
});
|
|
11789
|
+
const upgradeDurationMs = Date.now() - t0;
|
|
11790
|
+
console.log(JSON.stringify(result));
|
|
11791
|
+
reportUpgradeLarkToSlardar({
|
|
11792
|
+
scene,
|
|
11793
|
+
durationMs: upgradeDurationMs,
|
|
11794
|
+
success: result.ok,
|
|
11795
|
+
logFile: result.logFile,
|
|
11796
|
+
exitCode: result.exitCode,
|
|
11797
|
+
rollbackOk: result.rollbackOk,
|
|
11798
|
+
validationError: result.validationError,
|
|
11799
|
+
error: result.error
|
|
11800
|
+
});
|
|
11801
|
+
try {
|
|
11802
|
+
await reportCliRun({
|
|
11803
|
+
command: "upgrade-lark",
|
|
11804
|
+
runId: rc.runId,
|
|
11805
|
+
version: getVersion(),
|
|
11806
|
+
invocation: args.join(" "),
|
|
11807
|
+
durationMs: upgradeDurationMs,
|
|
11808
|
+
caller: rc.caller,
|
|
11809
|
+
traceId: rc.traceId,
|
|
11810
|
+
success: result.ok,
|
|
11811
|
+
result,
|
|
11812
|
+
error: result.ok ? void 0 : { message: result.error ?? "upgrade-lark failed" }
|
|
11813
|
+
});
|
|
11814
|
+
} catch (e) {
|
|
11815
|
+
console.error(`[telemetry] reportCliRun failed: ${e.message}`);
|
|
11816
|
+
}
|
|
11817
|
+
if (!result.ok) {
|
|
11818
|
+
node_process.default.exitCode = 1;
|
|
11819
|
+
return;
|
|
11820
|
+
}
|
|
11821
|
+
break;
|
|
11822
|
+
}
|
|
11823
|
+
case "channels-probe": {
|
|
11824
|
+
const result = runChannelsProbe(getFlag(args, "timeout") ? Number(getFlag(args, "timeout")) : void 0);
|
|
11825
|
+
console.log(JSON.stringify(result));
|
|
11826
|
+
break;
|
|
11827
|
+
}
|
|
11226
11828
|
default:
|
|
11227
11829
|
node_process.default.stderr.write(`Unknown command: ${mode}\n\n`);
|
|
11228
11830
|
node_process.default.stderr.write(formatTopLevelHelp(helpFlags.expert));
|