@lark-apaas/openclaw-scripts-diagnose-cli 0.1.14-alpha.3 → 0.1.14-alpha.5

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 +778 -227
  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.3";
55
+ return "0.1.14-alpha.5";
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,
@@ -3505,6 +3513,176 @@ function extractScopedNameFromSpec$1(spec) {
3505
3513
  const at = spec.indexOf("@", 1);
3506
3514
  return at === -1 ? spec : spec.slice(0, at);
3507
3515
  }
3516
+ /**
3517
+ * Returns true if the installed feishu plugin is version-incompatible with
3518
+ * the current openclaw (or is a legacy plugin that must be replaced).
3519
+ * Used by the upgrade_lark_needed rule and the upgrade-lark pre-check gate.
3520
+ */
3521
+ function needsLarkUpgrade(ctx) {
3522
+ const cc = resolveCompatContext(ctx);
3523
+ if (!cc) return false;
3524
+ const { ocCur, recommendedOc, installed, isLegacy } = cc;
3525
+ if (isForkPlugin(installed)) return false;
3526
+ if (recommendedOc) return resolveUpgradeDirection(installed, ocCur, recommendedOc, isLegacy) === "lark";
3527
+ return isLegacy || !isVersionCompatible(installed, ocCur);
3528
+ }
3529
+ //#endregion
3530
+ //#region src/channels-probe.ts
3531
+ const FEISHU_INVALID_CONFIG_MSG = "channels.feishu: invalid config: must NOT have additional properties";
3532
+ const CHANNEL_LINE_RE = /^-\s+Feishu\s+([^:]+):\s+(.+)$/;
3533
+ /**
3534
+ * Port of Python `_account_is_working` from the feishu-channel-success-rate skill.
3535
+ *
3536
+ * Strips colon-prefixed key:value bits (dm:, bot:, in:, out:, token:, allow:,
3537
+ * intents:, groups:, health:) and evaluates the canonical health formula.
3538
+ */
3539
+ function accountIsWorking(bits) {
3540
+ const bitTokens = /* @__PURE__ */ new Set();
3541
+ let hasError = false;
3542
+ let hasProbeFailed = false;
3543
+ for (const raw of bits) {
3544
+ const b = raw.trim();
3545
+ if (!b) continue;
3546
+ if (b.startsWith("error:")) {
3547
+ hasError = true;
3548
+ continue;
3549
+ }
3550
+ if (b === "probe failed") {
3551
+ hasProbeFailed = true;
3552
+ continue;
3553
+ }
3554
+ bitTokens.add(b.split(":")[0]);
3555
+ }
3556
+ if (!bitTokens.has("enabled") || !bitTokens.has("configured")) return false;
3557
+ if (bitTokens.has("works")) return true;
3558
+ if (bitTokens.has("running") && !hasError && !hasProbeFailed) return true;
3559
+ return false;
3560
+ }
3561
+ /**
3562
+ * Parse the raw stdout of `openclaw channels status --probe`.
3563
+ * Port of Python `extract_channels_probe` from the feishu-channel-success-rate skill.
3564
+ */
3565
+ function parseChannelsProbeOutput(text) {
3566
+ const gatewayReachable = text.includes("Gateway reachable");
3567
+ const feishuConfigInvalid = text.includes(FEISHU_INVALID_CONFIG_MSG);
3568
+ const accounts = [];
3569
+ let anyAccountWorking = false;
3570
+ for (const line of text.split("\n")) {
3571
+ const m = CHANNEL_LINE_RE.exec(line.trim());
3572
+ if (!m) continue;
3573
+ const [, acct, rest] = m;
3574
+ const bits = rest.split(",").map((b) => b.trim());
3575
+ const isWorking = accountIsWorking(bits);
3576
+ if (isWorking) anyAccountWorking = true;
3577
+ accounts.push({
3578
+ id: acct.trim(),
3579
+ bits,
3580
+ isWorking,
3581
+ raw: line.trim()
3582
+ });
3583
+ }
3584
+ return {
3585
+ gatewayReachable,
3586
+ feishuConfigInvalid,
3587
+ accounts,
3588
+ anyAccountWorking
3589
+ };
3590
+ }
3591
+ /**
3592
+ * Run `openclaw channels status --probe` and return a structured result.
3593
+ *
3594
+ * The command may exit non-zero when some bot accounts fail their probe — that
3595
+ * is still useful output. We therefore try to parse stdout even when the
3596
+ * process exits with a non-zero code, falling back to an unavailable result
3597
+ * only when there is genuinely no output to parse.
3598
+ *
3599
+ * @param timeoutMs Maximum wait time. Default is 60 s because v2026.4.x
3600
+ * lacks a per-request HTTP timeout and can block indefinitely.
3601
+ */
3602
+ function runChannelsProbe(timeoutMs = 6e4) {
3603
+ let stdout = "";
3604
+ let execError;
3605
+ try {
3606
+ stdout = (0, node_child_process.execSync)("openclaw channels status --probe", {
3607
+ encoding: "utf-8",
3608
+ timeout: timeoutMs,
3609
+ stdio: [
3610
+ "ignore",
3611
+ "pipe",
3612
+ "pipe"
3613
+ ]
3614
+ });
3615
+ } catch (e) {
3616
+ const err = e;
3617
+ stdout = err.stdout ?? "";
3618
+ execError = err.message;
3619
+ const stderrRaw = err.stderr;
3620
+ const stderr = (typeof stderrRaw === "string" ? stderrRaw : stderrRaw?.toString("utf-8") ?? "").trim();
3621
+ if (stderr) console.error(`channels-probe: stderr from CLI: ${stderr}`);
3622
+ }
3623
+ if (stdout.trim()) return {
3624
+ available: true,
3625
+ ...parseChannelsProbeOutput(stdout)
3626
+ };
3627
+ return {
3628
+ available: false,
3629
+ gatewayReachable: false,
3630
+ feishuConfigInvalid: false,
3631
+ accounts: [],
3632
+ anyAccountWorking: false,
3633
+ error: execError ?? "no output from openclaw channels status --probe"
3634
+ };
3635
+ }
3636
+ //#endregion
3637
+ //#region src/rules/upgrade-lark-needed.ts
3638
+ /**
3639
+ * Detects the condition that warrants running `upgrade-lark`:
3640
+ * - feishu plugin version incompatible with current openclaw, OR
3641
+ * - openclaw channels status --probe reports feishu channel config invalid; AND
3642
+ * - channels are not working.
3643
+ *
3644
+ * Both conditions must be true simultaneously. If version is compatible and
3645
+ * feishu config is valid, or channels are working, the rule passes (no action needed).
3646
+ *
3647
+ * feishuConfigInvalid is read from the channels probe output rather than running a
3648
+ * separate `openclaw status` call, since only `openclaw channels status --probe`
3649
+ * reliably surfaces the schema validation error.
3650
+ *
3651
+ * profile: experimental — runs only in full sweep mode, not in standard doctor.
3652
+ * level: silent — telemetry/sweep-only, does not trigger page-level repair UI.
3653
+ */
3654
+ let UpgradeLarkNeededRule = class UpgradeLarkNeededRule extends DiagnoseRule {
3655
+ validate(ctx) {
3656
+ let versionIncompatible = false;
3657
+ try {
3658
+ versionIncompatible = needsLarkUpgrade(ctx);
3659
+ } catch {
3660
+ versionIncompatible = true;
3661
+ }
3662
+ let probeResult;
3663
+ try {
3664
+ probeResult = runChannelsProbe(3e4);
3665
+ } catch {
3666
+ return { pass: true };
3667
+ }
3668
+ const feishuConfigInvalid = probeResult.feishuConfigInvalid;
3669
+ if (!(versionIncompatible || feishuConfigInvalid)) return { pass: true };
3670
+ if (probeResult.anyAccountWorking) return { pass: true };
3671
+ return {
3672
+ pass: false,
3673
+ action: "upgrade_lark",
3674
+ message: `飞书插件需要升级且 channels 不可用(版本不兼容=${versionIncompatible}, feishu配置无效=${feishuConfigInvalid}),建议执行 upgrade-lark 命令升级飞书插件`
3675
+ };
3676
+ }
3677
+ };
3678
+ UpgradeLarkNeededRule = __decorate([Rule({
3679
+ key: "upgrade_lark_needed",
3680
+ description: "检测飞书插件版本不兼容且 channels 不可用,判断是否需要执行 upgrade-lark 升级",
3681
+ repairMode: "check-only",
3682
+ level: "silent",
3683
+ profile: "experimental",
3684
+ usesVars: ["recommendedOpenclawTag"]
3685
+ })], UpgradeLarkNeededRule);
3508
3686
  //#endregion
