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

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 +1082 -583
  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-beta.0";
55
+ return "0.1.15-alpha.0";
56
56
  }
57
57
  //#endregion
58
58
  //#region src/rule-engine/base.ts
@@ -2520,337 +2520,6 @@ function upsertResourceConstrainedToolsBlock(content) {
2520
2520
  return `${content}${content.length > 0 && !content.endsWith("\n") ? "\n\n" : "\n"}${RESOURCE_CONSTRAINED_TOOLS_BLOCK}\n`;
2521
2521
  }
2522
2522
  //#endregion
2523
- //#region src/paths.ts
2524
- /**
2525
- * Central directory for all ephemeral diagnose/reset artifacts: task status
2526
- * files (`reset-<taskId>.json`) and human-readable step logs
2527
- * (`reset-<taskId>.log`). Having everything under one dir makes debugging a
2528
- * stuck reset much easier — `ls /tmp/openclaw-diagnose/` shows every recent
2529
- * run, and each run's log is right next to its state.
2530
- */
2531
- const DIAGNOSE_DIR = "/tmp/openclaw-diagnose";
2532
- function resetResultFile(taskId) {
2533
- return `${DIAGNOSE_DIR}/reset-${taskId}.json`;
2534
- }
2535
- function resetLogFile(taskId) {
2536
- return `${DIAGNOSE_DIR}/reset-${taskId}.log`;
2537
- }
2538
- /** Sandbox workspace root where openclaw config + agent state lives. */
2539
- const WORKSPACE_DIR = "/home/gem/workspace/agent";
2540
- /** File containing the provider key used by the openclaw miaoda provider. */
2541
- const PROVIDER_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-provider-key";
2542
- /** File containing the miaoda openclaw secrets JSON. */
2543
- const SECRETS_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-openclaw-secrets.json";
2544
- /** Absolute path to the openclaw config JSON. */
2545
- const CONFIG_PATH = `${WORKSPACE_DIR}/openclaw.json`;
2546
- //#endregion
2547
- //#region src/lark-cli-init.ts
2548
- const LARK_PLUGIN_NAMES$1 = ["openclaw-lark", "feishu-openclaw-plugin"];
2549
- const PE_XML_TAG = "lark-cli-pe";
2550
- const PE_PLACEHOLDER = `
2551
- <${PE_XML_TAG}>
2552
- **【强制要求 - 无例外】飞书所有操作必须先读 feishu-lark-cli skill。未读skill不得调用任何飞书工具,违者视为操作失误。**
2553
- </${PE_XML_TAG}>
2554
- `;
2555
- function isLarkPluginInstalled(configPath) {
2556
- const extDir = getExtensionsDir(configPath);
2557
- return LARK_PLUGIN_NAMES$1.some((name) => {
2558
- try {
2559
- return node_fs.default.existsSync(node_path.default.join(extDir, name, "package.json"));
2560
- } catch {
2561
- return false;
2562
- }
2563
- });
2564
- }
2565
- function isLarkCliAvailable$2() {
2566
- try {
2567
- return (0, node_child_process.spawnSync)("lark-cli", ["--version"], {
2568
- encoding: "utf-8",
2569
- timeout: 5e3,
2570
- stdio: [
2571
- "ignore",
2572
- "pipe",
2573
- "ignore"
2574
- ]
2575
- }).status === 0;
2576
- } catch {
2577
- return false;
2578
- }
2579
- }
2580
- function readConfig(configPath) {
2581
- try {
2582
- const raw = node_fs.default.readFileSync(configPath, "utf-8");
2583
- const parsed = loadJSON5().parse(raw);
2584
- return parsed != null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
2585
- } catch {
2586
- return null;
2587
- }
2588
- }
2589
- /**
2590
- * Resolve the feishu app secret for the given appId.
2591
- *
2592
- * Lookup order:
2593
- * 1. channels.feishu.appSecret (single-agent: feishu.appId === appId)
2594
- * 2. channels.feishu.accounts[key].appSecret (multi-agent: account.appId === appId)
2595
- *
2596
- * Value interpretation:
2597
- * - string → use directly
2598
- * - object → secret is managed by a provider; use `feishuAppSecret` param instead
2599
- *
2600
- * Returns null when the secret cannot be determined.
2601
- */
2602
- function resolveAppSecret(appId, config, feishuAppSecret) {
2603
- const feishu = getNestedMap(config, "channels", "feishu");
2604
- if (!feishu) return null;
2605
- let rawSecret;
2606
- if (typeof feishu.appId === "string" && feishu.appId === appId) rawSecret = feishu.appSecret;
2607
- else {
2608
- const accounts = asRecord(feishu.accounts);
2609
- if (accounts) for (const [, val] of Object.entries(accounts)) {
2610
- const account = asRecord(val);
2611
- if (account?.appId === appId) {
2612
- rawSecret = account.appSecret ?? feishu.appSecret;
2613
- break;
2614
- }
2615
- }
2616
- }
2617
- if (typeof rawSecret === "string" && rawSecret) return rawSecret;
2618
- if (rawSecret != null && typeof rawSecret === "object") return feishuAppSecret ?? null;
2619
- return null;
2620
- }
2621
- /**
2622
- * Resolve the agents.md path for the given appId from the openclaw config.
2623
- *
2624
- * Case 1: appId matches channels.feishu.appId (single-agent path)
2625
- * → WORKSPACE_DIR/AGENTS.md
2626
- *
2627
- * Case 2: appId found in channels.feishu.accounts (multi-agent path)
2628
- * → find account key where account.appId === appId
2629
- * → find binding where match.channel=feishu && match.accountId=that key
2630
- * → if agentId === 'main' → WORKSPACE_DIR/agents.md
2631
- * → else find agent in agents.list by id → agent.workspace/agents.md
2632
- *
2633
- * Returns null when the path cannot be determined.
2634
- */
2635
- function resolveAgentsMdPath(appId, config) {
2636
- const feishu = getNestedMap(config, "channels", "feishu");
2637
- if (!feishu) {
2638
- console.error("resolveAgentsMdPath: channels.feishu not found");
2639
- return null;
2640
- }
2641
- if (typeof feishu.appId === "string" && feishu.appId === appId) {
2642
- console.error(`resolveAgentsMdPath: case=single-agent feishu.appId=${feishu.appId}`);
2643
- return node_path.default.join(WORKSPACE_DIR, "workspace", "AGENTS.md");
2644
- }
2645
- const accounts = asRecord(feishu.accounts);
2646
- if (!accounts) {
2647
- console.error("resolveAgentsMdPath: feishu.accounts not found");
2648
- return null;
2649
- }
2650
- let accountId;
2651
- for (const [key, val] of Object.entries(accounts)) if (asRecord(val)?.appId === appId) {
2652
- accountId = key;
2653
- break;
2654
- }
2655
- if (!accountId) {
2656
- console.error(`resolveAgentsMdPath: no account found with appId=${appId} in feishu.accounts keys=[${Object.keys(accounts).join(",")}]`);
2657
- return null;
2658
- }
2659
- console.error(`resolveAgentsMdPath: found accountId=${accountId}`);
2660
- const bindings = Array.isArray(config.bindings) ? config.bindings : [];
2661
- let agentId;
2662
- for (const b of bindings) {
2663
- const binding = asRecord(b);
2664
- if (!binding) continue;
2665
- const match = asRecord(binding.match);
2666
- if (match?.channel === "feishu" && match?.accountId === accountId) {
2667
- if (typeof binding.agentId === "string") {
2668
- agentId = binding.agentId;
2669
- break;
2670
- }
2671
- }
2672
- }
2673
- if (!agentId) {
2674
- console.error(`resolveAgentsMdPath: no binding found for accountId=${accountId} in bindings(count=${bindings.length})`);
2675
- return null;
2676
- }
2677
- console.error(`resolveAgentsMdPath: found agentId=${agentId}`);
2678
- if (agentId === "main") {
2679
- console.error("resolveAgentsMdPath: case=multi-agent-main");
2680
- return node_path.default.join(WORKSPACE_DIR, "workspace", "AGENTS.md");
2681
- }
2682
- const agentsObj = asRecord(config.agents);
2683
- const list = Array.isArray(agentsObj?.list) ? agentsObj.list : [];
2684
- for (const a of list) {
2685
- const agent = asRecord(a);
2686
- if (agent?.id === agentId) {
2687
- const ws = typeof agent.workspace === "string" ? agent.workspace : node_path.default.join(WORKSPACE_DIR, "workspace");
2688
- console.error(`resolveAgentsMdPath: case=multi-agent-custom agentId=${agentId} workspace=${ws}`);
2689
- return node_path.default.join(ws, "AGENTS.md");
2690
- }
2691
- }
2692
- console.error(`resolveAgentsMdPath: agentId=${agentId} not found in agents.list(count=${list.length})`);
2693
- return null;
2694
- }
2695
- function appendPeToAgentsMd(agentsMdPath) {
2696
- const dir = node_path.default.dirname(agentsMdPath);
2697
- if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
2698
- const existing = node_fs.default.existsSync(agentsMdPath) ? node_fs.default.readFileSync(agentsMdPath, "utf-8") : "";
2699
- if (existing.includes(`<lark-cli-pe>`)) {
2700
- console.error(`lark-cli-init: <${PE_XML_TAG}> already present in ${agentsMdPath}, skipping`);
2701
- return;
2702
- }
2703
- const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
2704
- node_fs.default.appendFileSync(agentsMdPath, `${sep}${PE_PLACEHOLDER}`, "utf-8");
2705
- console.error(`lark-cli-init: appended PE placeholder to ${agentsMdPath}`);
2706
- }
2707
- /**
2708
- * Collect every Feishu bot appId declared in the openclaw config.
2709
- * Covers both single-agent (channels.feishu.appId) and multi-agent
2710
- * (channels.feishu.accounts[*].appId) layouts. Returns a deduplicated list.
2711
- */
2712
- function collectFeishuAppIds(configPath) {
2713
- const config = readConfig(configPath ?? CONFIG_PATH);
2714
- if (!config) return [];
2715
- const feishu = getNestedMap(config, "channels", "feishu");
2716
- if (!feishu) return [];
2717
- const appIds = /* @__PURE__ */ new Set();
2718
- const topAppId = feishu.appId;
2719
- if (typeof topAppId === "string" && topAppId.trim()) appIds.add(topAppId.trim());
2720
- const accounts = asRecord(feishu.accounts);
2721
- if (accounts) for (const val of Object.values(accounts)) {
2722
- const appId = asRecord(val)?.appId;
2723
- if (typeof appId === "string" && appId.trim()) appIds.add(appId.trim());
2724
- }
2725
- return [...appIds];
2726
- }
2727
- function runLarkCliInit(opts) {
2728
- const configPath = opts.configPath ?? CONFIG_PATH;
2729
- if (!isLarkPluginInstalled(configPath)) {
2730
- console.error("lark-cli-init: skipping — openclaw-lark plugin not installed");
2731
- return {
2732
- ok: true,
2733
- skipped: true,
2734
- skipReason: "openclaw-lark plugin not installed"
2735
- };
2736
- }
2737
- if (!isLarkCliAvailable$2()) {
2738
- console.error("lark-cli-init: skipping — lark-cli command not found");
2739
- return {
2740
- ok: true,
2741
- skipped: true,
2742
- skipReason: "lark-cli command not found"
2743
- };
2744
- }
2745
- const config = readConfig(configPath);
2746
- if (!config) return {
2747
- ok: false,
2748
- error: `could not read config at ${configPath}`
2749
- };
2750
- const agentsMdPath = resolveAgentsMdPath(opts.appId, config);
2751
- console.error(`lark-cli-init: resolved agents.md path=${agentsMdPath ?? "(null)"}`);
2752
- if (!agentsMdPath) return {
2753
- ok: false,
2754
- error: `could not resolve agents.md path for appId=${opts.appId}`
2755
- };
2756
- const appSecret = resolveAppSecret(opts.appId, config, opts.feishuAppSecret);
2757
- if (!appSecret) return {
2758
- ok: false,
2759
- error: `could not resolve appSecret for appId=${opts.appId}`
2760
- };
2761
- console.error(`lark-cli-init: running lark-cli config init --name ${opts.appId} --app-id ${opts.appId} --brand feishu --app-secret-stdin --force-init`);
2762
- const initRes = (0, node_child_process.spawnSync)("lark-cli", [
2763
- "config",
2764
- "init",
2765
- "--name",
2766
- opts.appId,
2767
- "--app-id",
2768
- opts.appId,
2769
- "--brand",
2770
- "feishu",
2771
- "--app-secret-stdin",
2772
- "--force-init"
2773
- ], {
2774
- stdio: [
2775
- "pipe",
2776
- "pipe",
2777
- "pipe"
2778
- ],
2779
- encoding: "utf-8",
2780
- input: appSecret
2781
- });
2782
- const configInitStdout = initRes.stdout?.trim() || void 0;
2783
- const configInitStderr = initRes.stderr?.trim() || void 0;
2784
- if (configInitStdout) console.error(`lark-cli config init stdout: ${configInitStdout}`);
2785
- if (configInitStderr) console.error(`lark-cli config init stderr: ${configInitStderr}`);
2786
- if (initRes.error) return {
2787
- ok: false,
2788
- configInitStdout,
2789
- configInitStderr,
2790
- error: `lark-cli config init spawn error: ${initRes.error.message}`
2791
- };
2792
- if (initRes.status !== 0) return {
2793
- ok: false,
2794
- configInitExitCode: initRes.status ?? void 0,
2795
- configInitStdout,
2796
- configInitStderr,
2797
- error: `lark-cli config init exited with code ${initRes.status}`
2798
- };
2799
- appendPeToAgentsMd(agentsMdPath);
2800
- return {
2801
- ok: true,
2802
- configInitExitCode: 0,
2803
- agentsMdPath
2804
- };
2805
- }
2806
- //#endregion
2807
- //#region src/rules/agents-md-lark-cli-pe.ts
2808
- function isLarkCliAvailable$1() {
2809
- try {
2810
- return (0, node_child_process.spawnSync)("lark-cli", ["--version"], {
2811
- encoding: "utf-8",
2812
- timeout: 5e3,
2813
- stdio: [
2814
- "ignore",
2815
- "pipe",
2816
- "ignore"
2817
- ]
2818
- }).status === 0;
2819
- } catch {
2820
- return false;
2821
- }
2822
- }
2823
- let AgentsMdLarkCliPeRule = class AgentsMdLarkCliPeRule extends DiagnoseRule {
2824
- validate(ctx) {
2825
- if (!isLarkCliAvailable$1()) return { pass: true };
2826
- const missingPath = collectExistingAgentsMdPaths(ctx).find((filePath) => {
2827
- return !node_fs.default.readFileSync(filePath, "utf-8").includes(`<${PE_XML_TAG}>`);
2828
- });
2829
- if (!missingPath) return { pass: true };
2830
- return {
2831
- pass: false,
2832
- message: `${missingPath} 中缺少 lark-cli-pe PE 内容,需要追加`
2833
- };
2834
- }
2835
- repair(ctx) {
2836
- if (!isLarkCliAvailable$1()) return;
2837
- for (const filePath of collectExistingAgentsMdPaths(ctx)) {
2838
- const content = node_fs.default.readFileSync(filePath, "utf-8");
2839
- if (content.includes(`<lark-cli-pe>`)) continue;
2840
- const sep = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
2841
- node_fs.default.appendFileSync(filePath, `${sep}${PE_PLACEHOLDER}`, "utf-8");
2842
- console.error(`agents-md-lark-cli-pe: appended PE to ${filePath}`);
2843
- }
2844
- }
2845
- };
2846
- AgentsMdLarkCliPeRule = __decorate([Rule({
2847
- key: "agents_md_lark_cli_pe",
2848
- description: "检测各智能体 AGENTS.md 中是否缺失 lark-cli-pe PE 内容,lark-cli 存在时自动追加",
2849
- dependsOn: ["config_syntax_check"],
2850
- repairMode: "standard",
2851
- level: "silent"
2852
- })], AgentsMdLarkCliPeRule);
2853
- //#endregion
2854
2523
  //#region src/rules/miaoda-official-plugins-install-spec-unlock.ts
