@lark-apaas/openclaw-scripts-diagnose-cli 0.1.14-alpha.7 → 0.1.14-alpha.9

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 +754 -226
  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.7";
55
+ return "0.1.14-alpha.9";
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);
@@ -3370,6 +3369,7 @@ let FeishuPluginOpenclawUpgradeRule = class FeishuPluginOpenclawUpgradeRule exte
3370
3369
  if (!cc) 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,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,
@@ -3451,14 +3459,16 @@ function describeCompatConstraint(entry, pluginVersion) {
3451
3459
  /**
3452
3460
  * @lark-apaas/openclaw-lark 豁免 VERSION_COMPAT_MAP,但仍要求 openclaw ≥ FORK_LARK_PLUGIN_MIN_OC_VERSION。
3453
3461
  * 其他 @lark-apaas scope 的 fork 插件继续无条件 pass。
3462
+ * recommendedOc 可为 undefined(doctor 模式),此时只检测最低版本要求,不指定目标升级版本。
3454
3463
  */
3455
3464
  function validateForkPlugin(installed, ocCur, recommendedOc) {
3456
3465
  if (installed.fullName !== FORK_LARK_PLUGIN_FULL_NAME) return { pass: true };
3457
3466
  if (compareCalVer(ocCur, FORK_LARK_PLUGIN_MIN_OC_VERSION) >= 0) return { pass: true };
3467
+ const recommendation = recommendedOc ? `;将 openclaw 升级到 ${recommendedOc} 即可满足` : `;请升级 openclaw 至 ${FORK_LARK_PLUGIN_MIN_OC_VERSION} 或更高版本`;
3458
3468
  return {
3459
3469
  pass: false,
3460
3470
  action: "upgrade_openclaw",
3461
- message: `飞书插件 ${describePlugin(installed)}(fork 版)要求 openclaw ≥ ${FORK_LARK_PLUGIN_MIN_OC_VERSION},当前 openclaw@${ocCur} 低于此要求;将 openclaw 升级到 ${recommendedOc} 即可满足`
3471
+ message: `飞书插件 ${describePlugin(installed)}(fork 版)要求 openclaw ≥ ${FORK_LARK_PLUGIN_MIN_OC_VERSION},当前 openclaw@${ocCur} 低于此要求${recommendation}`
3462
3472
  };
3463
3473
  }
3464
3474
  function describePlugin(p) {
@@ -3505,6 +3515,188 @@ function extractScopedNameFromSpec$1(spec) {
3505
3515
  const at = spec.indexOf("@", 1);
3506
3516
  return at === -1 ? spec : spec.slice(0, at);
3507
3517
  }
3518
+ /**
3519
+ * Returns true if the installed feishu plugin is version-incompatible with
3520
+ * the current openclaw (or is a legacy plugin that must be replaced).
3521
+ * Used by the upgrade_lark_needed rule and the upgrade-lark pre-check gate.
3522
+ */
3523
+ function needsLarkUpgrade(ctx) {
3524
+ const cc = resolveCompatContext(ctx);
3525
+ if (!cc) return false;
3526
+ const { ocCur, recommendedOc, installed, isLegacy } = cc;
3527
+ if (isForkPlugin(installed)) {
3528
+ if (recommendedOc) return false;
3529
+ if (installed.fullName === FORK_LARK_PLUGIN_FULL_NAME) return compareCalVer(ocCur, FORK_LARK_PLUGIN_MIN_OC_VERSION) < 0;
3530
+ return false;
3531
+ }
3532
+ if (recommendedOc) return resolveUpgradeDirection(installed, ocCur, recommendedOc, isLegacy) === "lark";
3533
+ return isLegacy || !isVersionCompatible(installed, ocCur);
3534
+ }
3535
+ //#endregion
3536
+ //#region src/channels-probe.ts
3537
+ const FEISHU_INVALID_CONFIG_MSG = "channels.feishu: invalid config: must NOT have additional properties";
3538
+ const CHANNEL_LINE_RE = /^-\s+Feishu\s+([^:]+):\s+(.+)$/;
3539
+ /**
3540
+ * Port of Python `_account_is_working` from the feishu-channel-success-rate skill.
3541
+ *
3542
+ * Strips colon-prefixed key:value bits (dm:, bot:, in:, out:, token:, allow:,
3543
+ * intents:, groups:, health:) and evaluates the canonical health formula.
3544
+ */
3545
+ function accountIsWorking(bits) {
3546
+ const bitTokens = /* @__PURE__ */ new Set();
3547
+ let hasError = false;
3548
+ let hasProbeFailed = false;
3549
+ for (const raw of bits) {
3550
+ const b = raw.trim();
3551
+ if (!b) continue;
3552
+ if (b.startsWith("error:")) {
3553
+ hasError = true;
3554
+ continue;
3555
+ }
3556
+ if (b === "probe failed") {
3557
+ hasProbeFailed = true;
3558
+ continue;
3559
+ }
3560
+ bitTokens.add(b.split(":")[0]);
3561
+ }
3562
+ if (!bitTokens.has("enabled") || !bitTokens.has("configured")) return false;
3563
+ if (bitTokens.has("works")) return true;
3564
+ if (bitTokens.has("running") && !hasError && !hasProbeFailed) return true;
3565
+ return false;
3566
+ }
3567
+ /**
3568
+ * Parse the raw stdout of `openclaw channels status --probe`.
3569
+ * Port of Python `extract_channels_probe` from the feishu-channel-success-rate skill.
3570
+ */
3571
+ function parseChannelsProbeOutput(text) {
3572
+ const gatewayReachable = text.includes("Gateway reachable");
3573
+ const feishuConfigInvalid = text.includes(FEISHU_INVALID_CONFIG_MSG);
3574
+ const accounts = [];
3575
+ let anyAccountWorking = false;
3576
+ for (const line of text.split("\n")) {
3577
+ const m = CHANNEL_LINE_RE.exec(line.trim());
3578
+ if (!m) continue;
3579
+ const [, acct, rest] = m;
3580
+ const bits = rest.split(",").map((b) => b.trim());
3581
+ const isWorking = accountIsWorking(bits);
3582
+ if (isWorking) anyAccountWorking = true;
3583
+ accounts.push({
3584
+ id: acct.trim(),
3585
+ bits,
3586
+ isWorking,
3587
+ raw: line.trim()
3588
+ });
3589
+ }
3590
+ return {
3591
+ gatewayReachable,
3592
+ feishuConfigInvalid,
3593
+ accounts,
3594
+ anyAccountWorking
3595
+ };
3596
+ }
3597
+ /**
3598
+ * Run `openclaw channels status --probe` and return a structured result.
3599
+ *
3600
+ * The command may exit non-zero when some bot accounts fail their probe — that
3601
+ * is still useful output. We therefore try to parse stdout even when the
3602
+ * process exits with a non-zero code, falling back to an unavailable result
3603
+ * only when there is genuinely no output to parse.
3604
+ *
3605
+ * @param timeoutMs Maximum wait time. Default is 60 s because v2026.4.x
3606
+ * lacks a per-request HTTP timeout and can block indefinitely.
3607
+ */
3608
+ function runChannelsProbe(timeoutMs = 6e4) {
3609
+ let stdout = "";
3610
+ let stderrText = "";
3611
+ let execError;
3612
+ try {
3613
+ stdout = (0, node_child_process.execSync)("openclaw channels status --probe", {
3614
+ encoding: "utf-8",
3615
+ timeout: timeoutMs,
3616
+ stdio: [
3617
+ "ignore",
3618
+ "pipe",
3619
+ "pipe"
3620
+ ]
3621
+ });
3622
+ } catch (e) {
3623
+ const err = e;
3624
+ const stdoutRaw = err.stdout;
3625
+ stdout = typeof stdoutRaw === "string" ? stdoutRaw : stdoutRaw?.toString("utf-8") ?? "";
3626
+ execError = err.message;
3627
+ const stderrRaw = err.stderr;
3628
+ stderrText = (typeof stderrRaw === "string" ? stderrRaw : stderrRaw?.toString("utf-8") ?? "").trim();
3629
+ if (stderrText) console.error(`channels-probe: stderr from CLI: ${stderrText}`);
3630
+ }
3631
+ if (stdout.trim()) return {
3632
+ available: true,
3633
+ ...parseChannelsProbeOutput(stdout)
3634
+ };
3635
+ return {
3636
+ available: false,
3637
+ gatewayReachable: false,
3638
+ feishuConfigInvalid: stderrText.includes(FEISHU_INVALID_CONFIG_MSG),
3639
+ accounts: [],
3640
+ anyAccountWorking: false,
3641
+ error: execError ?? "no output from openclaw channels status --probe"
3642
+ };
3643
+ }
3644
+ //#endregion
3645
+ //#region src/rules/upgrade-lark-needed.ts
3646
+ /**
3647
+ * Detects the condition that warrants running `upgrade-lark`:
3648
+ * - feishu plugin version incompatible with current openclaw, OR
3649
+ * - openclaw channels status --probe reports feishu channel config invalid; AND
3650
+ * - channels are not working.
3651
+ *
3652
+ * Both conditions must be true simultaneously. If version is compatible and
3653
+ * feishu config is valid, or channels are working, the rule passes (no action needed).
3654
+ *
3655
+ * feishuConfigInvalid is read from the channels probe output rather than running a
3656
+ * separate `openclaw status` call, since only `openclaw channels status --probe`
3657
+ * reliably surfaces the schema validation error.
3658
+ *
3659
+ * profile: experimental — runs only in full sweep mode, not in standard doctor.
3660
+ * level: silent — telemetry/sweep-only, does not trigger page-level repair UI.
3661
+ */
3662
+ let UpgradeLarkNeededRule = class UpgradeLarkNeededRule extends DiagnoseRule {
3663
+ validate(ctx) {
3664
+ let versionIncompatible = false;
3665
+ try {
3666
+ versionIncompatible = needsLarkUpgrade(ctx);
3667
+ } catch {
3668
+ versionIncompatible = true;
3669
+ }
3670
+ let probeResult;
3671
+ try {
3672
+ probeResult = runChannelsProbe(6e4);
3673
+ } catch {
3674
+ probeResult = {
3675
+ available: false,
3676
+ gatewayReachable: false,
3677
+ feishuConfigInvalid: false,
3678
+ accounts: [],
3679
+ anyAccountWorking: false
3680
+ };
3681
+ }
3682
+ const feishuConfigInvalid = probeResult.feishuConfigInvalid;
3683
+ if (!(versionIncompatible || feishuConfigInvalid)) return { pass: true };
3684
+ if (probeResult.anyAccountWorking) return { pass: true };
3685
+ return {
3686
+ pass: false,
3687
+ action: "upgrade_lark",
3688
+ message: `飞书插件需要升级且 channels 不可用(版本不兼容=${versionIncompatible}, feishu配置无效=${feishuConfigInvalid}),建议执行 upgrade-lark 命令升级飞书插件`
3689
+ };
3690
+ }
3691
+ };
3692
+ UpgradeLarkNeededRule = __decorate([Rule({
3693
+ key: "upgrade_lark_needed",
3694
+ description: "检测飞书插件版本不兼容且 channels 不可用,判断是否需要执行 upgrade-lark 升级",
3695
+ repairMode: "check-only",
3696
+ level: "silent",
3697
+ profile: "experimental",
3698
+ usesVars: ["recommendedOpenclawTag"]
3699
+ })], UpgradeLarkNeededRule);
3508
3700
  //#endregion
3509
3701
  //#region src/rules/cleanup-install-backup-dirs.ts
3510
3702
  const DIR_PREFIX = ".openclaw-install-";
@@ -3650,117 +3842,6 @@ LarkCliMissingForInstalledLarkPluginRule = __decorate([Rule({
3650
3842
  usesVars: ["recommendedOpenclawTag"]
3651
3843
  })], LarkCliMissingForInstalledLarkPluginRule);
3652
3844
  //#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);
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);
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
- * appSecret is validated based on its current type:
3692
- * - object → must match canonical provider-ref
3693
- * - string → must match larkApps plaintext
3694
- */
3695
- checkBot(label, bot, larkApp, issues) {
3696
- const creatorOpenID = larkApp.creatorOpenID;
3697
- const allowFrom = Array.isArray(bot.allowFrom) ? bot.allowFrom : [];
3698
- if (typeof creatorOpenID === "string" && creatorOpenID !== "") {
3699
- if (!allowFrom.includes(creatorOpenID)) issues.push(`${label} allowFrom missing creatorOpenID ${creatorOpenID.length > 8 ? creatorOpenID.slice(0, 4) + "***" + creatorOpenID.slice(-4) : "***"}`);
3700
- } else if (allowFrom.length === 0) issues.push(`${label} allowFrom is empty (creatorOpenID unavailable, cannot auto-fix)`);
3701
- const secret = bot.appSecret;
3702
- if (typeof secret === "object" && secret !== null && !Array.isArray(secret)) {
3703
- if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) issues.push(`${label} appSecret is a provider-ref but not the canonical one`);
3704
- } else if (typeof secret === "string") {
3705
- if (secret !== larkApp.appSecret) issues.push(`${label} appSecret plaintext mismatch`);
3706
- } else issues.push(`${label} appSecret has unexpected type ${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);
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);
3727
- }
3728
- }
3729
- /** Fix a single bot entry in-place.
3730
- * appSecret is repaired based on its current type:
3731
- * - object → fix to canonical provider-ref
3732
- * - string → fix to larkApps plaintext
3733
- */
3734
- fixBot(bot, larkApp) {
3735
- const creatorOpenID = larkApp.creatorOpenID;
3736
- if (typeof creatorOpenID === "string" && creatorOpenID !== "") {
3737
- const allowFrom = Array.isArray(bot.allowFrom) ? [...bot.allowFrom] : [];
3738
- if (!allowFrom.includes(creatorOpenID)) {
3739
- allowFrom.push(creatorOpenID);
3740
- bot.allowFrom = allowFrom;
3741
- }
3742
- }
3743
- const secret = bot.appSecret;
3744
- if (typeof secret === "object" && secret !== null && !Array.isArray(secret)) {
3745
- if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) bot.appSecret = { ...DEFAULT_FEISHU_APP_SECRET };
3746
- } else if (typeof secret === "string") {
3747
- if (secret !== larkApp.appSecret) bot.appSecret = larkApp.appSecret;
3748
- } else bot.appSecret = { ...DEFAULT_FEISHU_APP_SECRET };
3749
- }
3750
- };
3751
- FeishuBotChannelConfigRule = __decorate([Rule({
3752
- key: "feishu_bot_channel_config",
3753
- description: "确保飞书配置中 bot 账号的 allowFrom 包含其创建者 openID 且 appSecret 值正确",
3754
- dependsOn: [
3755
- "config_syntax_check",
3756
- "feishu_default_account",
3757
- "feishu_bot_id"
3758
- ],
3759
- repairMode: "standard",
3760
- usesVars: ["larkApps"],
3761
- level: "critical"
3762
- })], FeishuBotChannelConfigRule);
3763
- //#endregion
3764
3845
  //#region src/check.ts
3765
3846
  /** Telemetry-aware entry: returns both the legacy CheckResult (for stdout)
3766
3847
  * AND a DoctorReport-shape payload (for `openclaw.report_cli_run`). The
@@ -4224,6 +4305,9 @@ const PROVIDER_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-provider-
4224
4305
  const SECRETS_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-openclaw-secrets.json";
4225
4306
  /** Absolute path to the openclaw config JSON. */
4226
4307
  const CONFIG_PATH = `${WORKSPACE_DIR}/openclaw.json`;
4308
+ function upgradeLarkLogFile(runId) {
4309
+ return `${DIAGNOSE_DIR}/upgrade-lark-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/:/g, "-")}-${runId.slice(0, 8)}.log`;
4310
+ }
4227
4311
  //#endregion
4228
4312
  //#region src/run-log.ts
4229
4313
  let currentRunContext;
@@ -4360,10 +4444,9 @@ function makeLogger(logFile) {
4360
4444
  /**
4361
4445
  * Start an async reset task: spawn a detached child process and return the taskId.
4362
4446
  *
4363
- * The child process runs: node cli.js reset --worker --task-id=xxx
4364
- * The worker fetches ctx from innerApi itself — no --ctx passthrough.
4447
+ * The child process runs: node cli.js reset --worker --task-id=xxx --ctx=base64
4365
4448
  */
4366
- function startAsyncReset() {
4449
+ function startAsyncReset(ctxBase64) {
4367
4450
  const taskId = (0, node_crypto.randomUUID)();
4368
4451
  const resultFile = resetResultFile(taskId);
4369
4452
  const log = makeLogger(resetLogFile(taskId));
@@ -4387,7 +4470,8 @@ function startAsyncReset() {
4387
4470
  process.argv[1],
4388
4471
  "reset",
4389
4472
  "--worker",
4390
- `--task-id=${taskId}`
4473
+ `--task-id=${taskId}`,
4474
+ `--ctx=${ctxBase64}`
4391
4475
  ], {
4392
4476
  detached: true,
4393
4477
  stdio: "ignore",
@@ -6901,60 +6985,6 @@ function mergeCoreBackupAndOrigins(configPath, vars, resetData, log) {
6901
6985
  log(`allowedOrigins: added ${added.length} (${JSON.stringify(added)}), total now ${mergedOrigins.length}`);
6902
6986
  }
6903
6987
  /**
6904
- * Fix bot account allowFrom and appSecret using larkApps from innerApi.
6905
- *
6906
- * For each bot account (key starts with `bot-cli_`):
6907
- * - allowFrom must contain the bot's own creatorOpenID from larkApps
6908
- * - appSecret must be either the canonical provider-ref or match larkApps plaintext
6909
- *
6910
- * Runs after mergeCoreBackupAndOrigins so it operates on the final config state.
6911
- */
6912
- function fixBotChannelConfig(configPath, larkApps, log) {
6913
- if (!larkApps || larkApps.length === 0) {
6914
- log("no larkApps data, skip bot channel config fix");
6915
- return;
6916
- }
6917
- const config = loadJSON5().parse(node_fs.default.readFileSync(configPath, "utf-8"));
6918
- const accounts = asRecord(getNestedMap(config, "channels", "feishu")?.accounts);
6919
- if (!accounts) {
6920
- log("no feishu accounts in config, skip bot channel config fix");
6921
- return;
6922
- }
6923
- let fixCount = 0;
6924
- for (const [, account] of Object.entries(accounts)) {
6925
- const bot = asRecord(account);
6926
- if (!bot) continue;
6927
- const appId = bot.appId;
6928
- if (typeof appId !== "string" || !appId.startsWith("cli_")) continue;
6929
- const larkApp = larkApps.find((e) => e.larkAppID === appId);
6930
- if (!larkApp) continue;
6931
- const creatorOpenID = larkApp.creatorOpenID;
6932
- if (typeof creatorOpenID === "string" && creatorOpenID !== "") {
6933
- const allowFrom = Array.isArray(bot.allowFrom) ? [...bot.allowFrom] : [];
6934
- if (!allowFrom.includes(creatorOpenID)) {
6935
- allowFrom.push(creatorOpenID);
6936
- bot.allowFrom = allowFrom;
6937
- fixCount++;
6938
- }
6939
- }
6940
- const secret = bot.appSecret;
6941
- let needsFix = false;
6942
- if (typeof secret === "object" && secret !== null && !Array.isArray(secret)) {
6943
- if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) needsFix = true;
6944
- } else if (typeof secret === "string") {
6945
- if (secret !== larkApp.appSecret) needsFix = true;
6946
- } else needsFix = true;
6947
- if (needsFix) {
6948
- bot.appSecret = { ...DEFAULT_FEISHU_APP_SECRET };
6949
- fixCount++;
6950
- }
6951
- }
6952
- if (fixCount > 0) {
6953
- node_fs.default.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
6954
- log(`fixed ${fixCount} bot channel config issue(s) (allowFrom/appSecret)`);
6955
- } else log("bot channel config ok, no fixes needed");
6956
- }
6957
- /**
6958
6988
  * Step 7: Verify startup scripts landed in configDir/scripts/.
6959
6989
  *
6960
6990
  * Scripts are extracted directly to configDir/scripts/ during stageTemplate —
@@ -7099,7 +7129,6 @@ async function runReset(input, taskId, resultFile) {
7099
7129
  await step5InstallOpenclaw(openclawTag, ossFileMap, log);
7100
7130
  step(6);
7101
7131
  mergeCoreBackupAndOrigins(configPath, vars, resetData, log);
7102
- fixBotChannelConfig(configPath, vars.larkApps, log);
7103
7132
  step(7);
7104
7133
  verifyStartupScripts(configDir, log);
7105
7134
  step(8);
@@ -7898,8 +7927,7 @@ function normalizeCtx(raw) {
7898
7927
  reset: {
7899
7928
  templateVars: r.reset.templateVars ?? {},
7900
7929
  coreBackup: r.reset.coreBackup
7901
- },
7902
- larkApps: Array.isArray(r.larkApps) ? r.larkApps : []
7930
+ }
7903
7931
  };
7904
7932
  }
7905
7933
  const vars = r.vars ?? {};
@@ -7924,8 +7952,7 @@ function normalizeCtx(raw) {
7924
7952
  reset: {
7925
7953
  templateVars: resetData.templateVars ?? {},
7926
7954
  coreBackup: resetData.coreBackup
7927
- },
7928
- larkApps: Array.isArray(r.larkApps) ? r.larkApps : []
7955
+ }
7929
7956
  };
7930
7957
  }
7931
7958
  function fillApp(src) {
@@ -7990,8 +8017,7 @@ function buildCheckInput(raw, configPathOverride) {
7990
8017
  providerFilePath: PROVIDER_FILE_PATH,
7991
8018
  secretsFilePath: SECRETS_FILE_PATH,
7992
8019
  templateVars: ctx.app.templateVars,
7993
- recommendedOpenclawTag: ctx.app.recommendedOpenclawTag,
7994
- larkApps: ctx.larkApps
8020
+ recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
7995
8021
  },
7996
8022
  templateVars: ctx.app.templateVars
7997
8023
  };
@@ -8023,8 +8049,7 @@ function buildRepairInput(raw, configPathOverride) {
8023
8049
  providerFilePath: PROVIDER_FILE_PATH,
8024
8050
  secretsFilePath: SECRETS_FILE_PATH,
8025
8051
  templateVars: ctx.app.templateVars,
8026
- recommendedOpenclawTag: ctx.app.recommendedOpenclawTag,
8027
- larkApps: ctx.larkApps
8052
+ recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
8028
8053
  },
8029
8054
  repairData: {
8030
8055
  secretsContent: ctx.secrets.secretsContent,
@@ -8060,8 +8085,7 @@ function buildResetInput(raw, configPathOverride) {
8060
8085
  providerFilePath: PROVIDER_FILE_PATH,
8061
8086
  secretsFilePath: SECRETS_FILE_PATH,
8062
8087
  templateVars: ctx.app.templateVars,
8063
- recommendedOpenclawTag: ctx.app.recommendedOpenclawTag,
8064
- larkApps: ctx.larkApps
8088
+ recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
8065
8089
  },
8066
8090
  resetData: {
8067
8091
  templateVars: ctx.reset.templateVars,
@@ -10371,7 +10395,7 @@ async function reportCliRun(opts) {
10371
10395
  //#region src/help.ts
10372
10396
  const BIN = "mclaw-diagnose";
10373
10397
  function versionBanner() {
10374
- return `v0.1.14-alpha.7`;
10398
+ return `v0.1.14-alpha.9`;
10375
10399
  }
10376
10400
  const COMMANDS = [
10377
10401
  {
@@ -10475,12 +10499,16 @@ EXIT CODES
10475
10499
  hidden: true,
10476
10500
  summary: "Run rule-engine check only",
10477
10501
  help: `USAGE
