@lark-apaas/openclaw-scripts-diagnose-cli 0.1.14-alpha.1 → 0.1.14-alpha.10

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 +300 -233
  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.14-alpha.1";
55
+ return "0.1.14-alpha.10";
56
56
  }
57
57
  //#endregion
58
58
  //#region src/rule-engine/base.ts
@@ -3367,9 +3367,9 @@ let FeishuPluginOpenclawUpgradeRule = class FeishuPluginOpenclawUpgradeRule exte
3367
3367
  validate(ctx) {
3368
3368
  const cc = resolveCompatContext(ctx);
3369
3369
  if (!cc) return { pass: true };
3370
- if (!cc.recommendedOc) return { pass: true };
3371
3370
  const { ocCur, recommendedOc, installed, isLegacy } = cc;
3372
3371
  if (isForkPlugin(installed)) return validateForkPlugin(installed, ocCur, recommendedOc);
3372
+ if (!recommendedOc) return { pass: true };
3373
3373
  if (resolveUpgradeDirection(installed, ocCur, recommendedOc, isLegacy) !== "openclaw") return { pass: true };
3374
3374
  return {
3375
3375
  pass: false,
@@ -3397,19 +3397,17 @@ 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
- }
3408
- if (resolveUpgradeDirection(installed, ocCur, recommendedOc, isLegacy) !== "lark") return { pass: true };
3400
+ if (!isLarkUpgradeNeededFromCC(cc)) return { pass: true };
3401
+ const prefix = buildCompatPrefix(installed, ocCur, isLegacy);
3402
+ if (!recommendedOc) return {
3403
+ pass: false,
3404
+ action: "upgrade_lark",
3405
+ message: `${prefix};建议升级飞书插件至兼容版本`
3406
+ };
3409
3407
  return {
3410
3408
  pass: false,
3411
3409
  action: "upgrade_lark",
3412
- message: `${buildCompatPrefix(installed, ocCur, isLegacy)};当前 openclaw@${ocCur} 已达推荐版本,可直接升级飞书插件`
3410
+ message: `${prefix};当前 openclaw@${ocCur} 已达推荐版本,可直接升级飞书插件`
3413
3411
  };
3414
3412
  }
3415
3413
  };
@@ -3421,6 +3419,18 @@ FeishuPluginLarkUpgradeRule = __decorate([Rule({
3421
3419
  level: "critical",
3422
3420
  usesVars: ["recommendedOpenclawTag"]
3423
3421
  })], FeishuPluginLarkUpgradeRule);
3422
+ /**
3423
+ * Core predicate: returns true when the lark plugin needs upgrading for a
3424
+ * non-fork plugin, based on version compatibility with the current openclaw.
3425
+ *
3426
+ * Shared by FeishuPluginLarkUpgradeRule.validate and needsLarkUpgrade.
3427
+ * Callers must handle fork plugin cases before invoking this function.
3428
+ */
3429
+ function isLarkUpgradeNeededFromCC(cc) {
3430
+ const { ocCur, recommendedOc, installed, isLegacy } = cc;
3431
+ if (!recommendedOc) return isLegacy || !isVersionCompatible(installed, ocCur);
3432
+ return resolveUpgradeDirection(installed, ocCur, recommendedOc, isLegacy) === "lark";
3433
+ }
3424
3434
  function isForkPlugin(p) {
3425
3435
  return p.scope != null && FORK_SCOPES.includes(p.scope);
3426
3436
  }