2855
2524
  /**
2856
2525
  * Official miaoda-side plugins that must track manifest — version-locked specs
@@ -2954,11 +2623,11 @@ function getAllow$1(config) {
2954
2623
  //#region src/rules/lark-plugin-allow.ts
2955
2624
  const LARK_PLUGIN = "openclaw-lark";
2956
2625
  const LEGACY_LARK_PLUGIN = "feishu-openclaw-plugin";
2957
- const LARK_PLUGIN_NAMES = [LARK_PLUGIN, LEGACY_LARK_PLUGIN];
2626
+ const LARK_PLUGIN_NAMES$1 = [LARK_PLUGIN, LEGACY_LARK_PLUGIN];
2958
2627
  let LarkPluginAllowRule = class LarkPluginAllowRule extends DiagnoseRule {
2959
2628
  validate(ctx) {
2960
2629
  const allow = getAllow(ctx.config);
2961
- if (LARK_PLUGIN_NAMES.some((name) => allow.includes(name))) return { pass: true };
2630
+ if (LARK_PLUGIN_NAMES$1.some((name) => allow.includes(name))) return { pass: true };
2962
2631
  const installed = detectInstalledLarkPlugin(getExtensionsDir(ctx.configPath));
2963
2632
  if (installed == null) return { pass: true };
2964
2633
  return {
@@ -2977,7 +2646,7 @@ let LarkPluginAllowRule = class LarkPluginAllowRule extends DiagnoseRule {
2977
2646
  const rawAllow = pluginsMap.allow;
2978
2647
  const original = Array.isArray(rawAllow) ? rawAllow : [];
2979
2648
  const stringAllow = original.filter((e) => typeof e === "string");
2980
- if (LARK_PLUGIN_NAMES.some((name) => stringAllow.includes(name))) return;
2649
+ if (LARK_PLUGIN_NAMES$1.some((name) => stringAllow.includes(name))) return;
2981
2650
  original.push(installed);
2982
2651
  pluginsMap.allow = original;
2983
2652
  }
@@ -3678,7 +3347,6 @@ function resolveUpgradeDirection(installed, ocCur, recommendedOc, isLegacy) {
3678
3347
  /** 提取公共前置上下文;任何前置条件不满足时返回 null(规则 pass)。 */
3679
3348
  function resolveCompatContext(ctx) {
3680
3349
  const recommendedOc = ctx.vars.recommendedOpenclawTag;
3681
- if (!recommendedOc) return null;
3682
3350
  const ocCur = getOcVersion();
3683
3351
  if (!ocCur) return null;
3684
3352
  const installed = getInstalledPlugin(ctx);
@@ -3701,6 +3369,7 @@ let FeishuPluginOpenclawUpgradeRule = class FeishuPluginOpenclawUpgradeRule exte
3701
3369
  if (!cc) return { pass: true };
3702
3370
  const { ocCur, recommendedOc, installed, isLegacy } = cc;
3703
3371
  if (isForkPlugin(installed)) return validateForkPlugin(installed, ocCur, recommendedOc);
3372
+ if (!recommendedOc) return { pass: true };
3704
3373
  if (resolveUpgradeDirection(installed, ocCur, recommendedOc, isLegacy) !== "openclaw") return { pass: true };
3705
3374
  return {
3706
3375
  pass: false,
@@ -3728,11 +3397,17 @@ let FeishuPluginLarkUpgradeRule = class FeishuPluginLarkUpgradeRule extends Diag
3728
3397
  if (!cc) return { pass: true };
3729
3398
  const { ocCur, recommendedOc, installed, isLegacy } = cc;
3730
3399
  if (isForkPlugin(installed)) return { pass: true };
3731
- if (resolveUpgradeDirection(installed, ocCur, recommendedOc, isLegacy) !== "lark") 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
+ };
3732
3407
  return {
3733
3408
  pass: false,
3734
3409
  action: "upgrade_lark",
3735
- message: `${buildCompatPrefix(installed, ocCur, isLegacy)};当前 openclaw@${ocCur} 已达推荐版本,可直接升级飞书插件`
3410
+ message: `${prefix};当前 openclaw@${ocCur} 已达推荐版本,可直接升级飞书插件`
3736
3411
  };
3737
3412
  }
3738
3413
  };
@@ -3744,6 +3419,18 @@ FeishuPluginLarkUpgradeRule = __decorate([Rule({
3744
3419
  level: "critical",
3745
3420
  usesVars: ["recommendedOpenclawTag"]
3746
3421
  })], 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
+ }
3747
3434
  function isForkPlugin(p) {
3748
3435
  return p.scope != null && FORK_SCOPES.includes(p.scope);
3749
3436
  }
