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

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 +228 -765
  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.10";
55
+ return "0.1.14-alpha.11";
56
56
  }
57
57
  //#endregion
58
58
  //#region src/rule-engine/base.ts
@@ -3347,6 +3347,7 @@ 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;
3350
3351
  const ocCur = getOcVersion();
3351
3352
  if (!ocCur) return null;
3352
3353
  const installed = getInstalledPlugin(ctx);
@@ -3369,7 +3370,6 @@ let FeishuPluginOpenclawUpgradeRule = class FeishuPluginOpenclawUpgradeRule exte
3369
3370
  if (!cc) return { pass: true };
3370
3371
  const { ocCur, recommendedOc, installed, isLegacy } = cc;
3371
3372
  if (isForkPlugin(installed)) return validateForkPlugin(installed, ocCur, recommendedOc);
3372
- if (!recommendedOc) return { pass: true };
3373
3373
  if (resolveUpgradeDirection(installed, ocCur, recommendedOc, isLegacy) !== "openclaw") return { pass: true };
3374
3374
  return {
3375
3375
  pass: false,
@@ -3397,17 +3397,11 @@ 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 (!isLarkUpgradeNeededFromCC(cc)) return { pass: true };
3401
- const prefix = buildCompatPrefix(installed, ocCur, isLegacy);
3402
- if (!recommendedOc) return {
3403
- pass: false,
3404
- action: "upgrade_lark",
3405
- message: `${prefix};建议升级飞书插件至兼容版本`
3406
- };
3400
+ if (resolveUpgradeDirection(installed, ocCur, recommendedOc, isLegacy) !== "lark") return { pass: true };
3407
3401
  return {
3408
3402
  pass: false,
3409
3403
  action: "upgrade_lark",
3410
- message: `${prefix};当前 openclaw@${ocCur} 已达推荐版本,可直接升级飞书插件`
3404
+ message: `${buildCompatPrefix(installed, ocCur, isLegacy)};当前 openclaw@${ocCur} 已达推荐版本,可直接升级飞书插件`
3411
3405
  };
3412
3406
  }
3413
3407
  };
@@ -3419,18 +3413,6 @@ FeishuPluginLarkUpgradeRule = __decorate([Rule({
3419
3413
  level: "critical",
3420
3414
  usesVars: ["recommendedOpenclawTag"]
3421
3415
  })], FeishuPluginLarkUpgradeRule);
3422
- /**
3423
- * Core predicate: returns true when the lark plugin needs upgrading for a
3424
- * non-fork plugin, based on version compatibility with the current openclaw.
3425
- *
3426
- * Shared by FeishuPluginLarkUpgradeRule.validate and needsLarkUpgrade.
3427
- * Callers must handle fork plugin cases before invoking this function.
3428
- */
3429
- function isLarkUpgradeNeededFromCC(cc) {
3430
- const { ocCur, recommendedOc, installed, isLegacy } = cc;
3431
- if (!recommendedOc) return isLegacy || !isVersionCompatible(installed, ocCur);
3432
- return resolveUpgradeDirection(installed, ocCur, recommendedOc, isLegacy) === "lark";
3433
- }
3434
3416
  function isForkPlugin(p) {
3435
3417
  return p.scope != null && FORK_SCOPES.includes(p.scope);
3436
3418
  }