3509
3687
  //#region src/rules/cleanup-install-backup-dirs.ts
3510
3688
  const DIR_PREFIX = ".openclaw-install-";
@@ -3650,119 +3828,6 @@ LarkCliMissingForInstalledLarkPluginRule = __decorate([Rule({
3650
3828
  usesVars: ["recommendedOpenclawTag"]
3651
3829
  })], LarkCliMissingForInstalledLarkPluginRule);
3652
3830
  //#endregion
3653
- //#region src/rules/feishu-bot-channel-config.ts
3654
- /**
3655
- * Ensures each bot account's channel config is correct:
3656
- * 1. `allowFrom` contains its own `creatorOpenID` from larkApps
3657
- * 2. `appSecret` is either the canonical provider-ref or matches larkApps plaintext
3658
- *
3659
- * Covers both multi-account (channels.feishu.accounts) and single-account
3660
- * (channels.feishu.appId + allowFrom at top level) layouts.
3661
- */
3662
- let FeishuBotChannelConfigRule = class FeishuBotChannelConfigRule extends DiagnoseRule {
3663
- validate(ctx) {
3664
- const larkApps = ctx.vars.larkApps;
3665
- if (!larkApps || larkApps.length === 0) return { pass: true };
3666
- const feishu = asRecord(getNestedMap(ctx.config, "channels", "feishu"));
3667
- if (!feishu) return { pass: true };
3668
- const issues = [];
3669
- const accounts = asRecord(feishu.accounts);
3670
- if (accounts) for (const [accountId, account] of Object.entries(accounts)) {
3671
- const bot = asRecord(account);
3672
- if (!bot) continue;
3673
- const appId = bot.appId;
3674
- if (typeof appId !== "string" || !appId.startsWith("cli_")) continue;
3675
- const larkApp = larkApps.find((e) => e.larkAppID === appId);
3676
- if (!larkApp) continue;
3677
- this.checkBot(accountId, bot, larkApp, issues, false);
3678
- }
3679
- const singleAppId = feishu.appId;
3680
- if (typeof singleAppId === "string" && singleAppId.startsWith("cli_") && !accounts) {
3681
- const larkApp = larkApps.find((e) => e.larkAppID === singleAppId);
3682
- if (larkApp) this.checkBot("feishu", feishu, larkApp, issues, true);
3683
- }
3684
- if (issues.length === 0) return { pass: true };
3685
- return {
3686
- pass: false,
3687
- message: issues.join("; ")
3688
- };
3689
- }
3690
- /** Check a single bot entry (either an account object or the feishu channel itself).
3691
- * @param isSingleAccount true for single-account layout (expects provider-ref),
3692
- * false for multi-account (expects plaintext from larkApps).
3693
- */
3694
- checkBot(label, bot, larkApp, issues, isSingleAccount) {
3695
- const creatorOpenID = larkApp.creatorOpenID;
3696
- const allowFrom = Array.isArray(bot.allowFrom) ? bot.allowFrom : [];
3697
- if (typeof creatorOpenID === "string" && creatorOpenID !== "") {
3698
- if (!allowFrom.includes(creatorOpenID)) issues.push(`${label} allowFrom missing creatorOpenID ${creatorOpenID.length > 8 ? creatorOpenID.slice(0, 4) + "***" + creatorOpenID.slice(-4) : "***"}`);
3699
- } else if (allowFrom.length === 0) issues.push(`${label} allowFrom is empty (creatorOpenID unavailable, cannot auto-fix)`);
3700
- const secret = bot.appSecret;
3701
- if (isSingleAccount) if (typeof secret === "object" && secret !== null && !Array.isArray(secret)) {
3702
- if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) issues.push(`${label} appSecret is a provider-ref but not the canonical one`);
3703
- } else issues.push(`${label} appSecret should be provider-ref, got ${typeof secret}`);
3704
- else if (typeof secret === "string") {
3705
- if (secret !== larkApp.appSecret) issues.push(`${label} appSecret plaintext mismatch`);
3706
- } else issues.push(`${label} appSecret should be plaintext, got ${typeof secret}`);
3707
- }
3708
- repair(ctx) {
3709
- const larkApps = ctx.vars.larkApps;
3710
- if (!larkApps || larkApps.length === 0) return;
3711
- const feishu = asRecord(getNestedMap(ctx.config, "channels", "feishu"));
3712
- if (!feishu) return;
3713
- const accounts = asRecord(feishu.accounts);
3714
- if (accounts) for (const [, account] of Object.entries(accounts)) {
3715
- const bot = asRecord(account);
3716
- if (!bot) continue;
3717
- const appId = bot.appId;
3718
- if (typeof appId !== "string" || !appId.startsWith("cli_")) continue;
3719
- const larkApp = larkApps.find((e) => e.larkAppID === appId);
3720
- if (!larkApp) continue;
3721
- this.fixBot(bot, larkApp, false);
3722
- }
3723
- const singleAppId = feishu.appId;
3724
- if (typeof singleAppId === "string" && singleAppId.startsWith("cli_") && !accounts) {
3725
- const larkApp = larkApps.find((e) => e.larkAppID === singleAppId);
3726
- if (larkApp) this.fixBot(feishu, larkApp, true);
3727
- }
3728
- }
3729
- /** Fix a single bot entry in-place.
3730
- * @param isSingleAccount true for single-account layout (use provider-ref),
3731
- * false for multi-account (use plaintext from larkApps).
3732
- */
3733
- fixBot(bot, larkApp, isSingleAccount) {
3734
- const creatorOpenID = larkApp.creatorOpenID;
3735
- if (typeof creatorOpenID === "string" && creatorOpenID !== "") {
3736
- const allowFrom = Array.isArray(bot.allowFrom) ? [...bot.allowFrom] : [];
3737
- if (!allowFrom.includes(creatorOpenID)) {
3738
- allowFrom.push(creatorOpenID);
3739
- bot.allowFrom = allowFrom;
3740
- }
3741
- }
3742
- const secret = bot.appSecret;
3743
- let needsFix = false;
3744
- if (isSingleAccount) if (typeof secret === "object" && secret !== null && !Array.isArray(secret)) {
3745
- if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) needsFix = true;
3746
- } else needsFix = true;
3747
- else if (typeof secret === "string") {
3748
- if (secret !== larkApp.appSecret) needsFix = true;
3749
- } else needsFix = true;
3750
- if (needsFix) bot.appSecret = isSingleAccount ? { ...DEFAULT_FEISHU_APP_SECRET } : larkApp.appSecret;
3751
- }
3752
- };
3753
- FeishuBotChannelConfigRule = __decorate([Rule({
3754
- key: "feishu_bot_channel_config",
3755
- description: "确保飞书配置中 bot 账号的 allowFrom 包含其创建者 openID 且 appSecret 值正确",
3756
- dependsOn: [
3757
- "config_syntax_check",
3758
- "feishu_default_account",
3759
- "feishu_bot_id"
3760
- ],
3761
- repairMode: "standard",
3762
- usesVars: ["larkApps"],
3763
- level: "critical"
3764
- })], FeishuBotChannelConfigRule);
3765
- //#endregion
3766
3831
  //#region src/check.ts
3767
3832
  /** Telemetry-aware entry: returns both the legacy CheckResult (for stdout)
3768
3833
  * AND a DoctorReport-shape payload (for `openclaw.report_cli_run`). The
@@ -4226,6 +4291,9 @@ const PROVIDER_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-provider-
4226
4291
  const SECRETS_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-openclaw-secrets.json";
4227
4292
  /** Absolute path to the openclaw config JSON. */
4228
4293
  const CONFIG_PATH = `${WORKSPACE_DIR}/openclaw.json`;
4294
+ function upgradeLarkLogFile(runId) {
4295
+ return `${DIAGNOSE_DIR}/upgrade-lark-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/:/g, "-")}-${runId.slice(0, 8)}.log`;
4296
+ }
4229
4297
  //#endregion
4230
4298
  //#region src/run-log.ts
4231
4299
  let currentRunContext;