@@ -3782,14 +3469,16 @@ function describeCompatConstraint(entry, pluginVersion) {
3782
3469
  /**
3783
3470
  * @lark-apaas/openclaw-lark 豁免 VERSION_COMPAT_MAP,但仍要求 openclaw ≥ FORK_LARK_PLUGIN_MIN_OC_VERSION。
3784
3471
  * 其他 @lark-apaas scope 的 fork 插件继续无条件 pass。
3472
+ * recommendedOc 可为 undefined(doctor 模式),此时只检测最低版本要求,不指定目标升级版本。
3785
3473
  */
3786
3474
  function validateForkPlugin(installed, ocCur, recommendedOc) {
3787
3475
  if (installed.fullName !== FORK_LARK_PLUGIN_FULL_NAME) return { pass: true };
3788
3476
  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} 或更高版本`;
3789
3478
  return {
3790
3479
  pass: false,
3791
3480
  action: "upgrade_openclaw",
3792
- message: `飞书插件 ${describePlugin(installed)}(fork 版)要求 openclaw ≥ ${FORK_LARK_PLUGIN_MIN_OC_VERSION},当前 openclaw@${ocCur} 低于此要求;将 openclaw 升级到 ${recommendedOc} 即可满足`
3481
+ message: `飞书插件 ${describePlugin(installed)}(fork 版)要求 openclaw ≥ ${FORK_LARK_PLUGIN_MIN_OC_VERSION},当前 openclaw@${ocCur} 低于此要求${recommendation}`
3793
3482
  };
3794
3483
  }
3795
3484
  function describePlugin(p) {
@@ -3836,6 +3525,198 @@ function extractScopedNameFromSpec$1(spec) {
3836
3525
  const at = spec.indexOf("@", 1);
3837
3526
  return at === -1 ? spec : spec.slice(0, at);
3838
3527
  }
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({
3535
+ ...ctx,
3536
+ vars: {
3537
+ ...ctx.vars,
3538
+ recommendedOpenclawTag: void 0
3539
+ }
3540
+ });
3541
+ if (!cc) return false;
3542
+ const { ocCur, installed } = cc;
3543
+ if (isForkPlugin(installed)) {
3544
+ if (installed.fullName === FORK_LARK_PLUGIN_FULL_NAME) return compareCalVer(ocCur, FORK_LARK_PLUGIN_MIN_OC_VERSION) < 0;
3545
+ return false;
3546
+ }
3547
+ return isLarkUpgradeNeededFromCC(cc);
3548
+ }
3549
+ //#endregion
3550
+ //#region src/channels-probe.ts
3551
+ const FEISHU_INVALID_CONFIG_MSG = "channels.feishu: invalid config: must NOT have additional properties";
3552
+ const CHANNEL_LINE_RE = /^-\s+Feishu\s+([^:]+):\s+(.+)$/;
3553
+ /**
3554
+ * Port of Python `_account_is_working` from the feishu-channel-success-rate skill.
3555
+ *
3556
+ * Strips colon-prefixed key:value bits (dm:, bot:, in:, out:, token:, allow:,
3557
+ * intents:, groups:, health:) and evaluates the canonical health formula.
3558
+ *
3559
+ * @param ignoreProbeFailed When true, "probe failed" is not treated as a failure condition.
3560
+ * @param gatewayReachable When false, only enabled+configured is required — the service
3561
+ * is not started yet so "running" cannot be present, but config presence is sufficient.
3562
+ */
3563
+ function accountIsWorking(bits, ignoreProbeFailed = true, gatewayReachable = true) {
3564
+ const bitTokens = /* @__PURE__ */ new Set();
3565
+ let hasError = false;
3566
+ let hasProbeFailed = false;
3567
+ for (const raw of bits) {
3568
+ const b = raw.trim();
3569
+ if (!b) continue;
3570
+ if (b.startsWith("error:")) {
3571
+ hasError = true;
3572
+ continue;
3573
+ }
3574
+ if (b === "probe failed") {
3575
+ hasProbeFailed = true;
3576
+ continue;
3577
+ }
3578
+ bitTokens.add(b.split(":")[0]);
3579
+ }
3580
+ if (!bitTokens.has("enabled") || !bitTokens.has("configured")) return false;
3581
+ if (!gatewayReachable) return true;
3582
+ if (bitTokens.has("works")) return true;
3583
+ if (bitTokens.has("running") && !hasError && (ignoreProbeFailed || !hasProbeFailed)) return true;
3584
+ return false;
3585
+ }
3586
+ /**
3587
+ * Parse the raw stdout of `openclaw channels status --probe`.
3588
+ * Port of Python `extract_channels_probe` from the feishu-channel-success-rate skill.
3589
+ */
3590
+ function parseChannelsProbeOutput(text, { ignoreProbeFailed = true } = {}) {
3591
+ const gatewayReachable = text.includes("Gateway reachable");
3592
+ const feishuConfigInvalid = text.includes(FEISHU_INVALID_CONFIG_MSG);
3593
+ const accounts = [];
3594
+ let anyAccountWorking = false;
3595
+ for (const line of text.split("\n")) {
3596
+ const m = CHANNEL_LINE_RE.exec(line.trim());
3597
+ if (!m) continue;
3598
+ const [, acct, rest] = m;
3599
+ const bits = rest.split(",").map((b) => b.trim());
3600
+ const isWorking = accountIsWorking(bits, ignoreProbeFailed, gatewayReachable);
3601
+ if (isWorking) anyAccountWorking = true;
3602
+ accounts.push({
3603
+ id: acct.trim(),
3604
+ bits,
3605
+ isWorking,
3606
+ raw: line.trim()
3607
+ });
3608
+ }
3609
+ return {
3610
+ gatewayReachable,
3611
+ feishuConfigInvalid,
3612
+ accounts,
3613
+ anyAccountWorking
3614
+ };
3615
+ }
3616
+ /**
3617
+ * Run `openclaw channels status --probe` and return a structured result.
3618
+ *
3619
+ * The command may exit non-zero when some bot accounts fail their probe — that
3620
+ * is still useful output. We therefore try to parse stdout even when the
3621
+ * process exits with a non-zero code, falling back to an unavailable result
3622
+ * only when there is genuinely no output to parse.
3623
+ *
3624
+ * @param timeoutMs Maximum wait time. Default is 60 s because v2026.4.x
3625
+ * lacks a per-request HTTP timeout and can block indefinitely.
3626
+ * @param ignoreProbeFailed When true, accounts with "probe failed" are still
3627
+ * counted as working. Pass true for upgrade-gate checks where probe failures
3628
+ * reflect network conditions rather than plugin misconfiguration.
3629
+ */
3630
+ function runChannelsProbe(timeoutMs = 6e4, { ignoreProbeFailed = true } = {}) {
3631
+ let stdout = "";
3632
+ let stderrText = "";
3633
+ let execError;
3634
+ try {
3635
+ stdout = (0, node_child_process.execSync)("openclaw channels status --probe", {
3636
+ encoding: "utf-8",
3637
+ timeout: timeoutMs,
3638
+ stdio: [
3639
+ "ignore",
3640
+ "pipe",
3641
+ "pipe"
3642
+ ]
3643
+ });
3644
+ } catch (e) {
3645
+ const err = e;
3646
+ const stdoutRaw = err.stdout;
3647
+ stdout = typeof stdoutRaw === "string" ? stdoutRaw : stdoutRaw?.toString("utf-8") ?? "";
3648
+ execError = err.message;
3649
+ const stderrRaw = err.stderr;
3650
+ stderrText = (typeof stderrRaw === "string" ? stderrRaw : stderrRaw?.toString("utf-8") ?? "").trim();
3651
+ if (stderrText) console.error(`channels-probe: stderr from CLI: ${stderrText}`);
3652
+ }
3653
+ if (stdout.trim()) return {
3654
+ available: true,
3655
+ ...parseChannelsProbeOutput(stdout, { ignoreProbeFailed })
3656
+ };
3657
+ return {
3658
+ available: false,
3659
+ gatewayReachable: false,
3660
+ feishuConfigInvalid: stderrText.includes(FEISHU_INVALID_CONFIG_MSG),
3661
+ accounts: [],
3662
+ anyAccountWorking: false,
3663
+ error: execError ?? "no output from openclaw channels status --probe"
3664
+ };
3665
+ }
3666
+ //#endregion
3667
+ //#region src/rules/upgrade-lark-needed.ts
3668
+ /**
3669
+ * Detects the condition that warrants running `upgrade-lark`:
3670
+ * - feishu plugin version incompatible with current openclaw, OR
3671
+ * - openclaw channels status --probe reports feishu channel config invalid; AND
3672
+ * - channels are not working.
3673
+ *
3674
+ * Both conditions must be true simultaneously. If version is compatible and
3675
+ * feishu config is valid, or channels are working, the rule passes (no action needed).
3676
+ *
3677
+ * feishuConfigInvalid is read from the channels probe output rather than running a
3678
+ * separate `openclaw status` call, since only `openclaw channels status --probe`
3679
+ * reliably surfaces the schema validation error.
3680
+ *
3681
+ * profile: experimental — runs only in full sweep mode, not in standard doctor.
3682
+ * level: silent — telemetry/sweep-only, does not trigger page-level repair UI.
3683
+ */
3684
+ let UpgradeLarkNeededRule = class UpgradeLarkNeededRule extends DiagnoseRule {
3685
+ validate(ctx) {
3686
+ let versionIncompatible = false;
3687
+ try {
3688
+ versionIncompatible = needsLarkUpgrade(ctx);
3689
+ } catch {}
3690
+ let probeResult;
3691
+ try {
3692
+ probeResult = runChannelsProbe(6e4);
3693
+ } catch {
3694
+ probeResult = {
3695
+ available: false,
3696
+ gatewayReachable: false,
3697
+ feishuConfigInvalid: false,
3698
+ accounts: [],
3699
+ anyAccountWorking: false
3700
+ };
3701
+ }
3702
+ const feishuConfigInvalid = probeResult.feishuConfigInvalid;
3703
+ if (!(versionIncompatible || feishuConfigInvalid)) return { pass: true };
3704
+ if (probeResult.anyAccountWorking) return { pass: true };
3705
+ return {
3706
+ pass: false,
3707
+ action: "upgrade_lark",
3708
+ message: `飞书插件需要升级且 channels 不可用(版本不兼容=${versionIncompatible}, feishu配置无效=${feishuConfigInvalid}),建议执行 upgrade-lark 命令升级飞书插件`
3709
+ };
3710
+ }
3711
+ };
3712
+ UpgradeLarkNeededRule = __decorate([Rule({
3713
+ key: "upgrade_lark_needed",
3714
+ description: "检测飞书插件版本不兼容且 channels 不可用,判断是否需要执行 upgrade-lark 升级",
3715
+ dependsOn: ["config_syntax_check"],
3716
+ repairMode: "check-only",
3717
+ level: "silent",
3718
+ profile: "experimental"
3719
+ })], UpgradeLarkNeededRule);
3839
3720
  //#endregion
3840
3721
  //#region src/rules/cleanup-install-backup-dirs.ts
3841
3722
  const DIR_PREFIX = ".openclaw-install-";
@@ -3919,7 +3800,7 @@ function extractScopedNameFromSpec(spec) {
3919
3800
  const at = spec.indexOf("@", 1);
3920
3801
  return at === -1 ? spec : spec.slice(0, at);
3921
3802
  }
3922
- function isLarkCliAvailable() {
3803
+ function isLarkCliAvailable$1() {
3923
3804
  try {
3924
3805
  return (0, node_child_process.spawnSync)(LARK_CLI_NAME$1, ["--version"], {
3925
3806
  encoding: "utf-8",
@@ -3958,139 +3839,28 @@ function installLarkCliOnce(tag) {
3958
3839
  if (res.status !== 0) throw new Error(`install-cli exited with code ${res.status ?? "unknown"}`);
3959
3840
  }
3960
3841
  let LarkCliMissingForInstalledLarkPluginRule = class LarkCliMissingForInstalledLarkPluginRule extends DiagnoseRule {
3961
- validate(ctx) {
3962
- if (!isTargetForkPlugin(readInstalledLarkPlugin(ctx))) return { pass: true };
3963
- if (isLarkCliAvailable()) return { pass: true };
3964
- return {
3965
- pass: false,
3966
- message: `${FORK_PACKAGE_NAME}@${TARGET_VERSION} 已安装,但 lark-cli 不可用;将执行一次 lark-cli 安装`
3967
- };
3968
- }
3969
- repair(ctx) {
3970
- if (!isTargetForkPlugin(readInstalledLarkPlugin(ctx))) return;
3971
- if (isLarkCliAvailable()) return;
3972
- installLarkCliOnce(ctx.vars.recommendedOpenclawTag ?? TARGET_VERSION);
3973
- }
3974
- };
3975
- LarkCliMissingForInstalledLarkPluginRule = __decorate([Rule({
3976
- key: "lark_cli_missing_for_installed_lark_plugin",
3977
- description: "检测特定飞书插件版本已安装但 lark-cli 缺失的环境,并自动安装 lark-cli 一次",
3978
- dependsOn: ["config_syntax_check"],
3979
- repairMode: "standard",
3980
- level: "critical",
3981
- usesVars: ["recommendedOpenclawTag"]
3982
- })], LarkCliMissingForInstalledLarkPluginRule);
3983
- //#endregion
3984
- //#region src/rules/feishu-bot-channel-config.ts
3985
- /**
3986
- * Ensures each bot account's channel config is correct:
3987
- * 1. `allowFrom` contains its own `creatorOpenID` from larkApps
3988
- * 2. `appSecret` is either the canonical provider-ref or matches larkApps plaintext
3989
- *
3990
- * Covers both multi-account (channels.feishu.accounts) and single-account
3991
- * (channels.feishu.appId + allowFrom at top level) layouts.
3992
- */
3993
- let FeishuBotChannelConfigRule = class FeishuBotChannelConfigRule extends DiagnoseRule {
3994
- validate(ctx) {
3995
- const larkApps = ctx.vars.larkApps;
3996
- if (!larkApps || larkApps.length === 0) return { pass: true };
3997
- const feishu = asRecord(getNestedMap(ctx.config, "channels", "feishu"));
3998
- if (!feishu) return { pass: true };
3999
- const issues = [];
4000
- const accounts = asRecord(feishu.accounts);
4001
- if (accounts) for (const [accountId, account] of Object.entries(accounts)) {
4002
- const bot = asRecord(account);
4003
- if (!bot) continue;
4004
- const appId = bot.appId;
4005
- if (typeof appId !== "string" || !appId.startsWith("cli_")) continue;
4006
- const larkApp = larkApps.find((e) => e.larkAppID === appId);
4007
- if (!larkApp) continue;
4008
- this.checkBot(accountId, bot, larkApp, issues);
4009
- }
4010
- const singleAppId = feishu.appId;
4011
- if (typeof singleAppId === "string" && singleAppId.startsWith("cli_") && !accounts) {
4012
- const larkApp = larkApps.find((e) => e.larkAppID === singleAppId);
4013
- if (larkApp) this.checkBot("feishu", feishu, larkApp, issues);
4014
- }
4015
- if (issues.length === 0) return { pass: true };
3842
+ validate(ctx) {
3843
+ if (!isTargetForkPlugin(readInstalledLarkPlugin(ctx))) return { pass: true };
3844
+ if (isLarkCliAvailable$1()) return { pass: true };
4016
3845
  return {
4017
3846
  pass: false,
4018
- message: issues.join("; ")
3847
+ message: `${FORK_PACKAGE_NAME}@${TARGET_VERSION} 已安装,但 lark-cli 不可用;将执行一次 lark-cli 安装`
4019
3848
  };
4020
3849
  }
4021
- /** Check a single bot entry (either an account object or the feishu channel itself).
4022
- * appSecret is validated based on its current type:
4023
- * - object → must match canonical provider-ref
4024
- * - string → must match larkApps plaintext
4025
- */
4026
- checkBot(label, bot, larkApp, issues) {
4027
- const creatorOpenID = larkApp.creatorOpenID;
4028
- const allowFrom = Array.isArray(bot.allowFrom) ? bot.allowFrom : [];
4029
- if (typeof creatorOpenID === "string" && creatorOpenID !== "") {
4030
- if (!allowFrom.includes(creatorOpenID)) issues.push(`${label} allowFrom missing creatorOpenID ${creatorOpenID.length > 8 ? creatorOpenID.slice(0, 4) + "***" + creatorOpenID.slice(-4) : "***"}`);
4031
- } else if (allowFrom.length === 0) issues.push(`${label} allowFrom is empty (creatorOpenID unavailable, cannot auto-fix)`);
4032
- const secret = bot.appSecret;
4033
- if (typeof secret === "object" && secret !== null && !Array.isArray(secret)) {
4034
- if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) issues.push(`${label} appSecret is a provider-ref but not the canonical one`);
4035
- } else if (typeof secret === "string") {
4036
- if (secret !== larkApp.appSecret) issues.push(`${label} appSecret plaintext mismatch`);
4037
- } else issues.push(`${label} appSecret has unexpected type ${typeof secret}`);
4038
- }
4039
3850
  repair(ctx) {
4040
- const larkApps = ctx.vars.larkApps;
4041
- if (!larkApps || larkApps.length === 0) return;
4042
- const feishu = asRecord(getNestedMap(ctx.config, "channels", "feishu"));
4043
- if (!feishu) return;
4044
- const accounts = asRecord(feishu.accounts);
4045
- if (accounts) for (const [, account] of Object.entries(accounts)) {
4046
- const bot = asRecord(account);
4047
- if (!bot) continue;
4048
- const appId = bot.appId;
4049
- if (typeof appId !== "string" || !appId.startsWith("cli_")) continue;
4050
- const larkApp = larkApps.find((e) => e.larkAppID === appId);
4051
- if (!larkApp) continue;
4052
- this.fixBot(bot, larkApp);
4053
- }
4054
- const singleAppId = feishu.appId;
4055
- if (typeof singleAppId === "string" && singleAppId.startsWith("cli_") && !accounts) {
4056
- const larkApp = larkApps.find((e) => e.larkAppID === singleAppId);
4057
- if (larkApp) this.fixBot(feishu, larkApp);
4058
- }
4059
- }
4060
- /** Fix a single bot entry in-place.
4061
- * appSecret is repaired based on its current type:
4062
- * - object → fix to canonical provider-ref
4063
- * - string → fix to larkApps plaintext
4064
- */
4065
- fixBot(bot, larkApp) {
4066
- const creatorOpenID = larkApp.creatorOpenID;
4067
- if (typeof creatorOpenID === "string" && creatorOpenID !== "") {
4068
- const allowFrom = Array.isArray(bot.allowFrom) ? [...bot.allowFrom] : [];
4069
- if (!allowFrom.includes(creatorOpenID)) {
4070
- allowFrom.push(creatorOpenID);
4071
- bot.allowFrom = allowFrom;
4072
- }
4073
- }
4074
- const secret = bot.appSecret;
4075
- if (typeof secret === "object" && secret !== null && !Array.isArray(secret)) {
4076
- if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) bot.appSecret = { ...DEFAULT_FEISHU_APP_SECRET };
4077
- } else if (typeof secret === "string") {
4078
- if (secret !== larkApp.appSecret) bot.appSecret = larkApp.appSecret;
4079
- }
3851
+ if (!isTargetForkPlugin(readInstalledLarkPlugin(ctx))) return;
3852
+ if (isLarkCliAvailable$1()) return;
3853
+ installLarkCliOnce(ctx.vars.recommendedOpenclawTag ?? TARGET_VERSION);
4080
3854
  }
4081
3855
  };
4082
- FeishuBotChannelConfigRule = __decorate([Rule({
4083
- key: "feishu_bot_channel_config",
4084
- description: "确保飞书配置中 bot 账号的 allowFrom 包含其创建者 openID 且 appSecret 值正确",
4085
- dependsOn: [
4086
- "config_syntax_check",
4087
- "feishu_default_account",
4088
- "feishu_bot_id"
4089
- ],
3856
+ LarkCliMissingForInstalledLarkPluginRule = __decorate([Rule({
3857
+ key: "lark_cli_missing_for_installed_lark_plugin",
3858
+ description: "检测特定飞书插件版本已安装但 lark-cli 缺失的环境,并自动安装 lark-cli 一次",
3859
+ dependsOn: ["config_syntax_check"],
4090
3860
  repairMode: "standard",
4091
- usesVars: ["larkApps"],
4092
- level: "critical"
4093
- })], FeishuBotChannelConfigRule);
3861
+ level: "critical",
3862
+ usesVars: ["recommendedOpenclawTag"]
3863
+ })], LarkCliMissingForInstalledLarkPluginRule);
4094
3864
  //#endregion
4095
3865
  //#region src/check.ts
4096
3866
  /** Telemetry-aware entry: returns both the legacy CheckResult (for stdout)
@@ -4532,6 +4302,33 @@ function finalize$1(results, aborted) {
4532
4302
  };
4533
4303
  }
4534
4304
  //#endregion
4305
+ //#region src/paths.ts
4306
+ /**
4307
+ * Central directory for all ephemeral diagnose/reset artifacts: task status
4308
+ * files (`reset-<taskId>.json`) and human-readable step logs
4309
+ * (`reset-<taskId>.log`). Having everything under one dir makes debugging a
4310
+ * stuck reset much easier — `ls /tmp/openclaw-diagnose/` shows every recent
4311
+ * run, and each run's log is right next to its state.
4312
+ */
4313
+ const DIAGNOSE_DIR = "/tmp/openclaw-diagnose";
4314
+ function resetResultFile(taskId) {
4315
+ return `${DIAGNOSE_DIR}/reset-${taskId}.json`;
4316
+ }
4317
+ function resetLogFile(taskId) {
4318
+ return `${DIAGNOSE_DIR}/reset-${taskId}.log`;
4319
+ }
4320
+ /** Sandbox workspace root where openclaw config + agent state lives. */
4321
+ const WORKSPACE_DIR = "/home/gem/workspace/agent";
4322
+ /** File containing the provider key used by the openclaw miaoda provider. */
4323
+ const PROVIDER_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-provider-key";
4324
+ /** File containing the miaoda openclaw secrets JSON. */
4325
+ const SECRETS_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-openclaw-secrets.json";
4326
+ /** Absolute path to the openclaw config JSON. */
4327
+ const CONFIG_PATH = `${WORKSPACE_DIR}/openclaw.json`;
4328
+ function upgradeLarkLogFile(runId) {
4329
+ return `${DIAGNOSE_DIR}/upgrade-lark-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/:/g, "-")}-${runId.slice(0, 8)}.log`;
4330
+ }
4331
+ //#endregion
4535
4332
  //#region src/run-log.ts
4536
4333
  let currentRunContext;
4537
4334
  /**
@@ -4667,10 +4464,9 @@ function makeLogger(logFile) {
4667
4464
  /**
4668
4465
  * Start an async reset task: spawn a detached child process and return the taskId.
4669
4466
  *
4670
- * The child process runs: node cli.js reset --worker --task-id=xxx
4671
- * The worker fetches ctx from innerApi itself — no --ctx passthrough.
4467
+ * The child process runs: node cli.js reset --worker --task-id=xxx --ctx=base64
4672
4468
  */
4673
- function startAsyncReset() {
4469
+ function startAsyncReset(ctxBase64) {
4674
4470
  const taskId = (0, node_crypto.randomUUID)();
4675
4471
  const resultFile = resetResultFile(taskId);
4676
4472
  const log = makeLogger(resetLogFile(taskId));
@@ -4694,7 +4490,8 @@ function startAsyncReset() {
4694
4490
  process.argv[1],
4695
4491
  "reset",
4696
4492
  "--worker",
4697
- `--task-id=${taskId}`
4493
+ `--task-id=${taskId}`,
4494
+ `--ctx=${ctxBase64}`
4698
4495
  ], {
4699
4496
  detached: true,
4700
4497
  stdio: "ignore",
@@ -5096,13 +4893,273 @@ function installOne$1(pkg, tarball, homeBase) {
5096
4893
  } catch {}
5097
4894
  throw e;
5098
4895
  }
5099
- const hadOld = node_fs.default.existsSync(destDir);
5100
- if (hadOld) moveSafe(destDir, oldDir);
5101
- moveSafe(stagingDir, destDir);
5102
- if (hadOld && node_fs.default.existsSync(oldDir)) node_fs.default.rmSync(oldDir, {
5103
- recursive: true,
5104
- force: true
4896
+ const hadOld = node_fs.default.existsSync(destDir);
4897
+ if (hadOld) moveSafe(destDir, oldDir);
4898
+ moveSafe(stagingDir, destDir);
4899
+ if (hadOld && node_fs.default.existsSync(oldDir)) node_fs.default.rmSync(oldDir, {
4900
+ recursive: true,
4901
+ force: true
4902
+ });
4903
+ }
4904
+ //#endregion
4905
+ //#region src/lark-cli-init.ts
4906
+ const LARK_PLUGIN_NAMES = ["openclaw-lark", "feishu-openclaw-plugin"];
4907
+ const PE_XML_TAG = "lark-cli-pe";
4908
+ const PE_PLACEHOLDER = `
4909
+ <${PE_XML_TAG}>
4910
+ **【强制要求 - 无例外】飞书所有操作必须先读 feishu-lark-cli skill。未读skill不得调用任何飞书工具,违者视为操作失误。**
4911
+ </${PE_XML_TAG}>
4912
+ `;
4913
+ function isLarkPluginInstalled(configPath) {
4914
+ const extDir = getExtensionsDir(configPath);
4915
+ return LARK_PLUGIN_NAMES.some((name) => {
4916
+ try {
4917
+ return node_fs.default.existsSync(node_path.default.join(extDir, name, "package.json"));
4918
+ } catch {
4919
+ return false;
4920
+ }
4921
+ });
4922
+ }
4923
+ function isLarkCliAvailable() {
4924
+ try {
4925
+ return (0, node_child_process.spawnSync)("lark-cli", ["--version"], {
4926
+ encoding: "utf-8",
4927
+ timeout: 5e3,
4928
+ stdio: [
4929
+ "ignore",
4930
+ "pipe",
4931
+ "ignore"
4932
+ ]
4933
+ }).status === 0;
4934
+ } catch {
4935
+ return false;
4936
+ }
4937
+ }
4938
+ function readConfig(configPath) {
4939
+ try {
4940
+ const raw = node_fs.default.readFileSync(configPath, "utf-8");
4941
+ const parsed = loadJSON5().parse(raw);
4942
+ return parsed != null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
4943
+ } catch {
4944
+ return null;
4945
+ }
4946
+ }
4947
+ /**
4948
+ * Resolve the feishu app secret for the given appId.
4949
+ *
4950
+ * Lookup order:
4951
+ * 1. channels.feishu.appSecret (single-agent: feishu.appId === appId)
4952
+ * 2. channels.feishu.accounts[key].appSecret (multi-agent: account.appId === appId)
4953
+ *
4954
+ * Value interpretation:
4955
+ * - string → use directly
4956
+ * - object → secret is managed by a provider; use `feishuAppSecret` param instead
4957
+ *
4958
+ * Returns null when the secret cannot be determined.
4959
+ */
4960
+ function resolveAppSecret(appId, config, feishuAppSecret) {
4961
+ const feishu = getNestedMap(config, "channels", "feishu");
4962
+ if (!feishu) return null;
4963
+ let rawSecret;
4964
+ if (typeof feishu.appId === "string" && feishu.appId === appId) rawSecret = feishu.appSecret;
4965
+ else {
4966
+ const accounts = asRecord(feishu.accounts);
4967
+ if (accounts) for (const [, val] of Object.entries(accounts)) {
4968
+ const account = asRecord(val);
4969
+ if (account?.appId === appId) {
4970
+ rawSecret = account.appSecret ?? feishu.appSecret;
4971
+ break;
4972
+ }
4973
+ }
4974
+ }
4975
+ if (typeof rawSecret === "string" && rawSecret) return rawSecret;
4976
+ if (rawSecret != null && typeof rawSecret === "object") return feishuAppSecret ?? null;
4977
+ return null;
4978
+ }
4979
+ /**
4980
+ * Resolve the agents.md path for the given appId from the openclaw config.
4981
+ *
4982
+ * Case 1: appId matches channels.feishu.appId (single-agent path)
4983
+ * → WORKSPACE_DIR/AGENTS.md
4984
+ *
4985
+ * Case 2: appId found in channels.feishu.accounts (multi-agent path)
4986
+ * → find account key where account.appId === appId
4987
+ * → find binding where match.channel=feishu && match.accountId=that key
4988
+ * → if agentId === 'main' → WORKSPACE_DIR/agents.md
4989
+ * → else find agent in agents.list by id → agent.workspace/agents.md
4990
+ *
4991
+ * Returns null when the path cannot be determined.
4992
+ */
4993
+ function resolveAgentsMdPath(appId, config) {
4994
+ const feishu = getNestedMap(config, "channels", "feishu");
4995
+ if (!feishu) {
4996
+ console.error("resolveAgentsMdPath: channels.feishu not found");
4997
+ return null;
4998
+ }
4999
+ if (typeof feishu.appId === "string" && feishu.appId === appId) {
5000
+ console.error(`resolveAgentsMdPath: case=single-agent feishu.appId=${feishu.appId}`);
5001
+ return node_path.default.join(WORKSPACE_DIR, "workspace", "AGENTS.md");
5002
+ }
5003
+ const accounts = asRecord(feishu.accounts);
5004
+ if (!accounts) {
5005
+ console.error("resolveAgentsMdPath: feishu.accounts not found");
5006
+ return null;
5007
+ }
5008
+ let accountId;
5009
+ for (const [key, val] of Object.entries(accounts)) if (asRecord(val)?.appId === appId) {
5010
+ accountId = key;
5011
+ break;
5012
+ }
5013
+ if (!accountId) {
5014
+ console.error(`resolveAgentsMdPath: no account found with appId=${appId} in feishu.accounts keys=[${Object.keys(accounts).join(",")}]`);
5015
+ return null;
5016
+ }
5017
+ console.error(`resolveAgentsMdPath: found accountId=${accountId}`);
5018
+ const bindings = Array.isArray(config.bindings) ? config.bindings : [];
5019
+ let agentId;
5020
+ for (const b of bindings) {
5021
+ const binding = asRecord(b);
5022
+ if (!binding) continue;
5023
+ const match = asRecord(binding.match);
5024
+ if (match?.channel === "feishu" && match?.accountId === accountId) {
5025
+ if (typeof binding.agentId === "string") {
5026
+ agentId = binding.agentId;
5027
+ break;
5028
+ }
5029
+ }
5030
+ }
5031
+ if (!agentId) {
5032
+ console.error(`resolveAgentsMdPath: no binding found for accountId=${accountId} in bindings(count=${bindings.length})`);
5033
+ return null;
5034
+ }
5035
+ console.error(`resolveAgentsMdPath: found agentId=${agentId}`);
5036
+ if (agentId === "main") {
5037
+ console.error("resolveAgentsMdPath: case=multi-agent-main");
5038
+ return node_path.default.join(WORKSPACE_DIR, "workspace", "AGENTS.md");
5039
+ }
5040
+ const agentsObj = asRecord(config.agents);
5041
+ const list = Array.isArray(agentsObj?.list) ? agentsObj.list : [];
5042
+ for (const a of list) {
5043
+ const agent = asRecord(a);
5044
+ if (agent?.id === agentId) {
5045
+ const ws = typeof agent.workspace === "string" ? agent.workspace : node_path.default.join(WORKSPACE_DIR, "workspace");
5046
+ console.error(`resolveAgentsMdPath: case=multi-agent-custom agentId=${agentId} workspace=${ws}`);
5047
+ return node_path.default.join(ws, "AGENTS.md");
5048
+ }
5049
+ }
5050
+ console.error(`resolveAgentsMdPath: agentId=${agentId} not found in agents.list(count=${list.length})`);
5051
+ return null;
5052
+ }
5053
+ function appendPeToAgentsMd(agentsMdPath) {
5054
+ const dir = node_path.default.dirname(agentsMdPath);
5055
+ if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
5056
+ const existing = node_fs.default.existsSync(agentsMdPath) ? node_fs.default.readFileSync(agentsMdPath, "utf-8") : "";
5057
+ if (existing.includes(`<${PE_XML_TAG}>`)) {
5058
+ console.error(`lark-cli-init: <${PE_XML_TAG}> already present in ${agentsMdPath}, skipping`);
5059
+ return;
5060
+ }
5061
+ const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
5062
+ node_fs.default.appendFileSync(agentsMdPath, `${sep}${PE_PLACEHOLDER}`, "utf-8");
5063
+ console.error(`lark-cli-init: appended PE placeholder to ${agentsMdPath}`);
5064
+ }
5065
+ /**
5066
+ * Collect every Feishu bot appId declared in the openclaw config.
5067
+ * Covers both single-agent (channels.feishu.appId) and multi-agent
5068
+ * (channels.feishu.accounts[*].appId) layouts. Returns a deduplicated list.
5069
+ */
5070
+ function collectFeishuAppIds(configPath) {
5071
+ const config = readConfig(configPath ?? CONFIG_PATH);
5072
+ if (!config) return [];
5073
+ const feishu = getNestedMap(config, "channels", "feishu");
5074
+ if (!feishu) return [];
5075
+ const appIds = /* @__PURE__ */ new Set();
5076
+ const topAppId = feishu.appId;
5077
+ if (typeof topAppId === "string" && topAppId.trim()) appIds.add(topAppId.trim());
5078
+ const accounts = asRecord(feishu.accounts);
5079
+ if (accounts) for (const val of Object.values(accounts)) {
5080
+ const appId = asRecord(val)?.appId;
5081
+ if (typeof appId === "string" && appId.trim()) appIds.add(appId.trim());
5082
+ }
5083
+ return [...appIds];
5084
+ }
5085
+ function runLarkCliInit(opts) {
5086
+ const configPath = opts.configPath ?? CONFIG_PATH;
5087
+ if (!isLarkPluginInstalled(configPath)) {
5088
+ console.error("lark-cli-init: skipping — openclaw-lark plugin not installed");
5089
+ return {
5090
+ ok: true,
5091
+ skipped: true,
5092
+ skipReason: "openclaw-lark plugin not installed"
5093
+ };
5094
+ }
5095
+ if (!isLarkCliAvailable()) {
5096
+ console.error("lark-cli-init: skipping — lark-cli command not found");
5097
+ return {
5098
+ ok: true,
5099
+ skipped: true,
5100
+ skipReason: "lark-cli command not found"
5101
+ };
5102
+ }
5103
+ const config = readConfig(configPath);
5104
+ if (!config) return {
5105
+ ok: false,
5106
+ error: `could not read config at ${configPath}`
5107
+ };
5108
+ const agentsMdPath = resolveAgentsMdPath(opts.appId, config);
5109
+ console.error(`lark-cli-init: resolved agents.md path=${agentsMdPath ?? "(null)"}`);
5110
+ if (!agentsMdPath) return {
5111
+ ok: false,
5112
+ error: `could not resolve agents.md path for appId=${opts.appId}`
5113
+ };
5114
+ const appSecret = resolveAppSecret(opts.appId, config, opts.feishuAppSecret);
5115
+ if (!appSecret) return {
5116
+ ok: false,
5117
+ error: `could not resolve appSecret for appId=${opts.appId}`
5118
+ };
5119
+ console.error(`lark-cli-init: running lark-cli config init --name ${opts.appId} --app-id ${opts.appId} --brand feishu --app-secret-stdin --force-init`);
5120
+ const initRes = (0, node_child_process.spawnSync)("lark-cli", [
5121
+ "config",
5122
+ "init",
5123
+ "--name",
5124
+ opts.appId,
5125
+ "--app-id",
5126
+ opts.appId,
5127
+ "--brand",
5128
+ "feishu",
5129
+ "--app-secret-stdin",
5130
+ "--force-init"
5131
+ ], {
5132
+ stdio: [
5133
+ "pipe",
5134
+ "pipe",
5135
+ "pipe"
5136
+ ],
5137
+ encoding: "utf-8",
5138
+ input: appSecret
5105
5139
  });
5140
+ const configInitStdout = initRes.stdout?.trim() || void 0;
5141
+ const configInitStderr = initRes.stderr?.trim() || void 0;
5142
+ if (configInitStdout) console.error(`lark-cli config init stdout: ${configInitStdout}`);
5143
+ if (configInitStderr) console.error(`lark-cli config init stderr: ${configInitStderr}`);
5144
+ if (initRes.error) return {
5145
+ ok: false,
5146
+ configInitStdout,
5147
+ configInitStderr,
5148
+ error: `lark-cli config init spawn error: ${initRes.error.message}`
5149
+ };
5150
+ if (initRes.status !== 0) return {
5151
+ ok: false,
5152
+ configInitExitCode: initRes.status ?? void 0,
5153
+ configInitStdout,
5154
+ configInitStderr,
5155
+ error: `lark-cli config init exited with code ${initRes.status}`
5156
+ };
5157
+ appendPeToAgentsMd(agentsMdPath);
5158
+ return {
5159
+ ok: true,
5160
+ configInitExitCode: 0,
5161
+ agentsMdPath
5162
+ };
5106
5163
  }
5107
5164
  //#endregion
5108
5165
  //#region ../../openclaw-slardar/lib/client.js
@@ -6948,60 +7005,6 @@ function mergeCoreBackupAndOrigins(configPath, vars, resetData, log) {
6948
7005
  log(`allowedOrigins: added ${added.length} (${JSON.stringify(added)}), total now ${mergedOrigins.length}`);
6949
7006
  }
6950
7007
  /**
6951
- * Fix bot account allowFrom and appSecret using larkApps from innerApi.
6952
- *
6953
- * For each bot account (key starts with `bot-cli_`):
6954
- * - allowFrom must contain the bot's own creatorOpenID from larkApps
6955
- * - appSecret must be either the canonical provider-ref or match larkApps plaintext
6956
- *
6957
- * Runs after mergeCoreBackupAndOrigins so it operates on the final config state.
6958
- */
6959
- function fixBotChannelConfig(configPath, larkApps, log) {
6960
- if (!larkApps || larkApps.length === 0) {
6961
- log("no larkApps data, skip bot channel config fix");
6962
- return;
6963
- }
6964
- const config = loadJSON5().parse(node_fs.default.readFileSync(configPath, "utf-8"));
6965
- const accounts = asRecord(getNestedMap(config, "channels", "feishu")?.accounts);
6966
- if (!accounts) {
6967
- log("no feishu accounts in config, skip bot channel config fix");
6968
- return;
6969
- }
6970
- let fixCount = 0;
6971
- for (const [, account] of Object.entries(accounts)) {
6972
- const bot = asRecord(account);
6973
- if (!bot) continue;
6974
- const appId = bot.appId;
6975
- if (typeof appId !== "string" || !appId.startsWith("cli_")) continue;
6976
- const larkApp = larkApps.find((e) => e.larkAppID === appId);
6977
- if (!larkApp) continue;
6978
- const creatorOpenID = larkApp.creatorOpenID;
6979
- if (typeof creatorOpenID === "string" && creatorOpenID !== "") {
6980
- const allowFrom = Array.isArray(bot.allowFrom) ? [...bot.allowFrom] : [];
6981
- if (!allowFrom.includes(creatorOpenID)) {
6982
- allowFrom.push(creatorOpenID);
6983
- bot.allowFrom = allowFrom;
6984
- fixCount++;
6985
- }
6986
- }
6987
- const secret = bot.appSecret;
6988
- let needsFix = false;
6989
- if (typeof secret === "object" && secret !== null && !Array.isArray(secret)) {
6990
- if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) needsFix = true;
6991
- } else if (typeof secret === "string") {
6992
- if (secret !== larkApp.appSecret) needsFix = true;
6993
- } else needsFix = true;
6994
- if (needsFix) {
6995
- bot.appSecret = { ...DEFAULT_FEISHU_APP_SECRET };
6996
- fixCount++;
6997
- }
6998
- }
6999
- if (fixCount > 0) {
7000
- node_fs.default.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
7001
- log(`fixed ${fixCount} bot channel config issue(s) (allowFrom/appSecret)`);
7002
- } else log("bot channel config ok, no fixes needed");
7003
- }
7004
- /**
7005
7008
  * Step 7: Verify startup scripts landed in configDir/scripts/.
7006
7009
  *
7007
7010
  * Scripts are extracted directly to configDir/scripts/ during stageTemplate —
@@ -7146,7 +7149,6 @@ async function runReset(input, taskId, resultFile) {
7146
7149
  await step5InstallOpenclaw(openclawTag, ossFileMap, log);
7147
7150
  step(6);
7148
7151
  mergeCoreBackupAndOrigins(configPath, vars, resetData, log);
7149
- fixBotChannelConfig(configPath, vars.larkApps, log);
7150
7152
  step(7);
7151
7153
  verifyStartupScripts(configDir, log);
7152
7154
  step(8);
@@ -7945,8 +7947,7 @@ function normalizeCtx(raw) {
7945
7947
  reset: {
7946
7948
  templateVars: r.reset.templateVars ?? {},
7947
7949
  coreBackup: r.reset.coreBackup
7948
- },
7949
- larkApps: Array.isArray(r.larkApps) ? r.larkApps : []
7950
+ }
7950
7951
  };
7951
7952
  }
7952
7953
  const vars = r.vars ?? {};
@@ -7971,8 +7972,7 @@ function normalizeCtx(raw) {
7971
7972
  reset: {
7972
7973
  templateVars: resetData.templateVars ?? {},
7973
7974
  coreBackup: resetData.coreBackup
7974
- },
7975
- larkApps: Array.isArray(r.larkApps) ? r.larkApps : []
7975
+ }
7976
7976
  };
7977
7977
  }
7978
7978
  function fillApp(src) {
@@ -8037,8 +8037,7 @@ function buildCheckInput(raw, configPathOverride) {
8037
8037
  providerFilePath: PROVIDER_FILE_PATH,
8038
8038
  secretsFilePath: SECRETS_FILE_PATH,
8039
8039
  templateVars: ctx.app.templateVars,
8040
- recommendedOpenclawTag: ctx.app.recommendedOpenclawTag,
8041
- larkApps: ctx.larkApps
8040
+ recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
8042
8041
  },
8043
8042
  templateVars: ctx.app.templateVars
8044
8043
  };
@@ -8070,8 +8069,7 @@ function buildRepairInput(raw, configPathOverride) {
8070
8069
  providerFilePath: PROVIDER_FILE_PATH,
8071
8070
  secretsFilePath: SECRETS_FILE_PATH,
8072
8071
  templateVars: ctx.app.templateVars,
8073
- recommendedOpenclawTag: ctx.app.recommendedOpenclawTag,
8074
- larkApps: ctx.larkApps
8072
+ recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
8075
8073
  },
8076
8074
  repairData: {
8077
8075
  secretsContent: ctx.secrets.secretsContent,
@@ -8107,8 +8105,7 @@ function buildResetInput(raw, configPathOverride) {
8107
8105
  providerFilePath: PROVIDER_FILE_PATH,
8108
8106
  secretsFilePath: SECRETS_FILE_PATH,
8109
8107
  templateVars: ctx.app.templateVars,
8110
- recommendedOpenclawTag: ctx.app.recommendedOpenclawTag,
8111
- larkApps: ctx.larkApps
8108
+ recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
8112
8109
  },
8113
8110
  resetData: {
8114
8111
  templateVars: ctx.reset.templateVars,
@@ -10418,7 +10415,7 @@ async function reportCliRun(opts) {
10418
10415
  //#region src/help.ts
10419
10416
  const BIN = "mclaw-diagnose";
10420
10417
  function versionBanner() {
10421
- return `v0.1.14-beta.0`;
10418
+ return `v0.1.15-alpha.0`;
10422
10419
  }
10423
10420
  const COMMANDS = [
10424
10421
  {
@@ -10522,12 +10519,16 @@ EXIT CODES
10522
10519
  hidden: true,
10523
10520
  summary: "Run rule-engine check only",
10524
10521
  help: `USAGE