10478
- ${BIN} check
10502
+ ${BIN} check [--ctx=<base64>]
10479
10503
 
10480
10504
  DESCRIPTION
10481
10505
  Runs the rule engine against the sandbox's current openclaw config and
10482
- returns { failedRules }. Ctx is fetched from innerapi automatically.
10483
- End-users should prefer \`doctor\`.
10506
+ returns { failedRules }. Used by sandbox_console's push-style callers
10507
+ that already own the ctx — end-users should prefer \`doctor\`.
10508
+
10509
+ OPTIONS
10510
+ --ctx=<base64> Opaque ctx JSON (base64). When absent, fetched from
10511
+ innerapi (same path as doctor).
10484
10512
  `
10485
10513
  },
10486
10514
  {
@@ -10488,11 +10516,16 @@ DESCRIPTION
10488
10516
  hidden: true,
10489
10517
  summary: "Apply standard-mode repairs",
10490
10518
  help: `USAGE
10491
- ${BIN} repair
10519
+ ${BIN} repair [--ctx=<base64>]
10492
10520
 
10493
10521
  DESCRIPTION
10494
- Runs repair for the failing rules. Ctx is fetched from innerapi
10495
- automatically. End-users should use \`doctor --fix\` instead.
10522
+ Runs repair for the failing rules listed inside the ctx's repairData.
10523
+ Intended for sandbox_console's push path — end-users should use
10524
+ \`doctor --fix\` instead.
10525
+
10526
+ OPTIONS
10527
+ --ctx=<base64> Opaque ctx JSON (base64). When absent, fetched from
10528
+ innerapi.
10496
10529
  `
10497
10530
  },