@@ -4362,10 +4430,9 @@ function makeLogger(logFile) {
4362
4430
  /**
4363
4431
  * Start an async reset task: spawn a detached child process and return the taskId.
4364
4432
  *
4365
- * The child process runs: node cli.js reset --worker --task-id=xxx
4366
- * The worker fetches ctx from innerApi itself — no --ctx passthrough.
4433
+ * The child process runs: node cli.js reset --worker --task-id=xxx --ctx=base64
4367
4434
  */
4368
- function startAsyncReset() {
4435
+ function startAsyncReset(ctxBase64) {
4369
4436
  const taskId = (0, node_crypto.randomUUID)();
4370
4437
  const resultFile = resetResultFile(taskId);
4371
4438
  const log = makeLogger(resetLogFile(taskId));
@@ -4389,7 +4456,8 @@ function startAsyncReset() {
4389
4456
  process.argv[1],
4390
4457
  "reset",
4391
4458
  "--worker",
4392
- `--task-id=${taskId}`
4459
+ `--task-id=${taskId}`,
4460
+ `--ctx=${ctxBase64}`
4393
4461
  ], {
4394
4462
  detached: true,
4395
4463
  stdio: "ignore",
@@ -6903,60 +6971,6 @@ function mergeCoreBackupAndOrigins(configPath, vars, resetData, log) {
6903
6971
  log(`allowedOrigins: added ${added.length} (${JSON.stringify(added)}), total now ${mergedOrigins.length}`);
6904
6972
  }
6905
6973
  /**
6906
- * Fix bot account allowFrom and appSecret using larkApps from innerApi.
6907
- *
6908
- * For each bot account (key starts with `bot-cli_`):
6909
- * - allowFrom must contain the bot's own creatorOpenID from larkApps
6910
- * - appSecret must be either the canonical provider-ref or match larkApps plaintext
6911
- *
6912
- * Runs after mergeCoreBackupAndOrigins so it operates on the final config state.
6913
- */
6914
- function fixBotChannelConfig(configPath, larkApps, log) {
6915
- if (!larkApps || larkApps.length === 0) {
6916
- log("no larkApps data, skip bot channel config fix");
6917
- return;
6918
- }
6919
- const config = loadJSON5().parse(node_fs.default.readFileSync(configPath, "utf-8"));
6920
- const accounts = asRecord(getNestedMap(config, "channels", "feishu")?.accounts);
6921
- if (!accounts) {
6922
- log("no feishu accounts in config, skip bot channel config fix");
6923
- return;
6924
- }
6925
- let fixCount = 0;
6926
- for (const [, account] of Object.entries(accounts)) {
6927
- const bot = asRecord(account);
6928
- if (!bot) continue;
6929
- const appId = bot.appId;
6930
- if (typeof appId !== "string" || !appId.startsWith("cli_")) continue;
6931
- const larkApp = larkApps.find((e) => e.larkAppID === appId);
6932
- if (!larkApp) continue;
6933
- const creatorOpenID = larkApp.creatorOpenID;
6934
- if (typeof creatorOpenID === "string" && creatorOpenID !== "") {
6935
- const allowFrom = Array.isArray(bot.allowFrom) ? [...bot.allowFrom] : [];
6936
- if (!allowFrom.includes(creatorOpenID)) {
6937
- allowFrom.push(creatorOpenID);
6938
- bot.allowFrom = allowFrom;
6939
- fixCount++;
6940
- }
6941
- }
6942
- const secret = bot.appSecret;
6943
- let needsFix = false;
6944
- if (typeof secret === "object" && secret !== null && !Array.isArray(secret)) {
6945
- if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) needsFix = true;
6946
- } else if (typeof secret === "string") {
6947
- if (secret !== larkApp.appSecret) needsFix = true;
6948
- } else needsFix = true;
6949
- if (needsFix) {
6950
- bot.appSecret = { ...DEFAULT_FEISHU_APP_SECRET };
6951
- fixCount++;
6952
- }
6953
- }
6954
- if (fixCount > 0) {
6955
- node_fs.default.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
6956
- log(`fixed ${fixCount} bot channel config issue(s) (allowFrom/appSecret)`);
6957
- } else log("bot channel config ok, no fixes needed");
6958
- }
6959
- /**
6960
6974
  * Step 7: Verify startup scripts landed in configDir/scripts/.
6961
6975
  *
6962
6976
  * Scripts are extracted directly to configDir/scripts/ during stageTemplate —
@@ -7101,7 +7115,6 @@ async function runReset(input, taskId, resultFile) {
7101
7115
  await step5InstallOpenclaw(openclawTag, ossFileMap, log);
7102
7116
  step(6);
7103
7117
  mergeCoreBackupAndOrigins(configPath, vars, resetData, log);
7104
- fixBotChannelConfig(configPath, vars.larkApps, log);
7105
7118
  step(7);
7106
7119
  verifyStartupScripts(configDir, log);
7107
7120
  step(8);
@@ -7900,8 +7913,7 @@ function normalizeCtx(raw) {
7900
7913
  reset: {
7901
7914
  templateVars: r.reset.templateVars ?? {},
7902
7915
  coreBackup: r.reset.coreBackup
7903
- },
7904
- larkApps: Array.isArray(r.larkApps) ? r.larkApps : []
7916
+ }
7905
7917
  };
7906
7918
  }
7907
7919
  const vars = r.vars ?? {};
@@ -7926,8 +7938,7 @@ function normalizeCtx(raw) {
7926
7938
  reset: {
7927
7939
  templateVars: resetData.templateVars ?? {},
7928
7940
  coreBackup: resetData.coreBackup
7929
- },
7930
- larkApps: Array.isArray(r.larkApps) ? r.larkApps : []
7941
+ }
7931
7942
  };
7932
7943
  }
7933
7944
  function fillApp(src) {
@@ -7992,8 +8003,7 @@ function buildCheckInput(raw, configPathOverride) {
7992
8003
  providerFilePath: PROVIDER_FILE_PATH,
7993
8004
  secretsFilePath: SECRETS_FILE_PATH,
7994
8005
  templateVars: ctx.app.templateVars,
7995
- recommendedOpenclawTag: ctx.app.recommendedOpenclawTag,
7996
- larkApps: ctx.larkApps
8006
+ recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
7997
8007
  },
7998
8008
  templateVars: ctx.app.templateVars
7999
8009
  };
@@ -8025,8 +8035,7 @@ function buildRepairInput(raw, configPathOverride) {
8025
8035
  providerFilePath: PROVIDER_FILE_PATH,
8026
8036
  secretsFilePath: SECRETS_FILE_PATH,
8027
8037
  templateVars: ctx.app.templateVars,
8028
- recommendedOpenclawTag: ctx.app.recommendedOpenclawTag,
8029
- larkApps: ctx.larkApps
8038
+ recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
8030
8039
  },
8031
8040
  repairData: {
8032
8041
  secretsContent: ctx.secrets.secretsContent,
@@ -8062,8 +8071,7 @@ function buildResetInput(raw, configPathOverride) {
8062
8071
  providerFilePath: PROVIDER_FILE_PATH,
8063
8072
  secretsFilePath: SECRETS_FILE_PATH,
8064
8073
  templateVars: ctx.app.templateVars,
8065
- recommendedOpenclawTag: ctx.app.recommendedOpenclawTag,
8066
- larkApps: ctx.larkApps
8074
+ recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
8067
8075
  },
8068
8076
  resetData: {
8069
8077
  templateVars: ctx.reset.templateVars,
@@ -10373,7 +10381,7 @@ async function reportCliRun(opts) {
10373
10381
  //#region src/help.ts
10374
10382
  const BIN = "mclaw-diagnose";
10375
10383
  function versionBanner() {
10376
- return `v0.1.14-alpha.3`;
10384
+ return `v0.1.14-alpha.5`;
10377
10385
  }
10378
10386
  const COMMANDS = [
10379
10387
  {
@@ -10477,12 +10485,16 @@ EXIT CODES
10477
10485
  hidden: true,
10478
10486
  summary: "Run rule-engine check only",
10479
10487
  help: `USAGE
10480
- ${BIN} check
10488
+ ${BIN} check [--ctx=<base64>]
10481
10489
 
10482
10490
  DESCRIPTION
10483
10491
  Runs the rule engine against the sandbox's current openclaw config and
10484
- returns { failedRules }. Ctx is fetched from innerapi automatically.
10485
- End-users should prefer \`doctor\`.
10492
+ returns { failedRules }. Used by sandbox_console's push-style callers
10493
+ that already own the ctx — end-users should prefer \`doctor\`.
10494
+
10495
+ OPTIONS
10496
+ --ctx=<base64> Opaque ctx JSON (base64). When absent, fetched from
10497
+ innerapi (same path as doctor).
10486
10498
  `
10487
10499
  },
10488
10500
  {
@@ -10490,11 +10502,16 @@ DESCRIPTION
10490
10502
  hidden: true,
10491
10503
  summary: "Apply standard-mode repairs",
10492
10504
  help: `USAGE
10493
- ${BIN} repair
10505
+ ${BIN} repair [--ctx=<base64>]
10494
10506
 
10495
10507
  DESCRIPTION
10496
- Runs repair for the failing rules. Ctx is fetched from innerapi
10497
- automatically. End-users should use \`doctor --fix\` instead.
10508
+ Runs repair for the failing rules listed inside the ctx's repairData.
10509
+ Intended for sandbox_console's push path — end-users should use
10510
+ \`doctor --fix\` instead.
10511
+
10512
+ OPTIONS
10513
+ --ctx=<base64> Opaque ctx JSON (base64). When absent, fetched from
10514
+ innerapi.
10498
10515
  `
10499
10516
  },
10500
10517
  {
@@ -10502,15 +10519,14 @@ DESCRIPTION
10502
10519
  hidden: true,
10503
10520
  summary: "Re-initialize sandbox via the 9-step reset pipeline",
10504
10521
  help: `USAGE
10505
- ${BIN} reset --async
10506
- ${BIN} reset --worker --task-id=<id>
10522
+ ${BIN} reset --async [--ctx=<base64>]
10523
+ ${BIN} reset --worker --task-id=<id> [--ctx=<base64>]
10507
10524
 
10508
10525
  DESCRIPTION
10509
10526
  Two-phase pipeline driven asynchronously: the --async invocation spawns
10510
10527
  a detached worker and returns { taskId } immediately; the --worker
10511
10528
  invocation (spawned by --async) runs the actual 9 steps and writes
10512
10529
  progress to /tmp/openclaw-diagnose/reset-<taskId>.json.
10513
- Ctx is fetched from innerapi automatically.
10514
10530
 
10515
10531
  Poll progress with \`${BIN} get_reset_task --task-id=<id>\`.
10516
10532
 
@@ -10518,6 +10534,7 @@ OPTIONS
10518
10534
  --async Start a detached worker and return taskId on stdout.
10519
10535
  --worker Internal — run the 9-step pipeline (launched by --async).
10520
10536
  --task-id=<id> Required with --worker; identifies the progress file.
10537
+ --ctx=<base64> Opaque ctx JSON; fetched from innerapi when absent.
10521
10538
  `
10522
10539
  },
10523
10540
  {
@@ -10540,7 +10557,7 @@ OPTIONS
10540
10557
  hidden: true,
10541
10558
  summary: "Download + install the openclaw tarball",
10542
10559
  help: `USAGE
10543
- ${BIN} install-openclaw <tag> [--oss_file_map=<base64>]
10560
+ ${BIN} install-openclaw <tag> [--ctx=<base64> | --oss_file_map=<base64>]
10544
10561
 
10545
10562
  DESCRIPTION
10546
10563
  Downloads the openclaw@<tag> tgz via the signed OSS URL found in the
@@ -10552,9 +10569,9 @@ ARGUMENTS
10552
10569
  <tag> Openclaw version tag, e.g. 2026.4.11.
10553
10570
 
10554
10571
  OPTIONS
10572
+ --ctx=<base64> Opaque ctx; ossFileMap is extracted from it.
10555
10573
  --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi
10556
- entirely. When absent, ossFileMap is fetched from
10557
- innerapi automatically.
10574
+ entirely. Wins over --ctx when both provided.
10558
10575
  `
10559
10576
  },
10560
10577
  {
@@ -10580,7 +10597,8 @@ OPTIONS
10580
10597
  --home_base=<dir> Override the /home/gem base (tests).
10581
10598
  --config_path=<p> Override the openclaw.json path (tests).
10582
10599
  --skip-config-update Leave plugins.installs in openclaw.json untouched.
10583
- --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi.
10600
+ --ctx=<base64> Opaque ctx; see install-openclaw for semantics.
10601
+ --oss_file_map=... Pre-built OSS URL map (base64 JSON).
10584
10602
  `
10585
10603
  },
10586
10604
  {
@@ -10607,6 +10625,7 @@ OPTIONS
10607
10625
  --cli=<name> CLI package to install by short name or scoped
10608
10626
  packageName (repeatable, at least one required).
10609
10627
  --home_base=<dir> Override the /home/gem base (tests).
10628
+ --ctx=<base64> Opaque ctx; ossFileMap is extracted from it.
10610
10629
  --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi.
10611
10630
 
10612
10631
  EXAMPLES
@@ -10660,6 +10679,46 @@ OPTIONS
10660
10679
  EXIT CODES
10661
10680
  0 Success or skipped (prerequisites not met).
10662
10681
  1 Secret/path unresolvable, lark-cli failed, or config unreadable.
10682
+ `
10683
+ },
10684
+ {
10685
+ name: "upgrade-lark",
10686
+ hidden: false,
10687
+ summary: "Upgrade the Feishu/Lark plugin via @larksuite/openclaw-lark-tools",
10688
+ help: `USAGE
10689
+ ${BIN} upgrade-lark [--scene=<scene>] [--caller=<n>] [--trace-id=<id>]
10690
+
10691
+ DESCRIPTION
10692
+ Upgrades the Feishu/Lark plugin by running:
10693
+ npx -y @larksuite/openclaw-lark-tools update --use-existing
10694
+
10695
+ Before the upgrade, the following files are backed up:
10696
+ - openclaw.json
10697
+ - extensions/openclaw-lark/ (if present)
10698
+ - extensions/feishu-openclaw-plugin/ (if present)
10699
+ After the upgrade, the result is validated:
10700
+ - feishu.accounts bot count must not decrease
10701
+ - gateway config structure must remain valid (port/mode/bind/auth/trustedProxies)
10702
+ If the upgrade command fails, or validation fails, the backed-up files are
10703
+ restored to roll back the changes.
10704
+
10705
+ Execution is logged to /tmp/openclaw-diagnose/upgrade-lark-<runId>.log.
10706
+
10707
+ Output is a single JSON object on stdout:
10708
+ { "ok": true, "stdout": "...", "stderr": "...", "logFile": "..." }
10709
+ { "ok": false, "error": "...", "stderr": "...", "exitCode": 1,
10710
+ "rollbackOk": true, "validationError": "...", "logFile": "..." }
10711
+
10712
+ OPTIONS
10713
+ --scene=<scene> Telemetry label forwarded to Slardar only.
10714
+ Known values: PageUpgradeLark, etc. Custom strings accepted.
10715
+ --caller=<name> Optional metadata passed to innerapi.
10716
+ --trace-id=<id> Optional log-correlation id.
10717
+
10718
+ EXIT CODES
10719
+ 0 Success: upgrade ran and all validations passed.
10720
+ 1 Failure: npx error, validation failed, or git commit failed.
10721
+ File rollback was attempted (see rollbackOk in the JSON output).
10663
10722
  `
10664
10723
  },
10665
10724
  {
@@ -10693,6 +10752,41 @@ EXAMPLES
10693
10752
  ${BIN} rules # all rules
10694
10753
  ${BIN} rules --rule=gateway # single rule
10695
10754
  ${BIN} rules --rule=gateway --rule=feishu_channel # multiple rules
10755
+ `
10756
+ },
10757
+ {
10758
+ name: "channels-probe",
10759
+ hidden: true,
10760
+ summary: "Check feishu channel health via openclaw channels status --probe",
10761
+ help: `USAGE
10762
+ ${BIN} channels-probe [--timeout=<ms>]
10763
+
10764
+ DESCRIPTION
10765
+ Runs \`openclaw channels status --probe\` and returns a structured JSON
10766
+ summary of whether the current environment's feishu channels are
10767
+ configured and working correctly.
10768
+
10769
+ Output:
10770
+ {
10771
+ "available": true,
10772
+ "gatewayReachable": true,
10773
+ "accounts": [
10774
+ { "id": "default", "bits": ["enabled","configured","running","works"],
10775
+ "isWorking": true, "raw": "- Feishu default: ..." }
10776
+ ],
10777
+ "anyAccountWorking": true
10778
+ }
10779
+
10780
+ An account is considered working when:
10781
+ enabled ∧ configured ∧ ( works ∨ ( running ∧ no error: ∧ no probe failed ) )
10782
+
10783
+ "available": false means the CLI invocation itself failed (openclaw not
10784
+ found, gateway unreachable, or no parseable output returned).
10785
+
10786
+ OPTIONS
10787
+ --timeout=<ms> Max wait in milliseconds (default: 60000). The probe
10788
+ can hang indefinitely on openclaw v2026.4.x due to a
10789
+ missing per-request HTTP timeout — set this accordingly.
10696
10790
  `
10697
10791
  },
10698
10792
  {
@@ -10713,7 +10807,8 @@ OPTIONS
10713
10807
  --role=<role> Package role (e.g. template, config).
10714
10808
  --name=<name> Package name within the role.
10715
10809
  --dir=<dir> Target dir (defaults to dirname(pkg.installPath)).
10716
- --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi.
10810
+ --ctx=<base64> Opaque ctx; ossFileMap is extracted from it.
10811
+ --oss_file_map=... Pre-built OSS URL map (base64 JSON).
10717
10812
  `
10718
10813
  }
10719
10814
  ];
@@ -10789,31 +10884,31 @@ function planVarsFields(opts = {}) {
10789
10884
  *
10790
10885
  * Per-command group needs:
10791
10886
  *
10792
- * doctor / check app + larkApps
10793
- * repair app + secrets + larkApps
10794
- * reset app + secrets + install + reset + larkApps
10887
+ * doctor / check app (rule-driven)
10888
+ * repair app + secrets (writes secretsContent / providerKeyContent)
10889
+ * reset app + secrets + install + reset (the works)
10795
10890
  * install-* install only
10796
10891
  *
10797
10892
  * Empty result (`{}`) means "no group needed" — the CLI can skip the
10798
10893
  * `fetchCtxViaInnerApi` call entirely and run with a synthetic empty ctx.
10894
+ * Happens e.g. when the user pinned `--rule=<key>` to a vars-free rule on
10895
+ * `doctor`.
10799
10896
  */
10800
10897
  function planCtxPopulate(opts) {
10801
10898
  if (opts.command === "install") return { install: true };
10802
10899
  const populate = {};
10803
- if (planVarsFields({
10900
+ const appFields = planVarsFields({
10804
10901
  disabled: opts.disabled,
10805
10902
  onlyRules: opts.onlyRules,
10806
10903
  profile: opts.profile
10807
- }).length > 0) populate.app = true;
10808
- if (opts.command === "repair") {
10809
- populate.secrets = true;
10810
- populate.larkApps = true;
10811
- } else if (opts.command === "reset") {
10904
+ });
10905
+ if (appFields.length > 0) populate.app = appFields;
10906
+ if (opts.command === "repair") populate.secrets = true;
10907
+ else if (opts.command === "reset") {
10812
10908
  populate.secrets = true;
10813
10909
  populate.install = true;
10814
10910
  populate.reset = true;
10815
- populate.larkApps = true;
10816
- } else if (opts.command === "doctor" || opts.command === "check") populate.larkApps = true;
10911
+ }
10817
10912
  return populate;
10818
10913
  }
10819
10914
  //#endregion
@@ -10867,11 +10962,411 @@ function reportDoctorRunToSlardar(opts) {
10867
10962
  }
10868
10963
  });
10869
10964
  }