@@ -3469,16 +3451,14 @@ function describeCompatConstraint(entry, pluginVersion) {
3469
3451
  /**
3470
3452
  * @lark-apaas/openclaw-lark 豁免 VERSION_COMPAT_MAP,但仍要求 openclaw ≥ FORK_LARK_PLUGIN_MIN_OC_VERSION。
3471
3453
  * 其他 @lark-apaas scope 的 fork 插件继续无条件 pass。
3472
- * recommendedOc 可为 undefined(doctor 模式),此时只检测最低版本要求,不指定目标升级版本。
3473
3454
  */
3474
3455
  function validateForkPlugin(installed, ocCur, recommendedOc) {
3475
3456
  if (installed.fullName !== FORK_LARK_PLUGIN_FULL_NAME) return { pass: true };
3476
3457
  if (compareCalVer(ocCur, FORK_LARK_PLUGIN_MIN_OC_VERSION) >= 0) return { pass: true };
3477
- const recommendation = recommendedOc ? `;将 openclaw 升级到 ${recommendedOc} 即可满足` : `;请升级 openclaw 至 ${FORK_LARK_PLUGIN_MIN_OC_VERSION} 或更高版本`;
3478
3458
  return {
3479
3459
  pass: false,
3480
3460
  action: "upgrade_openclaw",
3481
- message: `飞书插件 ${describePlugin(installed)}(fork 版)要求 openclaw ≥ ${FORK_LARK_PLUGIN_MIN_OC_VERSION},当前 openclaw@${ocCur} 低于此要求${recommendation}`
3461
+ message: `飞书插件 ${describePlugin(installed)}(fork 版)要求 openclaw ≥ ${FORK_LARK_PLUGIN_MIN_OC_VERSION},当前 openclaw@${ocCur} 低于此要求;将 openclaw 升级到 ${recommendedOc} 即可满足`
3482
3462
  };
3483
3463
  }
3484
3464
  function describePlugin(p) {
@@ -3525,187 +3505,6 @@ function extractScopedNameFromSpec$1(spec) {
3525
3505
  const at = spec.indexOf("@", 1);
3526
3506
  return at === -1 ? spec : spec.slice(0, at);
3527
3507
  }
3528
- /**
3529
- * Returns true if the installed feishu plugin is version-incompatible with
3530
- * the current openclaw (or is a legacy plugin that must be replaced).
3531
- * Used by the upgrade_lark_needed rule and the upgrade-lark pre-check gate.
3532
- */
3533
- function needsLarkUpgrade(ctx) {
3534
- const cc = resolveCompatContext(ctx);
3535
- if (!cc) return false;
3536
- const { ocCur, recommendedOc, installed } = cc;
3537
- if (isForkPlugin(installed)) {
3538
- if (recommendedOc) return false;
3539
- if (installed.fullName === FORK_LARK_PLUGIN_FULL_NAME) return compareCalVer(ocCur, FORK_LARK_PLUGIN_MIN_OC_VERSION) < 0;
3540
- return false;
3541
- }
3542
- return isLarkUpgradeNeededFromCC(cc);
3543
- }
3544
- //#endregion
3545
- //#region src/channels-probe.ts
3546
- const FEISHU_INVALID_CONFIG_MSG = "channels.feishu: invalid config: must NOT have additional properties";
3547
- const CHANNEL_LINE_RE = /^-\s+Feishu\s+([^:]+):\s+(.+)$/;
3548
- /**
3549
- * Port of Python `_account_is_working` from the feishu-channel-success-rate skill.
3550
- *
3551
- * Strips colon-prefixed key:value bits (dm:, bot:, in:, out:, token:, allow:,
3552
- * intents:, groups:, health:) and evaluates the canonical health formula.
3553
- */
3554
- function accountIsWorking(bits) {
3555
- const bitTokens = /* @__PURE__ */ new Set();
3556
- let hasError = false;
3557
- let hasProbeFailed = false;
3558
- for (const raw of bits) {
3559
- const b = raw.trim();
3560
- if (!b) continue;
3561
- if (b.startsWith("error:")) {
3562
- hasError = true;
3563
- continue;
3564
- }
3565
- if (b === "probe failed") {
3566
- hasProbeFailed = true;
3567
- continue;
3568
- }
3569
- bitTokens.add(b.split(":")[0]);
3570
- }
3571
- if (!bitTokens.has("enabled") || !bitTokens.has("configured")) return false;
3572
- if (bitTokens.has("works")) return true;
3573
- if (bitTokens.has("running") && !hasError && !hasProbeFailed) return true;
3574
- return false;
3575
- }
3576
- /**
3577
- * Parse the raw stdout of `openclaw channels status --probe`.
3578
- * Port of Python `extract_channels_probe` from the feishu-channel-success-rate skill.
3579
- */
3580
- function parseChannelsProbeOutput(text) {
3581
- const gatewayReachable = text.includes("Gateway reachable");
3582
- const feishuConfigInvalid = text.includes(FEISHU_INVALID_CONFIG_MSG);
3583
- const accounts = [];
3584
- let anyAccountWorking = false;
3585
- for (const line of text.split("\n")) {
3586
- const m = CHANNEL_LINE_RE.exec(line.trim());
3587
- if (!m) continue;
3588
- const [, acct, rest] = m;
3589
- const bits = rest.split(",").map((b) => b.trim());
3590
- const isWorking = accountIsWorking(bits);
3591
- if (isWorking) anyAccountWorking = true;
3592
- accounts.push({
3593
- id: acct.trim(),
3594
- bits,
3595
- isWorking,
3596
- raw: line.trim()
3597
- });
3598
- }
3599
- return {
3600
- gatewayReachable,
3601
- feishuConfigInvalid,
3602
- accounts,
3603
- anyAccountWorking
3604
- };
3605
- }
3606
- /**
3607
- * Run `openclaw channels status --probe` and return a structured result.
3608
- *
3609
- * The command may exit non-zero when some bot accounts fail their probe — that
3610
- * is still useful output. We therefore try to parse stdout even when the
3611
- * process exits with a non-zero code, falling back to an unavailable result
3612
- * only when there is genuinely no output to parse.
3613
- *
3614
- * @param timeoutMs Maximum wait time. Default is 60 s because v2026.4.x
3615
- * lacks a per-request HTTP timeout and can block indefinitely.
3616
- */
3617
- function runChannelsProbe(timeoutMs = 6e4) {
3618
- let stdout = "";
3619
- let stderrText = "";
3620
- let execError;
3621
- try {
3622
- stdout = (0, node_child_process.execSync)("openclaw channels status --probe", {
3623
- encoding: "utf-8",
3624
- timeout: timeoutMs,
3625
- stdio: [
3626
- "ignore",
3627
- "pipe",
3628
- "pipe"
3629
- ]
3630
- });
3631
- } catch (e) {
3632
- const err = e;
3633
- const stdoutRaw = err.stdout;
3634
- stdout = typeof stdoutRaw === "string" ? stdoutRaw : stdoutRaw?.toString("utf-8") ?? "";
3635
- execError = err.message;
3636
- const stderrRaw = err.stderr;
3637
- stderrText = (typeof stderrRaw === "string" ? stderrRaw : stderrRaw?.toString("utf-8") ?? "").trim();
3638
- if (stderrText) console.error(`channels-probe: stderr from CLI: ${stderrText}`);
3639
- }
3640
- if (stdout.trim()) return {
3641
- available: true,
3642
- ...parseChannelsProbeOutput(stdout)
3643
- };
3644
- return {
3645
- available: false,
3646
- gatewayReachable: false,
3647
- feishuConfigInvalid: stderrText.includes(FEISHU_INVALID_CONFIG_MSG),
3648
- accounts: [],
3649
- anyAccountWorking: false,
3650
- error: execError ?? "no output from openclaw channels status --probe"
3651
- };
3652
- }
3653
- //#endregion
3654
- //#region src/rules/upgrade-lark-needed.ts
3655
- /**
3656
- * Detects the condition that warrants running `upgrade-lark`:
3657
- * - feishu plugin version incompatible with current openclaw, OR
3658
- * - openclaw channels status --probe reports feishu channel config invalid; AND
3659
- * - channels are not working.
3660
- *
3661
- * Both conditions must be true simultaneously. If version is compatible and
3662
- * feishu config is valid, or channels are working, the rule passes (no action needed).
3663
- *
3664
- * feishuConfigInvalid is read from the channels probe output rather than running a
3665
- * separate `openclaw status` call, since only `openclaw channels status --probe`
3666
- * reliably surfaces the schema validation error.
3667
- *
3668
- * profile: experimental — runs only in full sweep mode, not in standard doctor.
3669
- * level: silent — telemetry/sweep-only, does not trigger page-level repair UI.
3670
- */
3671
- let UpgradeLarkNeededRule = class UpgradeLarkNeededRule extends DiagnoseRule {
3672
- validate(ctx) {
3673
- let versionIncompatible = false;
3674
- try {
3675
- versionIncompatible = needsLarkUpgrade(ctx);
3676
- } catch {
3677
- versionIncompatible = true;
3678
- }
3679
- let probeResult;
3680
- try {
3681
- probeResult = runChannelsProbe(6e4);
3682
- } catch {
3683
- probeResult = {
3684
- available: false,
3685
- gatewayReachable: false,
3686
- feishuConfigInvalid: false,
3687
- accounts: [],
3688
- anyAccountWorking: false
3689
- };
3690
- }
3691
- const feishuConfigInvalid = probeResult.feishuConfigInvalid;
3692
- if (!(versionIncompatible || feishuConfigInvalid)) return { pass: true };
3693
- if (probeResult.anyAccountWorking) return { pass: true };
3694
- return {
3695
- pass: false,
3696
- action: "upgrade_lark",
3697
- message: `飞书插件需要升级且 channels 不可用(版本不兼容=${versionIncompatible}, feishu配置无效=${feishuConfigInvalid}),建议执行 upgrade-lark 命令升级飞书插件`
3698
- };
3699
- }
3700
- };
3701
- UpgradeLarkNeededRule = __decorate([Rule({
3702
- key: "upgrade_lark_needed",
3703
- description: "检测飞书插件版本不兼容且 channels 不可用,判断是否需要执行 upgrade-lark 升级",
3704
- repairMode: "check-only",
3705
- level: "silent",
3706
- profile: "experimental",
3707
- usesVars: ["recommendedOpenclawTag"]
3708
- })], UpgradeLarkNeededRule);
3709
3508
  //#endregion
3710
3509
  //#region src/rules/cleanup-install-backup-dirs.ts
3711
3510
  const DIR_PREFIX = ".openclaw-install-";
@@ -3851,6 +3650,117 @@ LarkCliMissingForInstalledLarkPluginRule = __decorate([Rule({
3851
3650
  usesVars: ["recommendedOpenclawTag"]
3852
3651
  })], LarkCliMissingForInstalledLarkPluginRule);
3853
3652
  //#endregion
3653
+ //#region src/rules/feishu-bot-channel-config.ts
3654
+ /**
3655
+ * Ensures each bot account's channel config is correct:
3656
+ * 1. `allowFrom` contains its own `creatorOpenID` from larkApps
3657
+ * 2. `appSecret` is either the canonical provider-ref or matches larkApps plaintext
3658
+ *
3659
+ * Covers both multi-account (channels.feishu.accounts) and single-account
3660
+ * (channels.feishu.appId + allowFrom at top level) layouts.
3661
+ */
3662
+ let FeishuBotChannelConfigRule = class FeishuBotChannelConfigRule extends DiagnoseRule {
3663
+ validate(ctx) {
3664
+ const larkApps = ctx.vars.larkApps;
3665
+ if (!larkApps || larkApps.length === 0) return { pass: true };
3666
+ const feishu = asRecord(getNestedMap(ctx.config, "channels", "feishu"));
3667
+ if (!feishu) return { pass: true };
3668
+ const issues = [];
3669
+ const accounts = asRecord(feishu.accounts);
3670
+ if (accounts) for (const [accountId, account] of Object.entries(accounts)) {
3671
+ const bot = asRecord(account);
3672
+ if (!bot) continue;
3673
+ const appId = bot.appId;
3674
+ if (typeof appId !== "string" || !appId.startsWith("cli_")) continue;
3675
+ const larkApp = larkApps.find((e) => e.larkAppID === appId);
3676
+ if (!larkApp) continue;
3677
+ this.checkBot(accountId, bot, larkApp, issues);
3678
+ }
3679
+ const singleAppId = feishu.appId;
3680
+ if (typeof singleAppId === "string" && singleAppId.startsWith("cli_") && !accounts) {
3681
+ const larkApp = larkApps.find((e) => e.larkAppID === singleAppId);
3682
+ if (larkApp) this.checkBot("feishu", feishu, larkApp, issues);
3683
+ }
3684
+ if (issues.length === 0) return { pass: true };
3685
+ return {
3686
+ pass: false,
3687
+ message: issues.join("; ")
3688
+ };
3689
+ }
3690
+ /** Check a single bot entry (either an account object or the feishu channel itself).
3691
+ * appSecret is validated based on its current type:
3692
+ * - object → must match canonical provider-ref
3693
+ * - string → must match larkApps plaintext
3694
+ */
3695
+ checkBot(label, bot, larkApp, issues) {
3696
+ const creatorOpenID = larkApp.creatorOpenID;
3697
+ const allowFrom = Array.isArray(bot.allowFrom) ? bot.allowFrom : [];
3698
+ if (typeof creatorOpenID === "string" && creatorOpenID !== "") {
3699
+ if (!allowFrom.includes(creatorOpenID)) issues.push(`${label} allowFrom missing creatorOpenID ${creatorOpenID.length > 8 ? creatorOpenID.slice(0, 4) + "***" + creatorOpenID.slice(-4) : "***"}`);
3700
+ } else if (allowFrom.length === 0) issues.push(`${label} allowFrom is empty (creatorOpenID unavailable, cannot auto-fix)`);
3701
+ const secret = bot.appSecret;
3702
+ if (typeof secret === "object" && secret !== null && !Array.isArray(secret)) {
3703
+ if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) issues.push(`${label} appSecret is a provider-ref but not the canonical one`);
3704
+ } else if (typeof secret === "string") {
3705
+ if (secret !== larkApp.appSecret) issues.push(`${label} appSecret plaintext mismatch`);
3706
+ } else issues.push(`${label} appSecret has unexpected type ${typeof secret}`);
3707
+ }
3708
+ repair(ctx) {
3709
+ const larkApps = ctx.vars.larkApps;
3710
+ if (!larkApps || larkApps.length === 0) return;
3711
+ const feishu = asRecord(getNestedMap(ctx.config, "channels", "feishu"));
3712
+ if (!feishu) return;
3713
+ const accounts = asRecord(feishu.accounts);
3714
+ if (accounts) for (const [, account] of Object.entries(accounts)) {
3715
+ const bot = asRecord(account);
3716
+ if (!bot) continue;
3717
+ const appId = bot.appId;
3718
+ if (typeof appId !== "string" || !appId.startsWith("cli_")) continue;
3719
+ const larkApp = larkApps.find((e) => e.larkAppID === appId);
3720
+ if (!larkApp) continue;
3721
+ this.fixBot(bot, larkApp);
3722
+ }
3723
+ const singleAppId = feishu.appId;
3724
+ if (typeof singleAppId === "string" && singleAppId.startsWith("cli_") && !accounts) {
3725
+ const larkApp = larkApps.find((e) => e.larkAppID === singleAppId);
3726
+ if (larkApp) this.fixBot(feishu, larkApp);
3727
+ }
3728
+ }
3729
+ /** Fix a single bot entry in-place.
3730
+ * appSecret is repaired based on its current type:
3731
+ * - object → fix to canonical provider-ref
3732
+ * - string → fix to larkApps plaintext
3733
+ */
3734
+ fixBot(bot, larkApp) {
3735
+ const creatorOpenID = larkApp.creatorOpenID;
3736
+ if (typeof creatorOpenID === "string" && creatorOpenID !== "") {
3737
+ const allowFrom = Array.isArray(bot.allowFrom) ? [...bot.allowFrom] : [];
3738
+ if (!allowFrom.includes(creatorOpenID)) {
3739
+ allowFrom.push(creatorOpenID);
3740
+ bot.allowFrom = allowFrom;
3741
+ }
3742
+ }
3743
+ const secret = bot.appSecret;
3744
+ if (typeof secret === "object" && secret !== null && !Array.isArray(secret)) {
3745
+ if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) bot.appSecret = { ...DEFAULT_FEISHU_APP_SECRET };
3746
+ } else if (typeof secret === "string") {
3747
+ if (secret !== larkApp.appSecret) bot.appSecret = larkApp.appSecret;
3748
+ }
3749
+ }
3750
+ };
3751
+ FeishuBotChannelConfigRule = __decorate([Rule({
3752
+ key: "feishu_bot_channel_config",
3753
+ description: "确保飞书配置中 bot 账号的 allowFrom 包含其创建者 openID 且 appSecret 值正确",
3754
+ dependsOn: [
3755
+ "config_syntax_check",
3756
+ "feishu_default_account",
3757
+ "feishu_bot_id"
3758
+ ],
3759
+ repairMode: "standard",
3760
+ usesVars: ["larkApps"],
3761
+ level: "critical"
3762
+ })], FeishuBotChannelConfigRule);
3763
+ //#endregion
3854
3764
  //#region src/check.ts