10498
10531
  {
@@ -10500,15 +10533,14 @@ DESCRIPTION
10500
10533
  hidden: true,
10501
10534
  summary: "Re-initialize sandbox via the 9-step reset pipeline",
10502
10535
  help: `USAGE
10503
- ${BIN} reset --async
10504
- ${BIN} reset --worker --task-id=<id>
10536
+ ${BIN} reset --async [--ctx=<base64>]
10537
+ ${BIN} reset --worker --task-id=<id> [--ctx=<base64>]
10505
10538
 
10506
10539
  DESCRIPTION
10507
10540
  Two-phase pipeline driven asynchronously: the --async invocation spawns
10508
10541
  a detached worker and returns { taskId } immediately; the --worker
10509
10542
  invocation (spawned by --async) runs the actual 9 steps and writes
10510
10543
  progress to /tmp/openclaw-diagnose/reset-<taskId>.json.
10511
- Ctx is fetched from innerapi automatically.
10512
10544
 
10513
10545
  Poll progress with \`${BIN} get_reset_task --task-id=<id>\`.
10514
10546
 
@@ -10516,6 +10548,7 @@ OPTIONS
10516
10548
  --async Start a detached worker and return taskId on stdout.
10517
10549
  --worker Internal — run the 9-step pipeline (launched by --async).
10518
10550
  --task-id=<id> Required with --worker; identifies the progress file.
10551
+ --ctx=<base64> Opaque ctx JSON; fetched from innerapi when absent.
10519
10552
  `
10520
10553
  },