10965
+ function readLogFile(filePath) {
10966
+ try {
10967
+ return node_fs.default.readFileSync(filePath, "utf-8");
10968
+ } catch {
10969
+ return "";
10970
+ }
10971
+ }
10972
+ function reportUpgradeLarkToSlardar(opts) {
10973
+ console.error(`[slardar] upgrade_lark_run scene=${opts.scene ?? ""} success=${opts.success} exitCode=${opts.exitCode ?? ""} rollbackOk=${opts.rollbackOk ?? ""}`);
10974
+ const logContent = readLogFile(opts.logFile);
10975
+ reportTask({
10976
+ eventName: "upgrade_lark_run",
10977
+ durationMs: opts.durationMs,
10978
+ status: opts.success ? "success" : "failed",
10979
+ extraCategories: {
10980
+ scene: opts.scene ?? "",
10981
+ exit_code: String(opts.exitCode ?? ""),
10982
+ rollback_ok: opts.rollbackOk != null ? String(opts.rollbackOk) : "",
10983
+ validation_error: opts.validationError ?? "",
10984
+ error_msg: opts.error ?? "",
10985
+ log_content: logContent
10986
+ }
10987
+ });
10988
+ }
10989
+ //#endregion
10990
+ //#region src/upgrade-lark.ts
10991
+ /** Plugin directories under extensions/ that are backed up before upgrade */
10992
+ const FEISHU_PLUGIN_DIRS = ["openclaw-lark", "feishu-openclaw-plugin"];
10993
+ /** Version compat rule keys checked in the doctor output after install */
10994
+ const VERSION_COMPAT_RULE_KEYS = ["feishu_plugin_version_compat_lark", "feishu_plugin_version_compat_openclaw"];
10995
+ function backupFiles(opts) {
10996
+ const { workspaceDir, configPath, backupDir, log } = opts;
10997
+ try {
10998
+ node_fs.default.mkdirSync(backupDir, { recursive: true });
10999
+ log(`backup dir: ${backupDir}`);
11000
+ if (node_fs.default.existsSync(configPath)) {
11001
+ const stat = node_fs.default.statSync(configPath);
11002
+ node_fs.default.copyFileSync(configPath, node_path.default.join(backupDir, "openclaw.json"));
11003
+ log(` backed up: openclaw.json (${stat.size} bytes)`);
11004
+ } else log(` skipped: openclaw.json (not found)`);
11005
+ const extSrc = node_path.default.join(workspaceDir, "extensions");
11006
+ for (const pluginDir of FEISHU_PLUGIN_DIRS) {
11007
+ const src = node_path.default.join(extSrc, pluginDir);
11008
+ if (node_fs.default.existsSync(src)) {
11009
+ const dst = node_path.default.join(backupDir, "extensions", pluginDir);
11010
+ node_fs.default.cpSync(src, dst, { recursive: true });
11011
+ const version = readPkgVersion(node_path.default.join(src, "package.json"));
11012
+ log(` backed up: extensions/${pluginDir}${version ? ` (version: ${version})` : ""}`);
11013
+ } else log(` skipped: extensions/${pluginDir} (not found)`);
11014
+ }
11015
+ return { ok: true };
11016
+ } catch (e) {
11017
+ return {
11018
+ ok: false,
11019
+ error: `backup failed: ${e.message}`
11020
+ };
11021
+ }
11022
+ }
11023
+ function restoreFiles(opts) {
11024
+ const { workspaceDir, configPath, backupDir, log } = opts;
11025
+ try {
11026
+ const configBackup = node_path.default.join(backupDir, "openclaw.json");
11027
+ if (node_fs.default.existsSync(configBackup)) {
11028
+ node_fs.default.copyFileSync(configBackup, configPath);
11029
+ log(` restored: openclaw.json`);
11030
+ }
11031
+ const extDst = node_path.default.join(workspaceDir, "extensions");
11032
+ for (const pluginDir of FEISHU_PLUGIN_DIRS) {
11033
+ const backupSrc = node_path.default.join(backupDir, "extensions", pluginDir);
11034
+ if (node_fs.default.existsSync(backupSrc)) {
11035
+ const dst = node_path.default.join(extDst, pluginDir);
11036
+ if (node_fs.default.existsSync(dst)) node_fs.default.rmSync(dst, {
11037
+ recursive: true,
11038
+ force: true
11039
+ });
11040
+ node_fs.default.cpSync(backupSrc, dst, { recursive: true });
11041
+ log(` restored: extensions/${pluginDir}`);
11042
+ }
11043
+ }
11044
+ return true;
11045
+ } catch (e) {
11046
+ log(` restore error: ${e.message}`);
11047
+ return false;
11048
+ }
11049
+ }
11050
+ function readPkgVersion(pkgPath) {
11051
+ try {
11052
+ const pkg = JSON.parse(node_fs.default.readFileSync(pkgPath, "utf-8"));
11053
+ return typeof pkg.version === "string" ? pkg.version : null;
11054
+ } catch {
11055
+ return null;
11056
+ }
11057
+ }
11058
+ function snapshotVersions(cwd, log) {
11059
+ const ocResult = (0, node_child_process.spawnSync)("openclaw", ["--version"], {
11060
+ cwd,
11061
+ encoding: "utf-8",
11062
+ stdio: [
11063
+ "ignore",
11064
+ "pipe",
11065
+ "pipe"
11066
+ ],
11067
+ timeout: 5e3
11068
+ });
11069
+ const ocRaw = (ocResult.stdout ?? "").trim() || (ocResult.stderr ?? "").trim();
11070
+ const extDir = node_path.default.join(cwd, "extensions");
11071
+ const larkPkg = node_path.default.join(extDir, "openclaw-lark", "package.json");
11072
+ const feishuPkg = node_path.default.join(extDir, "feishu-openclaw-plugin", "package.json");
11073
+ log(` version-check paths: ${larkPkg} [${node_fs.default.existsSync(larkPkg) ? "exists" : "missing"}]`);
11074
+ log(` version-check paths: ${feishuPkg} [${node_fs.default.existsSync(feishuPkg) ? "exists" : "missing"}]`);
11075
+ return {
11076
+ openclaw: ocRaw || null,
11077
+ openclawLark: readPkgVersion(larkPkg),
11078
+ feishuOpenclawPlugin: readPkgVersion(feishuPkg)
11079
+ };
11080
+ }
11081
+ function logVersionSnapshot(label, v, log) {
11082
+ log(`${label}: openclaw=${v.openclaw ?? "n/a"} openclaw-lark=${v.openclawLark ?? "n/a"} feishu-openclaw-plugin=${v.feishuOpenclawPlugin ?? "n/a"}`);
11083
+ }
11084
+ function countFeishuBots(configPath) {
11085
+ try {
11086
+ const raw = node_fs.default.readFileSync(configPath, "utf-8");
11087
+ const config = loadJSON5().parse(raw);
11088
+ const accounts = getNestedMap(config, "channels", "feishu", "accounts");
11089
+ if (accounts) return Object.keys(accounts).length;
11090
+ const feishu = getNestedMap(config, "channels", "feishu");
11091
+ return typeof feishu?.appId === "string" && feishu.appId ? 1 : 0;
11092
+ } catch {
11093
+ return 0;
11094
+ }
11095
+ }
11096
+ /**
11097
+ * Parse doctor stdout (first JSON line) and return an error string if any
11098
+ * version compat rule failed. Returns null on parse failure so a broken doctor
11099
+ * output does not block the install.
11100
+ */
11101
+ function checkVersionCompatFromDoctorOutput(stdout, log) {
11102
+ const firstLine = stdout.split("\n")[0]?.trim();
11103
+ if (!firstLine) {
11104
+ log(" doctor(compat): empty output, skipping version compat check");
11105
+ return null;
11106
+ }
11107
+ try {
11108
+ const report = JSON.parse(firstLine);
11109
+ for (const outcome of report.results) if (VERSION_COMPAT_RULE_KEYS.includes(outcome.rule)) {
11110
+ if (outcome.status === "failed" || outcome.status === "still-broken" || outcome.status === "error") return `version compat rule ${outcome.rule} ${outcome.status}: ${outcome.message ?? "(no message)"}`;
11111
+ }
11112
+ return null;
11113
+ } catch (e) {
11114
+ log(` doctor(compat): failed to parse output — ${e.message}`);
11115
+ return null;
11116
+ }
11117
+ }
11118
+ /** Run channels probe, log results, and return the result. Never throws. */
11119
+ function probeChannels(label, log, timeoutMs) {
11120
+ try {
11121
+ const r = runChannelsProbe(timeoutMs);
11122
+ log(` ${label} available=${r.available} anyAccountWorking=${r.anyAccountWorking}`);
11123
+ if (r.error) log(` ${label} error: ${r.error}`);
11124
+ if (r.gatewayReachable != null) log(` ${label} gatewayReachable: ${r.gatewayReachable}`);
11125
+ for (const acct of r.accounts ?? []) log(` ${label} account ${acct.id}: isWorking=${acct.isWorking} bits=[${acct.bits.join(",")}]`);
11126
+ return r;
11127
+ } catch (e) {
11128
+ log(` ${label} channels probe threw: ${e.message}`);
11129
+ return {
11130
+ available: false,
11131
+ accounts: [],
11132
+ anyAccountWorking: false
11133
+ };
11134
+ }
11135
+ }
11136
+ function runUpgradeLark(opts) {
11137
+ const cwd = opts.cwd ?? "/home/gem/workspace/agent";
11138
+ const configPath = opts.configPath ?? CONFIG_PATH;
11139
+ const logFile = upgradeLarkLogFile(opts.runId);
11140
+ const log = makeLogger(logFile);
11141
+ const fsOpts = {
11142
+ workspaceDir: cwd,
11143
+ configPath,
11144
+ backupDir: node_path.default.join(opts.backupBaseDir ?? "/tmp/openclaw-diagnose", `upgrade-lark-backup-${opts.runId}`),
11145
+ log
11146
+ };
11147
+ const cliScript = opts.cliScript ?? process.argv[1];
11148
+ const statusCheckDelayMs = opts.statusCheckDelayMs ?? 5e3;
11149
+ log(`${"=".repeat(60)}`);
11150
+ log(`upgrade-lark started runId=${opts.runId}`);
11151
+ log(` cwd : ${cwd}`);
11152
+ log(` configPath : ${configPath}`);
11153
+ log(`${"=".repeat(60)}`);
11154
+ log("");
11155
+ log("── [Pre-check A] channels probe(升级前)────────────────");
11156
+ const beforeChannels = probeChannels("before", log, 3e4);
11157
+ log("");
11158
+ log("── [Pre-check B] 版本兼容预检 ───────────────────────────");
11159
+ let versionIncompatible = false;
11160
+ try {
11161
+ const rawConfig = node_fs.default.readFileSync(configPath, "utf-8");
11162
+ versionIncompatible = needsLarkUpgrade({
11163
+ config: loadJSON5().parse(rawConfig),
11164
+ configPath,
11165
+ vars: {},
11166
+ providerDeps: {
11167
+ usesMiaodaProvider: false,
11168
+ usesMiaodaSecretProvider: false
11169
+ }
11170
+ });
11171
+ log(` version-compat pre-check: ${versionIncompatible ? "NEEDS_UPGRADE" : "ok"}`);
11172
+ } catch (e) {
11173
+ log(` version-compat pre-check error: ${e.message} — treating as needs-upgrade`);
11174
+ versionIncompatible = true;
11175
+ }
11176
+ const feishuConfigInvalid = beforeChannels.feishuConfigInvalid;
11177
+ log(` feishu config invalid : ${feishuConfigInvalid}`);
11178
+ log("");
11179
+ log("── [Gate] 升级前置条件检查 ───────────────────────────────");
11180
+ log(` versionIncompatible : ${versionIncompatible}`);
11181
+ log(` feishuConfigInvalid : ${feishuConfigInvalid}`);
11182
+ log(` channels working before: ${beforeChannels.anyAccountWorking}`);
11183
+ if (!(versionIncompatible || feishuConfigInvalid)) {
11184
+ const reason = "version compatible and feishu channel config valid — upgrade not needed";
11185
+ log(` SKIP: ${reason}`);
11186
+ log(`${"=".repeat(60)}`);
11187
+ log("upgrade-lark skipped (pre-check gate)");
11188
+ log(`${"=".repeat(60)}`);
11189
+ return {
11190
+ ok: true,
11191
+ skipped: true,
11192
+ skipReason: reason,
11193
+ logFile
11194
+ };
11195
+ }
11196
+ if (beforeChannels.anyAccountWorking) {
11197
+ const reason = "channels are working — upgrade not needed (issue detected but system is functional)";
11198
+ log(` SKIP: ${reason}`);
11199
+ log(`${"=".repeat(60)}`);
11200
+ log("upgrade-lark skipped (pre-check gate)");
11201
+ log(`${"=".repeat(60)}`);
11202
+ return {
11203
+ ok: true,
11204
+ skipped: true,
11205
+ skipReason: reason,
11206
+ logFile
11207
+ };
11208
+ }
11209
+ log(` PROCEED: requiresLarkUpgrade=true (version=${versionIncompatible}, feishuConfig=${feishuConfigInvalid}) AND channels not working → running upgrade`);
11210
+ log("");
11211
+ log("── [1/6] 文件备份 ────────────────────────────────────────");
11212
+ log(`before-state: botCount=${countFeishuBots(configPath)}`);
11213
+ const backup = backupFiles(fsOpts);
11214
+ if (!backup.ok) {
11215
+ log(`ERROR: ${backup.error}`);
11216
+ return {
11217
+ ok: false,
11218
+ error: backup.error,
11219
+ logFile
11220
+ };
11221
+ }
11222
+ log("backup: ok");
11223
+ logVersionSnapshot("before-versions", snapshotVersions(cwd, log), log);
11224
+ log("");
11225
+ log("── [2/6] 清理本地 openclaw shim ─────────────────────────");
11226
+ const localOpenclawBin = node_path.default.join(cwd, "node_modules", ".bin", "openclaw");
11227
+ if (node_fs.default.existsSync(localOpenclawBin)) try {
11228
+ node_fs.default.rmSync(localOpenclawBin);
11229
+ log(` removed: ${localOpenclawBin}`);
11230
+ } catch (e) {
11231
+ log(` WARN: failed to remove ${localOpenclawBin}: ${e.message}`);
11232
+ }
11233
+ else log(` skipped: ${localOpenclawBin} (not found)`);
11234
+ log("");
11235
+ log("── [3/6] npx install (@larksuite/openclaw-lark-tools update) ──");
11236
+ const npxResult = (0, node_child_process.spawnSync)("npx", [
11237
+ "-y",
11238
+ "@larksuite/openclaw-lark-tools",
11239
+ "update"
11240
+ ], {
11241
+ cwd,
11242
+ encoding: "utf-8",
11243
+ stdio: [
11244
+ "ignore",
11245
+ "pipe",
11246
+ "pipe"
11247
+ ],
11248
+ timeout: 12e4
11249
+ });
11250
+ const npxStdout = npxResult.stdout?.trim() ?? "";
11251
+ const npxStderr = npxResult.stderr?.trim() ?? "";
11252
+ const npxExitCode = npxResult.status ?? 1;
11253
+ if (npxStdout) log(`npx stdout:\n${npxStdout}`);
11254
+ if (npxStderr) log(`npx stderr:\n${npxStderr}`);
11255
+ log(`npx exit: ${npxExitCode}${npxResult.error ? ` error: ${npxResult.error.message}` : ""}`);
11256
+ if (statusCheckDelayMs > 0) {
11257
+ log("");
11258
+ log(`── 等待 ${statusCheckDelayMs / 1e3}s(让 openclaw 服务完成重启) ─────────────`);
11259
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, statusCheckDelayMs);
11260
+ log("wait done");
11261
+ }
11262
+ const doRollback = (reason) => {
11263
+ log(`ERROR: ${reason}`);
11264
+ const rollbackOk = restoreFiles(fsOpts);
11265
+ log(`rollback: ${rollbackOk ? "ok" : "FAILED"}`);
11266
+ return {
11267
+ ok: false,
11268
+ error: reason,
11269
+ validationError: reason,
11270
+ stdout: npxStdout,
11271
+ stderr: npxStderr,
11272
+ exitCode: npxExitCode,
11273
+ rollbackOk,
11274
+ logFile
11275
+ };
11276
+ };
11277
+ log("");
11278
+ log("── [4/6] 插件安装检查 + 版本兼容校验 ───────────────────");
11279
+ const larkExtDir = node_path.default.join(cwd, "extensions", "openclaw-lark");
11280
+ const larkVersion = readPkgVersion(node_path.default.join(larkExtDir, "package.json"));
11281
+ log(` extensions/openclaw-lark: ${node_fs.default.existsSync(larkExtDir) ? "exists" : "missing"}, version=${larkVersion ?? "n/a"}`);
11282
+ if (!node_fs.default.existsSync(larkExtDir)) return doRollback("extensions/openclaw-lark not found after install");
11283
+ if (!larkVersion) return doRollback("extensions/openclaw-lark/package.json has no valid version after install");
11284
+ log(" running doctor version compat check...");
11285
+ const compatArgs = ["doctor"];
11286
+ if (opts.scene) compatArgs.push(`--scene=${opts.scene}`);
11287
+ const compatResult = (0, node_child_process.spawnSync)(process.execPath, [cliScript, ...compatArgs], {
11288
+ cwd,
11289
+ encoding: "utf-8",
11290
+ stdio: [
11291
+ "ignore",
11292
+ "pipe",
11293
+ "pipe"
11294
+ ],
11295
+ timeout: 6e4,
11296
+ env: process.env
11297
+ });
11298
+ if (compatResult.stdout?.trim()) log(`doctor(compat) stdout:\n${compatResult.stdout.trim()}`);
11299
+ if (compatResult.stderr?.trim()) log(`doctor(compat) stderr:\n${compatResult.stderr.trim()}`);
11300
+ log(`doctor(compat) exit: ${compatResult.status ?? "null"}${compatResult.error ? ` error: ${compatResult.error.message}` : ""}`);
11301
+ const compatError = checkVersionCompatFromDoctorOutput(compatResult.stdout?.trim() ?? "", log);
11302
+ if (compatError) return doRollback(compatError);
11303
+ log(" version compat: ok");
11304
+ logVersionSnapshot("after-versions", snapshotVersions(cwd, log), log);
11305
+ log("");
11306
+ log("── [5/6] channels probe(升级后)────────────────────────");
11307
+ if (!probeChannels("after", log, 3e4).anyAccountWorking) {
11308
+ log(" channels: not working before or after install — pre-existing issue, skipping rollback");
11309
+ return {
11310
+ ok: false,
11311
+ error: "channels probe: no working account (pre-existing issue, not caused by install)",
11312
+ validationError: "channels probe: no working account (pre-existing)",
11313
+ stdout: npxStdout,
11314
+ stderr: npxStderr,
11315
+ exitCode: npxExitCode,
11316
+ logFile
11317
+ };
11318
+ }
11319
+ log(" channels: ok (recovered after install)");
11320
+ log("");
11321
+ log("── [6/6] doctor --fix ────────────────────────────────────");
11322
+ const fixArgs = ["doctor", "--fix"];
11323
+ if (opts.scene) fixArgs.push(`--scene=${opts.scene}`);
11324
+ const fixResult = (0, node_child_process.spawnSync)(process.execPath, [cliScript, ...fixArgs], {
11325
+ cwd,
11326
+ encoding: "utf-8",
11327
+ stdio: [
11328
+ "ignore",
11329
+ "pipe",
11330
+ "pipe"
11331
+ ],
11332
+ timeout: 6e4,
11333
+ env: process.env
11334
+ });
11335
+ if (fixResult.stdout?.trim()) log(`doctor(fix) stdout:\n${fixResult.stdout.trim()}`);
11336
+ if (fixResult.stderr?.trim()) log(`doctor(fix) stderr:\n${fixResult.stderr.trim()}`);
11337
+ log(`doctor(fix) exit: ${fixResult.status ?? "null"}${fixResult.error ? ` error: ${fixResult.error.message}` : ""}`);
11338
+ log("");
11339
+ log(`${"=".repeat(60)}`);
11340
+ log("upgrade-lark completed successfully");
11341
+ log(`${"=".repeat(60)}`);
11342
+ return {
11343
+ ok: true,
11344
+ stdout: npxStdout,
11345
+ stderr: npxStderr,
11346
+ exitCode: npxExitCode,
11347
+ logFile
11348
+ };
11349
+ }
10870
11350
  //#endregion