10525
- ${BIN} check
10522
+ ${BIN} check [--ctx=<base64>]
10526
10523
 
10527
10524
  DESCRIPTION
10528
10525
  Runs the rule engine against the sandbox's current openclaw config and
10529
- returns { failedRules }. Ctx is fetched from innerapi automatically.
10530
- End-users should prefer \`doctor\`.
10526
+ returns { failedRules }. Used by sandbox_console's push-style callers
10527
+ that already own the ctx — end-users should prefer \`doctor\`.
10528
+
10529
+ OPTIONS
10530
+ --ctx=<base64> Opaque ctx JSON (base64). When absent, fetched from
10531
+ innerapi (same path as doctor).
10531
10532
  `
10532
10533
  },
10533
10534
  {
@@ -10535,11 +10536,16 @@ DESCRIPTION
10535
10536
  hidden: true,
10536
10537
  summary: "Apply standard-mode repairs",
10537
10538
  help: `USAGE
10538
- ${BIN} repair
10539
+ ${BIN} repair [--ctx=<base64>]
10539
10540
 
10540
10541
  DESCRIPTION
10541
- Runs repair for the failing rules. Ctx is fetched from innerapi
10542
- automatically. End-users should use \`doctor --fix\` instead.
10542
+ Runs repair for the failing rules listed inside the ctx's repairData.
10543
+ Intended for sandbox_console's push path — end-users should use
10544
+ \`doctor --fix\` instead.
10545
+
10546
+ OPTIONS
10547
+ --ctx=<base64> Opaque ctx JSON (base64). When absent, fetched from
10548
+ innerapi.
10543
10549
  `
10544
10550
  },