10521
10554
  {
@@ -10538,7 +10571,7 @@ OPTIONS
10538
10571
  hidden: true,
10539
10572
  summary: "Download + install the openclaw tarball",
10540
10573
  help: `USAGE
10541
- ${BIN} install-openclaw <tag> [--oss_file_map=<base64>]
10574
+ ${BIN} install-openclaw <tag> [--ctx=<base64> | --oss_file_map=<base64>]
10542
10575
 
10543
10576
  DESCRIPTION
10544
10577
  Downloads the openclaw@<tag> tgz via the signed OSS URL found in the
@@ -10550,9 +10583,9 @@ ARGUMENTS
10550
10583
  <tag> Openclaw version tag, e.g. 2026.4.11.
10551
10584
 
10552
10585
  OPTIONS
10586
+ --ctx=<base64> Opaque ctx; ossFileMap is extracted from it.
10553
10587
  --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi
10554
- entirely. When absent, ossFileMap is fetched from
10555
- innerapi automatically.
10588
+ entirely. Wins over --ctx when both provided.
10556
10589
  `
10557
10590
  },
10558
10591
  {
@@ -10578,7 +10611,8 @@ OPTIONS
10578
10611
  --home_base=<dir> Override the /home/gem base (tests).
10579
10612
  --config_path=<p> Override the openclaw.json path (tests).
10580
10613
  --skip-config-update Leave plugins.installs in openclaw.json untouched.
10581
- --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi.
10614
+ --ctx=<base64> Opaque ctx; see install-openclaw for semantics.
10615
+ --oss_file_map=... Pre-built OSS URL map (base64 JSON).
10582
10616
  `
10583
10617
  },
10584
10618
  {
@@ -10605,6 +10639,7 @@ OPTIONS
10605
10639
  --cli=<name> CLI package to install by short name or scoped
10606
10640
  packageName (repeatable, at least one required).
10607
10641
  --home_base=<dir> Override the /home/gem base (tests).
10642
+ --ctx=<base64> Opaque ctx; ossFileMap is extracted from it.
10608
10643
  --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi.
10609
10644
 
10610
10645
  EXAMPLES
@@ -10658,6 +10693,46 @@ OPTIONS
10658
10693
  EXIT CODES
10659
10694
  0 Success or skipped (prerequisites not met).
10660
10695
  1 Secret/path unresolvable, lark-cli failed, or config unreadable.
10696
+ `
10697
+ },
10698
+ {
10699
+ name: "upgrade-lark",
10700
+ hidden: false,
10701
+ summary: "Upgrade the Feishu/Lark plugin via @larksuite/openclaw-lark-tools",
10702
+ help: `USAGE
10703
+ ${BIN} upgrade-lark [--scene=<scene>] [--caller=<n>] [--trace-id=<id>]
10704
+
10705
+ DESCRIPTION
10706
+ Upgrades the Feishu/Lark plugin by running:
10707
+ npx -y @larksuite/openclaw-lark-tools update --use-existing
10708
+
10709
+ Before the upgrade, the following files are backed up:
10710
+ - openclaw.json
10711
+ - extensions/openclaw-lark/ (if present)
10712
+ - extensions/feishu-openclaw-plugin/ (if present)
10713
+ After the upgrade, the result is validated:
10714
+ - feishu.accounts bot count must not decrease
10715
+ - gateway config structure must remain valid (port/mode/bind/auth/trustedProxies)
10716
+ If the upgrade command fails, or validation fails, the backed-up files are
10717
+ restored to roll back the changes.
10718
+
10719
+ Execution is logged to /tmp/openclaw-diagnose/upgrade-lark-<runId>.log.
10720
+
10721
+ Output is a single JSON object on stdout:
10722
+ { "ok": true, "stdout": "...", "stderr": "...", "logFile": "..." }
10723
+ { "ok": false, "error": "...", "stderr": "...", "exitCode": 1,
10724
+ "rollbackOk": true, "validationError": "...", "logFile": "..." }
10725
+
10726
+ OPTIONS
10727
+ --scene=<scene> Telemetry label forwarded to Slardar only.
10728
+ Known values: PageUpgradeLark, etc. Custom strings accepted.
10729
+ --caller=<name> Optional metadata passed to innerapi.
10730
+ --trace-id=<id> Optional log-correlation id.
10731
+
10732
+ EXIT CODES
10733
+ 0 Success: upgrade ran and all validations passed.
10734
+ 1 Failure: npx error, validation failed, or git commit failed.
10735
+ File rollback was attempted (see rollbackOk in the JSON output).
10661
10736
  `
10662
10737
  },
10663
10738
  {
@@ -10691,6 +10766,41 @@ EXAMPLES
10691
10766
  ${BIN} rules # all rules
10692
10767
  ${BIN} rules --rule=gateway # single rule
10693
10768
  ${BIN} rules --rule=gateway --rule=feishu_channel # multiple rules
10769
+ `
10770
+ },
10771
+ {
10772
+ name: "channels-probe",
10773
+ hidden: true,
10774
+ summary: "Check feishu channel health via openclaw channels status --probe",
10775
+ help: `USAGE
10776
+ ${BIN} channels-probe [--timeout=<ms>]
10777
+
10778
+ DESCRIPTION
10779
+ Runs \`openclaw channels status --probe\` and returns a structured JSON
10780
+ summary of whether the current environment's feishu channels are
10781
+ configured and working correctly.
10782
+
10783
+ Output:
10784
+ {
10785
+ "available": true,
10786
+ "gatewayReachable": true,
10787
+ "accounts": [
10788
+ { "id": "default", "bits": ["enabled","configured","running","works"],
10789
+ "isWorking": true, "raw": "- Feishu default: ..." }
10790
+ ],
10791
+ "anyAccountWorking": true
10792
+ }
10793
+
10794
+ An account is considered working when:
10795
+ enabled ∧ configured ∧ ( works ∨ ( running ∧ no error: ∧ no probe failed ) )
10796
+
10797
+ "available": false means the CLI invocation itself failed (openclaw not
10798
+ found, gateway unreachable, or no parseable output returned).
10799
+
10800
+ OPTIONS
10801
+ --timeout=<ms> Max wait in milliseconds (default: 60000). The probe
10802
+ can hang indefinitely on openclaw v2026.4.x due to a
10803
+ missing per-request HTTP timeout — set this accordingly.
10694
10804
  `
10695
10805
  },