@@ -3459,14 +3469,16 @@ function describeCompatConstraint(entry, pluginVersion) {
3459
3469
  /**
3460
3470
  * @lark-apaas/openclaw-lark 豁免 VERSION_COMPAT_MAP,但仍要求 openclaw ≥ FORK_LARK_PLUGIN_MIN_OC_VERSION。
3461
3471
  * 其他 @lark-apaas scope 的 fork 插件继续无条件 pass。
3472
+ * recommendedOc 可为 undefined(doctor 模式),此时只检测最低版本要求,不指定目标升级版本。
3462
3473
  */
3463
3474
  function validateForkPlugin(installed, ocCur, recommendedOc) {
3464
3475
  if (installed.fullName !== FORK_LARK_PLUGIN_FULL_NAME) return { pass: true };
3465
3476
  if (compareCalVer(ocCur, FORK_LARK_PLUGIN_MIN_OC_VERSION) >= 0) return { pass: true };
3477
+ const recommendation = recommendedOc ? `;将 openclaw 升级到 ${recommendedOc} 即可满足` : `;请升级 openclaw 至 ${FORK_LARK_PLUGIN_MIN_OC_VERSION} 或更高版本`;
3466
3478
  return {
3467
3479
  pass: false,
3468
3480
  action: "upgrade_openclaw",
3469
- message: `飞书插件 ${describePlugin(installed)}(fork 版)要求 openclaw ≥ ${FORK_LARK_PLUGIN_MIN_OC_VERSION},当前 openclaw@${ocCur} 低于此要求;将 openclaw 升级到 ${recommendedOc} 即可满足`
3481
+ message: `飞书插件 ${describePlugin(installed)}(fork 版)要求 openclaw ≥ ${FORK_LARK_PLUGIN_MIN_OC_VERSION},当前 openclaw@${ocCur} 低于此要求${recommendation}`
3470
3482
  };
3471
3483
  }
3472
3484
  function describePlugin(p) {
@@ -3513,6 +3525,187 @@ function extractScopedNameFromSpec$1(spec) {
3513
3525
  const at = spec.indexOf("@", 1);
3514
3526
  return at === -1 ? spec : spec.slice(0, at);
3515
3527
  }
3528
+ /**
3529
+ * Returns true if the installed feishu plugin is version-incompatible with
3530
+ * the current openclaw (or is a legacy plugin that must be replaced).
3531
+ * Used by the upgrade_lark_needed rule and the upgrade-lark pre-check gate.
3532
+ */
3533
+ function needsLarkUpgrade(ctx) {
3534
+ const cc = resolveCompatContext(ctx);
3535
+ if (!cc) return false;
3536
+ const { ocCur, recommendedOc, installed } = cc;
3537
+ if (isForkPlugin(installed)) {
3538
+ if (recommendedOc) return false;
3539
+ if (installed.fullName === FORK_LARK_PLUGIN_FULL_NAME) return compareCalVer(ocCur, FORK_LARK_PLUGIN_MIN_OC_VERSION) < 0;
3540
+ return false;
3541
+ }
3542
+ return isLarkUpgradeNeededFromCC(cc);
3543
+ }
3544
+ //#endregion
3545
+ //#region src/channels-probe.ts
3546
+ const FEISHU_INVALID_CONFIG_MSG = "channels.feishu: invalid config: must NOT have additional properties";
3547
+ const CHANNEL_LINE_RE = /^-\s+Feishu\s+([^:]+):\s+(.+)$/;
3548
+ /**
3549
+ * Port of Python `_account_is_working` from the feishu-channel-success-rate skill.
3550
+ *
3551
+ * Strips colon-prefixed key:value bits (dm:, bot:, in:, out:, token:, allow:,
3552
+ * intents:, groups:, health:) and evaluates the canonical health formula.
3553
+ */
3554
+ function accountIsWorking(bits) {
3555
+ const bitTokens = /* @__PURE__ */ new Set();
3556
+ let hasError = false;
3557
+ let hasProbeFailed = false;
3558
+ for (const raw of bits) {
3559
+ const b = raw.trim();
3560
+ if (!b) continue;
3561
+ if (b.startsWith("error:")) {
3562
+ hasError = true;
3563
+ continue;
3564
+ }
3565
+ if (b === "probe failed") {
3566
+ hasProbeFailed = true;
3567
+ continue;
3568
+ }
3569
+ bitTokens.add(b.split(":")[0]);
3570
+ }
3571
+ if (!bitTokens.has("enabled") || !bitTokens.has("configured")) return false;
3572
+ if (bitTokens.has("works")) return true;
3573
+ if (bitTokens.has("running") && !hasError && !hasProbeFailed) return true;
3574
+ return false;
3575
+ }
3576
+ /**
3577
+ * Parse the raw stdout of `openclaw channels status --probe`.
3578
+ * Port of Python `extract_channels_probe` from the feishu-channel-success-rate skill.
3579
+ */
3580
+ function parseChannelsProbeOutput(text) {
3581
+ const gatewayReachable = text.includes("Gateway reachable");
3582
+ const feishuConfigInvalid = text.includes(FEISHU_INVALID_CONFIG_MSG);
3583
+ const accounts = [];
3584
+ let anyAccountWorking = false;
3585
+ for (const line of text.split("\n")) {
3586
+ const m = CHANNEL_LINE_RE.exec(line.trim());
3587
+ if (!m) continue;
3588
+ const [, acct, rest] = m;
3589
+ const bits = rest.split(",").map((b) => b.trim());
3590
+ const isWorking = accountIsWorking(bits);
3591
+ if (isWorking) anyAccountWorking = true;
3592
+ accounts.push({
3593
+ id: acct.trim(),
3594
+ bits,
3595
+ isWorking,
3596
+ raw: line.trim()
3597
+ });
3598
+ }
3599
+ return {
3600
+ gatewayReachable,
3601
+ feishuConfigInvalid,
3602
+ accounts,
3603
+ anyAccountWorking
3604
+ };
3605
+ }
3606
+ /**
3607
+ * Run `openclaw channels status --probe` and return a structured result.
3608
+ *
3609
+ * The command may exit non-zero when some bot accounts fail their probe — that
3610
+ * is still useful output. We therefore try to parse stdout even when the
3611
+ * process exits with a non-zero code, falling back to an unavailable result
3612
+ * only when there is genuinely no output to parse.
3613
+ *
3614
+ * @param timeoutMs Maximum wait time. Default is 60 s because v2026.4.x
3615
+ * lacks a per-request HTTP timeout and can block indefinitely.
3616
+ */
3617
+ function runChannelsProbe(timeoutMs = 6e4) {
3618
+ let stdout = "";
3619
+ let stderrText = "";
3620
+ let execError;
3621
+ try {
3622
+ stdout = (0, node_child_process.execSync)("openclaw channels status --probe", {
3623
+ encoding: "utf-8",
3624
+ timeout: timeoutMs,
3625
+ stdio: [
3626
+ "ignore",
3627
+ "pipe",
3628
+ "pipe"
3629
+ ]
3630
+ });
3631
+ } catch (e) {
3632
+ const err = e;
3633
+ const stdoutRaw = err.stdout;
3634
+ stdout = typeof stdoutRaw === "string" ? stdoutRaw : stdoutRaw?.toString("utf-8") ?? "";
3635
+ execError = err.message;
3636
+ const stderrRaw = err.stderr;
3637
+ stderrText = (typeof stderrRaw === "string" ? stderrRaw : stderrRaw?.toString("utf-8") ?? "").trim();
3638
+ if (stderrText) console.error(`channels-probe: stderr from CLI: ${stderrText}`);
3639
+ }
3640
+ if (stdout.trim()) return {
3641
+ available: true,
3642
+ ...parseChannelsProbeOutput(stdout)
3643
+ };
3644
+ return {
3645
+ available: false,
3646
+ gatewayReachable: false,
3647
+ feishuConfigInvalid: stderrText.includes(FEISHU_INVALID_CONFIG_MSG),
3648
+ accounts: [],
3649
+ anyAccountWorking: false,
3650
+ error: execError ?? "no output from openclaw channels status --probe"
3651
+ };
3652
+ }
3653
+ //#endregion
3654
+ //#region src/rules/upgrade-lark-needed.ts
3655
+ /**
3656
+ * Detects the condition that warrants running `upgrade-lark`:
3657
+ * - feishu plugin version incompatible with current openclaw, OR
3658
+ * - openclaw channels status --probe reports feishu channel config invalid; AND
3659
+ * - channels are not working.
3660
+ *
3661
+ * Both conditions must be true simultaneously. If version is compatible and
3662
+ * feishu config is valid, or channels are working, the rule passes (no action needed).
3663
+ *
3664
+ * feishuConfigInvalid is read from the channels probe output rather than running a
3665
+ * separate `openclaw status` call, since only `openclaw channels status --probe`
3666
+ * reliably surfaces the schema validation error.
3667
+ *
3668
+ * profile: experimental — runs only in full sweep mode, not in standard doctor.
3669
+ * level: silent — telemetry/sweep-only, does not trigger page-level repair UI.
3670
+ */
3671
+ let UpgradeLarkNeededRule = class UpgradeLarkNeededRule extends DiagnoseRule {
3672
+ validate(ctx) {
3673
+ let versionIncompatible = false;
3674
+ try {
3675
+ versionIncompatible = needsLarkUpgrade(ctx);
3676
+ } catch {
3677
+ versionIncompatible = true;
3678
+ }
3679
+ let probeResult;
3680
+ try {
3681
+ probeResult = runChannelsProbe(6e4);
3682
+ } catch {
3683
+ probeResult = {
3684
+ available: false,
3685
+ gatewayReachable: false,
3686
+ feishuConfigInvalid: false,
3687
+ accounts: [],
3688
+ anyAccountWorking: false
3689
+ };
3690
+ }
3691
+ const feishuConfigInvalid = probeResult.feishuConfigInvalid;
3692
+ if (!(versionIncompatible || feishuConfigInvalid)) return { pass: true };
3693
+ if (probeResult.anyAccountWorking) return { pass: true };
3694
+ return {
3695
+ pass: false,
3696
+ action: "upgrade_lark",
3697
+ message: `飞书插件需要升级且 channels 不可用(版本不兼容=${versionIncompatible}, feishu配置无效=${feishuConfigInvalid}),建议执行 upgrade-lark 命令升级飞书插件`
3698
+ };
3699
+ }
3700
+ };
3701
+ UpgradeLarkNeededRule = __decorate([Rule({
3702
+ key: "upgrade_lark_needed",
3703
+ description: "检测飞书插件版本不兼容且 channels 不可用,判断是否需要执行 upgrade-lark 升级",
3704
+ repairMode: "check-only",
3705
+ level: "silent",
3706
+ profile: "experimental",
3707
+ usesVars: ["recommendedOpenclawTag"]
3708
+ })], UpgradeLarkNeededRule);
3516
3709
  //#endregion
3517
3710
  //#region src/rules/cleanup-install-backup-dirs.ts
3518
3711
  const DIR_PREFIX = ".openclaw-install-";
@@ -6236,23 +6429,6 @@ function reportError(params) {
6236
6429
  } catch {}
6237
6430
  }
6238
6431
  //#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
6256
6432
  //#region src/install-cli.ts
6257
6433
  const LARK_CLI_NAME = "lark-cli";
6258
6434
  const AGENT_SKILLS_NAME = "agent-skills";
@@ -10149,109 +10325,6 @@ function finalize(results, aborted) {
10149
10325
  };
10150
10326
  }
