@lark-apaas/openclaw-scripts-diagnose-cli 0.1.14-alpha.0 → 0.1.14-alpha.2

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 +855 -219
  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.0";
55
+ return "0.1.14-alpha.2";
56
56
  }
57
57
  //#endregion
58
58
  //#region src/rule-engine/base.ts
@@ -3038,7 +3038,7 @@ function extractTarballTolerant(tarball, destDir, opts = {}) {
3038
3038
  }
3039
3039
  //#endregion
3040
3040
  //#region src/rules/feishu-plugin-state-normalize.ts
3041
- const PLUGIN_NAME$1 = "openclaw-lark";
3041
+ const PLUGIN_NAME$2 = "openclaw-lark";
3042
3042
  const BUILTIN_FEISHU = "feishu";
3043
3043
  const LEGACY_PLUGIN_NAME = "feishu-openclaw-plugin";
3044
3044
  const LEGACY_DIRS_TO_REMOVE = [LEGACY_PLUGIN_NAME, BUILTIN_FEISHU];
@@ -3087,7 +3087,7 @@ let FeishuPluginStateNormalizeRule = class FeishuPluginStateNormalizeRule extend
3087
3087
  validate(ctx) {
3088
3088
  if (!isPluginInstalled(ctx)) return { pass: true };
3089
3089
  const fails = [];
3090
- if (!isNewPluginEnabled(ctx.config)) fails.push(`plugins.entries["${PLUGIN_NAME$1}"].enabled !== true(应启用)`);
3090
+ if (!isNewPluginEnabled(ctx.config)) fails.push(`plugins.entries["${PLUGIN_NAME$2}"].enabled !== true(应启用)`);
3091
3091
  if (isBuiltinFeishuEnabled(ctx.config)) fails.push("plugins.entries.feishu.enabled === true(应禁用)");
3092
3092
  if (isTopLevelMissingFeishuTools(ctx.config)) fails.push("tools.alsoAllow 缺 feishu_* tools");
3093
3093
  const legacyResiduals = findLegacyResiduals(ctx);
@@ -3099,7 +3099,7 @@ let FeishuPluginStateNormalizeRule = class FeishuPluginStateNormalizeRule extend
3099
3099
  };
3100
3100
  }