10545
10551
  {
@@ -10547,15 +10553,14 @@ DESCRIPTION
10547
10553
  hidden: true,
10548
10554
  summary: "Re-initialize sandbox via the 9-step reset pipeline",
10549
10555
  help: `USAGE
10550
- ${BIN} reset --async
10551
- ${BIN} reset --worker --task-id=<id>
10556
+ ${BIN} reset --async [--ctx=<base64>]
10557
+ ${BIN} reset --worker --task-id=<id> [--ctx=<base64>]
10552
10558
 
10553
10559
  DESCRIPTION
10554
10560
  Two-phase pipeline driven asynchronously: the --async invocation spawns
10555
10561
  a detached worker and returns { taskId } immediately; the --worker
10556
10562
  invocation (spawned by --async) runs the actual 9 steps and writes
10557
10563
  progress to /tmp/openclaw-diagnose/reset-<taskId>.json.
10558
- Ctx is fetched from innerapi automatically.
10559
10564
 
10560
10565
  Poll progress with \`${BIN} get_reset_task --task-id=<id>\`.
10561
10566
 
@@ -10563,6 +10568,7 @@ OPTIONS
10563
10568
  --async Start a detached worker and return taskId on stdout.
10564
10569
  --worker Internal — run the 9-step pipeline (launched by --async).
10565
10570
  --task-id=<id> Required with --worker; identifies the progress file.
10571
+ --ctx=<base64> Opaque ctx JSON; fetched from innerapi when absent.
10566
10572
  `
10567
10573
  },
