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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.cjs +790 -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.1";
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,7 +3508,7 @@ 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);
@@ -3555,110 +3563,100 @@ CleanupInstallBackupDirsRule = __decorate([Rule({
3555
3563
  level: "critical"
3556
3564
  })], CleanupInstallBackupDirsRule);
3557
3565
  //#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 {
3566
+ //#region src/rules/lark-cli-missing-for-installed-lark-plugin.ts
3567
+ const PLUGIN_NAME = "openclaw-lark";
3568
+ const FORK_PACKAGE_NAME = "@lark-apaas/openclaw-lark";
3569
+ const TARGET_VERSION = "2026.4.4";
3570
+ const LARK_CLI_NAME$1 = "lark-cli";
3571
+ function readInstalledLarkPlugin(ctx) {
3572
+ const pkgPath = node_path.default.join(getExtensionsDir(ctx.configPath), PLUGIN_NAME, "package.json");
3573
+ if (!node_fs.default.existsSync(pkgPath)) return null;
3574
+ let pkg = {};
3575
+ try {
3576
+ const parsed = JSON.parse(node_fs.default.readFileSync(pkgPath, "utf-8"));
3577
+ pkg = {
3578
+ name: typeof parsed.name === "string" ? parsed.name : void 0,
3579
+ version: typeof parsed.version === "string" ? parsed.version : void 0
3580
+ };
3581
+ } catch {
3582
+ pkg = {};
3583
+ }
3584
+ const installs = getNestedMap(ctx.config, "plugins", "installs");
3585
+ const installEntry = installs ? asRecord(installs[PLUGIN_NAME]) : void 0;
3586
+ return {
3587
+ name: pkg.name ?? extractScopedNameFromSpec(installEntry?.spec),
3588
+ version: pkg.version ?? (typeof installEntry?.version === "string" ? installEntry.version : void 0)
3589
+ };
3590
+ }
3591
+ function isTargetForkPlugin(plugin) {
3592
+ return plugin?.name === FORK_PACKAGE_NAME && plugin.version === TARGET_VERSION;
3593
+ }
3594
+ function extractScopedNameFromSpec(spec) {
3595
+ if (typeof spec !== "string") return void 0;
3596
+ const at = spec.indexOf("@", 1);
3597
+ return at === -1 ? spec : spec.slice(0, at);
3598
+ }
3599
+ function isLarkCliAvailable$1() {
3600
+ try {
3601
+ return (0, node_child_process.spawnSync)(LARK_CLI_NAME$1, ["--version"], {
3602
+ encoding: "utf-8",
3603
+ timeout: 5e3,
3604
+ stdio: [
3605
+ "ignore",
3606
+ "pipe",
3607
+ "ignore"
3608
+ ]
3609
+ }).status === 0;
3610
+ } catch {
3611
+ return false;
3612
+ }
3613
+ }
3614
+ function installLarkCliOnce(tag) {
3615
+ const entry = process.argv[1];
3616
+ if (!entry) throw new Error("cannot resolve diagnose-cli entrypoint for lark-cli install");
3617
+ const res = (0, node_child_process.spawnSync)(process.execPath, [
3618
+ entry,
3619
+ "install-cli",
3620
+ tag,
3621
+ "--cli=lark-cli"
3622
+ ], {
3623
+ encoding: "utf-8",
3624
+ stdio: [
3625
+ "ignore",
3626
+ "pipe",
3627
+ "pipe"
3628
+ ]
3629
+ });
3630
+ const stdout = res.stdout?.trim();
3631
+ const stderr = res.stderr?.trim();
3632
+ if (stdout) console.error(`[lark-cli-missing] install-cli stdout: ${stdout}`);
3633
+ if (stderr) console.error(`[lark-cli-missing] install-cli stderr: ${stderr}`);
3634
+ if (res.error) throw new Error(`install-cli spawn error: ${res.error.message}`);
3635
+ if (res.status !== 0) throw new Error(`install-cli exited with code ${res.status ?? "unknown"}`);
3636
+ }
3637
+ let LarkCliMissingForInstalledLarkPluginRule = class LarkCliMissingForInstalledLarkPluginRule extends DiagnoseRule {
3568
3638
  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 };
3639
+ if (!isTargetForkPlugin(readInstalledLarkPlugin(ctx))) return { pass: true };
3640
+ if (isLarkCliAvailable$1()) return { pass: true };
3590
3641
  return {
3591
3642
  pass: false,
3592
- message: issues.join("; ")
3643
+ message: `${FORK_PACKAGE_NAME}@${TARGET_VERSION} 已安装,但 lark-cli 不可用;将执行一次 lark-cli 安装`
3593
3644
  };
3594
3645
  }
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
3646
  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 };
3647
+ if (!isTargetForkPlugin(readInstalledLarkPlugin(ctx))) return;
3648
+ if (isLarkCliAvailable$1()) return;
3649
+ installLarkCliOnce(ctx.vars.recommendedOpenclawTag ?? TARGET_VERSION);
3648
3650
  }
3649
3651
  };
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
- ],
3652
+ LarkCliMissingForInstalledLarkPluginRule = __decorate([Rule({
3653
+ key: "lark_cli_missing_for_installed_lark_plugin",
3654
+ description: "检测特定飞书插件版本已安装但 lark-cli 缺失的环境,并自动安装 lark-cli 一次",
3655
+ dependsOn: ["config_syntax_check"],
3658
3656
  repairMode: "standard",
3659
- usesVars: ["larkApps"],
3660
- level: "critical"
3661
- })], FeishuBotChannelConfigRule);
3657
+ level: "critical",
3658
+ usesVars: ["recommendedOpenclawTag"]
3659
+ })], LarkCliMissingForInstalledLarkPluginRule);
3662
3660
  //#endregion
3663
3661
  //#region src/check.ts
3664
3662
  /** Telemetry-aware entry: returns both the legacy CheckResult (for stdout)
@@ -4123,6 +4121,9 @@ const PROVIDER_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-provider-
4123
4121
  const SECRETS_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-openclaw-secrets.json";
4124
4122
  /** Absolute path to the openclaw config JSON. */
4125
4123
  const CONFIG_PATH = `${WORKSPACE_DIR}/openclaw.json`;
4124
+ function upgradeLarkLogFile(runId) {
4125
+ return `${DIAGNOSE_DIR}/upgrade-lark-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/:/g, "-")}-${runId.slice(0, 8)}.log`;
4126
+ }
4126
4127
  //#endregion
4127
4128
  //#region src/run-log.ts
4128
4129
  let currentRunContext;