10696
10806
  {
@@ -10711,7 +10821,8 @@ OPTIONS
10711
10821
  --role=<role> Package role (e.g. template, config).
10712
10822
  --name=<name> Package name within the role.
10713
10823
  --dir=<dir> Target dir (defaults to dirname(pkg.installPath)).
10714
- --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi.
10824
+ --ctx=<base64> Opaque ctx; ossFileMap is extracted from it.
10825
+ --oss_file_map=... Pre-built OSS URL map (base64 JSON).
10715
10826
  `
10716
10827
  }
10717
10828
  ];
@@ -10787,31 +10898,31 @@ function planVarsFields(opts = {}) {
10787
10898
  *
10788
10899
  * Per-command group needs:
10789
10900
  *
10790
- * doctor / check app + larkApps
10791
- * repair app + secrets + larkApps
10792
- * reset app + secrets + install + reset + larkApps
10901
+ * doctor / check app (rule-driven)
10902
+ * repair app + secrets (writes secretsContent / providerKeyContent)
10903
+ * reset app + secrets + install + reset (the works)
10793
10904
  * install-* install only
10794
10905
  *
10795
10906
  * Empty result (`{}`) means "no group needed" — the CLI can skip the
10796
10907
  * `fetchCtxViaInnerApi` call entirely and run with a synthetic empty ctx.
10908
+ * Happens e.g. when the user pinned `--rule=<key>` to a vars-free rule on
10909
+ * `doctor`.
10797
10910
  */
10798
10911
  function planCtxPopulate(opts) {
10799
10912
  if (opts.command === "install") return { install: true };
10800
10913
  const populate = {};
10801
- if (planVarsFields({
10914
+ const appFields = planVarsFields({
10802
10915
  disabled: opts.disabled,
10803
10916
  onlyRules: opts.onlyRules,
10804
10917
  profile: opts.profile
10805
- }).length > 0) populate.app = true;
10806
- if (opts.command === "repair") {
10807
- populate.secrets = true;
10808
- populate.larkApps = true;
10809
- } else if (opts.command === "reset") {
10918
+ });
10919
+ if (appFields.length > 0) populate.app = appFields;
10920
+ if (opts.command === "repair") populate.secrets = true;
10921
+ else if (opts.command === "reset") {
10810
10922
  populate.secrets = true;
10811
10923
  populate.install = true;
10812
10924
  populate.reset = true;
10813
- populate.larkApps = true;
10814
- } else if (opts.command === "doctor" || opts.command === "check") populate.larkApps = true;
10925
+ }
10815
10926
  return populate;
10816
10927
  }
10817
10928
  //#endregion
@@ -10865,11 +10976,372 @@ function reportDoctorRunToSlardar(opts) {
10865
10976
  }
10866
10977
  });
10867
10978
  }
10979
+ function readLogFile(filePath) {
10980
+ try {
10981
+ return node_fs.default.readFileSync(filePath, "utf-8");
10982
+ } catch {
10983
+ return "";
10984
+ }
10985
+ }
10986
+ function reportUpgradeLarkToSlardar(opts) {
10987
+ console.error(`[slardar] upgrade_lark_run scene=${opts.scene ?? ""} success=${opts.success} exitCode=${opts.exitCode ?? ""} rollbackOk=${opts.rollbackOk ?? ""}`);
10988
+ const logContent = readLogFile(opts.logFile);
10989
+ reportTask({
10990
+ eventName: "upgrade_lark_run",
10991
+ durationMs: opts.durationMs,
10992
+ status: opts.success ? "success" : "failed",
10993
+ extraCategories: {
10994
+ scene: opts.scene ?? "",
10995
+ exit_code: String(opts.exitCode ?? ""),
10996
+ rollback_ok: opts.rollbackOk != null ? String(opts.rollbackOk) : "",
10997
+ validation_error: opts.validationError ?? "",
10998
+ error_msg: opts.error ?? "",
10999
+ log_content: logContent
11000
+ }
11001
+ });
11002
+ }
11003
+ //#endregion
11004
+ //#region src/upgrade-lark.ts
11005
+ /** Plugin directories under extensions/ that are backed up before upgrade */
11006
+ const FEISHU_PLUGIN_DIRS = ["openclaw-lark", "feishu-openclaw-plugin"];
11007
+ function backupFiles(opts) {
11008
+ const { workspaceDir, configPath, backupDir, log } = opts;
11009
+ try {
11010
+ node_fs.default.mkdirSync(backupDir, { recursive: true });
11011
+ log(`backup dir: ${backupDir}`);
11012
+ if (node_fs.default.existsSync(configPath)) {
11013
+ const stat = node_fs.default.statSync(configPath);
11014
+ node_fs.default.copyFileSync(configPath, node_path.default.join(backupDir, "openclaw.json"));
11015
+ log(` backed up: openclaw.json (${stat.size} bytes)`);
11016
+ } else log(` skipped: openclaw.json (not found)`);
11017
+ const extSrc = node_path.default.join(workspaceDir, "extensions");
11018
+ for (const pluginDir of FEISHU_PLUGIN_DIRS) {
11019
+ const src = node_path.default.join(extSrc, pluginDir);
11020
+ if (node_fs.default.existsSync(src)) {
11021
+ const dst = node_path.default.join(backupDir, "extensions", pluginDir);
11022
+ node_fs.default.cpSync(src, dst, { recursive: true });
11023
+ const version = readPkgVersion(node_path.default.join(src, "package.json"));
11024
+ log(` backed up: extensions/${pluginDir}${version ? ` (version: ${version})` : ""}`);
11025
+ } else log(` skipped: extensions/${pluginDir} (not found)`);
11026
+ }
11027
+ return { ok: true };
11028
+ } catch (e) {
11029
+ return {
11030
+ ok: false,
11031
+ error: `backup failed: ${e.message}`
11032
+ };
11033
+ }
11034
+ }
11035
+ function restoreFiles(opts) {
11036
+ const { workspaceDir, configPath, backupDir, log } = opts;
11037
+ try {
11038
+ const configBackup = node_path.default.join(backupDir, "openclaw.json");
11039
+ if (node_fs.default.existsSync(configBackup)) {
11040
+ node_fs.default.copyFileSync(configBackup, configPath);
11041
+ log(` restored: openclaw.json`);
11042
+ }
11043
+ const extDst = node_path.default.join(workspaceDir, "extensions");
11044
+ for (const pluginDir of FEISHU_PLUGIN_DIRS) {
11045
+ const backupSrc = node_path.default.join(backupDir, "extensions", pluginDir);
11046
+ if (node_fs.default.existsSync(backupSrc)) {
11047
+ const dst = node_path.default.join(extDst, pluginDir);
11048
+ if (node_fs.default.existsSync(dst)) node_fs.default.rmSync(dst, {
11049
+ recursive: true,
11050
+ force: true
11051
+ });
11052
+ node_fs.default.cpSync(backupSrc, dst, { recursive: true });
11053
+ log(` restored: extensions/${pluginDir}`);
11054
+ }
11055
+ }
11056
+ return true;
11057
+ } catch (e) {
11058
+ log(` restore error: ${e.message}`);
11059
+ return false;
11060
+ }
11061
+ }
11062
+ function readPkgVersion(pkgPath) {
11063
+ try {
11064
+ const pkg = JSON.parse(node_fs.default.readFileSync(pkgPath, "utf-8"));
11065
+ return typeof pkg.version === "string" ? pkg.version : null;
11066
+ } catch {
11067
+ return null;
11068
+ }
11069
+ }
11070
+ function snapshotVersions(cwd, log) {
11071
+ const ocResult = (0, node_child_process.spawnSync)("openclaw", ["--version"], {
11072
+ cwd,
11073
+ encoding: "utf-8",
11074
+ stdio: [
11075
+ "ignore",
11076
+ "pipe",
11077
+ "pipe"
11078
+ ],
11079
+ timeout: 5e3
11080
+ });
11081
+ const ocRaw = (ocResult.stdout ?? "").trim() || (ocResult.stderr ?? "").trim();
11082
+ const extDir = node_path.default.join(cwd, "extensions");
11083
+ const larkPkg = node_path.default.join(extDir, "openclaw-lark", "package.json");
11084
+ const feishuPkg = node_path.default.join(extDir, "feishu-openclaw-plugin", "package.json");
11085
+ log(` version-check paths: ${larkPkg} [${node_fs.default.existsSync(larkPkg) ? "exists" : "missing"}]`);
11086
+ log(` version-check paths: ${feishuPkg} [${node_fs.default.existsSync(feishuPkg) ? "exists" : "missing"}]`);
11087
+ return {
11088
+ openclaw: ocRaw || null,
11089
+ openclawLark: readPkgVersion(larkPkg),
11090
+ feishuOpenclawPlugin: readPkgVersion(feishuPkg)
11091
+ };
11092
+ }
11093
+ function logVersionSnapshot(label, v, log) {
11094
+ log(`${label}: openclaw=${v.openclaw ?? "n/a"} openclaw-lark=${v.openclawLark ?? "n/a"} feishu-openclaw-plugin=${v.feishuOpenclawPlugin ?? "n/a"}`);
11095
+ }
11096
+ function countFeishuBots(configPath) {
11097
+ try {
11098
+ const raw = node_fs.default.readFileSync(configPath, "utf-8");
11099
+ const config = loadJSON5().parse(raw);
11100
+ const accounts = getNestedMap(config, "channels", "feishu", "accounts");
11101
+ if (accounts) return Object.keys(accounts).length;
11102
+ const feishu = getNestedMap(config, "channels", "feishu");
11103
+ return typeof feishu?.appId === "string" && feishu.appId ? 1 : 0;
11104
+ } catch {
11105
+ return 0;
11106
+ }
11107
+ }
11108
+ /** Run channels probe, log results, and return the result. Never throws. */
11109
+ function probeChannels(label, log, timeoutMs) {
11110
+ try {
11111
+ const r = runChannelsProbe(timeoutMs);
11112
+ log(` ${label} available=${r.available} anyAccountWorking=${r.anyAccountWorking}`);
11113
+ if (r.error) log(` ${label} error: ${r.error}`);
11114
+ if (r.gatewayReachable != null) log(` ${label} gatewayReachable: ${r.gatewayReachable}`);
11115
+ for (const acct of r.accounts ?? []) log(` ${label} account ${acct.id}: isWorking=${acct.isWorking} bits=[${acct.bits.join(",")}]`);
11116
+ return r;
11117
+ } catch (e) {
11118
+ log(` ${label} channels probe threw: ${e.message}`);
11119
+ return {
11120
+ available: false,
11121
+ gatewayReachable: false,
11122
+ feishuConfigInvalid: false,
11123
+ accounts: [],
11124
+ anyAccountWorking: false
11125
+ };
11126
+ }
11127
+ }
11128
+ function runUpgradeLark(opts) {
11129
+ const cwd = opts.cwd ?? "/home/gem/workspace/agent";
11130
+ const configPath = opts.configPath ?? CONFIG_PATH;
11131
+ const logFile = upgradeLarkLogFile(opts.runId);
11132
+ const log = makeLogger(logFile);
11133
+ const fsOpts = {
11134
+ workspaceDir: cwd,
11135
+ configPath,
11136
+ backupDir: node_path.default.join(opts.backupBaseDir ?? "/tmp/openclaw-diagnose", `upgrade-lark-backup-${opts.runId}`),
11137
+ log
11138
+ };
11139
+ const cliScript = opts.cliScript ?? process.argv[1];
11140
+ const statusCheckDelayMs = opts.statusCheckDelayMs ?? 5e3;
11141
+ log(`${"=".repeat(60)}`);
11142
+ log(`upgrade-lark started runId=${opts.runId}`);
11143
+ log(` cwd : ${cwd}`);
11144
+ log(` configPath : ${configPath}`);
11145
+ log(`${"=".repeat(60)}`);
11146
+ log("");
11147
+ log("── [Pre-check A] channels probe(升级前)────────────────");
11148
+ const beforeChannels = probeChannels("before", log, 6e4);
11149
+ log("");
11150
+ log("── [Pre-check B] 版本兼容预检 ───────────────────────────");
11151
+ let versionIncompatible = false;
11152
+ try {
11153
+ const rawConfig = node_fs.default.readFileSync(configPath, "utf-8");
11154
+ versionIncompatible = needsLarkUpgrade({
11155
+ config: loadJSON5().parse(rawConfig),
11156
+ configPath,
11157
+ vars: {},
11158
+ providerDeps: {
11159
+ usesMiaodaProvider: false,
11160
+ usesMiaodaSecretProvider: false
11161
+ }
11162
+ });
11163
+ log(` version-compat pre-check: ${versionIncompatible ? "NEEDS_UPGRADE" : "ok"}`);
11164
+ } catch (e) {
11165
+ log(` version-compat pre-check error: ${e.message} — treating as needs-upgrade`);
11166
+ versionIncompatible = true;
11167
+ }
11168
+ const feishuConfigInvalid = beforeChannels.feishuConfigInvalid;
11169
+ log(` feishu config invalid : ${feishuConfigInvalid}`);
11170
+ log("");
11171
+ log("── [Gate] 升级前置条件检查 ───────────────────────────────");
11172
+ log(` versionIncompatible : ${versionIncompatible}`);
11173
+ log(` feishuConfigInvalid : ${feishuConfigInvalid}`);
11174
+ log(` channels working before: ${beforeChannels.anyAccountWorking}`);
11175
+ if (!(versionIncompatible || feishuConfigInvalid)) {
11176
+ const reason = "version compatible and feishu channel config valid — upgrade not needed";
11177
+ log(` SKIP: ${reason}`);
11178
+ log(`${"=".repeat(60)}`);
11179
+ log("upgrade-lark skipped (pre-check gate)");
11180
+ log(`${"=".repeat(60)}`);
11181
+ return {
11182
+ ok: true,
11183
+ skipped: true,
11184
+ skipReason: reason,
11185
+ logFile
11186
+ };
11187
+ }
11188
+ if (beforeChannels.anyAccountWorking) {
11189
+ const reason = "channels are working — upgrade not needed (issue detected but system is functional)";
11190
+ log(` SKIP: ${reason}`);
11191
+ log(`${"=".repeat(60)}`);
11192
+ log("upgrade-lark skipped (pre-check gate)");
11193
+ log(`${"=".repeat(60)}`);
11194
+ return {
11195
+ ok: true,
11196
+ skipped: true,
11197
+ skipReason: reason,
11198
+ logFile
11199
+ };
11200
+ }
11201
+ log(` PROCEED: requiresLarkUpgrade=true (version=${versionIncompatible}, feishuConfig=${feishuConfigInvalid}) AND channels not working → running upgrade`);
11202
+ log("");
11203
+ log("── [1/6] 文件备份 ────────────────────────────────────────");
11204
+ log(`before-state: botCount=${countFeishuBots(configPath)}`);
11205
+ const backup = backupFiles(fsOpts);
11206
+ if (!backup.ok) {
11207
+ log(`ERROR: ${backup.error}`);
11208
+ return {
11209
+ ok: false,
11210
+ error: backup.error,
11211
+ logFile
11212
+ };
11213
+ }
11214
+ log("backup: ok");
11215
+ logVersionSnapshot("before-versions", snapshotVersions(cwd, log), log);
11216
+ log("");
11217
+ log("── [2/6] 清理本地 openclaw shim ─────────────────────────");
11218
+ const localOpenclawBin = node_path.default.join(cwd, "node_modules", ".bin", "openclaw");
11219
+ if (node_fs.default.existsSync(localOpenclawBin)) try {
11220
+ node_fs.default.rmSync(localOpenclawBin);
11221
+ log(` removed: ${localOpenclawBin}`);
11222
+ } catch (e) {
11223
+ log(` WARN: failed to remove ${localOpenclawBin}: ${e.message}`);
11224
+ }
11225
+ else log(` skipped: ${localOpenclawBin} (not found)`);
11226
+ log("");
11227
+ log("── [3/6] npx install (@larksuite/openclaw-lark-tools update) ──");
11228
+ const npxResult = (0, node_child_process.spawnSync)("npx", [
11229
+ "-y",
11230
+ "@larksuite/openclaw-lark-tools",
11231
+ "update"
11232
+ ], {
11233
+ cwd,
11234
+ encoding: "utf-8",
11235
+ stdio: [
11236
+ "ignore",
11237
+ "pipe",
11238
+ "pipe"
11239
+ ],
11240
+ timeout: 12e4
11241
+ });
11242
+ const npxStdout = npxResult.stdout?.trim() ?? "";
11243
+ const npxStderr = npxResult.stderr?.trim() ?? "";
11244
+ const npxExitCode = npxResult.status ?? 1;
11245
+ if (npxStdout) log(`npx stdout:\n${npxStdout}`);
11246
+ if (npxStderr) log(`npx stderr:\n${npxStderr}`);
11247
+ log(`npx exit: ${npxExitCode}${npxResult.error ? ` error: ${npxResult.error.message}` : ""}`);
11248
+ if (statusCheckDelayMs > 0) {
11249
+ log("");
11250
+ log(`── 等待 ${statusCheckDelayMs / 1e3}s(让 openclaw 服务完成重启) ─────────────`);
11251
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, statusCheckDelayMs);
11252
+ log("wait done");
11253
+ }
11254
+ const doRollback = (reason) => {
11255
+ log(`ERROR: ${reason}`);
11256
+ const rollbackOk = restoreFiles(fsOpts);
11257
+ log(`rollback: ${rollbackOk ? "ok" : "FAILED"}`);
11258
+ return {
11259
+ ok: false,
11260
+ error: reason,
11261
+ validationError: reason,
11262
+ stdout: npxStdout,
11263
+ stderr: npxStderr,
11264
+ exitCode: npxExitCode,
11265
+ rollbackOk,
11266
+ logFile
11267
+ };
11268
+ };
11269
+ log("");
11270
+ log("── [4/5] 安装后诊断校验 ─────────────────────────────────");
11271
+ logVersionSnapshot("after-versions", snapshotVersions(cwd, log), log);
11272
+ let afterVersionIncompatible = false;
11273
+ try {
11274
+ const rawConfig = node_fs.default.readFileSync(configPath, "utf-8");
11275
+ afterVersionIncompatible = needsLarkUpgrade({
11276
+ config: loadJSON5().parse(rawConfig),
11277
+ configPath,
11278
+ vars: {},
11279
+ providerDeps: {
11280
+ usesMiaodaProvider: false,
11281
+ usesMiaodaSecretProvider: false
11282
+ }
11283
+ });
11284
+ log(` version-compat post-check: ${afterVersionIncompatible ? "STILL_INCOMPATIBLE" : "ok"}`);
11285
+ } catch (e) {
11286
+ log(` version-compat post-check error: ${e.message} — treating as still-incompatible`);
11287
+ afterVersionIncompatible = true;
11288
+ }
11289
+ const afterChannels = probeChannels("after", log, 6e4);
11290
+ log(` feishu config invalid after: ${afterChannels.feishuConfigInvalid}`);
11291
+ const stillNeedsUpgrade = (afterVersionIncompatible || afterChannels.feishuConfigInvalid) && !afterChannels.anyAccountWorking;
11292
+ log(` post-check: stillNeedsUpgrade=${stillNeedsUpgrade} (version=${afterVersionIncompatible}, feishuConfig=${afterChannels.feishuConfigInvalid}, channelsWorking=${afterChannels.anyAccountWorking})`);
11293
+ if (stillNeedsUpgrade) return doRollback(`post-install diagnosis still shows anomaly: versionIncompatible=${afterVersionIncompatible}, feishuConfigInvalid=${afterChannels.feishuConfigInvalid}, anyAccountWorking=${afterChannels.anyAccountWorking}`);
11294
+ log(" post-install diagnosis: ok (upgrade conditions resolved)");
11295
+ log("");
11296
+ log("── [6/6] doctor --fix ────────────────────────────────────");
11297
+ const fixArgs = ["doctor", "--fix"];
11298
+ if (opts.scene) fixArgs.push(`--scene=${opts.scene}`);
11299
+ const fixResult = (0, node_child_process.spawnSync)(process.execPath, [cliScript, ...fixArgs], {
11300
+ cwd,
11301
+ encoding: "utf-8",
11302
+ stdio: [
11303
+ "ignore",
11304
+ "pipe",
11305
+ "pipe"
11306
+ ],
11307
+ timeout: 6e4,
11308
+ env: process.env
11309
+ });
11310
+ if (fixResult.stdout?.trim()) log(`doctor(fix) stdout:\n${fixResult.stdout.trim()}`);
11311
+ if (fixResult.stderr?.trim()) log(`doctor(fix) stderr:\n${fixResult.stderr.trim()}`);
11312
+ log(`doctor(fix) exit: ${fixResult.status ?? "null"}${fixResult.error ? ` error: ${fixResult.error.message}` : ""}`);
11313
+ log("");
11314
+ log(`${"=".repeat(60)}`);
11315
+ log("upgrade-lark completed successfully");
11316
+ log(`${"=".repeat(60)}`);
11317
+ return {
11318
+ ok: true,
11319
+ stdout: npxStdout,
11320
+ stderr: npxStderr,
11321
+ exitCode: npxExitCode,
11322
+ logFile
11323
+ };
11324
+ }
10868
11325
  //#endregion
10869
11326
  //#region src/index.ts
10870
11327
  const args = node_process.default.argv.slice(2);
10871
11328
  const mode = args.find((a) => !a.startsWith("-"));
10872
11329
  /**
11330
+ * Decode `--ctx=<base64>` into an opaque JSON object. Returns undefined when
11331
+ * the flag isn't present — the caller decides whether to fall back to the
11332
+ * innerapi or to error out.
11333
+ *
11334
+ * The object's shape is not enforced here; downstream code consumes it via
11335
+ * either `normalizeCtx()` (new path) or direct field access for the legacy
11336
+ * check/repair/reset contract still used by sandbox_console push.
11337
+ */
11338
+ function parseCtxFlag(args) {
11339
+ const ctxArg = args.find((a) => a.startsWith("--ctx="));
11340
+ if (!ctxArg) return void 0;
11341
+ const b64 = ctxArg.slice(6);
11342
+ return JSON.parse(Buffer.from(b64, "base64").toString("utf-8"));
11343
+ }
11344
+ /**
10873
11345
  * Pull the first non-flag positional after the mode name.
10874
11346
  * (The mode itself is args[0] in the filtered set, so we skip index 0.)
10875
11347
  */
@@ -10897,8 +11369,8 @@ function getMultiFlag(args, name) {
10897
11369
  * case but is no longer consulted.
10898
11370
  */
10899
11371
  async function reportRun(command, rc, _raw, invocation, durationMs, outcome, slardar = {
10900
- scene: void 0,
10901
- profile: "standard",
11372
+ scene,
11373
+ profile,
10902
11374
  fix: false
10903
11375
  }) {
10904
11376
  console.error(`${command}: telemetry calling report_cli_run`);
@@ -10962,7 +11434,7 @@ async function main() {
10962
11434
  console.error(`${mode}: begin argv=[${args.join(" ")}] version=${getVersion()} traceId=${traceId ?? "-"} caller=${caller ?? "-"} runIdGenerated=${rc.generated}`);
10963
11435
  switch (mode) {
10964
11436
  case "check": {
10965
- const raw = await fetchCtxViaInnerApi({
11437
+ const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
10966
11438
  populate: planCtxPopulate({
10967
11439
  command: "check",
10968
11440
  profile
@@ -10987,7 +11459,7 @@ async function main() {
10987
11459
  break;
10988
11460
  }
10989
11461
  case "repair": {
10990
- const raw = await fetchCtxViaInnerApi({
11462
+ const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
10991
11463
  populate: planCtxPopulate({
10992
11464
  command: "repair",
10993
11465
  profile
@@ -11058,15 +11530,27 @@ async function main() {
11058
11530
  break;
11059
11531
  }
11060
11532
  case "reset":
11061
- if (args.includes("--async")) console.log(JSON.stringify(startAsyncReset()));
11062
- else if (args.includes("--worker")) {
11533
+ if (args.includes("--async")) {
11534
+ const ctxArg = args.find((a) => a.startsWith("--ctx="));
11535
+ let ctxBase64;
11536
+ if (ctxArg) ctxBase64 = ctxArg.slice(6);
11537
+ else {
11538
+ const fetched = await fetchCtxViaInnerApi({
11539
+ populate: planCtxPopulate({ command: "reset" }),
11540
+ caller,
11541
+ traceId
11542
+ });
11543
+ ctxBase64 = Buffer.from(JSON.stringify(fetched), "utf-8").toString("base64");
11544
+ }
11545
+ console.log(JSON.stringify(startAsyncReset(ctxBase64)));
11546
+ } else if (args.includes("--worker")) {
11063
11547
  const taskId = args.find((a) => a.startsWith("--task-id="))?.slice(10);
11064
11548
  if (!taskId) {
11065
11549
  console.error("Error: --task-id=<id> is required for worker");
11066
11550
  node_process.default.exit(1);
11067
11551
  }
11068
11552
  const resultFile = resetResultFile(taskId);
11069
- const raw = await fetchCtxViaInnerApi({
11553
+ const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11070
11554
  populate: planCtxPopulate({ command: "reset" }),
11071
11555
  caller,
11072
11556
  traceId
@@ -11090,7 +11574,7 @@ async function main() {
11090
11574
  return;
11091
11575
  }
11092
11576
  } else {
11093
- console.error("Usage: reset --async | reset --worker --task-id=<id>");
11577
+ console.error("Usage: reset --async [--ctx=<base64>] | reset --worker --task-id=<id> [--ctx=<base64>]");
11094
11578
  node_process.default.exit(1);
11095
11579
  }
11096
11580
  break;
@@ -11106,14 +11590,14 @@ async function main() {
11106
11590
  case "install-openclaw": {
11107
11591
  const tag = getPositionalTag(args, "install-openclaw");
11108
11592
  if (!tag) {
11109
- console.error("Usage: install-openclaw <tag> [--oss_file_map=<base64>]");
11593
+ console.error("Usage: install-openclaw <tag> [--ctx=<base64> | --oss_file_map=<base64>]");
11110
11594
  node_process.default.exit(1);
11111
11595
  }
11112
11596
  const ossFileMapFlag = getFlag(args, "oss_file_map");
11113
11597
  let installOssFileMap;
11114
11598
  let rawForTelemetry;
11115
11599
  if (!ossFileMapFlag) {
11116
- rawForTelemetry = await fetchCtxViaInnerApi({
11600
+ rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11117
11601
  populate: planCtxPopulate({ command: "install" }),
11118
11602
  caller,
11119
11603
  traceId
@@ -11148,7 +11632,7 @@ async function main() {
11148
11632
  case "install-extension": {
11149
11633
  const tag = getPositionalTag(args, "install-extension");
11150
11634
  if (!tag) {
11151
- console.error("Usage: install-extension <tag> (--all | --extension=<name>...) [--home_base=<dir>] [--config_path=<path>] [--skip-config-update] [--oss_file_map=<base64>]");
11635
+ console.error("Usage: install-extension <tag> (--all | --extension=<name>...) [--home_base=<dir>] [--config_path=<path>] [--skip-config-update] [--ctx=<base64> | --oss_file_map=<base64>]");
11152
11636
  node_process.default.exit(1);
11153
11637
  }
11154
11638
  const all = args.includes("--all");
@@ -11160,7 +11644,7 @@ async function main() {
11160
11644
  let installOssFileMap;
11161
11645
  let rawForTelemetry;
11162
11646
  if (!ossFileMapFlag) {
11163
- rawForTelemetry = await fetchCtxViaInnerApi({
11647
+ rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11164
11648
  populate: planCtxPopulate({ command: "install" }),
11165
11649
  caller,
11166
11650
  traceId
@@ -11206,12 +11690,12 @@ async function main() {
11206
11690
  case "install-cli": {
11207
11691
  const tag = getPositionalTag(args, "install-cli");
11208
11692
  if (!tag) {
11209
- console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--oss_file_map=<base64>]");
11693
+ console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--ctx=<base64> | --oss_file_map=<base64>]");
11210
11694
  node_process.default.exit(1);
11211
11695
  }
11212
11696
  const names = getMultiFlag(args, "cli");
11213
11697
  if (names.length === 0) {
11214
- console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--oss_file_map=<base64>]");
11698
+ console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--ctx=<base64> | --oss_file_map=<base64>]");
11215
11699
  node_process.default.exit(1);
11216
11700
  }
11217
11701
  const homeBase = getFlag(args, "home_base");
@@ -11219,7 +11703,7 @@ async function main() {
11219
11703
  let installOssFileMap;
11220
11704
  let rawForTelemetry;
11221
11705
  if (!ossFileMapFlag) {
11222
- rawForTelemetry = await fetchCtxViaInnerApi({
11706
+ rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11223
11707
  populate: planCtxPopulate({ command: "install" }),
11224
11708
  caller,
11225
11709
  traceId
@@ -11267,7 +11751,7 @@ async function main() {
11267
11751
  case "download-resource": {
11268
11752
  const tag = getPositionalTag(args, "download-resource");
11269
11753
  if (!tag) {
11270
- console.error("Usage: download-resource <tag> --role=<role> --name=<name> [--dir=<dir>] [--oss_file_map=<base64>]");
11754
+ console.error("Usage: download-resource <tag> --role=<role> --name=<name> [--dir=<dir>] [--ctx=<base64> | --oss_file_map=<base64>]");
11271
11755
  node_process.default.exit(1);
11272
11756
  }
11273
11757
  const role = getFlag(args, "role");
@@ -11281,7 +11765,7 @@ async function main() {
11281
11765
  let installOssFileMap;
11282
11766
  let rawForTelemetry;
11283
11767
  if (!ossFileMapFlag) {
11284
- rawForTelemetry = await fetchCtxViaInnerApi({
11768
+ rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11285
11769
  populate: planCtxPopulate({ command: "install" }),
11286
11770
  caller,
11287
11771
  traceId
@@ -11355,6 +11839,50 @@ async function main() {
11355
11839
  if (!result.ok) node_process.default.exit(1);
11356
11840
  break;
11357
11841
  }
11842
+ case "upgrade-lark": {
11843
+ const result = runUpgradeLark({
11844
+ runId: rc.runId,
11845
+ scene
11846
+ });
11847
+ const upgradeDurationMs = Date.now() - t0;
11848
+ console.log(JSON.stringify(result));
11849
+ reportUpgradeLarkToSlardar({
11850
+ scene,
11851
+ durationMs: upgradeDurationMs,
11852
+ success: result.ok,
11853
+ logFile: result.logFile,
11854
+ exitCode: result.exitCode,
11855
+ rollbackOk: result.rollbackOk,
11856
+ validationError: result.validationError,
11857
+ error: result.error
11858
+ });
11859
+ try {
11860
+ await reportCliRun({
11861
+ command: "upgrade-lark",
11862
+ runId: rc.runId,
11863
+ version: getVersion(),
11864
+ invocation: args.join(" "),
11865
+ durationMs: upgradeDurationMs,
11866
+ caller: rc.caller,
11867
+ traceId: rc.traceId,
11868
+ success: result.ok,
11869
+ result,
11870
+ error: result.ok ? void 0 : { message: result.error ?? "upgrade-lark failed" }
11871
+ });
11872
+ } catch (e) {
11873
+ console.error(`[telemetry] reportCliRun failed: ${e.message}`);
11874
+ }
11875
+ if (!result.ok) {
11876
+ node_process.default.exitCode = 1;
11877
+ return;
11878
+ }
11879
+ break;
11880
+ }
11881
+ case "channels-probe": {
11882
+ const result = runChannelsProbe(getFlag(args, "timeout") ? Number(getFlag(args, "timeout")) : void 0);
11883
+ console.log(JSON.stringify(result));
11884
+ break;
11885
+ }
11358
11886
  default:
11359
11887
  node_process.default.stderr.write(`Unknown command: ${mode}\n\n`);
11360
11888
  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.7",
3
+ "version": "0.1.14-alpha.9",
4
4
  "description": "CLI for OpenClaw config diagnose and repair with JSON5 support",
5
5
  "main": "dist/index.cjs",
6
6
  "bin": {