3101
3101
  repair(ctx) {
3102
- setEntryEnabled(ctx.config, PLUGIN_NAME$1, true);
3102
+ setEntryEnabled(ctx.config, PLUGIN_NAME$2, true);
3103
3103
  setEntryEnabled(ctx.config, BUILTIN_FEISHU, false);
3104
3104
  ensureFeishuTools(ctx.config);
3105
3105
  cleanupLegacyResiduals(ctx);
@@ -3113,10 +3113,10 @@ FeishuPluginStateNormalizeRule = __decorate([Rule({
3113
3113
  level: "critical"
3114
3114
  })], FeishuPluginStateNormalizeRule);
3115
3115
  function isPluginInstalled(ctx) {
3116
- return node_fs.default.existsSync(node_path.default.join(getExtensionsDir(ctx.configPath), PLUGIN_NAME$1));
3116
+ return node_fs.default.existsSync(node_path.default.join(getExtensionsDir(ctx.configPath), PLUGIN_NAME$2));
3117
3117
  }
3118
3118
  function isNewPluginEnabled(config) {
3119
- return asRecord(getNestedMap(config, "plugins", "entries")?.[PLUGIN_NAME$1])?.enabled === true;
3119
+ return asRecord(getNestedMap(config, "plugins", "entries")?.[PLUGIN_NAME$2])?.enabled === true;
3120
3120
  }
3121
3121
  function isBuiltinFeishuEnabled(config) {
3122
3122
  return asRecord(getNestedMap(config, "plugins", "entries")?.[BUILTIN_FEISHU])?.enabled === true;
@@ -3169,7 +3169,7 @@ function cleanupLegacyResiduals(ctx) {
3169
3169
  const allow = plugins.allow;
3170
3170
  if (Array.isArray(allow)) {
3171
3171
  for (let i = allow.length - 1; i >= 0; i--) if (allow[i] === LEGACY_PLUGIN_NAME) allow.splice(i, 1);
3172
- if (!allow.includes(PLUGIN_NAME$1)) allow.push(PLUGIN_NAME$1);
3172
+ if (!allow.includes(PLUGIN_NAME$2)) allow.push(PLUGIN_NAME$2);
3173
3173
  }
3174
3174
  }
3175
3175
  const extDir = getExtensionsDir(ctx.configPath);
@@ -3314,7 +3314,7 @@ function findClosestEntry(pluginVersion) {
3314
3314
  }
3315
3315
  //#endregion
3316
3316
  //#region src/rules/feishu-plugin-version-compat.ts
3317
- const PLUGIN_NAME = "openclaw-lark";
3317
+ const PLUGIN_NAME$1 = "openclaw-lark";
3318
3318
  const LEGACY_SHORT_NAMES = ["feishu-openclaw-plugin"];
3319
3319
  const FORK_SCOPES = ["@lark-apaas"];
3320
3320
  /** 特化 fork 版全名:虽免于 VERSION_COMPAT_MAP 检查,仍需 openclaw ≥ 此版本 */
@@ -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,
@@ -3483,13 +3491,13 @@ function detectInstalledPlugin(ctx) {
3483
3491
  const allow = Array.isArray(allowRaw) ? allowRaw.filter((e) => typeof e === "string") : [];
3484
3492
  const extDir = getExtensionsDir(ctx.configPath);
3485
3493
  const installs = getNestedMap(ctx.config, "plugins", "installs");
3486
- for (const name of [PLUGIN_NAME, ...LEGACY_SHORT_NAMES]) {
3494
+ for (const name of [PLUGIN_NAME$1, ...LEGACY_SHORT_NAMES]) {
3487
3495
  if (!allow.includes(name)) continue;
3488
3496
  const pkgPath = node_path.default.join(extDir, name, "package.json");
3489
3497
  if (!node_fs.default.existsSync(pkgPath)) continue;
3490
3498
  const pkg = readPluginPackageJson(pkgPath) ?? {};
3491
3499
  const installEntry = installs && asRecord(installs[name]);
3492
- const fullName = pkg.name ?? extractScopedNameFromSpec(installEntry?.spec);
3500
+ const fullName = pkg.name ?? extractScopedNameFromSpec$1(installEntry?.spec);
3493
3501
  return {
3494
3502
  allowName: name,
3495
3503
  fullName,
@@ -3500,11 +3508,166 @@ function detectInstalledPlugin(ctx) {
3500
3508
  return null;
3501
3509
  }
3502
3510
  /** "@scope/name@1.2.3" / "name@1.2.3" / "@scope/name" / "name" → 去掉 @version 后缀 */
3503
- function extractScopedNameFromSpec(spec) {
3511
+ function extractScopedNameFromSpec$1(spec) {
3504
3512
  if (typeof spec !== "string") return void 0;
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 CHANNEL_LINE_RE = /^-\s+Feishu\s+([^:]+):\s+(.+)$/;
3532
+ /**
3533
+ * Port of Python `_account_is_working` from the feishu-channel-success-rate skill.
3534
+ *
3535
+ * Strips colon-prefixed key:value bits (dm:, bot:, in:, out:, token:, allow:,
3536
+ * intents:, groups:, health:) and evaluates the canonical health formula.
3537
+ */
3538
+ function accountIsWorking(bits) {
3539
+ const bitTokens = /* @__PURE__ */ new Set();
3540
+ let hasError = false;
3541
+ let hasProbeFailed = false;
3542
+ for (const raw of bits) {
3543
+ const b = raw.trim();
3544
+ if (!b) continue;
3545
+ if (b.startsWith("error:")) {
3546
+ hasError = true;
3547
+ continue;
3548
+ }
3549
+ if (b === "probe failed") {
3550
+ hasProbeFailed = true;
3551
+ continue;
3552
+ }
3553
+ bitTokens.add(b.split(":")[0]);
3554
+ }
3555
+ if (!bitTokens.has("enabled") || !bitTokens.has("configured")) return false;
3556
+ if (bitTokens.has("works")) return true;
3557
+ if (bitTokens.has("running") && !hasError && !hasProbeFailed) return true;
3558
+ return false;
3559
+ }
3560
+ /**
3561
+ * Parse the raw stdout of `openclaw channels status --probe`.
3562
+ * Port of Python `extract_channels_probe` from the feishu-channel-success-rate skill.
3563
+ */
3564
+ function parseChannelsProbeOutput(text) {
3565
+ const gatewayReachable = text.includes("Gateway reachable");
3566
+ const accounts = [];
3567
+ let anyAccountWorking = false;
3568
+ for (const line of text.split("\n")) {
3569
+ const m = CHANNEL_LINE_RE.exec(line.trim());
3570
+ if (!m) continue;
3571
+ const [, acct, rest] = m;
3572
+ const bits = rest.split(",").map((b) => b.trim());
3573
+ const isWorking = accountIsWorking(bits);
3574
+ if (isWorking) anyAccountWorking = true;
3575
+ accounts.push({
3576
+ id: acct.trim(),
3577
+ bits,
3578
+ isWorking,
3579
+ raw: line.trim()
3580
+ });
3581
+ }
3582
+ return {
3583
+ gatewayReachable,
3584
+ accounts,
3585
+ anyAccountWorking
3586
+ };
3587
+ }
3588
+ /**
3589
+ * Run `openclaw channels status --probe` and return a structured result.
3590
+ *
3591
+ * The command may exit non-zero when some bot accounts fail their probe — that
3592
+ * is still useful output. We therefore try to parse stdout even when the
3593
+ * process exits with a non-zero code, falling back to an unavailable result
3594
+ * only when there is genuinely no output to parse.
3595
+ *
3596
+ * @param timeoutMs Maximum wait time. Default is 60 s because v2026.4.x
3597
+ * lacks a per-request HTTP timeout and can block indefinitely.
3598
+ */
3599
+ function runChannelsProbe(timeoutMs = 6e4) {
3600
+ let stdout = "";
3601
+ let execError;
3602
+ try {
3603
+ stdout = (0, node_child_process.execSync)("openclaw channels status --probe", {
3604
+ encoding: "utf-8",
3605
+ timeout: timeoutMs,
3606
+ stdio: [
3607
+ "ignore",
3608
+ "pipe",
3609
+ "pipe"
3610
+ ]
3611
+ });
3612
+ } catch (e) {
3613
+ const err = e;
3614
+ stdout = err.stdout ?? "";
3615
+ execError = err.message;
3616
+ const stderrRaw = err.stderr;
3617
+ const stderr = (typeof stderrRaw === "string" ? stderrRaw : stderrRaw?.toString("utf-8") ?? "").trim();
3618
+ if (stderr) console.error(`channels-probe: stderr from CLI: ${stderr}`);
3619
+ }
3620
+ if (stdout.trim()) return {
3621
+ available: true,
3622
+ ...parseChannelsProbeOutput(stdout)
3623
+ };
3624
+ return {
3625
+ available: false,
3626
+ gatewayReachable: false,
3627
+ accounts: [],
3628
+ anyAccountWorking: false,
3629
+ error: execError ?? "no output from openclaw channels status --probe"
3630
+ };
3631
+ }
3632
+ //#endregion
3633
+ //#region src/rules/upgrade-lark-needed.ts
3634
+ /**
3635
+ * Detects the condition that warrants running `upgrade-lark`:
3636
+ * - feishu plugin version incompatible with current openclaw, AND
3637
+ * - channels are not working.
3638
+ *
3639
+ * Both conditions must be true simultaneously. If version is compatible or
3640
+ * channels are working, the rule passes (no action needed).
3641
+ *
3642
+ * profile: experimental — runs only in full sweep mode, not in standard doctor.
3643
+ * level: silent — telemetry/sweep-only, does not trigger page-level repair UI.
3644
+ */
3645
+ let UpgradeLarkNeededRule = class UpgradeLarkNeededRule extends DiagnoseRule {
3646
+ validate(ctx) {
3647
+ if (!needsLarkUpgrade(ctx)) return { pass: true };
3648
+ let anyAccountWorking = false;
3649
+ try {
3650
+ anyAccountWorking = runChannelsProbe(3e4).anyAccountWorking;
3651
+ } catch {
3652
+ return { pass: true };
3653
+ }
3654
+ if (anyAccountWorking) return { pass: true };
3655
+ return {
3656
+ pass: false,
3657
+ action: "upgrade_lark",
3658
+ message: "飞书插件版本不兼容且 channels 不可用,建议执行 upgrade-lark 命令升级飞书插件"
3659
+ };
3660
+ }
3661
+ };
3662
+ UpgradeLarkNeededRule = __decorate([Rule({
3663
+ key: "upgrade_lark_needed",
3664
+ description: "检测飞书插件版本不兼容且 channels 不可用,判断是否需要执行 upgrade-lark 升级",
3665
+ dependsOn: ["feishu_plugin_version_compat_lark"],
3666
+ repairMode: "check-only",
3667
+ level: "silent",
3668
+ profile: "experimental",
3669
+ usesVars: ["recommendedOpenclawTag"]
3670
+ })], UpgradeLarkNeededRule);
3508
3671
  //#endregion
3509
3672
  //#region src/rules/cleanup-install-backup-dirs.ts
3510
3673
  const DIR_PREFIX = ".openclaw-install-";
@@ -3555,110 +3718,100 @@ CleanupInstallBackupDirsRule = __decorate([Rule({
3555
3718
  level: "critical"
3556
3719
  })], CleanupInstallBackupDirsRule);
3557
3720
  //#endregion
3558
- //#region src/rules/feishu-bot-channel-config.ts
3559
- /**
3560
- * Ensures each bot account's channel config is correct:
3561
- * 1. `allowFrom` contains its own `creatorOpenID` from larkApps
3562
- * 2. `appSecret` is either the canonical provider-ref or matches larkApps plaintext
3563
- *
3564
- * Covers both multi-account (channels.feishu.accounts) and single-account
3565
- * (channels.feishu.appId + allowFrom at top level) layouts.
3566
- */
3567
- let FeishuBotChannelConfigRule = class FeishuBotChannelConfigRule extends DiagnoseRule {
3721
+ //#region src/rules/lark-cli-missing-for-installed-lark-plugin.ts
3722
+ const PLUGIN_NAME = "openclaw-lark";
3723
+ const FORK_PACKAGE_NAME = "@lark-apaas/openclaw-lark";
3724
+ const TARGET_VERSION = "2026.4.4";
3725
+ const LARK_CLI_NAME$1 = "lark-cli";
3726
+ function readInstalledLarkPlugin(ctx) {
3727
+ const pkgPath = node_path.default.join(getExtensionsDir(ctx.configPath), PLUGIN_NAME, "package.json");
3728
+ if (!node_fs.default.existsSync(pkgPath)) return null;
3729
+ let pkg = {};
3730
+ try {
3731
+ const parsed = JSON.parse(node_fs.default.readFileSync(pkgPath, "utf-8"));
3732
+ pkg = {
3733
+ name: typeof parsed.name === "string" ? parsed.name : void 0,
3734
+ version: typeof parsed.version === "string" ? parsed.version : void 0
3735
+ };
3736
+ } catch {
3737
+ pkg = {};
3738
+ }
3739
+ const installs = getNestedMap(ctx.config, "plugins", "installs");
3740
+ const installEntry = installs ? asRecord(installs[PLUGIN_NAME]) : void 0;
3741
+ return {
3742
+ name: pkg.name ?? extractScopedNameFromSpec(installEntry?.spec),
3743
+ version: pkg.version ?? (typeof installEntry?.version === "string" ? installEntry.version : void 0)
3744
+ };
3745
+ }
3746
+ function isTargetForkPlugin(plugin) {
3747
+ return plugin?.name === FORK_PACKAGE_NAME && plugin.version === TARGET_VERSION;
3748
+ }
3749
+ function extractScopedNameFromSpec(spec) {
3750
+ if (typeof spec !== "string") return void 0;
3751
+ const at = spec.indexOf("@", 1);
3752
+ return at === -1 ? spec : spec.slice(0, at);
3753
+ }
3754
+ function isLarkCliAvailable$1() {
3755
+ try {
3756
+ return (0, node_child_process.spawnSync)(LARK_CLI_NAME$1, ["--version"], {
3757
+ encoding: "utf-8",
3758
+ timeout: 5e3,
3759
+ stdio: [
3760
+ "ignore",
3761
+ "pipe",
3762
+ "ignore"
3763
+ ]
3764
+ }).status === 0;
3765
+ } catch {
3766
+ return false;
3767
+ }
3768
+ }
3769
+ function installLarkCliOnce(tag) {
3770
+ const entry = process.argv[1];
3771
+ if (!entry) throw new Error("cannot resolve diagnose-cli entrypoint for lark-cli install");
3772
+ const res = (0, node_child_process.spawnSync)(process.execPath, [
3773
+ entry,
3774
+ "install-cli",
3775
+ tag,
3776
+ "--cli=lark-cli"
3777
+ ], {
3778
+ encoding: "utf-8",
3779
+ stdio: [
3780
+ "ignore",
3781
+ "pipe",
3782
+ "pipe"
3783
+ ]
3784
+ });
3785
+ const stdout = res.stdout?.trim();
3786
+ const stderr = res.stderr?.trim();
3787
+ if (stdout) console.error(`[lark-cli-missing] install-cli stdout: ${stdout}`);
3788
+ if (stderr) console.error(`[lark-cli-missing] install-cli stderr: ${stderr}`);
3789
+ if (res.error) throw new Error(`install-cli spawn error: ${res.error.message}`);
3790
+ if (res.status !== 0) throw new Error(`install-cli exited with code ${res.status ?? "unknown"}`);
3791
+ }
3792
+ let LarkCliMissingForInstalledLarkPluginRule = class LarkCliMissingForInstalledLarkPluginRule extends DiagnoseRule {
3568
3793
  validate(ctx) {
3569
- const larkApps = ctx.vars.larkApps;
3570
- if (!larkApps || larkApps.length === 0) return { pass: true };
3571
- const feishu = asRecord(getNestedMap(ctx.config, "channels", "feishu"));
3572
- if (!feishu) return { pass: true };
3573
- const issues = [];
3574
- const accounts = asRecord(feishu.accounts);
3575
- if (accounts) for (const [accountId, account] of Object.entries(accounts)) {
3576
- const bot = asRecord(account);
3577
- if (!bot) continue;
3578
- const appId = bot.appId;
3579
- if (typeof appId !== "string" || !appId.startsWith("cli_")) continue;
3580
- const larkApp = larkApps.find((e) => e.larkAppID === appId);
3581
- if (!larkApp) continue;
3582
- this.checkBot(accountId, bot, larkApp, issues);
3583
- }
3584
- const singleAppId = feishu.appId;
3585
- if (typeof singleAppId === "string" && singleAppId.startsWith("cli_") && !accounts) {
3586
- const larkApp = larkApps.find((e) => e.larkAppID === singleAppId);
3587
- if (larkApp) this.checkBot("feishu", feishu, larkApp, issues);
3588
- }
3589
- if (issues.length === 0) return { pass: true };
3794
+ if (!isTargetForkPlugin(readInstalledLarkPlugin(ctx))) return { pass: true };
3795
+ if (isLarkCliAvailable$1()) return { pass: true };
3590
3796
  return {
3591
3797
  pass: false,
3592
- message: issues.join("; ")
3798
+ message: `${FORK_PACKAGE_NAME}@${TARGET_VERSION} 已安装,但 lark-cli 不可用;将执行一次 lark-cli 安装`
3593
3799
  };
3594
3800
  }
3595
- /** Check a single bot entry (either an account object or the feishu channel itself). */
3596
- checkBot(label, bot, larkApp, issues) {
3597
- const creatorOpenID = larkApp.creatorOpenID;
3598
- const allowFrom = Array.isArray(bot.allowFrom) ? bot.allowFrom : [];
3599
- if (typeof creatorOpenID === "string" && creatorOpenID !== "") {
3600
- if (!allowFrom.includes(creatorOpenID)) issues.push(`${label} allowFrom missing creatorOpenID ${creatorOpenID.length > 8 ? creatorOpenID.slice(0, 4) + "***" + creatorOpenID.slice(-4) : "***"}`);
3601
- } else if (allowFrom.length === 0) issues.push(`${label} allowFrom is empty (creatorOpenID unavailable, cannot auto-fix)`);
3602
- const secret = bot.appSecret;
3603
- if (typeof secret === "object" && secret !== null && !Array.isArray(secret)) {
3604
- if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) issues.push(`${label} appSecret is a provider-ref but not the canonical one`);
3605
- } else if (typeof secret === "string") {
3606
- if (secret !== larkApp.appSecret) issues.push(`${label} appSecret plaintext mismatch`);
3607
- } else issues.push(`${label} appSecret has unexpected type`);
3608
- }
3609
3801
  repair(ctx) {
3610
- const larkApps = ctx.vars.larkApps;
3611
- if (!larkApps || larkApps.length === 0) return;
3612
- const feishu = asRecord(getNestedMap(ctx.config, "channels", "feishu"));
3613
- if (!feishu) return;
3614
- const accounts = asRecord(feishu.accounts);
3615
- if (accounts) for (const [, account] of Object.entries(accounts)) {
3616
- const bot = asRecord(account);
3617
- if (!bot) continue;
3618
- const appId = bot.appId;
3619
- if (typeof appId !== "string" || !appId.startsWith("cli_")) continue;
3620
- const larkApp = larkApps.find((e) => e.larkAppID === appId);
3621
- if (!larkApp) continue;
3622
- this.fixBot(bot, larkApp);
3623
- }
3624
- const singleAppId = feishu.appId;
3625
- if (typeof singleAppId === "string" && singleAppId.startsWith("cli_") && !accounts) {
3626
- const larkApp = larkApps.find((e) => e.larkAppID === singleAppId);
3627
- if (larkApp) this.fixBot(feishu, larkApp);
3628
- }
3629
- }
3630
- /** Fix a single bot entry in-place. */
3631
- fixBot(bot, larkApp) {
3632
- const creatorOpenID = larkApp.creatorOpenID;
3633
- if (typeof creatorOpenID === "string" && creatorOpenID !== "") {
3634
- const allowFrom = Array.isArray(bot.allowFrom) ? [...bot.allowFrom] : [];
3635
- if (!allowFrom.includes(creatorOpenID)) {
3636
- allowFrom.push(creatorOpenID);
3637
- bot.allowFrom = allowFrom;
3638
- }
3639
- }
3640
- const secret = bot.appSecret;
3641
- let needsFix = false;
3642
- if (typeof secret === "object" && secret !== null && !Array.isArray(secret)) {
3643
- if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) needsFix = true;
3644
- } else if (typeof secret === "string") {
3645
- if (secret !== larkApp.appSecret) needsFix = true;
3646
- } else needsFix = true;
3647
- if (needsFix) bot.appSecret = { ...DEFAULT_FEISHU_APP_SECRET };
3802
+ if (!isTargetForkPlugin(readInstalledLarkPlugin(ctx))) return;
3803
+ if (isLarkCliAvailable$1()) return;
3804
+ installLarkCliOnce(ctx.vars.recommendedOpenclawTag ?? TARGET_VERSION);
3648
3805
  }
3649
3806
  };
3650
- FeishuBotChannelConfigRule = __decorate([Rule({
3651
- key: "feishu_bot_channel_config",
3652
- description: "确保飞书配置中 bot 账号的 allowFrom 包含其创建者 openID 且 appSecret 值正确",
3653
- dependsOn: [
3654
- "config_syntax_check",
3655
- "feishu_default_account",
3656
- "feishu_bot_id"
3657
- ],
3807
+ LarkCliMissingForInstalledLarkPluginRule = __decorate([Rule({
3808
+ key: "lark_cli_missing_for_installed_lark_plugin",
3809
+ description: "检测特定飞书插件版本已安装但 lark-cli 缺失的环境,并自动安装 lark-cli 一次",
3810
+ dependsOn: ["config_syntax_check"],
3658
3811
  repairMode: "standard",
3659
- usesVars: ["larkApps"],
3660
- level: "critical"
3661
- })], FeishuBotChannelConfigRule);
3812
+ level: "critical",
3813
+ usesVars: ["recommendedOpenclawTag"]
3814
+ })], LarkCliMissingForInstalledLarkPluginRule);
3662
3815
  //#endregion
3663
3816
  //#region src/check.ts
3664
3817
  /** Telemetry-aware entry: returns both the legacy CheckResult (for stdout)
@@ -4123,6 +4276,9 @@ const PROVIDER_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-provider-
4123
4276
  const SECRETS_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-openclaw-secrets.json";
4124
4277
  /** Absolute path to the openclaw config JSON. */
4125
4278
  const CONFIG_PATH = `${WORKSPACE_DIR}/openclaw.json`;
4279
+ function upgradeLarkLogFile(runId) {
4280
+ return `${DIAGNOSE_DIR}/upgrade-lark-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/:/g, "-")}-${runId.slice(0, 8)}.log`;
4281
+ }
4126
4282
  //#endregion
4127
4283
  //#region src/run-log.ts
4128
4284
  let currentRunContext;
@@ -4259,10 +4415,9 @@ function makeLogger(logFile) {
4259
4415
  /**
4260
4416
  * Start an async reset task: spawn a detached child process and return the taskId.
4261
4417
  *
4262
- * The child process runs: node cli.js reset --worker --task-id=xxx
4263
- * The worker fetches ctx from innerApi itself — no --ctx passthrough.
4418
+ * The child process runs: node cli.js reset --worker --task-id=xxx --ctx=base64
4264
4419
  */
4265
- function startAsyncReset() {
4420
+ function startAsyncReset(ctxBase64) {
4266
4421
  const taskId = (0, node_crypto.randomUUID)();
4267
4422
  const resultFile = resetResultFile(taskId);
4268
4423
  const log = makeLogger(resetLogFile(taskId));
@@ -4286,7 +4441,8 @@ function startAsyncReset() {
4286
4441
  process.argv[1],
4287
4442
  "reset",
4288
4443
  "--worker",
4289
- `--task-id=${taskId}`
4444
+ `--task-id=${taskId}`,
4445
+ `--ctx=${ctxBase64}`
4290
4446
  ], {
4291
4447
  detached: true,
4292
4448
  stdio: "ignore",
@@ -6800,60 +6956,6 @@ function mergeCoreBackupAndOrigins(configPath, vars, resetData, log) {
6800
6956
  log(`allowedOrigins: added ${added.length} (${JSON.stringify(added)}), total now ${mergedOrigins.length}`);
6801
6957
  }
6802
6958
  /**
6803
- * Fix bot account allowFrom and appSecret using larkApps from innerApi.
6804
- *
6805
- * For each bot account (key starts with `bot-cli_`):
6806
- * - allowFrom must contain the bot's own creatorOpenID from larkApps
6807
- * - appSecret must be either the canonical provider-ref or match larkApps plaintext
6808
- *
6809
- * Runs after mergeCoreBackupAndOrigins so it operates on the final config state.
6810
- */
6811
- function fixBotChannelConfig(configPath, larkApps, log) {
6812
- if (!larkApps || larkApps.length === 0) {
6813
- log("no larkApps data, skip bot channel config fix");
6814
- return;
6815
- }
6816
- const config = loadJSON5().parse(node_fs.default.readFileSync(configPath, "utf-8"));
6817
- const accounts = asRecord(getNestedMap(config, "channels", "feishu")?.accounts);
6818
- if (!accounts) {
6819
- log("no feishu accounts in config, skip bot channel config fix");
6820
- return;
6821
- }
6822
- let fixCount = 0;
6823
- for (const [, account] of Object.entries(accounts)) {
6824
- const bot = asRecord(account);
6825
- if (!bot) continue;
6826
- const appId = bot.appId;
6827
- if (typeof appId !== "string" || !appId.startsWith("cli_")) continue;
6828
- const larkApp = larkApps.find((e) => e.larkAppID === appId);
6829
- if (!larkApp) continue;
6830
- const creatorOpenID = larkApp.creatorOpenID;
6831
- if (typeof creatorOpenID === "string" && creatorOpenID !== "") {
6832
- const allowFrom = Array.isArray(bot.allowFrom) ? [...bot.allowFrom] : [];
6833
- if (!allowFrom.includes(creatorOpenID)) {
6834
- allowFrom.push(creatorOpenID);
6835
- bot.allowFrom = allowFrom;
6836
- fixCount++;
6837
- }
6838
- }
6839
- const secret = bot.appSecret;
6840
- let needsFix = false;
6841
- if (typeof secret === "object" && secret !== null && !Array.isArray(secret)) {
6842
- if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) needsFix = true;
6843
- } else if (typeof secret === "string") {
6844
- if (secret !== larkApp.appSecret) needsFix = true;
6845
- } else needsFix = true;
6846
- if (needsFix) {
6847
- bot.appSecret = { ...DEFAULT_FEISHU_APP_SECRET };
6848
- fixCount++;
6849
- }
6850
- }
6851
- if (fixCount > 0) {
6852
- node_fs.default.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
6853
- log(`fixed ${fixCount} bot channel config issue(s) (allowFrom/appSecret)`);
6854
- } else log("bot channel config ok, no fixes needed");
6855
- }
6856
- /**
6857
6959
  * Step 7: Verify startup scripts landed in configDir/scripts/.
6858
6960
  *
6859
6961
  * Scripts are extracted directly to configDir/scripts/ during stageTemplate —
@@ -6998,7 +7100,6 @@ async function runReset(input, taskId, resultFile) {
6998
7100
  await step5InstallOpenclaw(openclawTag, ossFileMap, log);
6999
7101
  step(6);
7000
7102
  mergeCoreBackupAndOrigins(configPath, vars, resetData, log);
7001
- fixBotChannelConfig(configPath, vars.larkApps, log);
7002
7103
  step(7);
7003
7104
  verifyStartupScripts(configDir, log);
7004
7105
  step(8);
@@ -7797,8 +7898,7 @@ function normalizeCtx(raw) {
7797
7898
  reset: {
7798
7899
  templateVars: r.reset.templateVars ?? {},
7799
7900
  coreBackup: r.reset.coreBackup
7800
- },
7801
- larkApps: Array.isArray(r.larkApps) ? r.larkApps : []
7901
+ }
7802
7902
  };
7803
7903
  }
7804
7904
  const vars = r.vars ?? {};
@@ -7823,8 +7923,7 @@ function normalizeCtx(raw) {
7823
7923
  reset: {
7824
7924
  templateVars: resetData.templateVars ?? {},
7825
7925
  coreBackup: resetData.coreBackup
7826
- },
7827
- larkApps: Array.isArray(r.larkApps) ? r.larkApps : []
7926
+ }
7828
7927
  };
7829
7928
  }
7830
7929
  function fillApp(src) {
@@ -7889,8 +7988,7 @@ function buildCheckInput(raw, configPathOverride) {
7889
7988
  providerFilePath: PROVIDER_FILE_PATH,
7890
7989
  secretsFilePath: SECRETS_FILE_PATH,
7891
7990
  templateVars: ctx.app.templateVars,
7892
- recommendedOpenclawTag: ctx.app.recommendedOpenclawTag,
7893
- larkApps: ctx.larkApps
7991
+ recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
7894
7992
  },
7895
7993
  templateVars: ctx.app.templateVars
7896
7994
  };
@@ -7922,8 +8020,7 @@ function buildRepairInput(raw, configPathOverride) {
7922
8020
  providerFilePath: PROVIDER_FILE_PATH,
7923
8021
  secretsFilePath: SECRETS_FILE_PATH,
7924
8022
  templateVars: ctx.app.templateVars,
7925
- recommendedOpenclawTag: ctx.app.recommendedOpenclawTag,
7926
- larkApps: ctx.larkApps
8023
+ recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
7927
8024
  },
7928
8025
  repairData: {
7929
8026
  secretsContent: ctx.secrets.secretsContent,
@@ -7959,8 +8056,7 @@ function buildResetInput(raw, configPathOverride) {
7959
8056
  providerFilePath: PROVIDER_FILE_PATH,
7960
8057
  secretsFilePath: SECRETS_FILE_PATH,
7961
8058
  templateVars: ctx.app.templateVars,
7962
- recommendedOpenclawTag: ctx.app.recommendedOpenclawTag,
7963
- larkApps: ctx.larkApps
8059
+ recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
7964
8060
  },
7965
8061
  resetData: {
7966
8062
  templateVars: ctx.reset.templateVars,
@@ -10270,7 +10366,7 @@ async function reportCliRun(opts) {
10270
10366
  //#region src/help.ts
10271
10367
  const BIN = "mclaw-diagnose";
10272
10368
  function versionBanner() {
10273
- return `v0.1.14-alpha.0`;
10369
+ return `v0.1.14-alpha.2`;
10274
10370
  }
10275
10371
  const COMMANDS = [
10276
10372
  {
@@ -10374,12 +10470,16 @@ EXIT CODES
10374
10470
  hidden: true,
10375
10471
  summary: "Run rule-engine check only",
10376
10472
  help: `USAGE
10377
- ${BIN} check
10473
+ ${BIN} check [--ctx=<base64>]
10378
10474
 
10379
10475
  DESCRIPTION
10380
10476
  Runs the rule engine against the sandbox's current openclaw config and
10381
- returns { failedRules }. Ctx is fetched from innerapi automatically.
10382
- End-users should prefer \`doctor\`.
10477
+ returns { failedRules }. Used by sandbox_console's push-style callers
10478
+ that already own the ctx — end-users should prefer \`doctor\`.
10479
+
10480
+ OPTIONS
10481
+ --ctx=<base64> Opaque ctx JSON (base64). When absent, fetched from
10482
+ innerapi (same path as doctor).
10383
10483
  `
10384
10484
  },
10385
10485
  {
@@ -10387,11 +10487,16 @@ DESCRIPTION
10387
10487
  hidden: true,
10388
10488
  summary: "Apply standard-mode repairs",
10389
10489
  help: `USAGE
10390
- ${BIN} repair
10490
+ ${BIN} repair [--ctx=<base64>]
10391
10491
 
10392
10492
  DESCRIPTION
10393
- Runs repair for the failing rules. Ctx is fetched from innerapi
10394
- automatically. End-users should use \`doctor --fix\` instead.
10493
+ Runs repair for the failing rules listed inside the ctx's repairData.
10494
+ Intended for sandbox_console's push path — end-users should use
10495
+ \`doctor --fix\` instead.
10496
+
10497
+ OPTIONS
10498
+ --ctx=<base64> Opaque ctx JSON (base64). When absent, fetched from
10499
+ innerapi.
10395
10500
  `
10396
10501
  },
10397
10502
  {
@@ -10399,15 +10504,14 @@ DESCRIPTION
10399
10504
  hidden: true,
10400
10505
  summary: "Re-initialize sandbox via the 9-step reset pipeline",
10401
10506
  help: `USAGE
10402
- ${BIN} reset --async
10403
- ${BIN} reset --worker --task-id=<id>
10507
+ ${BIN} reset --async [--ctx=<base64>]
10508
+ ${BIN} reset --worker --task-id=<id> [--ctx=<base64>]
10404
10509
 
10405
10510
  DESCRIPTION
10406
10511
  Two-phase pipeline driven asynchronously: the --async invocation spawns
10407
10512
  a detached worker and returns { taskId } immediately; the --worker
10408
10513
  invocation (spawned by --async) runs the actual 9 steps and writes
10409
10514
  progress to /tmp/openclaw-diagnose/reset-<taskId>.json.
10410
- Ctx is fetched from innerapi automatically.
10411
10515
 
10412
10516
  Poll progress with \`${BIN} get_reset_task --task-id=<id>\`.
10413
10517
 
@@ -10415,6 +10519,7 @@ OPTIONS
10415
10519
  --async Start a detached worker and return taskId on stdout.
10416
10520
  --worker Internal — run the 9-step pipeline (launched by --async).
10417
10521
  --task-id=<id> Required with --worker; identifies the progress file.
10522
+ --ctx=<base64> Opaque ctx JSON; fetched from innerapi when absent.
10418
10523
  `
10419
10524
  },
10420
10525
  {
@@ -10437,7 +10542,7 @@ OPTIONS
10437
10542
  hidden: true,
10438
10543
  summary: "Download + install the openclaw tarball",
10439
10544
  help: `USAGE
10440
- ${BIN} install-openclaw <tag> [--oss_file_map=<base64>]
10545
+ ${BIN} install-openclaw <tag> [--ctx=<base64> | --oss_file_map=<base64>]
10441
10546
 
10442
10547
  DESCRIPTION
10443
10548
  Downloads the openclaw@<tag> tgz via the signed OSS URL found in the
@@ -10449,9 +10554,9 @@ ARGUMENTS
10449
10554
  <tag> Openclaw version tag, e.g. 2026.4.11.
10450
10555
 
10451
10556
  OPTIONS
10557
+ --ctx=<base64> Opaque ctx; ossFileMap is extracted from it.
10452
10558
  --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi
10453
- entirely. When absent, ossFileMap is fetched from
10454
- innerapi automatically.
10559
+ entirely. Wins over --ctx when both provided.
10455
10560
  `
10456
10561
  },
10457
10562
  {
@@ -10477,7 +10582,8 @@ OPTIONS
10477
10582
  --home_base=<dir> Override the /home/gem base (tests).
10478
10583
  --config_path=<p> Override the openclaw.json path (tests).
10479
10584
  --skip-config-update Leave plugins.installs in openclaw.json untouched.
10480
- --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi.
10585
+ --ctx=<base64> Opaque ctx; see install-openclaw for semantics.
10586
+ --oss_file_map=... Pre-built OSS URL map (base64 JSON).
10481
10587
  `
10482
10588
  },
10483
10589
  {
@@ -10504,6 +10610,7 @@ OPTIONS
10504
10610
  --cli=<name> CLI package to install by short name or scoped
10505
10611
  packageName (repeatable, at least one required).
10506
10612
  --home_base=<dir> Override the /home/gem base (tests).
10613
+ --ctx=<base64> Opaque ctx; ossFileMap is extracted from it.
10507
10614
  --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi.
10508
10615
 
10509
10616
  EXAMPLES
@@ -10557,6 +10664,46 @@ OPTIONS
10557
10664
  EXIT CODES
10558
10665
  0 Success or skipped (prerequisites not met).
10559
10666
  1 Secret/path unresolvable, lark-cli failed, or config unreadable.
10667
+ `
10668
+ },
10669
+ {
10670
+ name: "upgrade-lark",
10671
+ hidden: false,
10672
+ summary: "Upgrade the Feishu/Lark plugin via @larksuite/openclaw-lark-tools",
10673
+ help: `USAGE
10674
+ ${BIN} upgrade-lark [--scene=<scene>] [--caller=<n>] [--trace-id=<id>]
10675
+
10676
+ DESCRIPTION
10677
+ Upgrades the Feishu/Lark plugin by running:
10678
+ npx -y @larksuite/openclaw-lark-tools update --use-existing
10679
+
10680
+ Before the upgrade, the following files are backed up:
10681
+ - openclaw.json
10682
+ - extensions/openclaw-lark/ (if present)
10683
+ - extensions/feishu-openclaw-plugin/ (if present)
10684
+ After the upgrade, the result is validated:
10685
+ - feishu.accounts bot count must not decrease
10686
+ - gateway config structure must remain valid (port/mode/bind/auth/trustedProxies)
10687
+ If the upgrade command fails, or validation fails, the backed-up files are
10688
+ restored to roll back the changes.
10689
+
10690
+ Execution is logged to /tmp/openclaw-diagnose/upgrade-lark-<runId>.log.
10691
+
10692
+ Output is a single JSON object on stdout:
10693
+ { "ok": true, "stdout": "...", "stderr": "...", "logFile": "..." }
10694
+ { "ok": false, "error": "...", "stderr": "...", "exitCode": 1,
10695
+ "rollbackOk": true, "validationError": "...", "logFile": "..." }
10696
+
10697
+ OPTIONS
10698
+ --scene=<scene> Telemetry label forwarded to Slardar only.
10699
+ Known values: PageUpgradeLark, etc. Custom strings accepted.
10700
+ --caller=<name> Optional metadata passed to innerapi.
10701
+ --trace-id=<id> Optional log-correlation id.
10702
+
10703
+ EXIT CODES
10704
+ 0 Success: upgrade ran and all validations passed.
10705
+ 1 Failure: npx error, validation failed, or git commit failed.
10706
+ File rollback was attempted (see rollbackOk in the JSON output).
10560
10707
  `
10561
10708
  },
10562
10709
  {
@@ -10590,6 +10737,41 @@ EXAMPLES
10590
10737
  ${BIN} rules # all rules
10591
10738
  ${BIN} rules --rule=gateway # single rule
10592
10739
  ${BIN} rules --rule=gateway --rule=feishu_channel # multiple rules
10740
+ `
10741
+ },
10742
+ {
10743
+ name: "channels-probe",
10744
+ hidden: true,
10745
+ summary: "Check feishu channel health via openclaw channels status --probe",
10746
+ help: `USAGE
10747
+ ${BIN} channels-probe [--timeout=<ms>]
10748
+
10749
+ DESCRIPTION
10750
+ Runs \`openclaw channels status --probe\` and returns a structured JSON
10751
+ summary of whether the current environment's feishu channels are
10752
+ configured and working correctly.
10753
+
10754
+ Output:
10755
+ {
10756
+ "available": true,
10757
+ "gatewayReachable": true,
10758
+ "accounts": [
10759
+ { "id": "default", "bits": ["enabled","configured","running","works"],
10760
+ "isWorking": true, "raw": "- Feishu default: ..." }
10761
+ ],
10762
+ "anyAccountWorking": true
10763
+ }
10764
+
10765
+ An account is considered working when:
10766
+ enabled ∧ configured ∧ ( works ∨ ( running ∧ no error: ∧ no probe failed ) )
10767
+
10768
+ "available": false means the CLI invocation itself failed (openclaw not
10769
+ found, gateway unreachable, or no parseable output returned).
10770
+
10771
+ OPTIONS
10772
+ --timeout=<ms> Max wait in milliseconds (default: 60000). The probe
10773
+ can hang indefinitely on openclaw v2026.4.x due to a
10774
+ missing per-request HTTP timeout — set this accordingly.
10593
10775
  `
10594
10776
  },
10595
10777
  {
@@ -10610,7 +10792,8 @@ OPTIONS
10610
10792
  --role=<role> Package role (e.g. template, config).
10611
10793
  --name=<name> Package name within the role.
10612
10794
  --dir=<dir> Target dir (defaults to dirname(pkg.installPath)).
10613
- --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi.
10795
+ --ctx=<base64> Opaque ctx; ossFileMap is extracted from it.
10796
+ --oss_file_map=... Pre-built OSS URL map (base64 JSON).
10614
10797
  `
10615
10798
  }
10616
10799
  ];
@@ -10686,31 +10869,31 @@ function planVarsFields(opts = {}) {
10686
10869
  *
10687
10870
  * Per-command group needs:
10688
10871
  *
10689
- * doctor / check app + larkApps
10690
- * repair app + secrets + larkApps
10691
- * reset app + secrets + install + reset + larkApps
10872
+ * doctor / check app (rule-driven)
10873
+ * repair app + secrets (writes secretsContent / providerKeyContent)
10874
+ * reset app + secrets + install + reset (the works)
10692
10875
  * install-* install only
10693
10876
  *
10694
10877
  * Empty result (`{}`) means "no group needed" — the CLI can skip the
10695
10878
  * `fetchCtxViaInnerApi` call entirely and run with a synthetic empty ctx.
10879
+ * Happens e.g. when the user pinned `--rule=<key>` to a vars-free rule on
10880
+ * `doctor`.
10696
10881
  */
10697
10882
  function planCtxPopulate(opts) {
10698
10883
  if (opts.command === "install") return { install: true };
10699
10884
  const populate = {};
10700
- if (planVarsFields({
10885
+ const appFields = planVarsFields({
10701
10886
  disabled: opts.disabled,
10702
10887
  onlyRules: opts.onlyRules,
10703
10888
  profile: opts.profile
10704
- }).length > 0) populate.app = true;
10705
- if (opts.command === "repair") {
10706
- populate.secrets = true;
10707
- populate.larkApps = true;
10708
- } else if (opts.command === "reset") {
10889
+ });
10890
+ if (appFields.length > 0) populate.app = appFields;
10891
+ if (opts.command === "repair") populate.secrets = true;
10892
+ else if (opts.command === "reset") {
10709
10893
  populate.secrets = true;
10710
10894
  populate.install = true;
10711
10895
  populate.reset = true;
10712
- populate.larkApps = true;
10713
- } else if (opts.command === "doctor" || opts.command === "check") populate.larkApps = true;
10896
+ }
10714
10897
  return populate;
10715
10898
  }
10716
10899
  //#endregion
@@ -10764,11 +10947,408 @@ function reportDoctorRunToSlardar(opts) {
10764
10947
  }
10765
10948
  });
10766
10949
  }
10950
+ function readLogFile(filePath) {
10951
+ try {
10952
+ return node_fs.default.readFileSync(filePath, "utf-8");
10953
+ } catch {
10954
+ return "";
10955
+ }
10956
+ }
10957
+ function reportUpgradeLarkToSlardar(opts) {
10958
+ console.error(`[slardar] upgrade_lark_run scene=${opts.scene ?? ""} success=${opts.success} exitCode=${opts.exitCode ?? ""} rollbackOk=${opts.rollbackOk ?? ""}`);
10959
+ const logContent = readLogFile(opts.logFile);
10960
+ reportTask({
10961
+ eventName: "upgrade_lark_run",
10962
+ durationMs: opts.durationMs,
10963
+ status: opts.success ? "success" : "failed",
10964
+ extraCategories: {
10965
+ scene: opts.scene ?? "",
10966
+ exit_code: String(opts.exitCode ?? ""),
10967
+ rollback_ok: opts.rollbackOk != null ? String(opts.rollbackOk) : "",
10968
+ validation_error: opts.validationError ?? "",
10969
+ error_msg: opts.error ?? "",
10970
+ log_content: logContent
10971
+ }
10972
+ });
10973
+ }
10974
+ //#endregion
10975
+ //#region src/upgrade-lark.ts
10976
+ /** Plugin directories under extensions/ that are backed up before upgrade */
10977
+ const FEISHU_PLUGIN_DIRS = ["openclaw-lark", "feishu-openclaw-plugin"];
10978
+ /** Version compat rule keys checked in the doctor output after install */
10979
+ const VERSION_COMPAT_RULE_KEYS = ["feishu_plugin_version_compat_lark", "feishu_plugin_version_compat_openclaw"];
10980
+ function backupFiles(opts) {
10981
+ const { workspaceDir, configPath, backupDir, log } = opts;
10982
+ try {
10983
+ node_fs.default.mkdirSync(backupDir, { recursive: true });
10984
+ log(`backup dir: ${backupDir}`);
10985
+ if (node_fs.default.existsSync(configPath)) {
10986
+ const stat = node_fs.default.statSync(configPath);
10987
+ node_fs.default.copyFileSync(configPath, node_path.default.join(backupDir, "openclaw.json"));
10988
+ log(` backed up: openclaw.json (${stat.size} bytes)`);
10989
+ } else log(` skipped: openclaw.json (not found)`);
10990
+ const extSrc = node_path.default.join(workspaceDir, "extensions");
10991
+ for (const pluginDir of FEISHU_PLUGIN_DIRS) {
10992
+ const src = node_path.default.join(extSrc, pluginDir);
10993
+ if (node_fs.default.existsSync(src)) {
10994
+ const dst = node_path.default.join(backupDir, "extensions", pluginDir);
10995
+ node_fs.default.cpSync(src, dst, { recursive: true });
10996
+ const version = readPkgVersion(node_path.default.join(src, "package.json"));
10997
+ log(` backed up: extensions/${pluginDir}${version ? ` (version: ${version})` : ""}`);
10998
+ } else log(` skipped: extensions/${pluginDir} (not found)`);
10999
+ }
11000
+ return { ok: true };
11001
+ } catch (e) {
11002
+ return {
11003
+ ok: false,
11004
+ error: `backup failed: ${e.message}`
11005
+ };
11006
+ }
11007
+ }
11008
+ function restoreFiles(opts) {
11009
+ const { workspaceDir, configPath, backupDir, log } = opts;
11010
+ try {
11011
+ const configBackup = node_path.default.join(backupDir, "openclaw.json");
11012
+ if (node_fs.default.existsSync(configBackup)) {
11013
+ node_fs.default.copyFileSync(configBackup, configPath);
11014
+ log(` restored: openclaw.json`);
11015
+ }
11016
+ const extDst = node_path.default.join(workspaceDir, "extensions");
11017
+ for (const pluginDir of FEISHU_PLUGIN_DIRS) {
11018
+ const backupSrc = node_path.default.join(backupDir, "extensions", pluginDir);
11019
+ if (node_fs.default.existsSync(backupSrc)) {
11020
+ const dst = node_path.default.join(extDst, pluginDir);
11021
+ if (node_fs.default.existsSync(dst)) node_fs.default.rmSync(dst, {
11022
+ recursive: true,
11023
+ force: true
11024
+ });
11025
+ node_fs.default.cpSync(backupSrc, dst, { recursive: true });
11026
+ log(` restored: extensions/${pluginDir}`);
11027
+ }
11028
+ }
11029
+ return true;
11030
+ } catch (e) {
11031
+ log(` restore error: ${e.message}`);
11032
+ return false;
11033
+ }
11034
+ }
11035
+ function readPkgVersion(pkgPath) {
11036
+ try {
11037
+ const pkg = JSON.parse(node_fs.default.readFileSync(pkgPath, "utf-8"));
11038
+ return typeof pkg.version === "string" ? pkg.version : null;
11039
+ } catch {
11040
+ return null;
11041
+ }
11042
+ }
11043
+ function snapshotVersions(cwd, log) {
11044
+ const ocResult = (0, node_child_process.spawnSync)("openclaw", ["--version"], {
11045
+ cwd,
11046
+ encoding: "utf-8",
11047
+ stdio: [
11048
+ "ignore",
11049
+ "pipe",
11050
+ "pipe"
11051
+ ],
11052
+ timeout: 5e3
11053
+ });
11054
+ const ocRaw = (ocResult.stdout ?? "").trim() || (ocResult.stderr ?? "").trim();
11055
+ const extDir = node_path.default.join(cwd, "extensions");
11056
+ const larkPkg = node_path.default.join(extDir, "openclaw-lark", "package.json");
11057
+ const feishuPkg = node_path.default.join(extDir, "feishu-openclaw-plugin", "package.json");
11058
+ log(` version-check paths: ${larkPkg} [${node_fs.default.existsSync(larkPkg) ? "exists" : "missing"}]`);
11059
+ log(` version-check paths: ${feishuPkg} [${node_fs.default.existsSync(feishuPkg) ? "exists" : "missing"}]`);
11060
+ return {
11061
+ openclaw: ocRaw || null,
11062
+ openclawLark: readPkgVersion(larkPkg),
11063
+ feishuOpenclawPlugin: readPkgVersion(feishuPkg)
11064
+ };
11065
+ }
11066
+ function logVersionSnapshot(label, v, log) {
11067
+ log(`${label}: openclaw=${v.openclaw ?? "n/a"} openclaw-lark=${v.openclawLark ?? "n/a"} feishu-openclaw-plugin=${v.feishuOpenclawPlugin ?? "n/a"}`);
11068
+ }
11069
+ function countFeishuBots(configPath) {
11070
+ try {
11071
+ const raw = node_fs.default.readFileSync(configPath, "utf-8");
11072
+ const config = loadJSON5().parse(raw);
11073
+ const accounts = getNestedMap(config, "channels", "feishu", "accounts");
11074
+ if (accounts) return Object.keys(accounts).length;
11075
+ const feishu = getNestedMap(config, "channels", "feishu");
11076
+ return typeof feishu?.appId === "string" && feishu.appId ? 1 : 0;
11077
+ } catch {
11078
+ return 0;
11079
+ }
11080
+ }
11081
+ /**
11082
+ * Parse doctor stdout (first JSON line) and return an error string if any
11083
+ * version compat rule failed. Returns null on parse failure so a broken doctor
11084
+ * output does not block the install.
11085
+ */
11086
+ function checkVersionCompatFromDoctorOutput(stdout, log) {
11087
+ const firstLine = stdout.split("\n")[0]?.trim();
11088
+ if (!firstLine) {
11089
+ log(" doctor(compat): empty output, skipping version compat check");
11090
+ return null;
11091
+ }
11092
+ try {
11093
+ const report = JSON.parse(firstLine);
11094
+ for (const outcome of report.results) if (VERSION_COMPAT_RULE_KEYS.includes(outcome.rule)) {
11095
+ if (outcome.status === "failed" || outcome.status === "still-broken" || outcome.status === "error") return `version compat rule ${outcome.rule} ${outcome.status}: ${outcome.message ?? "(no message)"}`;
11096
+ }
11097
+ return null;
11098
+ } catch (e) {
11099
+ log(` doctor(compat): failed to parse output — ${e.message}`);
11100
+ return null;
11101
+ }
11102
+ }
11103
+ /** Run channels probe, log results, and return the result. Never throws. */
11104
+ function probeChannels(label, log, timeoutMs) {
11105
+ try {
11106
+ const r = runChannelsProbe(timeoutMs);
11107
+ log(` ${label} available=${r.available} anyAccountWorking=${r.anyAccountWorking}`);
11108
+ if (r.error) log(` ${label} error: ${r.error}`);
11109
+ if (r.gatewayReachable != null) log(` ${label} gatewayReachable: ${r.gatewayReachable}`);
11110
+ for (const acct of r.accounts ?? []) log(` ${label} account ${acct.id}: isWorking=${acct.isWorking} bits=[${acct.bits.join(",")}]`);
11111
+ return r;
11112
+ } catch (e) {
11113
+ log(` ${label} channels probe threw: ${e.message}`);
11114
+ return {
11115
+ available: false,
11116
+ accounts: [],
11117
+ anyAccountWorking: false
11118
+ };
11119
+ }
11120
+ }
11121
+ function runUpgradeLark(opts) {
11122
+ const cwd = opts.cwd ?? "/home/gem/workspace/agent";
11123
+ const configPath = opts.configPath ?? CONFIG_PATH;
11124
+ const logFile = upgradeLarkLogFile(opts.runId);
11125
+ const log = makeLogger(logFile);
11126
+ const fsOpts = {
11127
+ workspaceDir: cwd,
11128
+ configPath,
11129
+ backupDir: node_path.default.join(opts.backupBaseDir ?? "/tmp/openclaw-diagnose", `upgrade-lark-backup-${opts.runId}`),
11130
+ log
11131
+ };
11132
+ const cliScript = opts.cliScript ?? process.argv[1];
11133
+ const statusCheckDelayMs = opts.statusCheckDelayMs ?? 5e3;
11134
+ log(`${"=".repeat(60)}`);
11135
+ log(`upgrade-lark started runId=${opts.runId}`);
11136
+ log(` cwd : ${cwd}`);
11137
+ log(` configPath : ${configPath}`);
11138
+ log(`${"=".repeat(60)}`);
11139
+ log("");
11140
+ log("── [Pre-check A] channels probe(升级前)────────────────");
11141
+ const beforeChannels = probeChannels("before", log, 3e4);
11142
+ log("");
11143
+ log("── [Pre-check B] 版本兼容预检 ───────────────────────────");
11144
+ let versionNeedsUpgrade = false;
11145
+ try {
11146
+ const rawConfig = node_fs.default.readFileSync(configPath, "utf-8");
11147
+ versionNeedsUpgrade = needsLarkUpgrade({
11148
+ config: loadJSON5().parse(rawConfig),
11149
+ configPath,
11150
+ vars: {},
11151
+ providerDeps: {
11152
+ usesMiaodaProvider: false,
11153
+ usesMiaodaSecretProvider: false
11154
+ }
11155
+ });
11156
+ log(` version-compat pre-check: ${versionNeedsUpgrade ? "NEEDS_UPGRADE" : "ok"}`);
11157
+ } catch (e) {
11158
+ log(` version-compat pre-check error: ${e.message} — treating as needs-upgrade`);
11159
+ versionNeedsUpgrade = true;
11160
+ }
11161
+ log("");
11162
+ log("── [Gate] 升级前置条件检查 ───────────────────────────────");
11163
+ log(` version needs upgrade : ${versionNeedsUpgrade}`);
11164
+ log(` channels working before: ${beforeChannels.anyAccountWorking}`);
11165
+ if (!versionNeedsUpgrade) {
11166
+ const reason = "version already compatible — upgrade not needed";
11167
+ log(` SKIP: ${reason}`);
11168
+ log(`${"=".repeat(60)}`);
11169
+ log("upgrade-lark skipped (pre-check gate)");
11170
+ log(`${"=".repeat(60)}`);
11171
+ return {
11172
+ ok: true,
11173
+ skipped: true,
11174
+ skipReason: reason,
11175
+ logFile
11176
+ };
11177
+ }
11178
+ if (beforeChannels.anyAccountWorking) {
11179
+ const reason = "channels are working — upgrade not needed (version mismatch but system is functional)";
11180
+ log(` SKIP: ${reason}`);
11181
+ log(`${"=".repeat(60)}`);
11182
+ log("upgrade-lark skipped (pre-check gate)");
11183
+ log(`${"=".repeat(60)}`);
11184
+ return {
11185
+ ok: true,
11186
+ skipped: true,
11187
+ skipReason: reason,
11188
+ logFile
11189
+ };
11190
+ }
11191
+ log(" PROCEED: version incompatible AND channels not working → running upgrade");
11192
+ log("");
11193
+ log("── [1/6] 文件备份 ────────────────────────────────────────");
11194
+ log(`before-state: botCount=${countFeishuBots(configPath)}`);
11195
+ const backup = backupFiles(fsOpts);
11196
+ if (!backup.ok) {
11197
+ log(`ERROR: ${backup.error}`);
11198
+ return {
11199
+ ok: false,
11200
+ error: backup.error,
11201
+ logFile
11202
+ };
11203
+ }
11204
+ log("backup: ok");
11205
+ logVersionSnapshot("before-versions", snapshotVersions(cwd, log), log);
11206
+ log("");
11207
+ log("── [2/6] 清理本地 openclaw shim ─────────────────────────");
11208
+ const localOpenclawBin = node_path.default.join(cwd, "node_modules", ".bin", "openclaw");
11209
+ if (node_fs.default.existsSync(localOpenclawBin)) try {
11210
+ node_fs.default.rmSync(localOpenclawBin);
11211
+ log(` removed: ${localOpenclawBin}`);
11212
+ } catch (e) {
11213
+ log(` WARN: failed to remove ${localOpenclawBin}: ${e.message}`);
11214
+ }
11215
+ else log(` skipped: ${localOpenclawBin} (not found)`);
11216
+ log("");
11217
+ log("── [3/6] npx install (@larksuite/openclaw-lark-tools update) ──");
11218
+ const npxResult = (0, node_child_process.spawnSync)("npx", [
11219
+ "-y",
11220
+ "@larksuite/openclaw-lark-tools",
11221
+ "update"
11222
+ ], {
11223
+ cwd,
11224
+ encoding: "utf-8",
11225
+ stdio: [
11226
+ "ignore",
11227
+ "pipe",
11228
+ "pipe"
11229
+ ],
11230
+ timeout: 12e4
11231
+ });
11232
+ const npxStdout = npxResult.stdout?.trim() ?? "";
11233
+ const npxStderr = npxResult.stderr?.trim() ?? "";
11234
+ const npxExitCode = npxResult.status ?? 1;
11235
+ if (npxStdout) log(`npx stdout:\n${npxStdout}`);
11236
+ if (npxStderr) log(`npx stderr:\n${npxStderr}`);
11237
+ log(`npx exit: ${npxExitCode}${npxResult.error ? ` error: ${npxResult.error.message}` : ""}`);
11238
+ if (statusCheckDelayMs > 0) {
11239
+ log("");
11240
+ log(`── 等待 ${statusCheckDelayMs / 1e3}s(让 openclaw 服务完成重启) ─────────────`);
11241
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, statusCheckDelayMs);
11242
+ log("wait done");
11243
+ }
11244
+ const doRollback = (reason) => {
11245
+ log(`ERROR: ${reason}`);
11246
+ const rollbackOk = restoreFiles(fsOpts);
11247
+ log(`rollback: ${rollbackOk ? "ok" : "FAILED"}`);
11248
+ return {
11249
+ ok: false,
11250
+ error: reason,
11251
+ validationError: reason,
11252
+ stdout: npxStdout,
11253
+ stderr: npxStderr,
11254
+ exitCode: npxExitCode,
11255
+ rollbackOk,
11256
+ logFile
11257
+ };
11258
+ };
11259
+ log("");
11260
+ log("── [4/6] 插件安装检查 + 版本兼容校验 ───────────────────");
11261
+ const larkExtDir = node_path.default.join(cwd, "extensions", "openclaw-lark");
11262
+ const larkVersion = readPkgVersion(node_path.default.join(larkExtDir, "package.json"));
11263
+ log(` extensions/openclaw-lark: ${node_fs.default.existsSync(larkExtDir) ? "exists" : "missing"}, version=${larkVersion ?? "n/a"}`);
11264
+ if (!node_fs.default.existsSync(larkExtDir)) return doRollback("extensions/openclaw-lark not found after install");
11265
+ if (!larkVersion) return doRollback("extensions/openclaw-lark/package.json has no valid version after install");
11266
+ log(" running doctor version compat check...");
11267
+ const compatArgs = ["doctor"];
11268
+ if (opts.scene) compatArgs.push(`--scene=${opts.scene}`);
11269
+ const compatResult = (0, node_child_process.spawnSync)(process.execPath, [cliScript, ...compatArgs], {
11270
+ cwd,
11271
+ encoding: "utf-8",
11272
+ stdio: [
11273
+ "ignore",
11274
+ "pipe",
11275
+ "pipe"
11276
+ ],
11277
+ timeout: 6e4,
11278
+ env: process.env
11279
+ });
11280
+ if (compatResult.stdout?.trim()) log(`doctor(compat) stdout:\n${compatResult.stdout.trim()}`);
11281
+ if (compatResult.stderr?.trim()) log(`doctor(compat) stderr:\n${compatResult.stderr.trim()}`);
11282
+ log(`doctor(compat) exit: ${compatResult.status ?? "null"}${compatResult.error ? ` error: ${compatResult.error.message}` : ""}`);
11283
+ const compatError = checkVersionCompatFromDoctorOutput(compatResult.stdout?.trim() ?? "", log);
11284
+ if (compatError) return doRollback(compatError);
11285
+ log(" version compat: ok");
11286
+ logVersionSnapshot("after-versions", snapshotVersions(cwd, log), log);
11287
+ log("");
11288
+ log("── [5/6] channels probe(升级后)────────────────────────");
11289
+ if (!probeChannels("after", log, 3e4).anyAccountWorking) {
11290
+ log(" channels: not working before or after install — pre-existing issue, skipping rollback");
11291
+ return {
11292
+ ok: false,
11293
+ error: "channels probe: no working account (pre-existing issue, not caused by install)",
11294
+ validationError: "channels probe: no working account (pre-existing)",
11295
+ stdout: npxStdout,
11296
+ stderr: npxStderr,
11297
+ exitCode: npxExitCode,
11298
+ logFile
11299
+ };
11300
+ }
11301
+ log(" channels: ok (recovered after install)");
11302
+ log("");
11303
+ log("── [6/6] doctor --fix ────────────────────────────────────");
11304
+ const fixArgs = ["doctor", "--fix"];
11305
+ if (opts.scene) fixArgs.push(`--scene=${opts.scene}`);
11306
+ const fixResult = (0, node_child_process.spawnSync)(process.execPath, [cliScript, ...fixArgs], {
11307
+ cwd,
11308
+ encoding: "utf-8",
11309
+ stdio: [
11310
+ "ignore",
11311
+ "pipe",
11312
+ "pipe"
11313
+ ],
11314
+ timeout: 6e4,
11315
+ env: process.env
11316
+ });
11317
+ if (fixResult.stdout?.trim()) log(`doctor(fix) stdout:\n${fixResult.stdout.trim()}`);
11318
+ if (fixResult.stderr?.trim()) log(`doctor(fix) stderr:\n${fixResult.stderr.trim()}`);
11319
+ log(`doctor(fix) exit: ${fixResult.status ?? "null"}${fixResult.error ? ` error: ${fixResult.error.message}` : ""}`);
11320
+ log("");
11321
+ log(`${"=".repeat(60)}`);
11322
+ log("upgrade-lark completed successfully");
11323
+ log(`${"=".repeat(60)}`);
11324
+ return {
11325
+ ok: true,
11326
+ stdout: npxStdout,
11327
+ stderr: npxStderr,
11328
+ exitCode: npxExitCode,
11329
+ logFile
11330
+ };
11331
+ }
10767
11332
  //#endregion
10768
11333
  //#region src/index.ts
10769
11334
  const args = node_process.default.argv.slice(2);
10770
11335
  const mode = args.find((a) => !a.startsWith("-"));
10771
11336
  /**
11337
+ * Decode `--ctx=<base64>` into an opaque JSON object. Returns undefined when
11338
+ * the flag isn't present — the caller decides whether to fall back to the
11339
+ * innerapi or to error out.
11340
+ *
11341
+ * The object's shape is not enforced here; downstream code consumes it via
11342
+ * either `normalizeCtx()` (new path) or direct field access for the legacy
11343
+ * check/repair/reset contract still used by sandbox_console push.
11344
+ */
11345
+ function parseCtxFlag(args) {
11346
+ const ctxArg = args.find((a) => a.startsWith("--ctx="));
11347
+ if (!ctxArg) return void 0;
11348
+ const b64 = ctxArg.slice(6);
11349
+ return JSON.parse(Buffer.from(b64, "base64").toString("utf-8"));
11350
+ }
11351
+ /**
10772
11352
  * Pull the first non-flag positional after the mode name.
10773
11353
  * (The mode itself is args[0] in the filtered set, so we skip index 0.)
10774
11354
  */
@@ -10796,8 +11376,8 @@ function getMultiFlag(args, name) {
10796
11376
  * case but is no longer consulted.
10797
11377
  */
10798
11378
  async function reportRun(command, rc, _raw, invocation, durationMs, outcome, slardar = {
10799
- scene: void 0,
10800
- profile: "standard",
11379
+ scene,
11380
+ profile,
10801
11381
  fix: false
10802
11382
  }) {
10803
11383
  console.error(`${command}: telemetry calling report_cli_run`);
@@ -10861,7 +11441,7 @@ async function main() {
10861
11441
  console.error(`${mode}: begin argv=[${args.join(" ")}] version=${getVersion()} traceId=${traceId ?? "-"} caller=${caller ?? "-"} runIdGenerated=${rc.generated}`);
10862
11442
  switch (mode) {
10863
11443
  case "check": {
10864
- const raw = await fetchCtxViaInnerApi({
11444
+ const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
10865
11445
  populate: planCtxPopulate({
10866
11446
  command: "check",
10867
11447
  profile
@@ -10886,7 +11466,7 @@ async function main() {
10886
11466
  break;
10887
11467
  }
10888
11468
  case "repair": {
10889
- const raw = await fetchCtxViaInnerApi({
11469
+ const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
10890
11470
  populate: planCtxPopulate({
10891
11471
  command: "repair",
10892
11472
  profile
@@ -10957,15 +11537,27 @@ async function main() {
10957
11537
  break;
10958
11538
  }
10959
11539
  case "reset":
10960
- if (args.includes("--async")) console.log(JSON.stringify(startAsyncReset()));
10961
- else if (args.includes("--worker")) {
11540
+ if (args.includes("--async")) {
11541
+ const ctxArg = args.find((a) => a.startsWith("--ctx="));
11542
+ let ctxBase64;
11543
+ if (ctxArg) ctxBase64 = ctxArg.slice(6);
11544
+ else {
11545
+ const fetched = await fetchCtxViaInnerApi({
11546
+ populate: planCtxPopulate({ command: "reset" }),
11547
+ caller,
11548
+ traceId
11549
+ });
11550
+ ctxBase64 = Buffer.from(JSON.stringify(fetched), "utf-8").toString("base64");
11551
+ }
11552
+ console.log(JSON.stringify(startAsyncReset(ctxBase64)));
11553
+ } else if (args.includes("--worker")) {
10962
11554
  const taskId = args.find((a) => a.startsWith("--task-id="))?.slice(10);
10963
11555
  if (!taskId) {
10964
11556
  console.error("Error: --task-id=<id> is required for worker");
10965
11557
  node_process.default.exit(1);
10966
11558
  }
10967
11559
  const resultFile = resetResultFile(taskId);
10968
- const raw = await fetchCtxViaInnerApi({
11560
+ const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
10969
11561
  populate: planCtxPopulate({ command: "reset" }),
10970
11562
  caller,
10971
11563
  traceId
@@ -10989,7 +11581,7 @@ async function main() {
10989
11581
  return;
10990
11582
  }
10991
11583
  } else {
10992
- console.error("Usage: reset --async | reset --worker --task-id=<id>");
11584
+ console.error("Usage: reset --async [--ctx=<base64>] | reset --worker --task-id=<id> [--ctx=<base64>]");
10993
11585
  node_process.default.exit(1);
10994
11586
  }
10995
11587
  break;
@@ -11005,14 +11597,14 @@ async function main() {
11005
11597
  case "install-openclaw": {
11006
11598
  const tag = getPositionalTag(args, "install-openclaw");
11007
11599
  if (!tag) {
11008
- console.error("Usage: install-openclaw <tag> [--oss_file_map=<base64>]");
11600
+ console.error("Usage: install-openclaw <tag> [--ctx=<base64> | --oss_file_map=<base64>]");
11009
11601
  node_process.default.exit(1);
11010
11602
  }
11011
11603
  const ossFileMapFlag = getFlag(args, "oss_file_map");
11012
11604
  let installOssFileMap;
11013
11605
  let rawForTelemetry;
11014
11606
  if (!ossFileMapFlag) {
11015
- rawForTelemetry = await fetchCtxViaInnerApi({
11607
+ rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11016
11608
  populate: planCtxPopulate({ command: "install" }),
11017
11609
  caller,
11018
11610
  traceId
@@ -11047,7 +11639,7 @@ async function main() {
11047
11639
  case "install-extension": {
11048
11640
  const tag = getPositionalTag(args, "install-extension");
11049
11641
  if (!tag) {
11050
- console.error("Usage: install-extension <tag> (--all | --extension=<name>...) [--home_base=<dir>] [--config_path=<path>] [--skip-config-update] [--oss_file_map=<base64>]");
11642
+ console.error("Usage: install-extension <tag> (--all | --extension=<name>...) [--home_base=<dir>] [--config_path=<path>] [--skip-config-update] [--ctx=<base64> | --oss_file_map=<base64>]");
11051
11643
  node_process.default.exit(1);
11052
11644
  }
11053
11645
  const all = args.includes("--all");
@@ -11059,7 +11651,7 @@ async function main() {
11059
11651
  let installOssFileMap;
11060
11652
  let rawForTelemetry;
11061
11653
  if (!ossFileMapFlag) {
11062
- rawForTelemetry = await fetchCtxViaInnerApi({
11654
+ rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11063
11655
  populate: planCtxPopulate({ command: "install" }),
11064
11656
  caller,
11065
11657
  traceId
@@ -11105,12 +11697,12 @@ async function main() {
11105
11697
  case "install-cli": {
11106
11698
  const tag = getPositionalTag(args, "install-cli");
11107
11699
  if (!tag) {
11108
- console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--oss_file_map=<base64>]");
11700
+ console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--ctx=<base64> | --oss_file_map=<base64>]");
11109
11701
  node_process.default.exit(1);
11110
11702
  }
11111
11703
  const names = getMultiFlag(args, "cli");
11112
11704
  if (names.length === 0) {
11113
- console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--oss_file_map=<base64>]");
11705
+ console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--ctx=<base64> | --oss_file_map=<base64>]");
11114
11706
  node_process.default.exit(1);
11115
11707
  }
11116
11708
  const homeBase = getFlag(args, "home_base");
@@ -11118,7 +11710,7 @@ async function main() {
11118
11710
  let installOssFileMap;
11119
11711
  let rawForTelemetry;
11120
11712
  if (!ossFileMapFlag) {
11121
- rawForTelemetry = await fetchCtxViaInnerApi({
11713
+ rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11122
11714
  populate: planCtxPopulate({ command: "install" }),
11123
11715
  caller,
11124
11716
  traceId
@@ -11166,7 +11758,7 @@ async function main() {
11166
11758
  case "download-resource": {
11167
11759
  const tag = getPositionalTag(args, "download-resource");
11168
11760
  if (!tag) {
11169
- console.error("Usage: download-resource <tag> --role=<role> --name=<name> [--dir=<dir>] [--oss_file_map=<base64>]");
11761
+ console.error("Usage: download-resource <tag> --role=<role> --name=<name> [--dir=<dir>] [--ctx=<base64> | --oss_file_map=<base64>]");
11170
11762
  node_process.default.exit(1);
11171
11763
  }
11172
11764
  const role = getFlag(args, "role");
@@ -11180,7 +11772,7 @@ async function main() {
11180
11772
  let installOssFileMap;
11181
11773
  let rawForTelemetry;
11182
11774
  if (!ossFileMapFlag) {
11183
- rawForTelemetry = await fetchCtxViaInnerApi({
11775
+ rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11184
11776
  populate: planCtxPopulate({ command: "install" }),
11185
11777
  caller,
11186
11778
  traceId
@@ -11254,6 +11846,50 @@ async function main() {
11254
11846
  if (!result.ok) node_process.default.exit(1);
11255
11847
  break;
11256
11848
  }
11849
+ case "upgrade-lark": {
11850
+ const result = runUpgradeLark({
11851
+ runId: rc.runId,
11852
+ scene
11853
+ });
11854
+ const upgradeDurationMs = Date.now() - t0;
11855
+ console.log(JSON.stringify(result));
11856
+ reportUpgradeLarkToSlardar({
11857
+ scene,
11858
+ durationMs: upgradeDurationMs,
11859
+ success: result.ok,
11860
+ logFile: result.logFile,
11861
+ exitCode: result.exitCode,
11862
+ rollbackOk: result.rollbackOk,
11863
+ validationError: result.validationError,
11864
+ error: result.error
11865
+ });
11866
+ try {
11867
+ await reportCliRun({
11868
+ command: "upgrade-lark",
11869
+ runId: rc.runId,
11870
+ version: getVersion(),
11871
+ invocation: args.join(" "),
11872
+ durationMs: upgradeDurationMs,
11873
+ caller: rc.caller,
11874
+ traceId: rc.traceId,
11875
+ success: result.ok,
11876
+ result,
11877
+ error: result.ok ? void 0 : { message: result.error ?? "upgrade-lark failed" }
11878
+ });
11879
+ } catch (e) {
11880
+ console.error(`[telemetry] reportCliRun failed: ${e.message}`);
11881
+ }
11882
+ if (!result.ok) {
11883
+ node_process.default.exitCode = 1;
11884
+ return;
11885
+ }
11886
+ break;
11887
+ }
11888
+ case "channels-probe": {
11889
+ const result = runChannelsProbe(getFlag(args, "timeout") ? Number(getFlag(args, "timeout")) : void 0);
11890
+ console.log(JSON.stringify(result));
11891
+ break;
11892
+ }
11257
11893
  default:
11258
11894
  node_process.default.stderr.write(`Unknown command: ${mode}\n\n`);
11259
11895
  node_process.default.stderr.write(formatTopLevelHelp(helpFlags.expert));