@@ -4259,10 +4260,9 @@ function makeLogger(logFile) {
4259
4260
  /**
4260
4261
  * Start an async reset task: spawn a detached child process and return the taskId.
4261
4262
  *
4262
- * The child process runs: node cli.js reset --worker --task-id=xxx
4263
- * The worker fetches ctx from innerApi itself — no --ctx passthrough.
4263
+ * The child process runs: node cli.js reset --worker --task-id=xxx --ctx=base64
4264
4264
  */
4265
- function startAsyncReset() {
4265
+ function startAsyncReset(ctxBase64) {
4266
4266
  const taskId = (0, node_crypto.randomUUID)();
4267
4267
  const resultFile = resetResultFile(taskId);
4268
4268
  const log = makeLogger(resetLogFile(taskId));
@@ -4286,7 +4286,8 @@ function startAsyncReset() {
4286
4286
  process.argv[1],
4287
4287
  "reset",
4288
4288
  "--worker",
4289
- `--task-id=${taskId}`
4289
+ `--task-id=${taskId}`,
4290
+ `--ctx=${ctxBase64}`
4290
4291
  ], {
4291
4292
  detached: true,
4292
4293
  stdio: "ignore",
@@ -6235,6 +6236,23 @@ function reportError(params) {
6235
6236
  } catch {}
6236
6237
  }
6237
6238
  //#endregion
6239
+ //#region ../../openclaw-slardar/lib/report-log.js
6240
+ function reportLog(params) {
6241
+ try {
6242
+ const extra = mergeLogExtra(buildTelemetryFields(params), {
6243
+ event: params.event,
6244
+ phase: params.phase,
6245
+ status: params.status,
6246
+ ...params.extra
6247
+ });
6248
+ Slardar.sendLog?.({
6249
+ content: params.message,
6250
+ level: params.level ?? "info",
6251
+ extra
6252
+ });
6253
+ } catch {}
6254
+ }
6255
+ //#endregion
6238
6256
  //#region src/install-cli.ts
6239
6257
  const LARK_CLI_NAME = "lark-cli";
6240
6258
  const AGENT_SKILLS_NAME = "agent-skills";
@@ -6800,60 +6818,6 @@ function mergeCoreBackupAndOrigins(configPath, vars, resetData, log) {
6800
6818
  log(`allowedOrigins: added ${added.length} (${JSON.stringify(added)}), total now ${mergedOrigins.length}`);
6801
6819
  }
6802
6820
  /**
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
6821
  * Step 7: Verify startup scripts landed in configDir/scripts/.
6858
6822
  *
6859
6823
  * Scripts are extracted directly to configDir/scripts/ during stageTemplate —
@@ -6998,7 +6962,6 @@ async function runReset(input, taskId, resultFile) {
6998
6962
  await step5InstallOpenclaw(openclawTag, ossFileMap, log);
6999
6963
  step(6);
7000
6964
  mergeCoreBackupAndOrigins(configPath, vars, resetData, log);
7001
- fixBotChannelConfig(configPath, vars.larkApps, log);
7002
6965
  step(7);
7003
6966
  verifyStartupScripts(configDir, log);
7004
6967
  step(8);
@@ -7797,8 +7760,7 @@ function normalizeCtx(raw) {
7797
7760
  reset: {
7798
7761
  templateVars: r.reset.templateVars ?? {},
7799
7762
  coreBackup: r.reset.coreBackup
7800
- },
7801
- larkApps: Array.isArray(r.larkApps) ? r.larkApps : []
7763
+ }
7802
7764
  };
7803
7765
  }
7804
7766
  const vars = r.vars ?? {};
@@ -7823,8 +7785,7 @@ function normalizeCtx(raw) {
7823
7785
  reset: {
7824
7786
  templateVars: resetData.templateVars ?? {},
7825
7787
  coreBackup: resetData.coreBackup
7826
- },
7827
- larkApps: Array.isArray(r.larkApps) ? r.larkApps : []
7788
+ }
7828
7789
  };
7829
7790
  }
7830
7791
  function fillApp(src) {
@@ -7889,8 +7850,7 @@ function buildCheckInput(raw, configPathOverride) {
7889
7850
  providerFilePath: PROVIDER_FILE_PATH,
7890
7851
  secretsFilePath: SECRETS_FILE_PATH,
7891
7852
  templateVars: ctx.app.templateVars,
7892
- recommendedOpenclawTag: ctx.app.recommendedOpenclawTag,
7893
- larkApps: ctx.larkApps
7853
+ recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
7894
7854
  },
7895
7855
  templateVars: ctx.app.templateVars
7896
7856
  };
@@ -7922,8 +7882,7 @@ function buildRepairInput(raw, configPathOverride) {
7922
7882
  providerFilePath: PROVIDER_FILE_PATH,
7923
7883
  secretsFilePath: SECRETS_FILE_PATH,
7924
7884
  templateVars: ctx.app.templateVars,
7925
- recommendedOpenclawTag: ctx.app.recommendedOpenclawTag,
7926
- larkApps: ctx.larkApps
7885
+ recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
7927
7886
  },
7928
7887
  repairData: {
7929
7888
  secretsContent: ctx.secrets.secretsContent,
@@ -7959,8 +7918,7 @@ function buildResetInput(raw, configPathOverride) {
7959
7918
  providerFilePath: PROVIDER_FILE_PATH,
7960
7919
  secretsFilePath: SECRETS_FILE_PATH,
7961
7920
  templateVars: ctx.app.templateVars,
7962
- recommendedOpenclawTag: ctx.app.recommendedOpenclawTag,
7963
- larkApps: ctx.larkApps
7921
+ recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
7964
7922
  },
7965
7923
  resetData: {
7966
7924
  templateVars: ctx.reset.templateVars,
@@ -10191,6 +10149,109 @@ function finalize(results, aborted) {
10191
10149
  };
10192
10150
  }
10193
10151
  //#endregion
10152
+ //#region src/channels-probe.ts
10153
+ const CHANNEL_LINE_RE = /^-\s+Feishu\s+([^:]+):\s+(.+)$/;
10154
+ /**
10155
+ * Port of Python `_account_is_working` from the feishu-channel-success-rate skill.
10156
+ *
10157
+ * Strips colon-prefixed key:value bits (dm:, bot:, in:, out:, token:, allow:,
10158
+ * intents:, groups:, health:) and evaluates the canonical health formula.
10159
+ */
10160
+ function accountIsWorking(bits) {
10161
+ const bitTokens = /* @__PURE__ */ new Set();
10162
+ let hasError = false;
10163
+ let hasProbeFailed = false;
10164
+ for (const raw of bits) {
10165
+ const b = raw.trim();
10166
+ if (!b) continue;
10167
+ if (b.startsWith("error:")) {
10168
+ hasError = true;
10169
+ continue;
10170
+ }
10171
+ if (b === "probe failed") {
10172
+ hasProbeFailed = true;
10173
+ continue;
10174
+ }
10175
+ bitTokens.add(b.split(":")[0]);
10176
+ }
10177
+ if (!bitTokens.has("enabled") || !bitTokens.has("configured")) return false;
10178
+ if (bitTokens.has("works")) return true;
10179
+ if (bitTokens.has("running") && !hasError && !hasProbeFailed) return true;
10180
+ return false;
10181
+ }
10182
+ /**
10183
+ * Parse the raw stdout of `openclaw channels status --probe`.
10184
+ * Port of Python `extract_channels_probe` from the feishu-channel-success-rate skill.
10185
+ */
10186
+ function parseChannelsProbeOutput(text) {
10187
+ const gatewayReachable = text.includes("Gateway reachable");
10188
+ const accounts = [];
10189
+ let anyAccountWorking = false;
10190
+ for (const line of text.split("\n")) {
10191
+ const m = CHANNEL_LINE_RE.exec(line.trim());
10192
+ if (!m) continue;
10193
+ const [, acct, rest] = m;
10194
+ const bits = rest.split(",").map((b) => b.trim());
10195
+ const isWorking = accountIsWorking(bits);
10196
+ if (isWorking) anyAccountWorking = true;
10197
+ accounts.push({
10198
+ id: acct.trim(),
10199
+ bits,
10200
+ isWorking,
10201
+ raw: line.trim()
10202
+ });
10203
+ }
10204
+ return {
10205
+ gatewayReachable,
10206
+ accounts,
10207
+ anyAccountWorking
10208
+ };
10209
+ }
10210
+ /**
10211
+ * Run `openclaw channels status --probe` and return a structured result.
10212
+ *
10213
+ * The command may exit non-zero when some bot accounts fail their probe — that
10214
+ * is still useful output. We therefore try to parse stdout even when the
10215
+ * process exits with a non-zero code, falling back to an unavailable result
10216
+ * only when there is genuinely no output to parse.
10217
+ *
10218
+ * @param timeoutMs Maximum wait time. Default is 60 s because v2026.4.x
10219
+ * lacks a per-request HTTP timeout and can block indefinitely.
10220
+ */
10221
+ function runChannelsProbe(timeoutMs = 6e4) {
10222
+ let stdout = "";
10223
+ let execError;
10224
+ try {
10225
+ stdout = (0, node_child_process.execSync)("openclaw channels status --probe", {
10226
+ encoding: "utf-8",
10227
+ timeout: timeoutMs,
10228
+ stdio: [
10229
+ "ignore",
10230
+ "pipe",
10231
+ "pipe"
10232
+ ]
10233
+ });
10234
+ } catch (e) {
10235
+ const err = e;
10236
+ stdout = err.stdout ?? "";
10237
+ execError = err.message;
10238
+ const stderrRaw = err.stderr;
10239
+ const stderr = (typeof stderrRaw === "string" ? stderrRaw : stderrRaw?.toString("utf-8") ?? "").trim();
10240
+ if (stderr) console.error(`channels-probe: stderr from CLI: ${stderr}`);
10241
+ }
10242
+ if (stdout.trim()) return {
10243
+ available: true,
10244
+ ...parseChannelsProbeOutput(stdout)
10245
+ };
10246
+ return {
10247
+ available: false,
10248
+ gatewayReachable: false,
10249
+ accounts: [],
10250
+ anyAccountWorking: false,
10251
+ error: execError ?? "no output from openclaw channels status --probe"
10252
+ };
10253
+ }
10254
+ //#endregion
10194
10255
  //#region src/innerapi/reportCliRun.ts
10195
10256
  /**
10196
10257
  * CLI-side client for studio_server's `openclaw.report_cli_run` inner
@@ -10270,7 +10331,7 @@ async function reportCliRun(opts) {
10270
10331
  //#region src/help.ts
10271
10332
  const BIN = "mclaw-diagnose";
10272
10333
  function versionBanner() {
10273
- return `v0.1.14-alpha.0`;
10334
+ return `v0.1.14-alpha.1`;
10274
10335
  }
10275
10336
  const COMMANDS = [
10276
10337
  {
@@ -10374,12 +10435,16 @@ EXIT CODES
10374
10435
  hidden: true,
10375
10436
  summary: "Run rule-engine check only",
10376
10437
  help: `USAGE
10377
- ${BIN} check
10438
+ ${BIN} check [--ctx=<base64>]
10378
10439
 
10379
10440
  DESCRIPTION
10380
10441
  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\`.
10442
+ returns { failedRules }. Used by sandbox_console's push-style callers
10443
+ that already own the ctx — end-users should prefer \`doctor\`.
10444
+
10445
+ OPTIONS
10446
+ --ctx=<base64> Opaque ctx JSON (base64). When absent, fetched from
10447
+ innerapi (same path as doctor).
10383
10448
  `
10384
10449
  },
10385
10450
  {
@@ -10387,11 +10452,16 @@ DESCRIPTION
10387
10452
  hidden: true,
10388
10453
  summary: "Apply standard-mode repairs",
10389
10454
  help: `USAGE
10390
- ${BIN} repair
10455
+ ${BIN} repair [--ctx=<base64>]
10391
10456
 
10392
10457
  DESCRIPTION
10393
- Runs repair for the failing rules. Ctx is fetched from innerapi
10394
- automatically. End-users should use \`doctor --fix\` instead.
10458
+ Runs repair for the failing rules listed inside the ctx's repairData.
10459
+ Intended for sandbox_console's push path — end-users should use
10460
+ \`doctor --fix\` instead.
10461
+
10462
+ OPTIONS
10463
+ --ctx=<base64> Opaque ctx JSON (base64). When absent, fetched from
10464
+ innerapi.
10395
10465
  `
10396
10466
  },
10397
10467
  {
@@ -10399,15 +10469,14 @@ DESCRIPTION
10399
10469
  hidden: true,
10400
10470
  summary: "Re-initialize sandbox via the 9-step reset pipeline",
10401
10471
  help: `USAGE
10402
- ${BIN} reset --async
10403
- ${BIN} reset --worker --task-id=<id>
10472
+ ${BIN} reset --async [--ctx=<base64>]
10473
+ ${BIN} reset --worker --task-id=<id> [--ctx=<base64>]
10404
10474
 
10405
10475
  DESCRIPTION
10406
10476
  Two-phase pipeline driven asynchronously: the --async invocation spawns
10407
10477
  a detached worker and returns { taskId } immediately; the --worker
10408
10478
  invocation (spawned by --async) runs the actual 9 steps and writes
10409
10479
  progress to /tmp/openclaw-diagnose/reset-<taskId>.json.
10410
- Ctx is fetched from innerapi automatically.
10411
10480
 
10412
10481
  Poll progress with \`${BIN} get_reset_task --task-id=<id>\`.
10413
10482
 
@@ -10415,6 +10484,7 @@ OPTIONS
10415
10484
  --async Start a detached worker and return taskId on stdout.
10416
10485
  --worker Internal — run the 9-step pipeline (launched by --async).
10417
10486
  --task-id=<id> Required with --worker; identifies the progress file.
10487
+ --ctx=<base64> Opaque ctx JSON; fetched from innerapi when absent.
10418
10488
  `
10419
10489
  },
10420
10490
  {
@@ -10437,7 +10507,7 @@ OPTIONS
10437
10507
  hidden: true,
10438
10508
  summary: "Download + install the openclaw tarball",
10439
10509
  help: `USAGE
10440
- ${BIN} install-openclaw <tag> [--oss_file_map=<base64>]
10510
+ ${BIN} install-openclaw <tag> [--ctx=<base64> | --oss_file_map=<base64>]
10441
10511
 
10442
10512
  DESCRIPTION
10443
10513
  Downloads the openclaw@<tag> tgz via the signed OSS URL found in the
@@ -10449,9 +10519,9 @@ ARGUMENTS
10449
10519
  <tag> Openclaw version tag, e.g. 2026.4.11.
10450
10520
 
10451
10521
  OPTIONS
10522
+ --ctx=<base64> Opaque ctx; ossFileMap is extracted from it.
10452
10523
  --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi
10453
- entirely. When absent, ossFileMap is fetched from
10454
- innerapi automatically.
10524
+ entirely. Wins over --ctx when both provided.
10455
10525
  `
10456
10526
  },
10457
10527
  {
@@ -10477,7 +10547,8 @@ OPTIONS
10477
10547
  --home_base=<dir> Override the /home/gem base (tests).
10478
10548
  --config_path=<p> Override the openclaw.json path (tests).
10479
10549
  --skip-config-update Leave plugins.installs in openclaw.json untouched.
10480
- --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi.
10550
+ --ctx=<base64> Opaque ctx; see install-openclaw for semantics.
10551
+ --oss_file_map=... Pre-built OSS URL map (base64 JSON).
10481
10552
  `
10482
10553
  },
10483
10554
  {
@@ -10504,6 +10575,7 @@ OPTIONS
10504
10575
  --cli=<name> CLI package to install by short name or scoped
10505
10576
  packageName (repeatable, at least one required).
10506
10577
  --home_base=<dir> Override the /home/gem base (tests).
10578
+ --ctx=<base64> Opaque ctx; ossFileMap is extracted from it.
10507
10579
  --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi.
10508
10580
 
10509
10581
  EXAMPLES
@@ -10557,6 +10629,46 @@ OPTIONS
10557
10629
  EXIT CODES
10558
10630
  0 Success or skipped (prerequisites not met).
10559
10631
  1 Secret/path unresolvable, lark-cli failed, or config unreadable.
10632
+ `
10633
+ },
10634
+ {
10635
+ name: "upgrade-lark",
10636
+ hidden: false,
10637
+ summary: "Upgrade the Feishu/Lark plugin via @larksuite/openclaw-lark-tools",
10638
+ help: `USAGE
10639
+ ${BIN} upgrade-lark [--scene=<scene>] [--caller=<n>] [--trace-id=<id>]
10640
+
10641
+ DESCRIPTION
10642
+ Upgrades the Feishu/Lark plugin by running:
10643
+ npx -y @larksuite/openclaw-lark-tools update --use-existing
10644
+
10645
+ Before the upgrade, the following files are backed up:
10646
+ - openclaw.json
10647
+ - extensions/openclaw-lark/ (if present)
10648
+ - extensions/feishu-openclaw-plugin/ (if present)
10649
+ After the upgrade, the result is validated:
10650
+ - feishu.accounts bot count must not decrease
10651
+ - gateway config structure must remain valid (port/mode/bind/auth/trustedProxies)
10652
+ If the upgrade command fails, or validation fails, the backed-up files are
10653
+ restored to roll back the changes.
10654
+
10655
+ Execution is logged to /tmp/openclaw-diagnose/upgrade-lark-<runId>.log.
10656
+
10657
+ Output is a single JSON object on stdout:
10658
+ { "ok": true, "stdout": "...", "stderr": "...", "logFile": "..." }
10659
+ { "ok": false, "error": "...", "stderr": "...", "exitCode": 1,
10660
+ "rollbackOk": true, "validationError": "...", "logFile": "..." }
10661
+
10662
+ OPTIONS
10663
+ --scene=<scene> Telemetry label forwarded to Slardar only.
10664
+ Known values: PageUpgradeLark, etc. Custom strings accepted.
10665
+ --caller=<name> Optional metadata passed to innerapi.
10666
+ --trace-id=<id> Optional log-correlation id.
10667
+
10668
+ EXIT CODES
10669
+ 0 Success: upgrade ran and all validations passed.
10670
+ 1 Failure: npx error, validation failed, or git commit failed.
10671
+ File rollback was attempted (see rollbackOk in the JSON output).
10560
10672
  `
10561
10673
  },
10562
10674
  {
@@ -10590,6 +10702,41 @@ EXAMPLES
10590
10702
  ${BIN} rules # all rules
10591
10703
  ${BIN} rules --rule=gateway # single rule
10592
10704
  ${BIN} rules --rule=gateway --rule=feishu_channel # multiple rules
10705
+ `
10706
+ },
10707
+ {
10708
+ name: "channels-probe",
10709
+ hidden: true,
10710
+ summary: "Check feishu channel health via openclaw channels status --probe",
10711
+ help: `USAGE
10712
+ ${BIN} channels-probe [--timeout=<ms>]
10713
+
10714
+ DESCRIPTION
10715
+ Runs \`openclaw channels status --probe\` and returns a structured JSON
10716
+ summary of whether the current environment's feishu channels are
10717
+ configured and working correctly.
10718
+
10719
+ Output:
10720
+ {
10721
+ "available": true,
10722
+ "gatewayReachable": true,
10723
+ "accounts": [
10724
+ { "id": "default", "bits": ["enabled","configured","running","works"],
10725
+ "isWorking": true, "raw": "- Feishu default: ..." }
10726
+ ],
10727
+ "anyAccountWorking": true
10728
+ }
10729
+
10730
+ An account is considered working when:
10731
+ enabled ∧ configured ∧ ( works ∨ ( running ∧ no error: ∧ no probe failed ) )
10732
+
10733
+ "available": false means the CLI invocation itself failed (openclaw not
10734
+ found, gateway unreachable, or no parseable output returned).
10735
+
10736
+ OPTIONS
10737
+ --timeout=<ms> Max wait in milliseconds (default: 60000). The probe
10738
+ can hang indefinitely on openclaw v2026.4.x due to a
10739
+ missing per-request HTTP timeout — set this accordingly.
10593
10740
  `
10594
10741
  },
10595
10742
  {
@@ -10610,7 +10757,8 @@ OPTIONS
10610
10757
  --role=<role> Package role (e.g. template, config).
10611
10758
  --name=<name> Package name within the role.
10612
10759
  --dir=<dir> Target dir (defaults to dirname(pkg.installPath)).
10613
- --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi.
10760
+ --ctx=<base64> Opaque ctx; ossFileMap is extracted from it.
10761
+ --oss_file_map=... Pre-built OSS URL map (base64 JSON).
10614
10762
  `
10615
10763
  }
10616
10764
  ];
@@ -10686,31 +10834,31 @@ function planVarsFields(opts = {}) {
10686
10834
  *
10687
10835
  * Per-command group needs:
10688
10836
  *
10689
- * doctor / check app + larkApps
10690
- * repair app + secrets + larkApps
10691
- * reset app + secrets + install + reset + larkApps
10837
+ * doctor / check app (rule-driven)
10838
+ * repair app + secrets (writes secretsContent / providerKeyContent)
10839
+ * reset app + secrets + install + reset (the works)
10692
10840
  * install-* install only
10693
10841
  *
10694
10842
  * Empty result (`{}`) means "no group needed" — the CLI can skip the
10695
10843
  * `fetchCtxViaInnerApi` call entirely and run with a synthetic empty ctx.
10844
+ * Happens e.g. when the user pinned `--rule=<key>` to a vars-free rule on
10845
+ * `doctor`.
10696
10846
  */
10697
10847
  function planCtxPopulate(opts) {
10698
10848
  if (opts.command === "install") return { install: true };
10699
10849
  const populate = {};
10700
- if (planVarsFields({
10850
+ const appFields = planVarsFields({
10701
10851
  disabled: opts.disabled,
10702
10852
  onlyRules: opts.onlyRules,
10703
10853
  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") {
10854
+ });
10855
+ if (appFields.length > 0) populate.app = appFields;
10856
+ if (opts.command === "repair") populate.secrets = true;
10857
+ else if (opts.command === "reset") {
10709
10858
  populate.secrets = true;
10710
10859
  populate.install = true;
10711
10860
  populate.reset = true;
10712
- populate.larkApps = true;
10713
- } else if (opts.command === "doctor" || opts.command === "check") populate.larkApps = true;
10861
+ }
10714
10862
  return populate;
10715
10863
  }
10716
10864
  //#endregion
@@ -10764,11 +10912,378 @@ function reportDoctorRunToSlardar(opts) {
10764
10912
  }
10765
10913
  });
10766
10914
  }
10915
+ /** Read the tail of a file, up to maxBytes. Returns empty string on any error. */
10916
+ function readFileTail(filePath, maxBytes = 4e3) {
10917
+ try {
10918
+ const content = node_fs.default.readFileSync(filePath, "utf-8");
10919
+ if (content.length <= maxBytes) return content;
10920
+ return `...(truncated — showing last ${maxBytes} chars)...\n` + content.slice(-maxBytes);
10921
+ } catch {
10922
+ return "";
10923
+ }
10924
+ }
10925
+ function reportUpgradeLarkToSlardar(opts) {
10926
+ console.error(`[slardar] upgrade-lark reportTask scene=${opts.scene ?? ""} success=${opts.success} exitCode=${opts.exitCode ?? ""} rollbackOk=${opts.rollbackOk ?? ""}`);
10927
+ reportTask({
10928
+ eventName: "upgrade_lark_run",
10929
+ durationMs: opts.durationMs,
10930
+ status: opts.success ? "success" : "failed",
10931
+ extraCategories: {
10932
+ scene: opts.scene ?? "",
10933
+ exit_code: String(opts.exitCode ?? ""),
10934
+ rollback_ok: opts.rollbackOk != null ? String(opts.rollbackOk) : "",
10935
+ validation_error: (opts.validationError ?? "").slice(0, 200),
10936
+ error_msg: (opts.error ?? "").slice(0, 200)
10937
+ }
10938
+ });
10939
+ const logContent = readFileTail(opts.logFile);
10940
+ console.error(`[slardar] upgrade_lark_detail logFile=${opts.logFile} contentLength=${logContent.length} sendLogAvailable=${typeof Slardar.sendLog}`);
10941
+ reportLog({
10942
+ event: "upgrade_lark_detail",
10943
+ message: logContent || "(no log content)",
10944
+ level: opts.success ? "info" : "error",
10945
+ extra: {
10946
+ log_file: opts.logFile,
10947
+ scene: opts.scene ?? "",
10948
+ success: String(opts.success),
10949
+ exit_code: String(opts.exitCode ?? ""),
10950
+ rollback_ok: opts.rollbackOk != null ? String(opts.rollbackOk) : ""
10951
+ }
10952
+ });
10953
+ }
10954
+ //#endregion
10955
+ //#region src/upgrade-lark.ts
10956
+ /** Plugin directories under extensions/ that are backed up before upgrade */
10957
+ const FEISHU_PLUGIN_DIRS = ["openclaw-lark", "feishu-openclaw-plugin"];
10958
+ /** Version compat rule keys checked in the doctor output after install */
10959
+ const VERSION_COMPAT_RULE_KEYS = ["feishu_plugin_version_compat_lark", "feishu_plugin_version_compat_openclaw"];
10960
+ function backupFiles(opts) {
10961
+ const { workspaceDir, configPath, backupDir, log } = opts;
10962
+ try {
10963
+ node_fs.default.mkdirSync(backupDir, { recursive: true });
10964
+ log(`backup dir: ${backupDir}`);
10965
+ if (node_fs.default.existsSync(configPath)) {
10966
+ const stat = node_fs.default.statSync(configPath);
10967
+ node_fs.default.copyFileSync(configPath, node_path.default.join(backupDir, "openclaw.json"));
10968
+ log(` backed up: openclaw.json (${stat.size} bytes)`);
10969
+ } else log(` skipped: openclaw.json (not found)`);
10970
+ const extSrc = node_path.default.join(workspaceDir, "extensions");
10971
+ for (const pluginDir of FEISHU_PLUGIN_DIRS) {
10972
+ const src = node_path.default.join(extSrc, pluginDir);
10973
+ if (node_fs.default.existsSync(src)) {
10974
+ const dst = node_path.default.join(backupDir, "extensions", pluginDir);
10975
+ node_fs.default.cpSync(src, dst, { recursive: true });
10976
+ const version = readPkgVersion(node_path.default.join(src, "package.json"));
10977
+ log(` backed up: extensions/${pluginDir}${version ? ` (version: ${version})` : ""}`);
10978
+ } else log(` skipped: extensions/${pluginDir} (not found)`);
10979
+ }
10980
+ return { ok: true };
10981
+ } catch (e) {
10982
+ return {
10983
+ ok: false,
10984
+ error: `backup failed: ${e.message}`
10985
+ };
10986
+ }
10987
+ }
10988
+ function restoreFiles(opts) {
10989
+ const { workspaceDir, configPath, backupDir, log } = opts;
10990
+ try {
10991
+ const configBackup = node_path.default.join(backupDir, "openclaw.json");
10992
+ if (node_fs.default.existsSync(configBackup)) {
10993
+ node_fs.default.copyFileSync(configBackup, configPath);
10994
+ log(` restored: openclaw.json`);
10995
+ }
10996
+ const extDst = node_path.default.join(workspaceDir, "extensions");
10997
+ for (const pluginDir of FEISHU_PLUGIN_DIRS) {
10998
+ const backupSrc = node_path.default.join(backupDir, "extensions", pluginDir);
10999
+ if (node_fs.default.existsSync(backupSrc)) {
11000
+ const dst = node_path.default.join(extDst, pluginDir);
11001
+ if (node_fs.default.existsSync(dst)) node_fs.default.rmSync(dst, {
11002
+ recursive: true,
11003
+ force: true
11004
+ });
11005
+ node_fs.default.cpSync(backupSrc, dst, { recursive: true });
11006
+ log(` restored: extensions/${pluginDir}`);
11007
+ }
11008
+ }
11009
+ return true;
11010
+ } catch (e) {
11011
+ log(` restore error: ${e.message}`);
11012
+ return false;
11013
+ }
11014
+ }
11015
+ function readPkgVersion(pkgPath) {
11016
+ try {
11017
+ const pkg = JSON.parse(node_fs.default.readFileSync(pkgPath, "utf-8"));
11018
+ return typeof pkg.version === "string" ? pkg.version : null;
11019
+ } catch {
11020
+ return null;
11021
+ }
11022
+ }
11023
+ function snapshotVersions(cwd, log) {
11024
+ const ocResult = (0, node_child_process.spawnSync)("openclaw", ["--version"], {
11025
+ cwd,
11026
+ encoding: "utf-8",
11027
+ stdio: [
11028
+ "ignore",
11029
+ "pipe",
11030
+ "pipe"
11031
+ ],
11032
+ timeout: 5e3
11033
+ });
11034
+ const ocRaw = (ocResult.stdout ?? "").trim() || (ocResult.stderr ?? "").trim();
11035
+ const extDir = node_path.default.join(cwd, "extensions");
11036
+ const larkPkg = node_path.default.join(extDir, "openclaw-lark", "package.json");
11037
+ const feishuPkg = node_path.default.join(extDir, "feishu-openclaw-plugin", "package.json");
11038
+ log(` version-check paths: ${larkPkg} [${node_fs.default.existsSync(larkPkg) ? "exists" : "missing"}]`);
11039
+ log(` version-check paths: ${feishuPkg} [${node_fs.default.existsSync(feishuPkg) ? "exists" : "missing"}]`);
11040
+ return {
11041
+ openclaw: ocRaw || null,
11042
+ openclawLark: readPkgVersion(larkPkg),
11043
+ feishuOpenclawPlugin: readPkgVersion(feishuPkg)
11044
+ };
11045
+ }
11046
+ function logVersionSnapshot(label, v, log) {
11047
+ log(`${label}: openclaw=${v.openclaw ?? "n/a"} openclaw-lark=${v.openclawLark ?? "n/a"} feishu-openclaw-plugin=${v.feishuOpenclawPlugin ?? "n/a"}`);
11048
+ }
11049
+ function countFeishuBots(configPath) {
11050
+ try {
11051
+ const raw = node_fs.default.readFileSync(configPath, "utf-8");
11052
+ const config = loadJSON5().parse(raw);
11053
+ const accounts = getNestedMap(config, "channels", "feishu", "accounts");
11054
+ if (accounts) return Object.keys(accounts).length;
11055
+ const feishu = getNestedMap(config, "channels", "feishu");
11056
+ return typeof feishu?.appId === "string" && feishu.appId ? 1 : 0;
11057
+ } catch {
11058
+ return 0;
11059
+ }
11060
+ }
11061
+ /**
11062
+ * Parse doctor stdout (first JSON line) and return an error string if any
11063
+ * version compat rule failed. Returns null on parse failure so a broken doctor
11064
+ * output does not block the install.
11065
+ */
11066
+ function checkVersionCompatFromDoctorOutput(stdout, log) {
11067
+ const firstLine = stdout.split("\n")[0]?.trim();
11068
+ if (!firstLine) {
11069
+ log(" doctor(compat): empty output, skipping version compat check");
11070
+ return null;
11071
+ }
11072
+ try {
11073
+ const report = JSON.parse(firstLine);
11074
+ for (const outcome of report.results) if (VERSION_COMPAT_RULE_KEYS.includes(outcome.rule)) {
11075
+ if (outcome.status === "failed" || outcome.status === "still-broken" || outcome.status === "error") return `version compat rule ${outcome.rule} ${outcome.status}: ${outcome.message ?? "(no message)"}`;
11076
+ }
11077
+ return null;
11078
+ } catch (e) {
11079
+ log(` doctor(compat): failed to parse output — ${e.message}`);
11080
+ return null;
11081
+ }
11082
+ }
11083
+ /** Run channels probe, log results, and return the result. Never throws. */
11084
+ function probeChannels(label, log, timeoutMs) {
11085
+ try {
11086
+ const r = runChannelsProbe(timeoutMs);
11087
+ log(` ${label} available=${r.available} anyAccountWorking=${r.anyAccountWorking}`);
11088
+ if (r.error) log(` ${label} error: ${r.error}`);
11089
+ if (r.gatewayReachable != null) log(` ${label} gatewayReachable: ${r.gatewayReachable}`);
11090
+ for (const acct of r.accounts ?? []) log(` ${label} account ${acct.id}: isWorking=${acct.isWorking} bits=[${acct.bits.join(",")}]`);
11091
+ return r;
11092
+ } catch (e) {
11093
+ log(` ${label} channels probe threw: ${e.message}`);
11094
+ return {
11095
+ available: false,
11096
+ accounts: [],
11097
+ anyAccountWorking: false
11098
+ };
11099
+ }
11100
+ }
11101
+ function runUpgradeLark(opts) {
11102
+ const cwd = opts.cwd ?? "/home/gem/workspace/agent";
11103
+ const configPath = opts.configPath ?? CONFIG_PATH;
11104
+ const logFile = upgradeLarkLogFile(opts.runId);
11105
+ const log = makeLogger(logFile);
11106
+ const fsOpts = {
11107
+ workspaceDir: cwd,
11108
+ configPath,
11109
+ backupDir: node_path.default.join(opts.backupBaseDir ?? "/tmp/openclaw-diagnose", `upgrade-lark-backup-${opts.runId}`),
11110
+ log
11111
+ };
11112
+ const cliScript = opts.cliScript ?? process.argv[1];
11113
+ const statusCheckDelayMs = opts.statusCheckDelayMs ?? 5e3;
11114
+ log(`${"=".repeat(60)}`);
11115
+ log(`upgrade-lark started runId=${opts.runId}`);
11116
+ log(` cwd : ${cwd}`);
11117
+ log(` configPath : ${configPath}`);
11118
+ log(`${"=".repeat(60)}`);
11119
+ log("");
11120
+ log("── [1/8] 升级前状态快照 ──────────────────────────────────");
11121
+ log(`before-state: botCount=${countFeishuBots(configPath)}`);
11122
+ log("");
11123
+ log("── [2/8] 文件备份 ────────────────────────────────────────");
11124
+ const backup = backupFiles(fsOpts);
11125
+ if (!backup.ok) {
11126
+ log(`ERROR: ${backup.error}`);
11127
+ return {
11128
+ ok: false,
11129
+ error: backup.error,
11130
+ logFile
11131
+ };
11132
+ }
11133
+ log("backup: ok");
11134
+ logVersionSnapshot("before-versions", snapshotVersions(cwd, log), log);
11135
+ log("");
11136
+ log("── [3/8] channels probe(升级前)────────────────────────");
11137
+ const beforeChannels = probeChannels("before", log, 3e4);
11138
+ log("");
11139
+ log("── [4/8] 清理本地 openclaw shim ─────────────────────────");
11140
+ const localOpenclawBin = node_path.default.join(cwd, "node_modules", ".bin", "openclaw");
11141
+ if (node_fs.default.existsSync(localOpenclawBin)) try {
11142
+ node_fs.default.rmSync(localOpenclawBin);
11143
+ log(` removed: ${localOpenclawBin}`);
11144
+ } catch (e) {
11145
+ log(` WARN: failed to remove ${localOpenclawBin}: ${e.message}`);
11146
+ }
11147
+ else log(` skipped: ${localOpenclawBin} (not found)`);
11148
+ log("");
11149
+ log("── [5/8] npx install (@larksuite/openclaw-lark-tools update) ──");
11150
+ const npxResult = (0, node_child_process.spawnSync)("npx", [
11151
+ "-y",
11152
+ "@larksuite/openclaw-lark-tools",
11153
+ "update"
11154
+ ], {
11155
+ cwd,
11156
+ encoding: "utf-8",
11157
+ stdio: [
11158
+ "ignore",
11159
+ "pipe",
11160
+ "pipe"
11161
+ ],
11162
+ timeout: 12e4
11163
+ });
11164
+ const npxStdout = npxResult.stdout?.trim() ?? "";
11165
+ const npxStderr = npxResult.stderr?.trim() ?? "";
11166
+ const npxExitCode = npxResult.status ?? 1;
11167
+ if (npxStdout) log(`npx stdout:\n${npxStdout}`);
11168
+ if (npxStderr) log(`npx stderr:\n${npxStderr}`);
11169
+ log(`npx exit: ${npxExitCode}${npxResult.error ? ` error: ${npxResult.error.message}` : ""}`);
11170
+ if (statusCheckDelayMs > 0) {
11171
+ log("");
11172
+ log(`── 等待 ${statusCheckDelayMs / 1e3}s(让 openclaw 服务完成重启) ─────────────`);
11173
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, statusCheckDelayMs);
11174
+ log("wait done");
11175
+ }
11176
+ const doRollback = (reason) => {
11177
+ log(`ERROR: ${reason}`);
11178
+ const rollbackOk = restoreFiles(fsOpts);
11179
+ log(`rollback: ${rollbackOk ? "ok" : "FAILED"}`);
11180
+ return {
11181
+ ok: false,
11182
+ error: reason,
11183
+ validationError: reason,
11184
+ stdout: npxStdout,
11185
+ stderr: npxStderr,
11186
+ exitCode: npxExitCode,
11187
+ rollbackOk,
11188
+ logFile
11189
+ };
11190
+ };
11191
+ log("");
11192
+ log("── [6/8] 插件安装检查 + 版本兼容校验 ───────────────────");
11193
+ const larkExtDir = node_path.default.join(cwd, "extensions", "openclaw-lark");
11194
+ const larkVersion = readPkgVersion(node_path.default.join(larkExtDir, "package.json"));
11195
+ log(` extensions/openclaw-lark: ${node_fs.default.existsSync(larkExtDir) ? "exists" : "missing"}, version=${larkVersion ?? "n/a"}`);
11196
+ if (!node_fs.default.existsSync(larkExtDir)) return doRollback("extensions/openclaw-lark not found after install");
11197
+ if (!larkVersion) return doRollback("extensions/openclaw-lark/package.json has no valid version after install");
11198
+ log(" running doctor version compat check...");
11199
+ const compatArgs = ["doctor"];
11200
+ if (opts.scene) compatArgs.push(`--scene=${opts.scene}`);
11201
+ const compatResult = (0, node_child_process.spawnSync)(process.execPath, [cliScript, ...compatArgs], {
11202
+ cwd,
11203
+ encoding: "utf-8",
11204
+ stdio: [
11205
+ "ignore",
11206
+ "pipe",
11207
+ "pipe"
11208
+ ],
11209
+ timeout: 6e4,
11210
+ env: process.env
11211
+ });
11212
+ if (compatResult.stdout?.trim()) log(`doctor(compat) stdout:\n${compatResult.stdout.trim()}`);
11213
+ if (compatResult.stderr?.trim()) log(`doctor(compat) stderr:\n${compatResult.stderr.trim()}`);
11214
+ log(`doctor(compat) exit: ${compatResult.status ?? "null"}${compatResult.error ? ` error: ${compatResult.error.message}` : ""}`);
11215
+ const compatError = checkVersionCompatFromDoctorOutput(compatResult.stdout?.trim() ?? "", log);
11216
+ if (compatError) return doRollback(compatError);
11217
+ log(" version compat: ok");
11218
+ logVersionSnapshot("after-versions", snapshotVersions(cwd, log), log);
11219
+ log("");
11220
+ log("── [7/8] channels probe(升级后)────────────────────────");
11221
+ if (!probeChannels("after", log, 3e4).anyAccountWorking) {
11222
+ if (!beforeChannels.anyAccountWorking) {
11223
+ log(" channels: not working before or after install — pre-existing issue, skipping rollback");
11224
+ return {
11225
+ ok: false,
11226
+ error: "channels probe: no working account (pre-existing issue, not caused by install)",
11227
+ validationError: "channels probe: no working account (pre-existing)",
11228
+ stdout: npxStdout,
11229
+ stderr: npxStderr,
11230
+ exitCode: npxExitCode,
11231
+ logFile
11232
+ };
11233
+ }
11234
+ return doRollback("channels probe: no working account after install (was working before)");
11235
+ }
11236
+ log(" channels: ok");
11237
+ log("");
11238
+ log("── [8/8] doctor --fix ────────────────────────────────────");
11239
+ const fixArgs = ["doctor", "--fix"];
11240
+ if (opts.scene) fixArgs.push(`--scene=${opts.scene}`);
11241
+ const fixResult = (0, node_child_process.spawnSync)(process.execPath, [cliScript, ...fixArgs], {
11242
+ cwd,
11243
+ encoding: "utf-8",
11244
+ stdio: [
11245
+ "ignore",
11246
+ "pipe",
11247
+ "pipe"
11248
+ ],
11249
+ timeout: 6e4,
11250
+ env: process.env
11251
+ });
11252
+ if (fixResult.stdout?.trim()) log(`doctor(fix) stdout:\n${fixResult.stdout.trim()}`);
11253
+ if (fixResult.stderr?.trim()) log(`doctor(fix) stderr:\n${fixResult.stderr.trim()}`);
11254
+ log(`doctor(fix) exit: ${fixResult.status ?? "null"}${fixResult.error ? ` error: ${fixResult.error.message}` : ""}`);
11255
+ log("");
11256
+ log(`${"=".repeat(60)}`);
11257
+ log("upgrade-lark completed successfully");
11258
+ log(`${"=".repeat(60)}`);
11259
+ return {
11260
+ ok: true,
11261
+ stdout: npxStdout,
11262
+ stderr: npxStderr,
11263
+ exitCode: npxExitCode,
11264
+ logFile
11265
+ };
11266
+ }
10767
11267
  //#endregion
10768
11268
  //#region src/index.ts
10769
11269
  const args = node_process.default.argv.slice(2);
10770
11270
  const mode = args.find((a) => !a.startsWith("-"));
10771
11271
  /**
11272
+ * Decode `--ctx=<base64>` into an opaque JSON object. Returns undefined when
11273
+ * the flag isn't present — the caller decides whether to fall back to the
11274
+ * innerapi or to error out.
11275
+ *
11276
+ * The object's shape is not enforced here; downstream code consumes it via
11277
+ * either `normalizeCtx()` (new path) or direct field access for the legacy
11278
+ * check/repair/reset contract still used by sandbox_console push.
11279
+ */
11280
+ function parseCtxFlag(args) {
11281
+ const ctxArg = args.find((a) => a.startsWith("--ctx="));
11282
+ if (!ctxArg) return void 0;
11283
+ const b64 = ctxArg.slice(6);
11284
+ return JSON.parse(Buffer.from(b64, "base64").toString("utf-8"));
11285
+ }
11286
+ /**
10772
11287
  * Pull the first non-flag positional after the mode name.
10773
11288
  * (The mode itself is args[0] in the filtered set, so we skip index 0.)
10774
11289
  */
@@ -10796,8 +11311,8 @@ function getMultiFlag(args, name) {
10796
11311
  * case but is no longer consulted.
10797
11312
  */
10798
11313
  async function reportRun(command, rc, _raw, invocation, durationMs, outcome, slardar = {
10799
- scene: void 0,
10800
- profile: "standard",
11314
+ scene,
11315
+ profile,
10801
11316
  fix: false
10802
11317
  }) {
10803
11318
  console.error(`${command}: telemetry calling report_cli_run`);
@@ -10861,7 +11376,7 @@ async function main() {
10861
11376
  console.error(`${mode}: begin argv=[${args.join(" ")}] version=${getVersion()} traceId=${traceId ?? "-"} caller=${caller ?? "-"} runIdGenerated=${rc.generated}`);
10862
11377
  switch (mode) {
10863
11378
  case "check": {
10864
- const raw = await fetchCtxViaInnerApi({
11379
+ const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
10865
11380
  populate: planCtxPopulate({
10866
11381
  command: "check",
10867
11382
  profile
@@ -10886,7 +11401,7 @@ async function main() {
10886
11401
  break;
10887
11402
  }
10888
11403
  case "repair": {
10889
- const raw = await fetchCtxViaInnerApi({
11404
+ const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
10890
11405
  populate: planCtxPopulate({
10891
11406
  command: "repair",
10892
11407
  profile
@@ -10957,15 +11472,27 @@ async function main() {
10957
11472
  break;
10958
11473
  }
10959
11474
  case "reset":
10960
- if (args.includes("--async")) console.log(JSON.stringify(startAsyncReset()));
10961
- else if (args.includes("--worker")) {
11475
+ if (args.includes("--async")) {
11476
+ const ctxArg = args.find((a) => a.startsWith("--ctx="));
11477
+ let ctxBase64;
11478
+ if (ctxArg) ctxBase64 = ctxArg.slice(6);
11479
+ else {
11480
+ const fetched = await fetchCtxViaInnerApi({
11481
+ populate: planCtxPopulate({ command: "reset" }),
11482
+ caller,
11483
+ traceId
11484
+ });
11485
+ ctxBase64 = Buffer.from(JSON.stringify(fetched), "utf-8").toString("base64");
11486
+ }
11487
+ console.log(JSON.stringify(startAsyncReset(ctxBase64)));
11488
+ } else if (args.includes("--worker")) {
10962
11489
  const taskId = args.find((a) => a.startsWith("--task-id="))?.slice(10);
10963
11490
  if (!taskId) {
10964
11491
  console.error("Error: --task-id=<id> is required for worker");
10965
11492
  node_process.default.exit(1);
10966
11493
  }
10967
11494
  const resultFile = resetResultFile(taskId);
10968
- const raw = await fetchCtxViaInnerApi({
11495
+ const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
10969
11496
  populate: planCtxPopulate({ command: "reset" }),
10970
11497
  caller,
10971
11498
  traceId
@@ -10989,7 +11516,7 @@ async function main() {
10989
11516
  return;
10990
11517
  }
10991
11518
  } else {
10992
- console.error("Usage: reset --async | reset --worker --task-id=<id>");
11519
+ console.error("Usage: reset --async [--ctx=<base64>] | reset --worker --task-id=<id> [--ctx=<base64>]");
10993
11520
  node_process.default.exit(1);
10994
11521
  }
10995
11522
  break;
@@ -11005,14 +11532,14 @@ async function main() {
11005
11532
  case "install-openclaw": {
11006
11533
  const tag = getPositionalTag(args, "install-openclaw");
11007
11534
  if (!tag) {
11008
- console.error("Usage: install-openclaw <tag> [--oss_file_map=<base64>]");
11535
+ console.error("Usage: install-openclaw <tag> [--ctx=<base64> | --oss_file_map=<base64>]");
11009
11536
  node_process.default.exit(1);
11010
11537
  }
11011
11538
  const ossFileMapFlag = getFlag(args, "oss_file_map");
11012
11539
  let installOssFileMap;
11013
11540
  let rawForTelemetry;
11014
11541
  if (!ossFileMapFlag) {
11015
- rawForTelemetry = await fetchCtxViaInnerApi({
11542
+ rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11016
11543
  populate: planCtxPopulate({ command: "install" }),
11017
11544
  caller,
11018
11545
  traceId
@@ -11047,7 +11574,7 @@ async function main() {
11047
11574
  case "install-extension": {
11048
11575
  const tag = getPositionalTag(args, "install-extension");
11049
11576
  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>]");
11577
+ 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
11578
  node_process.default.exit(1);
11052
11579
  }
11053
11580
  const all = args.includes("--all");
@@ -11059,7 +11586,7 @@ async function main() {
11059
11586
  let installOssFileMap;
11060
11587
  let rawForTelemetry;
11061
11588
  if (!ossFileMapFlag) {
11062
- rawForTelemetry = await fetchCtxViaInnerApi({
11589
+ rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11063
11590
  populate: planCtxPopulate({ command: "install" }),
11064
11591
  caller,
11065
11592
  traceId
@@ -11105,12 +11632,12 @@ async function main() {
11105
11632
  case "install-cli": {
11106
11633
  const tag = getPositionalTag(args, "install-cli");
11107
11634
  if (!tag) {
11108
- console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--oss_file_map=<base64>]");
11635
+ console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--ctx=<base64> | --oss_file_map=<base64>]");
11109
11636
  node_process.default.exit(1);
11110
11637
  }
11111
11638
  const names = getMultiFlag(args, "cli");
11112
11639
  if (names.length === 0) {
11113
- console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--oss_file_map=<base64>]");
11640
+ console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--ctx=<base64> | --oss_file_map=<base64>]");
11114
11641
  node_process.default.exit(1);
11115
11642
  }
11116
11643
  const homeBase = getFlag(args, "home_base");
@@ -11118,7 +11645,7 @@ async function main() {
11118
11645
  let installOssFileMap;
11119
11646
  let rawForTelemetry;
11120
11647
  if (!ossFileMapFlag) {
11121
- rawForTelemetry = await fetchCtxViaInnerApi({
11648
+ rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11122
11649
  populate: planCtxPopulate({ command: "install" }),
11123
11650
  caller,
11124
11651
  traceId
@@ -11166,7 +11693,7 @@ async function main() {
11166
11693
  case "download-resource": {
11167
11694
  const tag = getPositionalTag(args, "download-resource");
11168
11695
  if (!tag) {
11169
- console.error("Usage: download-resource <tag> --role=<role> --name=<name> [--dir=<dir>] [--oss_file_map=<base64>]");
11696
+ console.error("Usage: download-resource <tag> --role=<role> --name=<name> [--dir=<dir>] [--ctx=<base64> | --oss_file_map=<base64>]");
11170
11697
  node_process.default.exit(1);
11171
11698
  }
11172
11699
  const role = getFlag(args, "role");
@@ -11180,7 +11707,7 @@ async function main() {
11180
11707
  let installOssFileMap;
11181
11708
  let rawForTelemetry;
11182
11709
  if (!ossFileMapFlag) {
11183
- rawForTelemetry = await fetchCtxViaInnerApi({
11710
+ rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11184
11711
  populate: planCtxPopulate({ command: "install" }),
11185
11712
  caller,
11186
11713
  traceId
@@ -11254,6 +11781,50 @@ async function main() {
11254
11781
  if (!result.ok) node_process.default.exit(1);
11255
11782
  break;
11256
11783
  }
11784
+ case "upgrade-lark": {
11785
+ const result = runUpgradeLark({
11786
+ runId: rc.runId,
11787
+ scene
11788
+ });
11789
+ const upgradeDurationMs = Date.now() - t0;
11790
+ console.log(JSON.stringify(result));
11791
+ reportUpgradeLarkToSlardar({
11792
+ scene,
11793
+ durationMs: upgradeDurationMs,
11794
+ success: result.ok,
11795
+ logFile: result.logFile,
11796
+ exitCode: result.exitCode,
11797
+ rollbackOk: result.rollbackOk,
11798
+ validationError: result.validationError,
11799
+ error: result.error
11800
+ });
11801
+ try {
11802
+ await reportCliRun({
11803
+ command: "upgrade-lark",
11804
+ runId: rc.runId,
11805
+ version: getVersion(),
11806
+ invocation: args.join(" "),
11807
+ durationMs: upgradeDurationMs,
11808
+ caller: rc.caller,
11809
+ traceId: rc.traceId,
11810
+ success: result.ok,
11811
+ result,
11812
+ error: result.ok ? void 0 : { message: result.error ?? "upgrade-lark failed" }
11813
+ });
11814
+ } catch (e) {
11815
+ console.error(`[telemetry] reportCliRun failed: ${e.message}`);
11816
+ }
11817
+ if (!result.ok) {
11818
+ node_process.default.exitCode = 1;
11819
+ return;
11820
+ }
11821
+ break;
11822
+ }
11823
+ case "channels-probe": {
11824
+ const result = runChannelsProbe(getFlag(args, "timeout") ? Number(getFlag(args, "timeout")) : void 0);
11825
+ console.log(JSON.stringify(result));
11826
+ break;
11827
+ }
11257
11828
  default:
11258
11829
  node_process.default.stderr.write(`Unknown command: ${mode}\n\n`);
11259
11830
  node_process.default.stderr.write(formatTopLevelHelp(helpFlags.expert));