10151
10327
  //#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
10255
10328
  //#region src/innerapi/reportCliRun.ts
10256
10329
  /**
10257
10330
  * CLI-side client for studio_server's `openclaw.report_cli_run` inner
@@ -10331,7 +10404,7 @@ async function reportCliRun(opts) {
10331
10404
  //#region src/help.ts
10332
10405
  const BIN = "mclaw-diagnose";
10333
10406
  function versionBanner() {
10334
- return `v0.1.14-alpha.1`;
10407
+ return `v0.1.14-alpha.10`;
10335
10408
  }
10336
10409
  const COMMANDS = [
10337
10410
  {
@@ -10912,18 +10985,16 @@ function reportDoctorRunToSlardar(opts) {
10912
10985
  }
10913
10986
  });
10914
10987
  }
10915
- /** Read the tail of a file, up to maxBytes. Returns empty string on any error. */
10916
- function readFileTail(filePath, maxBytes = 4e3) {
10988
+ function readLogFile(filePath) {
10917
10989
  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);
10990
+ return node_fs.default.readFileSync(filePath, "utf-8");
10921
10991
  } catch {
10922
10992
  return "";
10923
10993
  }
10924
10994
  }
10925
10995
  function reportUpgradeLarkToSlardar(opts) {
10926
- console.error(`[slardar] upgrade-lark reportTask scene=${opts.scene ?? ""} success=${opts.success} exitCode=${opts.exitCode ?? ""} rollbackOk=${opts.rollbackOk ?? ""}`);
10996
+ console.error(`[slardar] upgrade_lark_run scene=${opts.scene ?? ""} success=${opts.success} exitCode=${opts.exitCode ?? ""} rollbackOk=${opts.rollbackOk ?? ""}`);
10997
+ const logContent = readLogFile(opts.logFile);
10927
10998
  reportTask({
10928
10999
  eventName: "upgrade_lark_run",
10929
11000
  durationMs: opts.durationMs,
@@ -10932,22 +11003,9 @@ function reportUpgradeLarkToSlardar(opts) {
10932
11003
  scene: opts.scene ?? "",
10933
11004
  exit_code: String(opts.exitCode ?? ""),
10934
11005
  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) : ""
11006
+ validation_error: opts.validationError ?? "",
11007
+ error_msg: opts.error ?? "",
11008
+ log_content: logContent
10951
11009
  }
10952
11010
  });