10568
10574
  {
@@ -10585,7 +10591,7 @@ OPTIONS
10585
10591
  hidden: true,
10586
10592
  summary: "Download + install the openclaw tarball",
10587
10593
  help: `USAGE
10588
- ${BIN} install-openclaw <tag> [--oss_file_map=<base64>]
10594
+ ${BIN} install-openclaw <tag> [--ctx=<base64> | --oss_file_map=<base64>]
10589
10595
 
10590
10596
  DESCRIPTION
10591
10597
  Downloads the openclaw@<tag> tgz via the signed OSS URL found in the
@@ -10597,9 +10603,9 @@ ARGUMENTS
10597
10603
  <tag> Openclaw version tag, e.g. 2026.4.11.
10598
10604
 
10599
10605
  OPTIONS
10606
+ --ctx=<base64> Opaque ctx; ossFileMap is extracted from it.
10600
10607
  --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi
10601
- entirely. When absent, ossFileMap is fetched from
10602
- innerapi automatically.
10608
+ entirely. Wins over --ctx when both provided.
10603
10609
  `
10604
10610
  },
10605
10611
  {
@@ -10625,7 +10631,8 @@ OPTIONS
10625
10631
  --home_base=<dir> Override the /home/gem base (tests).
10626
10632
  --config_path=<p> Override the openclaw.json path (tests).
10627
10633
  --skip-config-update Leave plugins.installs in openclaw.json untouched.
10628
- --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi.
10634
+ --ctx=<base64> Opaque ctx; see install-openclaw for semantics.
10635
+ --oss_file_map=... Pre-built OSS URL map (base64 JSON).
10629
10636
  `
10630
10637
  },
10631
10638
  {
@@ -10652,6 +10659,7 @@ OPTIONS
10652
10659
  --cli=<name> CLI package to install by short name or scoped
10653
10660
  packageName (repeatable, at least one required).
10654
10661
  --home_base=<dir> Override the /home/gem base (tests).
10662
+ --ctx=<base64> Opaque ctx; ossFileMap is extracted from it.
10655
10663
  --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi.
10656
10664
 
10657
10665
  EXAMPLES
@@ -10705,6 +10713,46 @@ OPTIONS
10705
10713
  EXIT CODES
10706
10714
  0 Success or skipped (prerequisites not met).
10707
10715
  1 Secret/path unresolvable, lark-cli failed, or config unreadable.
10716
+ `
10717
+ },
10718
+ {
10719
+ name: "upgrade-lark",
10720
+ hidden: false,
10721
+ summary: "Upgrade the Feishu/Lark plugin via @larksuite/openclaw-lark-tools",
10722
+ help: `USAGE
10723
+ ${BIN} upgrade-lark [--scene=<scene>] [--caller=<n>] [--trace-id=<id>]
10724
+
10725
+ DESCRIPTION
10726
+ Upgrades the Feishu/Lark plugin by running:
10727
+ npx -y @larksuite/openclaw-lark-tools update --use-existing
10728
+
10729
+ Before the upgrade, the following files are backed up:
10730
+ - openclaw.json
10731
+ - extensions/openclaw-lark/ (if present)
10732
+ - extensions/feishu-openclaw-plugin/ (if present)
10733
+ After the upgrade, the result is validated:
10734
+ - feishu.accounts bot count must not decrease
10735
+ - gateway config structure must remain valid (port/mode/bind/auth/trustedProxies)
10736
+ If the upgrade command fails, or validation fails, the backed-up files are
10737
+ restored to roll back the changes.
10738
+
10739
+ Execution is logged to /tmp/openclaw-diagnose/upgrade-lark-<runId>.log.
10740
+
10741
+ Output is a single JSON object on stdout:
10742
+ { "ok": true, "stdout": "...", "stderr": "...", "logFile": "..." }
10743
+ { "ok": false, "error": "...", "stderr": "...", "exitCode": 1,
10744
+ "rollbackOk": true, "validationError": "...", "logFile": "..." }
10745
+
10746
+ OPTIONS
10747
+ --scene=<scene> Telemetry label forwarded to Slardar only.
10748
+ Known values: PageUpgradeLark, etc. Custom strings accepted.
10749
+ --caller=<name> Optional metadata passed to innerapi.
10750
+ --trace-id=<id> Optional log-correlation id.
10751
+
10752
+ EXIT CODES
10753
+ 0 Success: upgrade ran and all validations passed.
10754
+ 1 Failure: npx error, validation failed, or git commit failed.
10755
+ File rollback was attempted (see rollbackOk in the JSON output).
10708
10756
  `
10709
10757
  },
10710
10758
  {
@@ -10738,6 +10786,41 @@ EXAMPLES
10738
10786
  ${BIN} rules # all rules
10739
10787
  ${BIN} rules --rule=gateway # single rule
10740
10788
  ${BIN} rules --rule=gateway --rule=feishu_channel # multiple rules
10789
+ `
10790
+ },
10791
+ {
10792
+ name: "channels-probe",
10793
+ hidden: true,
10794
+ summary: "Check feishu channel health via openclaw channels status --probe",
10795
+ help: `USAGE
10796
+ ${BIN} channels-probe [--timeout=<ms>]
10797
+
10798
+ DESCRIPTION
10799
+ Runs \`openclaw channels status --probe\` and returns a structured JSON
10800
+ summary of whether the current environment's feishu channels are
10801
+ configured and working correctly.
10802
+
10803
+ Output:
10804
+ {
10805
+ "available": true,
10806
+ "gatewayReachable": true,
10807
+ "accounts": [
10808
+ { "id": "default", "bits": ["enabled","configured","running","works"],
10809
+ "isWorking": true, "raw": "- Feishu default: ..." }
10810
+ ],
10811
+ "anyAccountWorking": true
10812
+ }
10813
+
10814
+ An account is considered working when:
10815
+ enabled ∧ configured ∧ ( works ∨ ( running ∧ no error: ∧ no probe failed ) )
10816
+
10817
+ "available": false means the CLI invocation itself failed (openclaw not
10818
+ found, gateway unreachable, or no parseable output returned).
10819
+
10820
+ OPTIONS
10821
+ --timeout=<ms> Max wait in milliseconds (default: 60000). The probe
10822
+ can hang indefinitely on openclaw v2026.4.x due to a
10823
+ missing per-request HTTP timeout — set this accordingly.
10741
10824
  `
10742
10825
  },