3855
3765
  /** Telemetry-aware entry: returns both the legacy CheckResult (for stdout)
3856
3766
  * AND a DoctorReport-shape payload (for `openclaw.report_cli_run`). The
@@ -4314,9 +4224,6 @@ const PROVIDER_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-provider-
4314
4224
  const SECRETS_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-openclaw-secrets.json";
4315
4225
  /** Absolute path to the openclaw config JSON. */
4316
4226
  const CONFIG_PATH = `${WORKSPACE_DIR}/openclaw.json`;
4317
- function upgradeLarkLogFile(runId) {
4318
- return `${DIAGNOSE_DIR}/upgrade-lark-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/:/g, "-")}-${runId.slice(0, 8)}.log`;
4319
- }
4320
4227
  //#endregion
4321
4228
  //#region src/run-log.ts
4322
4229
  let currentRunContext;
@@ -4453,9 +4360,10 @@ function makeLogger(logFile) {
4453
4360
  /**
4454
4361
  * Start an async reset task: spawn a detached child process and return the taskId.
4455
4362
  *
4456
- * The child process runs: node cli.js reset --worker --task-id=xxx --ctx=base64
4363
+ * The child process runs: node cli.js reset --worker --task-id=xxx
4364
+ * The worker fetches ctx from innerApi itself — no --ctx passthrough.
4457
4365
  */
4458
- function startAsyncReset(ctxBase64) {
4366
+ function startAsyncReset() {
4459
4367
  const taskId = (0, node_crypto.randomUUID)();
4460
4368
  const resultFile = resetResultFile(taskId);
4461
4369
  const log = makeLogger(resetLogFile(taskId));
@@ -4479,8 +4387,7 @@ function startAsyncReset(ctxBase64) {
4479
4387
  process.argv[1],
4480
4388
  "reset",
4481
4389
  "--worker",
4482
- `--task-id=${taskId}`,
4483
- `--ctx=${ctxBase64}`
4390
+ `--task-id=${taskId}`
4484
4391
  ], {
4485
4392
  detached: true,
4486
4393
  stdio: "ignore",
@@ -6994,6 +6901,60 @@ function mergeCoreBackupAndOrigins(configPath, vars, resetData, log) {
6994
6901
  log(`allowedOrigins: added ${added.length} (${JSON.stringify(added)}), total now ${mergedOrigins.length}`);
6995
6902
  }
6996
6903
  /**
6904
+ * Fix bot account allowFrom and appSecret using larkApps from innerApi.
6905
+ *
6906
+ * For each bot account (key starts with `bot-cli_`):
6907
+ * - allowFrom must contain the bot's own creatorOpenID from larkApps
6908
+ * - appSecret must be either the canonical provider-ref or match larkApps plaintext
6909
+ *
6910
+ * Runs after mergeCoreBackupAndOrigins so it operates on the final config state.
6911
+ */
6912
+ function fixBotChannelConfig(configPath, larkApps, log) {
6913
+ if (!larkApps || larkApps.length === 0) {
6914
+ log("no larkApps data, skip bot channel config fix");
6915
+ return;
6916
+ }
6917
+ const config = loadJSON5().parse(node_fs.default.readFileSync(configPath, "utf-8"));
6918
+ const accounts = asRecord(getNestedMap(config, "channels", "feishu")?.accounts);
6919
+ if (!accounts) {
6920
+ log("no feishu accounts in config, skip bot channel config fix");
6921
+ return;
6922
+ }
6923
+ let fixCount = 0;
6924
+ for (const [, account] of Object.entries(accounts)) {
6925
+ const bot = asRecord(account);
6926
+ if (!bot) continue;
6927
+ const appId = bot.appId;
6928
+ if (typeof appId !== "string" || !appId.startsWith("cli_")) continue;
6929
+ const larkApp = larkApps.find((e) => e.larkAppID === appId);
6930
+ if (!larkApp) continue;
6931
+ const creatorOpenID = larkApp.creatorOpenID;
6932
+ if (typeof creatorOpenID === "string" && creatorOpenID !== "") {
6933
+ const allowFrom = Array.isArray(bot.allowFrom) ? [...bot.allowFrom] : [];
6934
+ if (!allowFrom.includes(creatorOpenID)) {
6935
+ allowFrom.push(creatorOpenID);
6936
+ bot.allowFrom = allowFrom;
6937
+ fixCount++;
6938
+ }
6939
+ }
6940
+ const secret = bot.appSecret;
6941
+ let needsFix = false;
6942
+ if (typeof secret === "object" && secret !== null && !Array.isArray(secret)) {
6943
+ if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) needsFix = true;
6944
+ } else if (typeof secret === "string") {
6945
+ if (secret !== larkApp.appSecret) needsFix = true;
6946
+ } else needsFix = true;
6947
+ if (needsFix) {
6948
+ bot.appSecret = { ...DEFAULT_FEISHU_APP_SECRET };
6949
+ fixCount++;
6950
+ }
6951
+ }
6952
+ if (fixCount > 0) {
6953
+ node_fs.default.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
6954
+ log(`fixed ${fixCount} bot channel config issue(s) (allowFrom/appSecret)`);
6955
+ } else log("bot channel config ok, no fixes needed");
6956
+ }
6957
+ /**
6997
6958
  * Step 7: Verify startup scripts landed in configDir/scripts/.
6998
6959
  *
6999
6960
  * Scripts are extracted directly to configDir/scripts/ during stageTemplate —
@@ -7138,6 +7099,7 @@ async function runReset(input, taskId, resultFile) {
7138
7099
  await step5InstallOpenclaw(openclawTag, ossFileMap, log);
7139
7100
  step(6);
7140
7101
  mergeCoreBackupAndOrigins(configPath, vars, resetData, log);
7102
+ fixBotChannelConfig(configPath, vars.larkApps, log);
7141
7103
  step(7);
7142
7104
  verifyStartupScripts(configDir, log);
7143
7105
  step(8);
@@ -7936,7 +7898,8 @@ function normalizeCtx(raw) {
7936
7898
  reset: {
7937
7899
  templateVars: r.reset.templateVars ?? {},
7938
7900
  coreBackup: r.reset.coreBackup
7939
- }
7901
+ },
7902
+ larkApps: Array.isArray(r.larkApps) ? r.larkApps : []
7940
7903
  };
7941
7904
  }
7942
7905
  const vars = r.vars ?? {};
@@ -7961,7 +7924,8 @@ function normalizeCtx(raw) {
7961
7924
  reset: {
7962
7925
  templateVars: resetData.templateVars ?? {},
7963
7926
  coreBackup: resetData.coreBackup
7964
- }
7927
+ },
7928
+ larkApps: Array.isArray(r.larkApps) ? r.larkApps : []
7965
7929
  };
7966
7930
  }
7967
7931
  function fillApp(src) {
@@ -8026,7 +7990,8 @@ function buildCheckInput(raw, configPathOverride) {
8026
7990
  providerFilePath: PROVIDER_FILE_PATH,
8027
7991
  secretsFilePath: SECRETS_FILE_PATH,
8028
7992
  templateVars: ctx.app.templateVars,
8029
- recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
7993
+ recommendedOpenclawTag: ctx.app.recommendedOpenclawTag,
7994
+ larkApps: ctx.larkApps
8030
7995
  },
8031
7996
  templateVars: ctx.app.templateVars
8032
7997
  };
@@ -8058,7 +8023,8 @@ function buildRepairInput(raw, configPathOverride) {
8058
8023
  providerFilePath: PROVIDER_FILE_PATH,
8059
8024
  secretsFilePath: SECRETS_FILE_PATH,
8060
8025
  templateVars: ctx.app.templateVars,
8061
- recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
8026
+ recommendedOpenclawTag: ctx.app.recommendedOpenclawTag,
8027
+ larkApps: ctx.larkApps
8062
8028
  },
8063
8029
  repairData: {
8064
8030
  secretsContent: ctx.secrets.secretsContent,
@@ -8094,7 +8060,8 @@ function buildResetInput(raw, configPathOverride) {
8094
8060
  providerFilePath: PROVIDER_FILE_PATH,
8095
8061
  secretsFilePath: SECRETS_FILE_PATH,
8096
8062
  templateVars: ctx.app.templateVars,
8097
- recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
8063
+ recommendedOpenclawTag: ctx.app.recommendedOpenclawTag,
8064
+ larkApps: ctx.larkApps
8098
8065
  },
8099
8066
  resetData: {
8100
8067
  templateVars: ctx.reset.templateVars,
@@ -10404,7 +10371,7 @@ async function reportCliRun(opts) {
10404
10371
  //#region src/help.ts
10405
10372
  const BIN = "mclaw-diagnose";
10406
10373
  function versionBanner() {
10407
- return `v0.1.14-alpha.10`;
10374
+ return `v0.1.14-alpha.11`;
10408
10375
  }
10409
10376
  const COMMANDS = [
10410
10377
  {
@@ -10508,16 +10475,12 @@ EXIT CODES
10508
10475
  hidden: true,
10509
10476
  summary: "Run rule-engine check only",
10510
10477
  help: `USAGE
10511
- ${BIN} check [--ctx=<base64>]
10478
+ ${BIN} check
10512
10479
 
10513
10480
  DESCRIPTION
10514
10481
  Runs the rule engine against the sandbox's current openclaw config and
10515
- returns { failedRules }. Used by sandbox_console's push-style callers
10516
- that already own the ctx — end-users should prefer \`doctor\`.
10517
-
10518
- OPTIONS
10519
- --ctx=<base64> Opaque ctx JSON (base64). When absent, fetched from
10520
- innerapi (same path as doctor).
10482
+ returns { failedRules }. Ctx is fetched from innerapi automatically.
10483
+ End-users should prefer \`doctor\`.
10521
10484
  `
10522
10485
  },
10523
10486
  {
@@ -10525,16 +10488,11 @@ OPTIONS
10525
10488
  hidden: true,
10526
10489
  summary: "Apply standard-mode repairs",
10527
10490
  help: `USAGE
10528
- ${BIN} repair [--ctx=<base64>]
10491
+ ${BIN} repair
10529
10492
 
10530
10493
  DESCRIPTION
10531
- Runs repair for the failing rules listed inside the ctx's repairData.
10532
- Intended for sandbox_console's push path — end-users should use
10533
- \`doctor --fix\` instead.
10534
-
10535
- OPTIONS
10536
- --ctx=<base64> Opaque ctx JSON (base64). When absent, fetched from
10537
- innerapi.
10494
+ Runs repair for the failing rules. Ctx is fetched from innerapi
10495
+ automatically. End-users should use \`doctor --fix\` instead.
10538
10496
  `
10539
10497
  },
10540
10498
  {
@@ -10542,14 +10500,15 @@ OPTIONS
10542
10500
  hidden: true,
10543
10501
  summary: "Re-initialize sandbox via the 9-step reset pipeline",
10544
10502
  help: `USAGE
10545
- ${BIN} reset --async [--ctx=<base64>]
10546
- ${BIN} reset --worker --task-id=<id> [--ctx=<base64>]
10503
+ ${BIN} reset --async
10504
+ ${BIN} reset --worker --task-id=<id>
10547
10505
 
10548
10506
  DESCRIPTION
10549
10507
  Two-phase pipeline driven asynchronously: the --async invocation spawns
10550
10508
  a detached worker and returns { taskId } immediately; the --worker
10551
10509
  invocation (spawned by --async) runs the actual 9 steps and writes
10552
10510
  progress to /tmp/openclaw-diagnose/reset-<taskId>.json.
10511
+ Ctx is fetched from innerapi automatically.
10553
10512
 
10554
10513
  Poll progress with \`${BIN} get_reset_task --task-id=<id>\`.
10555
10514
 
@@ -10557,7 +10516,6 @@ OPTIONS
10557
10516
  --async Start a detached worker and return taskId on stdout.
10558
10517
  --worker Internal — run the 9-step pipeline (launched by --async).
10559
10518
  --task-id=<id> Required with --worker; identifies the progress file.
10560
- --ctx=<base64> Opaque ctx JSON; fetched from innerapi when absent.
10561
10519
  `
10562
10520
  },
10563
10521
  {
@@ -10580,7 +10538,7 @@ OPTIONS
10580
10538
  hidden: true,
10581
10539
  summary: "Download + install the openclaw tarball",
10582
10540
  help: `USAGE
10583
- ${BIN} install-openclaw <tag> [--ctx=<base64> | --oss_file_map=<base64>]
10541
+ ${BIN} install-openclaw <tag> [--oss_file_map=<base64>]
10584
10542
 
10585
10543
  DESCRIPTION
10586
10544
  Downloads the openclaw@<tag> tgz via the signed OSS URL found in the
@@ -10592,9 +10550,9 @@ ARGUMENTS
10592
10550
  <tag> Openclaw version tag, e.g. 2026.4.11.
10593
10551
 
10594
10552
  OPTIONS
10595
- --ctx=<base64> Opaque ctx; ossFileMap is extracted from it.
10596
10553
  --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi
10597
- entirely. Wins over --ctx when both provided.
10554
+ entirely. When absent, ossFileMap is fetched from
10555
+ innerapi automatically.
10598
10556
  `
10599
10557
  },
10600
10558
  {
@@ -10620,8 +10578,7 @@ OPTIONS
10620
10578
  --home_base=<dir> Override the /home/gem base (tests).
10621
10579
  --config_path=<p> Override the openclaw.json path (tests).
10622
10580
  --skip-config-update Leave plugins.installs in openclaw.json untouched.
10623
- --ctx=<base64> Opaque ctx; see install-openclaw for semantics.
10624
- --oss_file_map=... Pre-built OSS URL map (base64 JSON).
10581
+ --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi.
10625
10582
  `
10626
10583
  },
10627
10584
  {
@@ -10648,7 +10605,6 @@ OPTIONS
10648
10605
  --cli=<name> CLI package to install by short name or scoped
10649
10606
  packageName (repeatable, at least one required).
10650
10607
  --home_base=<dir> Override the /home/gem base (tests).
10651
- --ctx=<base64> Opaque ctx; ossFileMap is extracted from it.
10652
10608
  --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi.
10653
10609
 
10654
10610
  EXAMPLES
@@ -10702,46 +10658,6 @@ OPTIONS
10702
10658
  EXIT CODES
10703
10659
  0 Success or skipped (prerequisites not met).
10704
10660
  1 Secret/path unresolvable, lark-cli failed, or config unreadable.
10705
- `
10706
- },
10707
- {
10708
- name: "upgrade-lark",
10709
- hidden: false,
10710
- summary: "Upgrade the Feishu/Lark plugin via @larksuite/openclaw-lark-tools",
10711
- help: `USAGE
10712
- ${BIN} upgrade-lark [--scene=<scene>] [--caller=<n>] [--trace-id=<id>]
10713
-
10714
- DESCRIPTION
10715
- Upgrades the Feishu/Lark plugin by running:
10716
- npx -y @larksuite/openclaw-lark-tools update --use-existing
10717
-
10718
- Before the upgrade, the following files are backed up:
10719
- - openclaw.json
10720
- - extensions/openclaw-lark/ (if present)
10721
- - extensions/feishu-openclaw-plugin/ (if present)
10722
- After the upgrade, the result is validated:
10723
- - feishu.accounts bot count must not decrease
10724
- - gateway config structure must remain valid (port/mode/bind/auth/trustedProxies)
10725
- If the upgrade command fails, or validation fails, the backed-up files are
10726
- restored to roll back the changes.
10727
-
10728
- Execution is logged to /tmp/openclaw-diagnose/upgrade-lark-<runId>.log.
10729
-
10730
- Output is a single JSON object on stdout:
10731
- { "ok": true, "stdout": "...", "stderr": "...", "logFile": "..." }
10732
- { "ok": false, "error": "...", "stderr": "...", "exitCode": 1,
10733
- "rollbackOk": true, "validationError": "...", "logFile": "..." }
10734
-
10735
- OPTIONS
10736
- --scene=<scene> Telemetry label forwarded to Slardar only.
10737
- Known values: PageUpgradeLark, etc. Custom strings accepted.
10738
- --caller=<name> Optional metadata passed to innerapi.
10739
- --trace-id=<id> Optional log-correlation id.
10740
-
10741
- EXIT CODES
10742
- 0 Success: upgrade ran and all validations passed.
10743
- 1 Failure: npx error, validation failed, or git commit failed.
10744
- File rollback was attempted (see rollbackOk in the JSON output).
10745
10661
  `
10746
10662
  },
10747
10663
  {
@@ -10775,41 +10691,6 @@ EXAMPLES
10775
10691
  ${BIN} rules # all rules
10776
10692
  ${BIN} rules --rule=gateway # single rule
10777
10693
  ${BIN} rules --rule=gateway --rule=feishu_channel # multiple rules
10778
- `
10779
- },
10780
- {
10781
- name: "channels-probe",
10782
- hidden: true,
10783
- summary: "Check feishu channel health via openclaw channels status --probe",
10784
- help: `USAGE
10785
- ${BIN} channels-probe [--timeout=<ms>]
10786
-
10787
- DESCRIPTION
10788
- Runs \`openclaw channels status --probe\` and returns a structured JSON
10789
- summary of whether the current environment's feishu channels are
10790
- configured and working correctly.
10791
-
10792
- Output:
10793
- {
10794
- "available": true,
10795
- "gatewayReachable": true,
10796
- "accounts": [
10797
- { "id": "default", "bits": ["enabled","configured","running","works"],
10798
- "isWorking": true, "raw": "- Feishu default: ..." }
10799
- ],
10800
- "anyAccountWorking": true
10801
- }
10802
-
10803
- An account is considered working when:
10804
- enabled ∧ configured ∧ ( works ∨ ( running ∧ no error: ∧ no probe failed ) )
10805
-
10806
- "available": false means the CLI invocation itself failed (openclaw not
10807
- found, gateway unreachable, or no parseable output returned).
10808
-
10809
- OPTIONS
10810
- --timeout=<ms> Max wait in milliseconds (default: 60000). The probe
10811
- can hang indefinitely on openclaw v2026.4.x due to a
10812
- missing per-request HTTP timeout — set this accordingly.
10813
10694
  `
10814
10695
  },
10815
10696
  {
@@ -10830,8 +10711,7 @@ OPTIONS
10830
10711
  --role=<role> Package role (e.g. template, config).
10831
10712
  --name=<name> Package name within the role.
10832
10713
  --dir=<dir> Target dir (defaults to dirname(pkg.installPath)).
10833
- --ctx=<base64> Opaque ctx; ossFileMap is extracted from it.
10834
- --oss_file_map=... Pre-built OSS URL map (base64 JSON).
10714
+ --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi.
10835
10715
  `
10836
10716
  }
10837
10717
  ];
@@ -10907,31 +10787,31 @@ function planVarsFields(opts = {}) {
10907
10787
  *
10908
10788
  * Per-command group needs:
10909
10789
  *
10910
- * doctor / check app (rule-driven)
10911
- * repair app + secrets (writes secretsContent / providerKeyContent)
10912
- * reset app + secrets + install + reset (the works)
10790
+ * doctor / check app + larkApps
10791
+ * repair app + secrets + larkApps
10792
+ * reset app + secrets + install + reset + larkApps
10913
10793
  * install-* install only
10914
10794
  *
10915
10795
  * Empty result (`{}`) means "no group needed" — the CLI can skip the
10916
10796
  * `fetchCtxViaInnerApi` call entirely and run with a synthetic empty ctx.
10917
- * Happens e.g. when the user pinned `--rule=<key>` to a vars-free rule on
10918
- * `doctor`.
10919
10797
  */
10920
10798
  function planCtxPopulate(opts) {
10921
10799
  if (opts.command === "install") return { install: true };
10922
10800
  const populate = {};
10923
- const appFields = planVarsFields({
10801
+ if (planVarsFields({
10924
10802
  disabled: opts.disabled,
10925
10803
  onlyRules: opts.onlyRules,
10926
10804
  profile: opts.profile
10927
- });
10928
- if (appFields.length > 0) populate.app = appFields;
10929
- if (opts.command === "repair") populate.secrets = true;
10930
- else if (opts.command === "reset") {
10805
+ }).length > 0) populate.app = true;
10806
+ if (opts.command === "repair") {
10807
+ populate.secrets = true;
10808
+ populate.larkApps = true;
10809
+ } else if (opts.command === "reset") {
10931
10810
  populate.secrets = true;
10932
10811
  populate.install = true;
10933
10812
  populate.reset = true;
10934
- }
10813
+ populate.larkApps = true;
10814
+ } else if (opts.command === "doctor" || opts.command === "check") populate.larkApps = true;
10935
10815
  return populate;
10936
10816
  }
10937
10817
  //#endregion
@@ -10985,372 +10865,11 @@ function reportDoctorRunToSlardar(opts) {
10985
10865
  }
10986
10866
  });
10987
10867
  }