10953
11011
  }
@@ -10955,8 +11013,6 @@ function reportUpgradeLarkToSlardar(opts) {
10955
11013
  //#region src/upgrade-lark.ts
10956
11014
  /** Plugin directories under extensions/ that are backed up before upgrade */
10957
11015
  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
11016
  function backupFiles(opts) {
10961
11017
  const { workspaceDir, configPath, backupDir, log } = opts;
10962
11018
  try {
@@ -11058,28 +11114,6 @@ function countFeishuBots(configPath) {
11058
11114
  return 0;
11059
11115
  }
11060
11116
  }
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
11117
  /** Run channels probe, log results, and return the result. Never throws. */
11084
11118
  function probeChannels(label, log, timeoutMs) {
11085
11119
  try {
@@ -11093,6 +11127,8 @@ function probeChannels(label, log, timeoutMs) {
11093
11127
  log(` ${label} channels probe threw: ${e.message}`);
11094
11128
  return {
11095
11129
  available: false,
11130
+ gatewayReachable: false,
11131
+ feishuConfigInvalid: false,
11096
11132
  accounts: [],
11097
11133
  anyAccountWorking: false
11098
11134
  };
@@ -11117,10 +11153,64 @@ function runUpgradeLark(opts) {
11117
11153
  log(` configPath : ${configPath}`);
11118
11154
  log(`${"=".repeat(60)}`);
11119
11155
  log("");
11120
- log("── [1/8] 升级前状态快照 ──────────────────────────────────");
11121
- log(`before-state: botCount=${countFeishuBots(configPath)}`);
11156
+ log("── [Pre-check A] channels probe(升级前)────────────────");
11157
+ const beforeChannels = probeChannels("before", log, 6e4);
11122
11158
  log("");
11123
- log("── [2/8] 文件备份 ────────────────────────────────────────");
11159
+ log("── [Pre-check B] 版本兼容预检 ───────────────────────────");
11160
+ let versionIncompatible = false;
11161
+ try {
11162
+ const rawConfig = node_fs.default.readFileSync(configPath, "utf-8");
11163
+ versionIncompatible = needsLarkUpgrade({
11164
+ config: loadJSON5().parse(rawConfig),
11165
+ configPath,
11166
+ vars: {},
11167
+ providerDeps: {
11168
+ usesMiaodaProvider: false,
11169
+ usesMiaodaSecretProvider: false
11170
+ }
11171
+ });
11172
+ log(` version-compat pre-check: ${versionIncompatible ? "NEEDS_UPGRADE" : "ok"}`);
11173
+ } catch (e) {
11174
+ log(` version-compat pre-check error: ${e.message} — treating as needs-upgrade`);
11175
+ versionIncompatible = true;
11176
+ }
11177
+ const feishuConfigInvalid = beforeChannels.feishuConfigInvalid;
11178
+ log(` feishu config invalid : ${feishuConfigInvalid}`);
11179
+ log("");
11180
+ log("── [Gate] 升级前置条件检查 ───────────────────────────────");
11181
+ log(` versionIncompatible : ${versionIncompatible}`);
11182
+ log(` feishuConfigInvalid : ${feishuConfigInvalid}`);
11183
+ log(` channels working before: ${beforeChannels.anyAccountWorking}`);
11184
+ if (!(versionIncompatible || feishuConfigInvalid)) {
11185
+ const reason = "version compatible and feishu channel config valid — upgrade not needed";
11186
+ log(` SKIP: ${reason}`);
11187
+ log(`${"=".repeat(60)}`);
11188
+ log("upgrade-lark skipped (pre-check gate)");
11189
+ log(`${"=".repeat(60)}`);
11190
+ return {
11191
+ ok: true,
11192
+ skipped: true,
11193
+ skipReason: reason,
11194
+ logFile
11195
+ };
11196
+ }
11197
+ if (beforeChannels.anyAccountWorking) {
11198
+ const reason = "channels are working — upgrade not needed (issue detected but system is functional)";
11199
+ log(` SKIP: ${reason}`);
11200
+ log(`${"=".repeat(60)}`);
11201
+ log("upgrade-lark skipped (pre-check gate)");
11202
+ log(`${"=".repeat(60)}`);
11203
+ return {
11204
+ ok: true,
11205
+ skipped: true,
11206
+ skipReason: reason,
11207
+ logFile
11208
+ };
11209
+ }
11210
+ log(` PROCEED: requiresLarkUpgrade=true (version=${versionIncompatible}, feishuConfig=${feishuConfigInvalid}) AND channels not working → running upgrade`);
11211
+ log("");
11212
+ log("── [1/6] 文件备份 ────────────────────────────────────────");
11213
+ log(`before-state: botCount=${countFeishuBots(configPath)}`);
11124
11214
  const backup = backupFiles(fsOpts);
11125
11215
  if (!backup.ok) {
11126
11216
  log(`ERROR: ${backup.error}`);
@@ -11133,10 +11223,7 @@ function runUpgradeLark(opts) {
11133
11223
  log("backup: ok");
11134
11224
  logVersionSnapshot("before-versions", snapshotVersions(cwd, log), log);
11135
11225
  log("");
11136
- log("── [3/8] channels probe(升级前)────────────────────────");
11137
- const beforeChannels = probeChannels("before", log, 3e4);
11138
- log("");
11139
- log("── [4/8] 清理本地 openclaw shim ─────────────────────────");
11226
+ log("── [2/6] 清理本地 openclaw shim ─────────────────────────");
11140
11227
  const localOpenclawBin = node_path.default.join(cwd, "node_modules", ".bin", "openclaw");
11141
11228
  if (node_fs.default.existsSync(localOpenclawBin)) try {
11142
11229
  node_fs.default.rmSync(localOpenclawBin);
@@ -11146,7 +11233,7 @@ function runUpgradeLark(opts) {
11146
11233
  }
11147
11234
  else log(` skipped: ${localOpenclawBin} (not found)`);
11148
11235
  log("");
11149
- log("── [5/8] npx install (@larksuite/openclaw-lark-tools update) ──");
11236
+ log("── [3/6] npx install (@larksuite/openclaw-lark-tools update) ──");
11150
11237
  const npxResult = (0, node_child_process.spawnSync)("npx", [
11151
11238
  "-y",
11152
11239
  "@larksuite/openclaw-lark-tools",
@@ -11189,53 +11276,33 @@ function runUpgradeLark(opts) {
11189
11276
  };
11190
11277
  };
11191
11278
  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");
11279
+ log("── [4/5] 安装后诊断校验 ─────────────────────────────────");
11218
11280
  logVersionSnapshot("after-versions", snapshotVersions(cwd, log), log);
11281
+ let afterVersionIncompatible = false;
11282
+ try {
11283
+ const rawConfig = node_fs.default.readFileSync(configPath, "utf-8");
11284
+ afterVersionIncompatible = needsLarkUpgrade({
11285
+ config: loadJSON5().parse(rawConfig),
11286
+ configPath,
11287
+ vars: {},
11288
+ providerDeps: {
11289
+ usesMiaodaProvider: false,
11290
+ usesMiaodaSecretProvider: false
11291
+ }
11292
+ });
11293
+ log(` version-compat post-check: ${afterVersionIncompatible ? "STILL_INCOMPATIBLE" : "ok"}`);
11294
+ } catch (e) {
11295
+ log(` version-compat post-check error: ${e.message} — treating as still-incompatible`);
11296
+ afterVersionIncompatible = true;
11297
+ }
11298
+ const afterChannels = probeChannels("after", log, 6e4);
11299
+ log(` feishu config invalid after: ${afterChannels.feishuConfigInvalid}`);
11300
+ const stillNeedsUpgrade = (afterVersionIncompatible || afterChannels.feishuConfigInvalid) && !afterChannels.anyAccountWorking;
11301
+ log(` post-check: stillNeedsUpgrade=${stillNeedsUpgrade} (version=${afterVersionIncompatible}, feishuConfig=${afterChannels.feishuConfigInvalid}, channelsWorking=${afterChannels.anyAccountWorking})`);
11302
+ if (stillNeedsUpgrade) return doRollback(`post-install diagnosis still shows anomaly: versionIncompatible=${afterVersionIncompatible}, feishuConfigInvalid=${afterChannels.feishuConfigInvalid}, anyAccountWorking=${afterChannels.anyAccountWorking}`);
11303
+ log(" post-install diagnosis: ok (upgrade conditions resolved)");
11219
11304
  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 ────────────────────────────────────");
11305
+ log("── [6/6] doctor --fix ────────────────────────────────────");
11239
11306
  const fixArgs = ["doctor", "--fix"];
11240
11307
  if (opts.scene) fixArgs.push(`--scene=${opts.scene}`);
11241
11308
  const fixResult = (0, node_child_process.spawnSync)(process.execPath, [cliScript, ...fixArgs], {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/openclaw-scripts-diagnose-cli",
3
- "version": "0.1.14-alpha.1",
3
+ "version": "0.1.14-alpha.10",
4
4
  "description": "CLI for OpenClaw config diagnose and repair with JSON5 support",
5
5
  "main": "dist/index.cjs",
6
6
  "bin": {