10871
11351
  //#region src/index.ts
10872
11352
  const args = node_process.default.argv.slice(2);
10873
11353
  const mode = args.find((a) => !a.startsWith("-"));
10874
11354
  /**
11355
+ * Decode `--ctx=<base64>` into an opaque JSON object. Returns undefined when
11356
+ * the flag isn't present — the caller decides whether to fall back to the
11357
+ * innerapi or to error out.
11358
+ *
11359
+ * The object's shape is not enforced here; downstream code consumes it via
11360
+ * either `normalizeCtx()` (new path) or direct field access for the legacy
11361
+ * check/repair/reset contract still used by sandbox_console push.
11362
+ */
11363
+ function parseCtxFlag(args) {
11364
+ const ctxArg = args.find((a) => a.startsWith("--ctx="));
11365
+ if (!ctxArg) return void 0;
11366
+ const b64 = ctxArg.slice(6);
11367
+ return JSON.parse(Buffer.from(b64, "base64").toString("utf-8"));
11368
+ }
11369
+ /**
10875
11370
  * Pull the first non-flag positional after the mode name.
10876
11371
  * (The mode itself is args[0] in the filtered set, so we skip index 0.)
10877
11372
  */
@@ -10899,8 +11394,8 @@ function getMultiFlag(args, name) {
10899
11394
  * case but is no longer consulted.
10900
11395
  */
10901
11396
  async function reportRun(command, rc, _raw, invocation, durationMs, outcome, slardar = {
10902
- scene: void 0,
10903
- profile: "standard",
11397
+ scene,
11398
+ profile,
10904
11399
  fix: false
10905
11400
  }) {
10906
11401
  console.error(`${command}: telemetry calling report_cli_run`);
@@ -10964,7 +11459,7 @@ async function main() {
10964
11459
  console.error(`${mode}: begin argv=[${args.join(" ")}] version=${getVersion()} traceId=${traceId ?? "-"} caller=${caller ?? "-"} runIdGenerated=${rc.generated}`);
10965
11460
  switch (mode) {
10966
11461
  case "check": {
10967
- const raw = await fetchCtxViaInnerApi({
11462
+ const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
10968
11463
  populate: planCtxPopulate({
10969
11464
  command: "check",
10970
11465
  profile
@@ -10989,7 +11484,7 @@ async function main() {
10989
11484
  break;
10990
11485
  }
10991
11486
  case "repair": {
10992
- const raw = await fetchCtxViaInnerApi({
11487
+ const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
10993
11488
  populate: planCtxPopulate({
10994
11489
  command: "repair",
10995
11490
  profile
@@ -11060,15 +11555,27 @@ async function main() {
11060
11555
  break;
11061
11556
  }
11062
11557
  case "reset":
11063
- if (args.includes("--async")) console.log(JSON.stringify(startAsyncReset()));
11064
- else if (args.includes("--worker")) {
11558
+ if (args.includes("--async")) {
11559
+ const ctxArg = args.find((a) => a.startsWith("--ctx="));
11560
+ let ctxBase64;
11561
+ if (ctxArg) ctxBase64 = ctxArg.slice(6);
11562
+ else {
11563
+ const fetched = await fetchCtxViaInnerApi({
11564
+ populate: planCtxPopulate({ command: "reset" }),
11565
+ caller,
11566
+ traceId
11567
+ });
11568
+ ctxBase64 = Buffer.from(JSON.stringify(fetched), "utf-8").toString("base64");
11569
+ }
11570
+ console.log(JSON.stringify(startAsyncReset(ctxBase64)));
11571
+ } else if (args.includes("--worker")) {
11065
11572
  const taskId = args.find((a) => a.startsWith("--task-id="))?.slice(10);
11066
11573
  if (!taskId) {
11067
11574
  console.error("Error: --task-id=<id> is required for worker");
11068
11575
  node_process.default.exit(1);
11069
11576
  }
11070
11577
  const resultFile = resetResultFile(taskId);
11071
- const raw = await fetchCtxViaInnerApi({
11578
+ const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11072
11579
  populate: planCtxPopulate({ command: "reset" }),
11073
11580
  caller,
11074
11581
  traceId
@@ -11092,7 +11599,7 @@ async function main() {
11092
11599
  return;
11093
11600
  }
11094
11601
  } else {
11095
- console.error("Usage: reset --async | reset --worker --task-id=<id>");
11602
+ console.error("Usage: reset --async [--ctx=<base64>] | reset --worker --task-id=<id> [--ctx=<base64>]");
11096
11603
  node_process.default.exit(1);
11097
11604
  }
11098
11605
  break;
@@ -11108,14 +11615,14 @@ async function main() {
11108
11615
  case "install-openclaw": {
11109
11616
  const tag = getPositionalTag(args, "install-openclaw");
11110
11617
  if (!tag) {
11111
- console.error("Usage: install-openclaw <tag> [--oss_file_map=<base64>]");
11618
+ console.error("Usage: install-openclaw <tag> [--ctx=<base64> | --oss_file_map=<base64>]");
11112
11619
  node_process.default.exit(1);
11113
11620
  }
11114
11621
  const ossFileMapFlag = getFlag(args, "oss_file_map");
11115
11622
  let installOssFileMap;
11116
11623
  let rawForTelemetry;
11117
11624
  if (!ossFileMapFlag) {
11118
- rawForTelemetry = await fetchCtxViaInnerApi({
11625
+ rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11119
11626
  populate: planCtxPopulate({ command: "install" }),
11120
11627
  caller,
11121
11628
  traceId
@@ -11150,7 +11657,7 @@ async function main() {
11150
11657
  case "install-extension": {
11151
11658
  const tag = getPositionalTag(args, "install-extension");
11152
11659
  if (!tag) {
11153
- console.error("Usage: install-extension <tag> (--all | --extension=<name>...) [--home_base=<dir>] [--config_path=<path>] [--skip-config-update] [--oss_file_map=<base64>]");
11660
+ console.error("Usage: install-extension <tag> (--all | --extension=<name>...) [--home_base=<dir>] [--config_path=<path>] [--skip-config-update] [--ctx=<base64> | --oss_file_map=<base64>]");
11154
11661
  node_process.default.exit(1);
11155
11662
  }
11156
11663
  const all = args.includes("--all");
@@ -11162,7 +11669,7 @@ async function main() {
11162
11669
  let installOssFileMap;
11163
11670
  let rawForTelemetry;
11164
11671
  if (!ossFileMapFlag) {
11165
- rawForTelemetry = await fetchCtxViaInnerApi({
11672
+ rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11166
11673
  populate: planCtxPopulate({ command: "install" }),
11167
11674
  caller,
11168
11675
  traceId
@@ -11208,12 +11715,12 @@ async function main() {
11208
11715
  case "install-cli": {
11209
11716
  const tag = getPositionalTag(args, "install-cli");
11210
11717
  if (!tag) {
11211
- console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--oss_file_map=<base64>]");
11718
+ console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--ctx=<base64> | --oss_file_map=<base64>]");
11212
11719
  node_process.default.exit(1);
11213
11720
  }
11214
11721
  const names = getMultiFlag(args, "cli");
11215
11722
  if (names.length === 0) {
11216
- console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--oss_file_map=<base64>]");
11723
+ console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--ctx=<base64> | --oss_file_map=<base64>]");
11217
11724
  node_process.default.exit(1);
11218
11725
  }
11219
11726
  const homeBase = getFlag(args, "home_base");
@@ -11221,7 +11728,7 @@ async function main() {
11221
11728
  let installOssFileMap;
11222
11729
  let rawForTelemetry;
11223
11730
  if (!ossFileMapFlag) {
11224
- rawForTelemetry = await fetchCtxViaInnerApi({
11731
+ rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11225
11732
  populate: planCtxPopulate({ command: "install" }),
11226
11733
  caller,
11227
11734
  traceId
@@ -11269,7 +11776,7 @@ async function main() {
11269
11776
  case "download-resource": {
11270
11777
  const tag = getPositionalTag(args, "download-resource");
11271
11778
  if (!tag) {
11272
- console.error("Usage: download-resource <tag> --role=<role> --name=<name> [--dir=<dir>] [--oss_file_map=<base64>]");
11779
+ console.error("Usage: download-resource <tag> --role=<role> --name=<name> [--dir=<dir>] [--ctx=<base64> | --oss_file_map=<base64>]");
11273
11780
  node_process.default.exit(1);
11274
11781
  }
11275
11782
  const role = getFlag(args, "role");
@@ -11283,7 +11790,7 @@ async function main() {
11283
11790
  let installOssFileMap;
11284
11791
  let rawForTelemetry;
11285
11792
  if (!ossFileMapFlag) {
11286
- rawForTelemetry = await fetchCtxViaInnerApi({
11793
+ rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11287
11794
  populate: planCtxPopulate({ command: "install" }),
11288
11795
  caller,
11289
11796
  traceId
@@ -11357,6 +11864,50 @@ async function main() {
11357
11864
  if (!result.ok) node_process.default.exit(1);
11358
11865
  break;
11359
11866
  }
11867
+ case "upgrade-lark": {
11868
+ const result = runUpgradeLark({
11869
+ runId: rc.runId,
11870
+ scene
11871
+ });
11872
+ const upgradeDurationMs = Date.now() - t0;
11873
+ console.log(JSON.stringify(result));
11874
+ reportUpgradeLarkToSlardar({
11875
+ scene,
11876
+ durationMs: upgradeDurationMs,
11877
+ success: result.ok,
11878
+ logFile: result.logFile,
11879
+ exitCode: result.exitCode,
11880
+ rollbackOk: result.rollbackOk,
11881
+ validationError: result.validationError,
11882
+ error: result.error
11883
+ });
11884
+ try {
11885
+ await reportCliRun({
11886
+ command: "upgrade-lark",
11887
+ runId: rc.runId,
11888
+ version: getVersion(),
11889
+ invocation: args.join(" "),
11890
+ durationMs: upgradeDurationMs,
11891
+ caller: rc.caller,
11892
+ traceId: rc.traceId,
11893
+ success: result.ok,
11894
+ result,
11895
+ error: result.ok ? void 0 : { message: result.error ?? "upgrade-lark failed" }
11896
+ });
11897
+ } catch (e) {
11898
+ console.error(`[telemetry] reportCliRun failed: ${e.message}`);
11899
+ }
11900
+ if (!result.ok) {
11901
+ node_process.default.exitCode = 1;
11902
+ return;
11903
+ }
11904
+ break;
11905
+ }
11906
+ case "channels-probe": {
11907
+ const result = runChannelsProbe(getFlag(args, "timeout") ? Number(getFlag(args, "timeout")) : void 0);
11908
+ console.log(JSON.stringify(result));
11909
+ break;
11910
+ }
11360
11911
  default:
11361
11912
  node_process.default.stderr.write(`Unknown command: ${mode}\n\n`);
11362
11913
  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.14-alpha.3",
3
+ "version": "0.1.14-alpha.5",
4
4
  "description": "CLI for OpenClaw config diagnose and repair with JSON5 support",
5
5
  "main": "dist/index.cjs",
6
6
  "bin": {