10988
- function readLogFile(filePath) {
10989
- try {
10990
- return node_fs.default.readFileSync(filePath, "utf-8");
10991
- } catch {
10992
- return "";
10993
- }
10994
- }
10995
- function reportUpgradeLarkToSlardar(opts) {
10996
- console.error(`[slardar] upgrade_lark_run scene=${opts.scene ?? ""} success=${opts.success} exitCode=${opts.exitCode ?? ""} rollbackOk=${opts.rollbackOk ?? ""}`);
10997
- const logContent = readLogFile(opts.logFile);
10998
- reportTask({
10999
- eventName: "upgrade_lark_run",
11000
- durationMs: opts.durationMs,
11001
- status: opts.success ? "success" : "failed",
11002
- extraCategories: {
11003
- scene: opts.scene ?? "",
11004
- exit_code: String(opts.exitCode ?? ""),
11005
- rollback_ok: opts.rollbackOk != null ? String(opts.rollbackOk) : "",
11006
- validation_error: opts.validationError ?? "",
11007
- error_msg: opts.error ?? "",
11008
- log_content: logContent
11009
- }
11010
- });
11011
- }
11012
- //#endregion
11013
- //#region src/upgrade-lark.ts
11014
- /** Plugin directories under extensions/ that are backed up before upgrade */
11015
- const FEISHU_PLUGIN_DIRS = ["openclaw-lark", "feishu-openclaw-plugin"];
11016
- function backupFiles(opts) {
11017
- const { workspaceDir, configPath, backupDir, log } = opts;
11018
- try {
11019
- node_fs.default.mkdirSync(backupDir, { recursive: true });
11020
- log(`backup dir: ${backupDir}`);
11021
- if (node_fs.default.existsSync(configPath)) {
11022
- const stat = node_fs.default.statSync(configPath);
11023
- node_fs.default.copyFileSync(configPath, node_path.default.join(backupDir, "openclaw.json"));
11024
- log(` backed up: openclaw.json (${stat.size} bytes)`);
11025
- } else log(` skipped: openclaw.json (not found)`);
11026
- const extSrc = node_path.default.join(workspaceDir, "extensions");
11027
- for (const pluginDir of FEISHU_PLUGIN_DIRS) {
11028
- const src = node_path.default.join(extSrc, pluginDir);
11029
- if (node_fs.default.existsSync(src)) {
11030
- const dst = node_path.default.join(backupDir, "extensions", pluginDir);
11031
- node_fs.default.cpSync(src, dst, { recursive: true });
11032
- const version = readPkgVersion(node_path.default.join(src, "package.json"));
11033
- log(` backed up: extensions/${pluginDir}${version ? ` (version: ${version})` : ""}`);
11034
- } else log(` skipped: extensions/${pluginDir} (not found)`);
11035
- }
11036
- return { ok: true };
11037
- } catch (e) {
11038
- return {
11039
- ok: false,
11040
- error: `backup failed: ${e.message}`
11041
- };
11042
- }
11043
- }
11044
- function restoreFiles(opts) {
11045
- const { workspaceDir, configPath, backupDir, log } = opts;
11046
- try {
11047
- const configBackup = node_path.default.join(backupDir, "openclaw.json");
11048
- if (node_fs.default.existsSync(configBackup)) {
11049
- node_fs.default.copyFileSync(configBackup, configPath);
11050
- log(` restored: openclaw.json`);
11051
- }
11052
- const extDst = node_path.default.join(workspaceDir, "extensions");
11053
- for (const pluginDir of FEISHU_PLUGIN_DIRS) {
11054
- const backupSrc = node_path.default.join(backupDir, "extensions", pluginDir);
11055
- if (node_fs.default.existsSync(backupSrc)) {
11056
- const dst = node_path.default.join(extDst, pluginDir);
11057
- if (node_fs.default.existsSync(dst)) node_fs.default.rmSync(dst, {
11058
- recursive: true,
11059
- force: true
11060
- });
11061
- node_fs.default.cpSync(backupSrc, dst, { recursive: true });
11062
- log(` restored: extensions/${pluginDir}`);
11063
- }
11064
- }
11065
- return true;
11066
- } catch (e) {
11067
- log(` restore error: ${e.message}`);
11068
- return false;
11069
- }
11070
- }
11071
- function readPkgVersion(pkgPath) {
11072
- try {
11073
- const pkg = JSON.parse(node_fs.default.readFileSync(pkgPath, "utf-8"));
11074
- return typeof pkg.version === "string" ? pkg.version : null;
11075
- } catch {
11076
- return null;
11077
- }
11078
- }
11079
- function snapshotVersions(cwd, log) {
11080
- const ocResult = (0, node_child_process.spawnSync)("openclaw", ["--version"], {
11081
- cwd,
11082
- encoding: "utf-8",
11083
- stdio: [
11084
- "ignore",
11085
- "pipe",
11086
- "pipe"
11087
- ],
11088
- timeout: 5e3
11089
- });
11090
- const ocRaw = (ocResult.stdout ?? "").trim() || (ocResult.stderr ?? "").trim();
11091
- const extDir = node_path.default.join(cwd, "extensions");
11092
- const larkPkg = node_path.default.join(extDir, "openclaw-lark", "package.json");
11093
- const feishuPkg = node_path.default.join(extDir, "feishu-openclaw-plugin", "package.json");
11094
- log(` version-check paths: ${larkPkg} [${node_fs.default.existsSync(larkPkg) ? "exists" : "missing"}]`);
11095
- log(` version-check paths: ${feishuPkg} [${node_fs.default.existsSync(feishuPkg) ? "exists" : "missing"}]`);
11096
- return {
11097
- openclaw: ocRaw || null,
11098
- openclawLark: readPkgVersion(larkPkg),
11099
- feishuOpenclawPlugin: readPkgVersion(feishuPkg)
11100
- };
11101
- }
11102
- function logVersionSnapshot(label, v, log) {
11103
- log(`${label}: openclaw=${v.openclaw ?? "n/a"} openclaw-lark=${v.openclawLark ?? "n/a"} feishu-openclaw-plugin=${v.feishuOpenclawPlugin ?? "n/a"}`);
11104
- }
11105
- function countFeishuBots(configPath) {
11106
- try {
11107
- const raw = node_fs.default.readFileSync(configPath, "utf-8");
11108
- const config = loadJSON5().parse(raw);
11109
- const accounts = getNestedMap(config, "channels", "feishu", "accounts");
11110
- if (accounts) return Object.keys(accounts).length;
11111
- const feishu = getNestedMap(config, "channels", "feishu");
11112
- return typeof feishu?.appId === "string" && feishu.appId ? 1 : 0;
11113
- } catch {
11114
- return 0;
11115
- }
11116
- }
11117
- /** Run channels probe, log results, and return the result. Never throws. */
11118
- function probeChannels(label, log, timeoutMs) {
11119
- try {
11120
- const r = runChannelsProbe(timeoutMs);
11121
- log(` ${label} available=${r.available} anyAccountWorking=${r.anyAccountWorking}`);
11122
- if (r.error) log(` ${label} error: ${r.error}`);
11123
- if (r.gatewayReachable != null) log(` ${label} gatewayReachable: ${r.gatewayReachable}`);
11124
- for (const acct of r.accounts ?? []) log(` ${label} account ${acct.id}: isWorking=${acct.isWorking} bits=[${acct.bits.join(",")}]`);
11125
- return r;
11126
- } catch (e) {
11127
- log(` ${label} channels probe threw: ${e.message}`);
11128
- return {
11129
- available: false,
11130
- gatewayReachable: false,
11131
- feishuConfigInvalid: false,
11132
- accounts: [],
11133
- anyAccountWorking: false
11134
- };
11135
- }
11136
- }
11137
- function runUpgradeLark(opts) {
11138
- const cwd = opts.cwd ?? "/home/gem/workspace/agent";
11139
- const configPath = opts.configPath ?? CONFIG_PATH;
11140
- const logFile = upgradeLarkLogFile(opts.runId);
11141
- const log = makeLogger(logFile);
11142
- const fsOpts = {
11143
- workspaceDir: cwd,
11144
- configPath,
11145
- backupDir: node_path.default.join(opts.backupBaseDir ?? "/tmp/openclaw-diagnose", `upgrade-lark-backup-${opts.runId}`),
11146
- log
11147
- };
11148
- const cliScript = opts.cliScript ?? process.argv[1];
11149
- const statusCheckDelayMs = opts.statusCheckDelayMs ?? 5e3;
11150
- log(`${"=".repeat(60)}`);
11151
- log(`upgrade-lark started runId=${opts.runId}`);
11152
- log(` cwd : ${cwd}`);
11153
- log(` configPath : ${configPath}`);
11154
- log(`${"=".repeat(60)}`);
11155
- log("");
11156
- log("── [Pre-check A] channels probe(升级前)────────────────");
11157
- const beforeChannels = probeChannels("before", log, 6e4);
11158
- log("");
11159
- log("── [Pre-check B] 版本兼容预检 ───────────────────────────");
11160
- let versionIncompatible = false;
11161
- try {
11162
- const rawConfig = node_fs.default.readFileSync(configPath, "utf-8");
11163
- versionIncompatible = needsLarkUpgrade({
11164
- config: loadJSON5().parse(rawConfig),
11165
- configPath,
11166
- vars: {},
11167
- providerDeps: {
11168
- usesMiaodaProvider: false,
11169
- usesMiaodaSecretProvider: false
11170
- }
11171
- });
11172
- log(` version-compat pre-check: ${versionIncompatible ? "NEEDS_UPGRADE" : "ok"}`);
11173
- } catch (e) {
11174
- log(` version-compat pre-check error: ${e.message} — treating as needs-upgrade`);
11175
- versionIncompatible = true;
11176
- }
11177
- const feishuConfigInvalid = beforeChannels.feishuConfigInvalid;
11178
- log(` feishu config invalid : ${feishuConfigInvalid}`);
11179
- log("");
11180
- log("── [Gate] 升级前置条件检查 ───────────────────────────────");
11181
- log(` versionIncompatible : ${versionIncompatible}`);
11182
- log(` feishuConfigInvalid : ${feishuConfigInvalid}`);
11183
- log(` channels working before: ${beforeChannels.anyAccountWorking}`);
11184
- if (!(versionIncompatible || feishuConfigInvalid)) {
11185
- const reason = "version compatible and feishu channel config valid — upgrade not needed";
11186
- log(` SKIP: ${reason}`);
11187
- log(`${"=".repeat(60)}`);
11188
- log("upgrade-lark skipped (pre-check gate)");
11189
- log(`${"=".repeat(60)}`);
11190
- return {
11191
- ok: true,
11192
- skipped: true,
11193
- skipReason: reason,
11194
- logFile
11195
- };
11196
- }
11197
- if (beforeChannels.anyAccountWorking) {
11198
- const reason = "channels are working — upgrade not needed (issue detected but system is functional)";
11199
- log(` SKIP: ${reason}`);
11200
- log(`${"=".repeat(60)}`);
11201
- log("upgrade-lark skipped (pre-check gate)");
11202
- log(`${"=".repeat(60)}`);
11203
- return {
11204
- ok: true,
11205
- skipped: true,
11206
- skipReason: reason,
11207
- logFile
11208
- };
11209
- }
11210
- log(` PROCEED: requiresLarkUpgrade=true (version=${versionIncompatible}, feishuConfig=${feishuConfigInvalid}) AND channels not working → running upgrade`);
11211
- log("");
11212
- log("── [1/6] 文件备份 ────────────────────────────────────────");
11213
- log(`before-state: botCount=${countFeishuBots(configPath)}`);
11214
- const backup = backupFiles(fsOpts);
11215
- if (!backup.ok) {
11216
- log(`ERROR: ${backup.error}`);
11217
- return {
11218
- ok: false,
11219
- error: backup.error,
11220
- logFile
11221
- };
11222
- }
11223
- log("backup: ok");
11224
- logVersionSnapshot("before-versions", snapshotVersions(cwd, log), log);
11225
- log("");
11226
- log("── [2/6] 清理本地 openclaw shim ─────────────────────────");
11227
- const localOpenclawBin = node_path.default.join(cwd, "node_modules", ".bin", "openclaw");
11228
- if (node_fs.default.existsSync(localOpenclawBin)) try {
11229
- node_fs.default.rmSync(localOpenclawBin);
11230
- log(` removed: ${localOpenclawBin}`);
11231
- } catch (e) {
11232
- log(` WARN: failed to remove ${localOpenclawBin}: ${e.message}`);
11233
- }
11234
- else log(` skipped: ${localOpenclawBin} (not found)`);
11235
- log("");
11236
- log("── [3/6] npx install (@larksuite/openclaw-lark-tools update) ──");
11237
- const npxResult = (0, node_child_process.spawnSync)("npx", [
11238
- "-y",
11239
- "@larksuite/openclaw-lark-tools",
11240
- "update"
11241
- ], {
11242
- cwd,
11243
- encoding: "utf-8",
11244
- stdio: [
11245
- "ignore",
11246
- "pipe",
11247
- "pipe"
11248
- ],
11249
- timeout: 12e4
11250
- });
11251
- const npxStdout = npxResult.stdout?.trim() ?? "";
11252
- const npxStderr = npxResult.stderr?.trim() ?? "";
11253
- const npxExitCode = npxResult.status ?? 1;
11254
- if (npxStdout) log(`npx stdout:\n${npxStdout}`);
11255
- if (npxStderr) log(`npx stderr:\n${npxStderr}`);
11256
- log(`npx exit: ${npxExitCode}${npxResult.error ? ` error: ${npxResult.error.message}` : ""}`);
11257
- if (statusCheckDelayMs > 0) {
11258
- log("");
11259
- log(`── 等待 ${statusCheckDelayMs / 1e3}s(让 openclaw 服务完成重启) ─────────────`);
11260
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, statusCheckDelayMs);
11261
- log("wait done");
11262
- }
11263
- const doRollback = (reason) => {
11264
- log(`ERROR: ${reason}`);
11265
- const rollbackOk = restoreFiles(fsOpts);
11266
- log(`rollback: ${rollbackOk ? "ok" : "FAILED"}`);
11267
- return {
11268
- ok: false,
11269
- error: reason,
11270
- validationError: reason,
11271
- stdout: npxStdout,
11272
- stderr: npxStderr,
11273
- exitCode: npxExitCode,
11274
- rollbackOk,
11275
- logFile
11276
- };
11277
- };
11278
- log("");
11279
- log("── [4/5] 安装后诊断校验 ─────────────────────────────────");
11280
- logVersionSnapshot("after-versions", snapshotVersions(cwd, log), log);
11281
- let afterVersionIncompatible = false;
11282
- try {
11283
- const rawConfig = node_fs.default.readFileSync(configPath, "utf-8");
11284
- afterVersionIncompatible = needsLarkUpgrade({
11285
- config: loadJSON5().parse(rawConfig),
11286
- configPath,
11287
- vars: {},
11288
- providerDeps: {
11289
- usesMiaodaProvider: false,
11290
- usesMiaodaSecretProvider: false
11291
- }
11292
- });
11293
- log(` version-compat post-check: ${afterVersionIncompatible ? "STILL_INCOMPATIBLE" : "ok"}`);
11294
- } catch (e) {
11295
- log(` version-compat post-check error: ${e.message} — treating as still-incompatible`);
11296
- afterVersionIncompatible = true;
11297
- }
11298
- const afterChannels = probeChannels("after", log, 6e4);
11299
- log(` feishu config invalid after: ${afterChannels.feishuConfigInvalid}`);
11300
- const stillNeedsUpgrade = (afterVersionIncompatible || afterChannels.feishuConfigInvalid) && !afterChannels.anyAccountWorking;
11301
- log(` post-check: stillNeedsUpgrade=${stillNeedsUpgrade} (version=${afterVersionIncompatible}, feishuConfig=${afterChannels.feishuConfigInvalid}, channelsWorking=${afterChannels.anyAccountWorking})`);
11302
- if (stillNeedsUpgrade) return doRollback(`post-install diagnosis still shows anomaly: versionIncompatible=${afterVersionIncompatible}, feishuConfigInvalid=${afterChannels.feishuConfigInvalid}, anyAccountWorking=${afterChannels.anyAccountWorking}`);
11303
- log(" post-install diagnosis: ok (upgrade conditions resolved)");
11304
- log("");
11305
- log("── [6/6] doctor --fix ────────────────────────────────────");
11306
- const fixArgs = ["doctor", "--fix"];
11307
- if (opts.scene) fixArgs.push(`--scene=${opts.scene}`);
11308
- const fixResult = (0, node_child_process.spawnSync)(process.execPath, [cliScript, ...fixArgs], {
11309
- cwd,
11310
- encoding: "utf-8",
11311
- stdio: [
11312
- "ignore",
11313
- "pipe",
11314
- "pipe"
11315
- ],
11316
- timeout: 6e4,
11317
- env: process.env
11318
- });
11319
- if (fixResult.stdout?.trim()) log(`doctor(fix) stdout:\n${fixResult.stdout.trim()}`);
11320
- if (fixResult.stderr?.trim()) log(`doctor(fix) stderr:\n${fixResult.stderr.trim()}`);
11321
- log(`doctor(fix) exit: ${fixResult.status ?? "null"}${fixResult.error ? ` error: ${fixResult.error.message}` : ""}`);
11322
- log("");
11323
- log(`${"=".repeat(60)}`);
11324
- log("upgrade-lark completed successfully");
11325
- log(`${"=".repeat(60)}`);
11326
- return {
11327
- ok: true,
11328
- stdout: npxStdout,
11329
- stderr: npxStderr,
11330
- exitCode: npxExitCode,
11331
- logFile
11332
- };
11333
- }
11334
10868
  //#endregion
