@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.
Files changed (2) hide show
  1. package/dist/index.cjs +605 -3
  2. 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.13";
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.13`;
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));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/openclaw-scripts-diagnose-cli",
3
- "version": "0.1.13",
3
+ "version": "0.1.14-alpha.1",
4
4
  "description": "CLI for OpenClaw config diagnose and repair with JSON5 support",
5
5
  "main": "dist/index.cjs",
6
6
  "bin": {