10743
10826
  {
@@ -10758,7 +10841,8 @@ OPTIONS
10758
10841
  --role=<role> Package role (e.g. template, config).
10759
10842
  --name=<name> Package name within the role.
10760
10843
  --dir=<dir> Target dir (defaults to dirname(pkg.installPath)).
10761
- --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi.
10844
+ --ctx=<base64> Opaque ctx; ossFileMap is extracted from it.
10845
+ --oss_file_map=... Pre-built OSS URL map (base64 JSON).
10762
10846
  `
10763
10847
  }
10764
10848
  ];
@@ -10834,31 +10918,31 @@ function planVarsFields(opts = {}) {
10834
10918
  *
10835
10919
  * Per-command group needs:
10836
10920
  *
10837
- * doctor / check app + larkApps
10838
- * repair app + secrets + larkApps
10839
- * reset app + secrets + install + reset + larkApps
10921
+ * doctor / check app (rule-driven)
10922
+ * repair app + secrets (writes secretsContent / providerKeyContent)
10923
+ * reset app + secrets + install + reset (the works)
10840
10924
  * install-* install only
10841
10925
  *
10842
10926
  * Empty result (`{}`) means "no group needed" — the CLI can skip the
10843
10927
  * `fetchCtxViaInnerApi` call entirely and run with a synthetic empty ctx.
10928
+ * Happens e.g. when the user pinned `--rule=<key>` to a vars-free rule on
10929
+ * `doctor`.
10844
10930
  */
10845
10931
  function planCtxPopulate(opts) {
10846
10932
  if (opts.command === "install") return { install: true };
10847
10933
  const populate = {};
10848
- if (planVarsFields({
10934
+ const appFields = planVarsFields({
10849
10935
  disabled: opts.disabled,
10850
10936
  onlyRules: opts.onlyRules,
10851
10937
  profile: opts.profile
10852
- }).length > 0) populate.app = true;
10853
- if (opts.command === "repair") {
10854
- populate.secrets = true;
10855
- populate.larkApps = true;
10856
- } else if (opts.command === "reset") {
10938
+ });
10939
+ if (appFields.length > 0) populate.app = appFields;
10940
+ if (opts.command === "repair") populate.secrets = true;
10941
+ else if (opts.command === "reset") {
10857
10942
  populate.secrets = true;
10858
10943
  populate.install = true;
10859
10944
  populate.reset = true;
10860
- populate.larkApps = true;
10861
- } else if (opts.command === "doctor" || opts.command === "check") populate.larkApps = true;
10945
+ }
10862
10946
  return populate;
10863
10947
  }
10864
10948
  //#endregion
@@ -10912,11 +10996,370 @@ function reportDoctorRunToSlardar(opts) {
10912
10996
  }
10913
10997
  });
10914
10998
  }
10999
+ function readLogFile(filePath) {
11000
+ try {
11001
+ return node_fs.default.readFileSync(filePath, "utf-8");
11002
+ } catch {
11003
+ return "";
11004
+ }
11005
+ }
11006
+ function reportUpgradeLarkToSlardar(opts) {
11007
+ console.error(`[slardar] upgrade_lark_run scene=${opts.scene ?? ""} success=${opts.success} exitCode=${opts.exitCode ?? ""} rollbackOk=${opts.rollbackOk ?? ""}`);
11008
+ const logContent = readLogFile(opts.logFile);
11009
+ reportTask({
11010
+ eventName: "upgrade_lark_run",
11011
+ durationMs: opts.durationMs,
11012
+ status: opts.success ? "success" : "failed",
11013
+ extraCategories: {
11014
+ scene: opts.scene ?? "",
11015
+ exit_code: String(opts.exitCode ?? ""),
11016
+ rollback_ok: opts.rollbackOk != null ? String(opts.rollbackOk) : "",
11017
+ validation_error: opts.validationError ?? "",
11018
+ error_msg: opts.error ?? "",
11019
+ log_content: logContent
11020
+ }
11021
+ });
11022
+ }
11023
+ //#endregion
11024
+ //#region src/upgrade-lark.ts
11025
+ /** Plugin directories under extensions/ that are backed up before upgrade */
11026
+ const FEISHU_PLUGIN_DIRS = ["openclaw-lark", "feishu-openclaw-plugin"];
11027
+ function backupFiles(opts) {
11028
+ const { workspaceDir, configPath, backupDir, log } = opts;
11029
+ try {
11030
+ node_fs.default.mkdirSync(backupDir, { recursive: true });
11031
+ log(`backup dir: ${backupDir}`);
11032
+ if (node_fs.default.existsSync(configPath)) {
11033
+ const stat = node_fs.default.statSync(configPath);
11034
+ node_fs.default.copyFileSync(configPath, node_path.default.join(backupDir, "openclaw.json"));
11035
+ log(` backed up: openclaw.json (${stat.size} bytes)`);
11036
+ } else log(` skipped: openclaw.json (not found)`);
11037
+ const extSrc = node_path.default.join(workspaceDir, "extensions");
11038
+ for (const pluginDir of FEISHU_PLUGIN_DIRS) {
11039
+ const src = node_path.default.join(extSrc, pluginDir);
11040
+ if (node_fs.default.existsSync(src)) {
11041
+ const dst = node_path.default.join(backupDir, "extensions", pluginDir);
11042
+ node_fs.default.cpSync(src, dst, { recursive: true });
11043
+ const version = readPkgVersion(node_path.default.join(src, "package.json"));
11044
+ log(` backed up: extensions/${pluginDir}${version ? ` (version: ${version})` : ""}`);
11045
+ } else log(` skipped: extensions/${pluginDir} (not found)`);
11046
+ }
11047
+ return { ok: true };
11048
+ } catch (e) {
11049
+ return {
11050
+ ok: false,
11051
+ error: `backup failed: ${e.message}`
11052
+ };
11053
+ }
11054
+ }
11055
+ function restoreFiles(opts) {
11056
+ const { workspaceDir, configPath, backupDir, log } = opts;
11057
+ try {
11058
+ const configBackup = node_path.default.join(backupDir, "openclaw.json");
11059
+ if (node_fs.default.existsSync(configBackup)) {
11060
+ node_fs.default.copyFileSync(configBackup, configPath);
11061
+ log(` restored: openclaw.json`);
11062
+ }
11063
+ const extDst = node_path.default.join(workspaceDir, "extensions");
11064
+ for (const pluginDir of FEISHU_PLUGIN_DIRS) {
11065
+ const backupSrc = node_path.default.join(backupDir, "extensions", pluginDir);
11066
+ if (node_fs.default.existsSync(backupSrc)) {
11067
+ const dst = node_path.default.join(extDst, pluginDir);
11068
+ if (node_fs.default.existsSync(dst)) node_fs.default.rmSync(dst, {
11069
+ recursive: true,
11070
+ force: true
11071
+ });
11072
+ node_fs.default.cpSync(backupSrc, dst, { recursive: true });
11073
+ log(` restored: extensions/${pluginDir}`);
11074
+ }
11075
+ }
11076
+ return true;
11077
+ } catch (e) {
11078
+ log(` restore error: ${e.message}`);
11079
+ return false;
11080
+ }
11081
+ }
11082
+ function readPkgVersion(pkgPath) {
11083
+ try {
11084
+ const pkg = JSON.parse(node_fs.default.readFileSync(pkgPath, "utf-8"));
11085
+ return typeof pkg.version === "string" ? pkg.version : null;
11086
+ } catch {
11087
+ return null;
11088
+ }
11089
+ }
11090
+ function snapshotVersions(cwd, log) {
11091
+ const ocResult = (0, node_child_process.spawnSync)("openclaw", ["--version"], {
11092
+ cwd,
11093
+ encoding: "utf-8",
11094
+ stdio: [
11095
+ "ignore",
11096
+ "pipe",
11097
+ "pipe"
11098
+ ],
11099
+ timeout: 5e3
11100
+ });
11101
+ const ocRaw = (ocResult.stdout ?? "").trim() || (ocResult.stderr ?? "").trim();
11102
+ const extDir = node_path.default.join(cwd, "extensions");
11103
+ const larkPkg = node_path.default.join(extDir, "openclaw-lark", "package.json");
11104
+ const feishuPkg = node_path.default.join(extDir, "feishu-openclaw-plugin", "package.json");
11105
+ log(` version-check paths: ${larkPkg} [${node_fs.default.existsSync(larkPkg) ? "exists" : "missing"}]`);
11106
+ log(` version-check paths: ${feishuPkg} [${node_fs.default.existsSync(feishuPkg) ? "exists" : "missing"}]`);
11107
+ return {
11108
+ openclaw: ocRaw || null,
11109
+ openclawLark: readPkgVersion(larkPkg),
11110
+ feishuOpenclawPlugin: readPkgVersion(feishuPkg)
11111
+ };
11112
+ }
11113
+ function logVersionSnapshot(label, v, log) {
11114
+ log(`${label}: openclaw=${v.openclaw ?? "n/a"} openclaw-lark=${v.openclawLark ?? "n/a"} feishu-openclaw-plugin=${v.feishuOpenclawPlugin ?? "n/a"}`);
11115
+ }
11116
+ function countFeishuBots(configPath) {
11117
+ try {
11118
+ const raw = node_fs.default.readFileSync(configPath, "utf-8");
11119
+ const config = loadJSON5().parse(raw);
11120
+ const accounts = getNestedMap(config, "channels", "feishu", "accounts");
11121
+ if (accounts) return Object.keys(accounts).length;
11122
+ const feishu = getNestedMap(config, "channels", "feishu");
11123
+ return typeof feishu?.appId === "string" && feishu.appId ? 1 : 0;
11124
+ } catch {
11125
+ return 0;
11126
+ }
11127
+ }
11128
+ /** Run channels probe, log results, and return the result. Never throws. */
11129
+ function probeChannels(label, log, timeoutMs) {
11130
+ try {
11131
+ const r = runChannelsProbe(timeoutMs);
11132
+ log(` ${label} available=${r.available} anyAccountWorking=${r.anyAccountWorking}`);
11133
+ if (r.error) log(` ${label} error: ${r.error}`);
11134
+ if (r.gatewayReachable != null) log(` ${label} gatewayReachable: ${r.gatewayReachable}`);
11135
+ for (const acct of r.accounts ?? []) log(` ${label} account ${acct.id}: isWorking=${acct.isWorking} bits=[${acct.bits.join(",")}]`);
11136
+ return r;
11137
+ } catch (e) {
11138
+ log(` ${label} channels probe threw: ${e.message}`);
11139
+ return {
11140
+ available: false,
11141
+ gatewayReachable: false,
11142
+ feishuConfigInvalid: false,
11143
+ accounts: [],
11144
+ anyAccountWorking: false
11145
+ };
11146
+ }
11147
+ }
11148
+ function runUpgradeLark(opts) {
11149
+ const cwd = opts.cwd ?? "/home/gem/workspace/agent";
11150
+ const configPath = opts.configPath ?? CONFIG_PATH;
11151
+ const logFile = upgradeLarkLogFile(opts.runId);
11152
+ const log = makeLogger(logFile);
11153
+ const fsOpts = {
11154
+ workspaceDir: cwd,
11155
+ configPath,
11156
+ backupDir: node_path.default.join(opts.backupBaseDir ?? "/tmp/openclaw-diagnose", `upgrade-lark-backup-${opts.runId}`),
11157
+ log
11158
+ };
11159
+ const cliScript = opts.cliScript ?? process.argv[1];
11160
+ const statusCheckDelayMs = opts.statusCheckDelayMs ?? 5e3;
11161
+ log(`${"=".repeat(60)}`);
11162
+ log(`upgrade-lark started runId=${opts.runId}`);
11163
+ log(` cwd : ${cwd}`);
11164
+ log(` configPath : ${configPath}`);
11165
+ log(`${"=".repeat(60)}`);
11166
+ log("");
11167
+ log("── [Pre-check A] channels probe(升级前)────────────────");
11168
+ const beforeChannels = probeChannels("before", log, 6e4);
11169
+ log("");
11170
+ log("── [Pre-check B] 版本兼容预检 ───────────────────────────");
11171
+ let versionIncompatible = false;
11172
+ try {
11173
+ const rawConfig = node_fs.default.readFileSync(configPath, "utf-8");
11174
+ versionIncompatible = needsLarkUpgrade({
11175
+ config: loadJSON5().parse(rawConfig),
11176
+ configPath,
11177
+ vars: {},
11178
+ providerDeps: {
11179
+ usesMiaodaProvider: false,
11180
+ usesMiaodaSecretProvider: false
11181
+ }
11182
+ });
11183
+ log(` version-compat pre-check: ${versionIncompatible ? "NEEDS_UPGRADE" : "ok"}`);
11184
+ } catch (e) {
11185
+ log(` version-compat pre-check error: ${e.message} — version signal unavailable`);
11186
+ }
11187
+ const feishuConfigInvalid = beforeChannels.feishuConfigInvalid;
11188
+ log(` feishu config invalid : ${feishuConfigInvalid}`);
11189
+ log("");
11190
+ log("── [Gate] 升级前置条件检查 ───────────────────────────────");
11191
+ log(` versionIncompatible : ${versionIncompatible}`);
11192
+ log(` feishuConfigInvalid : ${feishuConfigInvalid}`);
11193
+ log(` channels working before: ${beforeChannels.anyAccountWorking}`);
11194
+ if (!(versionIncompatible || feishuConfigInvalid)) {
11195
+ const reason = "version compatible and feishu channel config valid — upgrade not needed";
11196
+ log(` SKIP: ${reason}`);
11197
+ log(`${"=".repeat(60)}`);
11198
+ log("upgrade-lark skipped (pre-check gate)");
11199
+ log(`${"=".repeat(60)}`);
11200
+ return {
11201
+ ok: true,
11202
+ skipped: true,
11203
+ skipReason: reason,
11204
+ logFile
11205
+ };
11206
+ }
11207
+ if (beforeChannels.anyAccountWorking) {
11208
+ const reason = "channels are working — upgrade not needed (issue detected but system is functional)";
11209
+ log(` SKIP: ${reason}`);
11210
+ log(`${"=".repeat(60)}`);
11211
+ log("upgrade-lark skipped (pre-check gate)");
11212
+ log(`${"=".repeat(60)}`);
11213
+ return {
11214
+ ok: true,
11215
+ skipped: true,
11216
+ skipReason: reason,
11217
+ logFile
11218
+ };
11219
+ }
11220
+ log(` PROCEED: requiresLarkUpgrade=true (version=${versionIncompatible}, feishuConfig=${feishuConfigInvalid}) AND channels not working → running upgrade`);
11221
+ log("");
11222
+ log("── [1/6] 文件备份 ────────────────────────────────────────");
11223
+ log(`before-state: botCount=${countFeishuBots(configPath)}`);
11224
+ const backup = backupFiles(fsOpts);
11225
+ if (!backup.ok) {
11226
+ log(`ERROR: ${backup.error}`);
11227
+ return {
11228
+ ok: false,
11229
+ error: backup.error,
11230
+ logFile
11231
+ };
11232
+ }
11233
+ log("backup: ok");
11234
+ logVersionSnapshot("before-versions", snapshotVersions(cwd, log), log);
11235
+ log("");
11236
+ log("── [2/6] 清理本地 openclaw shim ─────────────────────────");
11237
+ const localOpenclawBin = node_path.default.join(cwd, "node_modules", ".bin", "openclaw");
11238
+ if (node_fs.default.existsSync(localOpenclawBin)) try {
11239
+ node_fs.default.rmSync(localOpenclawBin);
11240
+ log(` removed: ${localOpenclawBin}`);
11241
+ } catch (e) {
11242
+ log(` WARN: failed to remove ${localOpenclawBin}: ${e.message}`);
11243
+ }
11244
+ else log(` skipped: ${localOpenclawBin} (not found)`);
11245
+ log("");
11246
+ log("── [3/6] npx install (@larksuite/openclaw-lark-tools update) ──");
11247
+ const npxResult = (0, node_child_process.spawnSync)("npx", [
11248
+ "-y",
11249
+ "@larksuite/openclaw-lark-tools",
11250
+ "update"
11251
+ ], {
11252
+ cwd,
11253
+ encoding: "utf-8",
11254
+ stdio: [
11255
+ "ignore",
11256
+ "pipe",
11257
+ "pipe"
11258
+ ],
11259
+ timeout: 12e4
11260
+ });
11261
+ const npxStdout = npxResult.stdout?.trim() ?? "";
11262
+ const npxStderr = npxResult.stderr?.trim() ?? "";
11263
+ const npxExitCode = npxResult.status ?? 1;
11264
+ if (npxStdout) log(`npx stdout:\n${npxStdout}`);
11265
+ if (npxStderr) log(`npx stderr:\n${npxStderr}`);
11266
+ log(`npx exit: ${npxExitCode}${npxResult.error ? ` error: ${npxResult.error.message}` : ""}`);
11267
+ if (statusCheckDelayMs > 0) {
11268
+ log("");
11269
+ log(`── 等待 ${statusCheckDelayMs / 1e3}s(让 openclaw 服务完成重启) ─────────────`);
11270
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, statusCheckDelayMs);
11271
+ log("wait done");
11272
+ }
11273
+ const doRollback = (reason) => {
11274
+ log(`ERROR: ${reason}`);
11275
+ const rollbackOk = restoreFiles(fsOpts);
11276
+ log(`rollback: ${rollbackOk ? "ok" : "FAILED"}`);
11277
+ return {
11278
+ ok: false,
11279
+ error: reason,
11280
+ validationError: reason,
11281
+ stdout: npxStdout,
11282
+ stderr: npxStderr,
11283
+ exitCode: npxExitCode,
11284
+ rollbackOk,
11285
+ logFile
11286
+ };
11287
+ };
11288
+ log("");
11289
+ log("── [4/5] 安装后诊断校验 ─────────────────────────────────");
11290
+ logVersionSnapshot("after-versions", snapshotVersions(cwd, log), log);
11291
+ let afterVersionIncompatible = false;
11292
+ try {
11293
+ const rawConfig = node_fs.default.readFileSync(configPath, "utf-8");
11294
+ afterVersionIncompatible = needsLarkUpgrade({
11295
+ config: loadJSON5().parse(rawConfig),
11296
+ configPath,
11297
+ vars: {},
11298
+ providerDeps: {
11299
+ usesMiaodaProvider: false,
11300
+ usesMiaodaSecretProvider: false
11301
+ }
11302
+ });
11303
+ log(` version-compat post-check: ${afterVersionIncompatible ? "STILL_INCOMPATIBLE" : "ok"}`);
11304
+ } catch (e) {
11305
+ log(` version-compat post-check error: ${e.message} — version signal unavailable`);
11306
+ }
11307
+ const afterChannels = probeChannels("after", log, 6e4);
11308
+ log(` feishu config invalid after: ${afterChannels.feishuConfigInvalid}`);
11309
+ const stillNeedsUpgrade = (afterVersionIncompatible || afterChannels.feishuConfigInvalid) && !afterChannels.anyAccountWorking;
11310
+ log(` post-check: stillNeedsUpgrade=${stillNeedsUpgrade} (version=${afterVersionIncompatible}, feishuConfig=${afterChannels.feishuConfigInvalid}, channelsWorking=${afterChannels.anyAccountWorking})`);
11311
+ if (stillNeedsUpgrade) return doRollback(`post-install diagnosis still shows anomaly: versionIncompatible=${afterVersionIncompatible}, feishuConfigInvalid=${afterChannels.feishuConfigInvalid}, anyAccountWorking=${afterChannels.anyAccountWorking}`);
11312
+ log(" post-install diagnosis: ok (upgrade conditions resolved)");
11313
+ log("");
11314
+ log("── [6/6] doctor --fix ────────────────────────────────────");
11315
+ const fixArgs = ["doctor", "--fix"];
11316
+ if (opts.scene) fixArgs.push(`--scene=${opts.scene}`);
11317
+ const fixResult = (0, node_child_process.spawnSync)(process.execPath, [cliScript, ...fixArgs], {
11318
+ cwd,
11319
+ encoding: "utf-8",
11320
+ stdio: [
11321
+ "ignore",
11322
+ "pipe",
11323
+ "pipe"
11324
+ ],
11325
+ timeout: 6e4,
11326
+ env: process.env
11327
+ });
11328
+ if (fixResult.stdout?.trim()) log(`doctor(fix) stdout:\n${fixResult.stdout.trim()}`);
11329
+ if (fixResult.stderr?.trim()) log(`doctor(fix) stderr:\n${fixResult.stderr.trim()}`);
11330
+ log(`doctor(fix) exit: ${fixResult.status ?? "null"}${fixResult.error ? ` error: ${fixResult.error.message}` : ""}`);
11331
+ log("");
11332
+ log(`${"=".repeat(60)}`);
11333
+ log("upgrade-lark completed successfully");
11334
+ log(`${"=".repeat(60)}`);
11335
+ return {
11336
+ ok: true,
11337
+ stdout: npxStdout,
11338
+ stderr: npxStderr,
11339
+ exitCode: npxExitCode,
11340
+ logFile
11341
+ };
11342
+ }
10915
11343
  //#endregion
10916
11344
  //#region src/index.ts
10917
11345
  const args = node_process.default.argv.slice(2);
10918
11346
  const mode = args.find((a) => !a.startsWith("-"));
10919
11347
  /**
11348
+ * Decode `--ctx=<base64>` into an opaque JSON object. Returns undefined when
11349
+ * the flag isn't present — the caller decides whether to fall back to the
11350
+ * innerapi or to error out.
11351
+ *
11352
+ * The object's shape is not enforced here; downstream code consumes it via
11353
+ * either `normalizeCtx()` (new path) or direct field access for the legacy
11354
+ * check/repair/reset contract still used by sandbox_console push.
11355
+ */
11356
+ function parseCtxFlag(args) {
11357
+ const ctxArg = args.find((a) => a.startsWith("--ctx="));
11358
+ if (!ctxArg) return void 0;
11359
+ const b64 = ctxArg.slice(6);
11360
+ return JSON.parse(Buffer.from(b64, "base64").toString("utf-8"));
11361
+ }
11362
+ /**
10920
11363
  * Pull the first non-flag positional after the mode name.
10921
11364
  * (The mode itself is args[0] in the filtered set, so we skip index 0.)
10922
11365
  */
@@ -10944,8 +11387,8 @@ function getMultiFlag(args, name) {
10944
11387
  * case but is no longer consulted.
10945
11388
  */
10946
11389
  async function reportRun(command, rc, _raw, invocation, durationMs, outcome, slardar = {
10947
- scene: void 0,
10948
- profile: "standard",
11390
+ scene,
11391
+ profile,
10949
11392
  fix: false
10950
11393
  }) {
10951
11394
  console.error(`${command}: telemetry calling report_cli_run`);
@@ -11009,7 +11452,7 @@ async function main() {
11009
11452
  console.error(`${mode}: begin argv=[${args.join(" ")}] version=${getVersion()} traceId=${traceId ?? "-"} caller=${caller ?? "-"} runIdGenerated=${rc.generated}`);
11010
11453
  switch (mode) {
11011
11454
  case "check": {
11012
- const raw = await fetchCtxViaInnerApi({
11455
+ const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11013
11456
  populate: planCtxPopulate({
11014
11457
  command: "check",
11015
11458
  profile
@@ -11034,7 +11477,7 @@ async function main() {
11034
11477
  break;
11035
11478
  }
11036
11479
  case "repair": {
11037
- const raw = await fetchCtxViaInnerApi({
11480
+ const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11038
11481
  populate: planCtxPopulate({
11039
11482
  command: "repair",
11040
11483
  profile
@@ -11105,15 +11548,27 @@ async function main() {
11105
11548
  break;
11106
11549
  }
11107
11550
  case "reset":
11108
- if (args.includes("--async")) console.log(JSON.stringify(startAsyncReset()));
11109
- else if (args.includes("--worker")) {
11551
+ if (args.includes("--async")) {
11552
+ const ctxArg = args.find((a) => a.startsWith("--ctx="));
11553
+ let ctxBase64;
11554
+ if (ctxArg) ctxBase64 = ctxArg.slice(6);
11555
+ else {
11556
+ const fetched = await fetchCtxViaInnerApi({
11557
+ populate: planCtxPopulate({ command: "reset" }),
11558
+ caller,
11559
+ traceId
11560
+ });
11561
+ ctxBase64 = Buffer.from(JSON.stringify(fetched), "utf-8").toString("base64");
11562
+ }
11563
+ console.log(JSON.stringify(startAsyncReset(ctxBase64)));
11564
+ } else if (args.includes("--worker")) {
11110
11565
  const taskId = args.find((a) => a.startsWith("--task-id="))?.slice(10);
11111
11566
  if (!taskId) {
11112
11567
  console.error("Error: --task-id=<id> is required for worker");
11113
11568
  node_process.default.exit(1);
11114
11569
  }
11115
11570
  const resultFile = resetResultFile(taskId);
11116
- const raw = await fetchCtxViaInnerApi({
11571
+ const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11117
11572
  populate: planCtxPopulate({ command: "reset" }),
11118
11573
  caller,
11119
11574
  traceId
@@ -11137,7 +11592,7 @@ async function main() {
11137
11592
  return;
11138
11593
  }
11139
11594
  } else {
11140
- console.error("Usage: reset --async | reset --worker --task-id=<id>");
11595
+ console.error("Usage: reset --async [--ctx=<base64>] | reset --worker --task-id=<id> [--ctx=<base64>]");
11141
11596
  node_process.default.exit(1);
11142
11597
  }
11143
11598
  break;
@@ -11153,14 +11608,14 @@ async function main() {
11153
11608
  case "install-openclaw": {
11154
11609
  const tag = getPositionalTag(args, "install-openclaw");
11155
11610
  if (!tag) {
11156
- console.error("Usage: install-openclaw <tag> [--oss_file_map=<base64>]");
11611
+ console.error("Usage: install-openclaw <tag> [--ctx=<base64> | --oss_file_map=<base64>]");
11157
11612
  node_process.default.exit(1);
11158
11613
  }
11159
11614
  const ossFileMapFlag = getFlag(args, "oss_file_map");
11160
11615
  let installOssFileMap;
11161
11616
  let rawForTelemetry;
11162
11617
  if (!ossFileMapFlag) {
11163
- rawForTelemetry = await fetchCtxViaInnerApi({
11618
+ rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11164
11619
  populate: planCtxPopulate({ command: "install" }),
11165
11620
  caller,
11166
11621
  traceId
@@ -11195,7 +11650,7 @@ async function main() {
11195
11650
  case "install-extension": {
11196
11651
  const tag = getPositionalTag(args, "install-extension");
11197
11652
  if (!tag) {
11198
- console.error("Usage: install-extension <tag> (--all | --extension=<name>...) [--home_base=<dir>] [--config_path=<path>] [--skip-config-update] [--oss_file_map=<base64>]");
11653
+ console.error("Usage: install-extension <tag> (--all | --extension=<name>...) [--home_base=<dir>] [--config_path=<path>] [--skip-config-update] [--ctx=<base64> | --oss_file_map=<base64>]");
11199
11654
  node_process.default.exit(1);
11200
11655
  }
11201
11656
  const all = args.includes("--all");
@@ -11207,7 +11662,7 @@ async function main() {
11207
11662
  let installOssFileMap;
11208
11663
  let rawForTelemetry;
11209
11664
  if (!ossFileMapFlag) {
11210
- rawForTelemetry = await fetchCtxViaInnerApi({
11665
+ rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11211
11666
  populate: planCtxPopulate({ command: "install" }),
11212
11667
  caller,
11213
11668
  traceId
@@ -11253,12 +11708,12 @@ async function main() {
11253
11708
  case "install-cli": {
11254
11709
  const tag = getPositionalTag(args, "install-cli");
11255
11710
  if (!tag) {
11256
- console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--oss_file_map=<base64>]");
11711
+ console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--ctx=<base64> | --oss_file_map=<base64>]");
11257
11712
  node_process.default.exit(1);
11258
11713
  }
11259
11714
  const names = getMultiFlag(args, "cli");
11260
11715
  if (names.length === 0) {
11261
- console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--oss_file_map=<base64>]");
11716
+ console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--ctx=<base64> | --oss_file_map=<base64>]");
11262
11717
  node_process.default.exit(1);
11263
11718
  }
11264
11719
  const homeBase = getFlag(args, "home_base");
@@ -11266,7 +11721,7 @@ async function main() {
11266
11721
  let installOssFileMap;
11267
11722
  let rawForTelemetry;
11268
11723
  if (!ossFileMapFlag) {
11269
- rawForTelemetry = await fetchCtxViaInnerApi({
11724
+ rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11270
11725
  populate: planCtxPopulate({ command: "install" }),
11271
11726
  caller,
11272
11727
  traceId
@@ -11314,7 +11769,7 @@ async function main() {
11314
11769
  case "download-resource": {
11315
11770
  const tag = getPositionalTag(args, "download-resource");
11316
11771
  if (!tag) {
11317
- console.error("Usage: download-resource <tag> --role=<role> --name=<name> [--dir=<dir>] [--oss_file_map=<base64>]");
11772
+ console.error("Usage: download-resource <tag> --role=<role> --name=<name> [--dir=<dir>] [--ctx=<base64> | --oss_file_map=<base64>]");
11318
11773
  node_process.default.exit(1);
11319
11774
  }
11320
11775
  const role = getFlag(args, "role");
@@ -11328,7 +11783,7 @@ async function main() {
11328
11783
  let installOssFileMap;
11329
11784
  let rawForTelemetry;
11330
11785
  if (!ossFileMapFlag) {
11331
- rawForTelemetry = await fetchCtxViaInnerApi({
11786
+ rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11332
11787
  populate: planCtxPopulate({ command: "install" }),
11333
11788
  caller,
11334
11789
  traceId
@@ -11402,6 +11857,50 @@ async function main() {
11402
11857
  if (!result.ok) node_process.default.exit(1);
11403
11858
  break;
11404
11859
  }
11860
+ case "upgrade-lark": {
11861
+ const result = runUpgradeLark({
11862
+ runId: rc.runId,
11863
+ scene
11864
+ });
11865
+ const upgradeDurationMs = Date.now() - t0;
11866
+ console.log(JSON.stringify(result));
11867
+ reportUpgradeLarkToSlardar({
11868
+ scene,
11869
+ durationMs: upgradeDurationMs,
11870
+ success: result.ok,
11871
+ logFile: result.logFile,
11872
+ exitCode: result.exitCode,
11873
+ rollbackOk: result.rollbackOk,
11874
+ validationError: result.validationError,
11875
+ error: result.error
11876
+ });
11877
+ try {
11878
+ await reportCliRun({
11879
+ command: "upgrade-lark",
11880
+ runId: rc.runId,
11881
+ version: getVersion(),
11882
+ invocation: args.join(" "),
11883
+ durationMs: upgradeDurationMs,
11884
+ caller: rc.caller,
11885
+ traceId: rc.traceId,
11886
+ success: result.ok,
11887
+ result,
11888
+ error: result.ok ? void 0 : { message: result.error ?? "upgrade-lark failed" }
11889
+ });
11890
+ } catch (e) {
11891
+ console.error(`[telemetry] reportCliRun failed: ${e.message}`);
11892
+ }
11893
+ if (!result.ok) {
11894
+ node_process.default.exitCode = 1;
11895
+ return;
11896
+ }
11897
+ break;
11898
+ }
11899
+ case "channels-probe": {
11900
+ const result = runChannelsProbe(getFlag(args, "timeout") ? Number(getFlag(args, "timeout")) : void 0);
11901
+ console.log(JSON.stringify(result));
11902
+ break;
11903
+ }
11405
11904
  default:
11406
11905
  node_process.default.stderr.write(`Unknown command: ${mode}\n\n`);
11407
11906
  node_process.default.stderr.write(formatTopLevelHelp(helpFlags.expert));