11335
10869
  //#region src/index.ts
11336
10870
  const args = node_process.default.argv.slice(2);
11337
10871
  const mode = args.find((a) => !a.startsWith("-"));
11338
10872
  /**
11339
- * Decode `--ctx=<base64>` into an opaque JSON object. Returns undefined when
11340
- * the flag isn't present — the caller decides whether to fall back to the
11341
- * innerapi or to error out.
11342
- *
11343
- * The object's shape is not enforced here; downstream code consumes it via
11344
- * either `normalizeCtx()` (new path) or direct field access for the legacy
11345
- * check/repair/reset contract still used by sandbox_console push.
11346
- */
11347
- function parseCtxFlag(args) {
11348
- const ctxArg = args.find((a) => a.startsWith("--ctx="));
11349
- if (!ctxArg) return void 0;
11350
- const b64 = ctxArg.slice(6);
11351
- return JSON.parse(Buffer.from(b64, "base64").toString("utf-8"));
11352
- }
11353
- /**
11354
10873
  * Pull the first non-flag positional after the mode name.
11355
10874
  * (The mode itself is args[0] in the filtered set, so we skip index 0.)
11356
10875
  */
@@ -11378,8 +10897,8 @@ function getMultiFlag(args, name) {
11378
10897
  * case but is no longer consulted.
11379
10898
  */
11380
10899
  async function reportRun(command, rc, _raw, invocation, durationMs, outcome, slardar = {
11381
- scene,
11382
- profile,
10900
+ scene: void 0,
10901
+ profile: "standard",
11383
10902
  fix: false
11384
10903
  }) {
11385
10904
  console.error(`${command}: telemetry calling report_cli_run`);
@@ -11443,7 +10962,7 @@ async function main() {
11443
10962
  console.error(`${mode}: begin argv=[${args.join(" ")}] version=${getVersion()} traceId=${traceId ?? "-"} caller=${caller ?? "-"} runIdGenerated=${rc.generated}`);
11444
10963
  switch (mode) {
11445
10964
  case "check": {
11446
- const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
10965
+ const raw = await fetchCtxViaInnerApi({
11447
10966
  populate: planCtxPopulate({
11448
10967
  command: "check",
11449
10968
  profile
@@ -11468,7 +10987,7 @@ async function main() {
11468
10987
  break;
11469
10988
  }
11470
10989
  case "repair": {
11471
- const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
10990
+ const raw = await fetchCtxViaInnerApi({
11472
10991
  populate: planCtxPopulate({
11473
10992
  command: "repair",
11474
10993
  profile
@@ -11539,27 +11058,15 @@ async function main() {
11539
11058
  break;
11540
11059
  }
11541
11060
  case "reset":
11542
- if (args.includes("--async")) {
11543
- const ctxArg = args.find((a) => a.startsWith("--ctx="));
11544
- let ctxBase64;
11545
- if (ctxArg) ctxBase64 = ctxArg.slice(6);
11546
- else {
11547
- const fetched = await fetchCtxViaInnerApi({
11548
- populate: planCtxPopulate({ command: "reset" }),
11549
- caller,
11550
- traceId
11551
- });
11552
- ctxBase64 = Buffer.from(JSON.stringify(fetched), "utf-8").toString("base64");
11553
- }
11554
- console.log(JSON.stringify(startAsyncReset(ctxBase64)));
11555
- } else if (args.includes("--worker")) {
11061
+ if (args.includes("--async")) console.log(JSON.stringify(startAsyncReset()));
11062
+ else if (args.includes("--worker")) {
11556
11063
  const taskId = args.find((a) => a.startsWith("--task-id="))?.slice(10);
11557
11064
  if (!taskId) {
11558
11065
  console.error("Error: --task-id=<id> is required for worker");
11559
11066
  node_process.default.exit(1);
11560
11067
  }
11561
11068
  const resultFile = resetResultFile(taskId);
11562
- const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11069
+ const raw = await fetchCtxViaInnerApi({
11563
11070
  populate: planCtxPopulate({ command: "reset" }),
11564
11071
  caller,
11565
11072
  traceId
@@ -11583,7 +11090,7 @@ async function main() {
11583
11090
  return;
11584
11091
  }
11585
11092
  } else {
11586
- console.error("Usage: reset --async [--ctx=<base64>] | reset --worker --task-id=<id> [--ctx=<base64>]");
11093
+ console.error("Usage: reset --async | reset --worker --task-id=<id>");
11587
11094
  node_process.default.exit(1);
11588
11095
  }
11589
11096
  break;
@@ -11599,14 +11106,14 @@ async function main() {
11599
11106
  case "install-openclaw": {
11600
11107
  const tag = getPositionalTag(args, "install-openclaw");
11601
11108
  if (!tag) {
11602
- console.error("Usage: install-openclaw <tag> [--ctx=<base64> | --oss_file_map=<base64>]");
11109
+ console.error("Usage: install-openclaw <tag> [--oss_file_map=<base64>]");
11603
11110
  node_process.default.exit(1);
11604
11111
  }
11605
11112
  const ossFileMapFlag = getFlag(args, "oss_file_map");
11606
11113
  let installOssFileMap;
11607
11114
  let rawForTelemetry;
11608
11115
  if (!ossFileMapFlag) {
11609
- rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11116
+ rawForTelemetry = await fetchCtxViaInnerApi({
11610
11117
  populate: planCtxPopulate({ command: "install" }),
11611
11118
  caller,
11612
11119
  traceId
@@ -11641,7 +11148,7 @@ async function main() {
11641
11148
  case "install-extension": {
11642
11149
  const tag = getPositionalTag(args, "install-extension");
11643
11150
  if (!tag) {
11644
- console.error("Usage: install-extension <tag> (--all | --extension=<name>...) [--home_base=<dir>] [--config_path=<path>] [--skip-config-update] [--ctx=<base64> | --oss_file_map=<base64>]");
11151
+ console.error("Usage: install-extension <tag> (--all | --extension=<name>...) [--home_base=<dir>] [--config_path=<path>] [--skip-config-update] [--oss_file_map=<base64>]");
11645
11152
  node_process.default.exit(1);
11646
11153
  }
11647
11154
  const all = args.includes("--all");
@@ -11653,7 +11160,7 @@ async function main() {
11653
11160
  let installOssFileMap;
11654
11161
  let rawForTelemetry;
11655
11162
  if (!ossFileMapFlag) {
11656
- rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11163
+ rawForTelemetry = await fetchCtxViaInnerApi({
11657
11164
  populate: planCtxPopulate({ command: "install" }),
11658
11165
  caller,
11659
11166
  traceId
@@ -11699,12 +11206,12 @@ async function main() {
11699
11206
  case "install-cli": {
11700
11207
  const tag = getPositionalTag(args, "install-cli");
11701
11208
  if (!tag) {
11702
- console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--ctx=<base64> | --oss_file_map=<base64>]");
11209
+ console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--oss_file_map=<base64>]");
11703
11210
  node_process.default.exit(1);
11704
11211
  }
11705
11212
  const names = getMultiFlag(args, "cli");
11706
11213
  if (names.length === 0) {
11707
- console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--ctx=<base64> | --oss_file_map=<base64>]");
11214
+ console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--oss_file_map=<base64>]");
11708
11215
  node_process.default.exit(1);
11709
11216
  }
11710
11217
  const homeBase = getFlag(args, "home_base");
@@ -11712,7 +11219,7 @@ async function main() {
11712
11219
  let installOssFileMap;
11713
11220
  let rawForTelemetry;
11714
11221
  if (!ossFileMapFlag) {
11715
- rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11222
+ rawForTelemetry = await fetchCtxViaInnerApi({
11716
11223
  populate: planCtxPopulate({ command: "install" }),
11717
11224
  caller,
11718
11225
  traceId
@@ -11760,7 +11267,7 @@ async function main() {
11760
11267
  case "download-resource": {
11761
11268
  const tag = getPositionalTag(args, "download-resource");
11762
11269
  if (!tag) {
11763
- console.error("Usage: download-resource <tag> --role=<role> --name=<name> [--dir=<dir>] [--ctx=<base64> | --oss_file_map=<base64>]");
11270
+ console.error("Usage: download-resource <tag> --role=<role> --name=<name> [--dir=<dir>] [--oss_file_map=<base64>]");
11764
11271
  node_process.default.exit(1);
11765
11272
  }
11766
11273
  const role = getFlag(args, "role");
@@ -11774,7 +11281,7 @@ async function main() {
11774
11281
  let installOssFileMap;
11775
11282
  let rawForTelemetry;
11776
11283
  if (!ossFileMapFlag) {
11777
- rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11284
+ rawForTelemetry = await fetchCtxViaInnerApi({
11778
11285
  populate: planCtxPopulate({ command: "install" }),
11779
11286
  caller,
11780
11287
  traceId
@@ -11848,50 +11355,6 @@ async function main() {
11848
11355
  if (!result.ok) node_process.default.exit(1);
11849
11356
  break;
11850
11357
  }
11851
- case "upgrade-lark": {
11852
- const result = runUpgradeLark({
11853
- runId: rc.runId,
11854
- scene
11855
- });
11856
- const upgradeDurationMs = Date.now() - t0;
11857
- console.log(JSON.stringify(result));
11858
- reportUpgradeLarkToSlardar({
11859
- scene,
11860
- durationMs: upgradeDurationMs,
11861
- success: result.ok,
11862
- logFile: result.logFile,
11863
- exitCode: result.exitCode,
11864
- rollbackOk: result.rollbackOk,
11865
- validationError: result.validationError,
11866
- error: result.error
11867
- });
11868
- try {
11869
- await reportCliRun({
11870
- command: "upgrade-lark",
11871
- runId: rc.runId,
11872
- version: getVersion(),
11873
- invocation: args.join(" "),
11874
- durationMs: upgradeDurationMs,
11875
- caller: rc.caller,
11876
- traceId: rc.traceId,
11877
- success: result.ok,
11878
- result,
11879
- error: result.ok ? void 0 : { message: result.error ?? "upgrade-lark failed" }
11880
- });
11881
- } catch (e) {
11882
- console.error(`[telemetry] reportCliRun failed: ${e.message}`);
11883
- }
11884
- if (!result.ok) {
11885
- node_process.default.exitCode = 1;
11886
- return;
11887
- }
11888
- break;
11889
- }
11890
- case "channels-probe": {
11891
- const result = runChannelsProbe(getFlag(args, "timeout") ? Number(getFlag(args, "timeout")) : void 0);
11892
- console.log(JSON.stringify(result));
11893
- break;
11894
- }
11895
11358
  default:
11896
11359
  node_process.default.stderr.write(`Unknown command: ${mode}\n\n`);
11897
11360
  node_process.default.stderr.write(formatTopLevelHelp(helpFlags.expert));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/openclaw-scripts-diagnose-cli",
3
- "version": "0.1.14-alpha.10",
3
+ "version": "0.1.14-alpha.11",
4
4
  "description": "CLI for OpenClaw config diagnose and repair with JSON5 support",
5
5
  "main": "dist/index.cjs",
6
6
  "bin": {