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

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 +1231 -836
  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.15-alpha.0";
55
+ return "0.1.15-alpha.10";
56
56
  }
57
57
  //#endregion
58
58
  //#region src/rule-engine/base.ts
@@ -1770,6 +1770,96 @@ SessionPersistenceRule = __decorate([Rule({
1770
1770
  level: "silent"
1771
1771
  })], SessionPersistenceRule);
1772
1772
  //#endregion
1773
+ //#region src/rules/tools-allow-also-allow-conflict.ts
1774
+ /**
1775
+ * 检测 tools 配置中 allow 与 alsoAllow 同时非空的冲突。
1776
+ *
1777
+ * openclaw 的 Zod schema 在配置加载时会拒绝这种组合,原因是语义互斥:
1778
+ * - allow:完全替换式白名单,只允许列出的工具
1779
+ * - alsoAllow:追加式,在 profile 或默认基线上叠加额外工具
1780
+ * 两者同时存在会导致 openclaw 服务启动失败。
1781
+ *
1782
+ * 修复策略:将 alsoAllow 的条目去重合并进 allow,删除 alsoAllow。
1783
+ * 这是最保守的修复:保留原 allow 的精确白名单,不扩大权限范围。
1784
+ *
1785
+ * 检测范围:
1786
+ * - 顶层 tools
1787
+ * - tools.byProvider.*
1788
+ * - agents.list[].tools
1789
+ * - agents.list[].tools.byProvider.*
1790
+ */
1791
+ let ToolsAllowAlsoAllowConflictRule = class ToolsAllowAlsoAllowConflictRule extends DiagnoseRule {
1792
+ validate(ctx) {
1793
+ const conflicts = [];
1794
+ visitAllScopes(ctx.config, (scope, label) => {
1795
+ if (hasConflict(scope)) conflicts.push(label);
1796
+ });
1797
+ if (conflicts.length === 0) return { pass: true };
1798
+ return {
1799
+ pass: false,
1800
+ message: `tools allow 与 alsoAllow 冲突(${conflicts.length} 处):${conflicts.join(";")}。修复方式:将 alsoAllow 合并进 allow 并删除 alsoAllow`
1801
+ };
1802
+ }
1803
+ repair(ctx) {
1804
+ visitAllScopes(ctx.config, (scope) => mergeInScope(scope));
1805
+ }
1806
+ };
1807
+ ToolsAllowAlsoAllowConflictRule = __decorate([Rule({
1808
+ key: "tools_allow_also_allow_conflict",
1809
+ description: "tools 配置中 allow 与 alsoAllow 同时设置会导致 openclaw 启动失败;自动将 alsoAllow 合并进 allow(实验性)",
1810
+ dependsOn: ["config_syntax_check"],
1811
+ repairMode: "standard",
1812
+ level: "critical",
1813
+ profile: "experimental"
1814
+ })], ToolsAllowAlsoAllowConflictRule);
1815
+ /**
1816
+ * 遍历所有 tools-policy scope,对每个 scope 调用 callback。
1817
+ * validate 和 repair 共用同一套遍历逻辑,确保两者覆盖的 scope 始终一致。
1818
+ */
1819
+ function visitAllScopes(config, cb) {
1820
+ const topTools = asRecord(asRecord(config)?.tools);
1821
+ if (topTools) {
1822
+ cb(topTools, "tools");
1823
+ const byProvider = asRecord(topTools.byProvider);
1824
+ if (byProvider) for (const key of Object.keys(byProvider)) {
1825
+ const scope = asRecord(byProvider[key]);
1826
+ if (scope) cb(scope, `tools.byProvider.${key}`);
1827
+ }
1828
+ }
1829
+ const agents = asRecord(asRecord(config)?.agents);
1830
+ const list = Array.isArray(agents?.list) ? agents.list : [];
1831
+ for (let i = 0; i < list.length; i++) {
1832
+ const agent = asRecord(list[i]);
1833
+ const agentId = typeof agent?.id === "string" ? agent.id : String(i);
1834
+ const tools = asRecord(agent?.tools);
1835
+ if (tools) {
1836
+ cb(tools, `agents.list[${agentId}].tools`);
1837
+ const byProvider = asRecord(tools.byProvider);
1838
+ if (byProvider) for (const key of Object.keys(byProvider)) {
1839
+ const scope = asRecord(byProvider[key]);
1840
+ if (scope) cb(scope, `agents.list[${agentId}].tools.byProvider.${key}`);
1841
+ }
1842
+ }
1843
+ }
1844
+ }
1845
+ /** scope 中 allow 与 alsoAllow 同时非空 */
1846
+ function hasConflict(scope) {
1847
+ const allow = scope.allow;
1848
+ const alsoAllow = scope.alsoAllow;
1849
+ return Array.isArray(allow) && allow.length > 0 && Array.isArray(alsoAllow) && alsoAllow.length > 0;
1850
+ }
1851
+ /**
1852
+ * 将 alsoAllow 去重合并进 allow,删除 alsoAllow。
1853
+ * 仅保留字符串元素,过滤掉格式异常的非字符串条目,避免写入损坏配置。
1854
+ */
1855
+ function mergeInScope(scope) {
1856
+ if (!hasConflict(scope)) return;
1857
+ const allow = scope.allow.filter((v) => typeof v === "string");
1858
+ const alsoAllow = scope.alsoAllow.filter((v) => typeof v === "string");
1859
+ scope.allow = [...new Set([...allow, ...alsoAllow])];
1860
+ delete scope.alsoAllow;
1861
+ }
1862
+ //#endregion
1773
1863
  //#region src/rules/feishu-default-account.ts
1774
1864
  /**
1775
1865
  * Owns the multi-agent feishu-channel migration: turns legacy v1/v2
@@ -2520,242 +2610,581 @@ function upsertResourceConstrainedToolsBlock(content) {
2520
2610
  return `${content}${content.length > 0 && !content.endsWith("\n") ? "\n\n" : "\n"}${RESOURCE_CONSTRAINED_TOOLS_BLOCK}\n`;
2521
2611
  }
2522
2612
  //#endregion
2523
- //#region src/rules/miaoda-official-plugins-install-spec-unlock.ts
2613
+ //#region src/paths.ts
2524
2614
  /**
2525
- * Official miaoda-side plugins that must track manifest version-locked specs
2526
- * here block upgrades. Third-party / user-installed plugins are intentionally
2527
- * out of scope (users may pin them deliberately).
2615
+ * Central directory for all ephemeral diagnose/reset artifacts: task status
2616
+ * files (`reset-<taskId>.json`) and human-readable step logs
2617
+ * (`reset-<taskId>.log`). Having everything under one dir makes debugging a
2618
+ * stuck reset much easier — `ls /tmp/openclaw-diagnose/` shows every recent
2619
+ * run, and each run's log is right next to its state.
2528
2620
  */
2529
- const OFFICIAL_PLUGIN_NAMES = new Set([
2530
- "openclaw-extension-miaoda",
2531
- "openclaw-extension-miaoda-coding",
2532
- "openclaw-guardian-plugin",
2533
- "openclaw-mem0-plugin",
2534
- "openclaw-lark"
2535
- ]);
2536
- const LOCKED_NPM_SPEC = /^(@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*@[^@/:#\s]+$/i;
2537
- function isLockedNpmSpec(spec) {
2538
- return typeof spec === "string" && LOCKED_NPM_SPEC.test(spec);
2621
+ const DIAGNOSE_DIR = "/tmp/openclaw-diagnose";
2622
+ function resetResultFile(taskId) {
2623
+ return `${DIAGNOSE_DIR}/reset-${taskId}.json`;
2539
2624
  }
2540
- function unlockSpec(spec) {
2541
- const slash = spec.indexOf("/");
2542
- const cut = slash === -1 ? spec.indexOf("@") : spec.indexOf("@", slash + 1);
2543
- return spec.slice(0, cut);
2625
+ function resetLogFile(taskId) {
2626
+ return `${DIAGNOSE_DIR}/reset-${taskId}.log`;
2544
2627
  }
2545
- /** Yield `[key, lockedSpec]` for every official-plugin install whose `spec` is locked. */
2546
- function* iterLockedOfficialInstalls(config) {
2547
- const installs = getNestedMap(config, "plugins", "installs");
2548
- if (!installs) return;
2549
- for (const [key, entry] of Object.entries(installs)) {
2550
- if (!OFFICIAL_PLUGIN_NAMES.has(key)) continue;
2551
- const spec = asRecord(entry)?.spec;
2552
- if (isLockedNpmSpec(spec)) yield [key, spec];
2553
- }
2628
+ /** Sandbox workspace root where openclaw config + agent state lives. */
2629
+ const WORKSPACE_DIR = "/home/gem/workspace/agent";
2630
+ /** File containing the provider key used by the openclaw miaoda provider. */
2631
+ const PROVIDER_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-provider-key";
2632
+ /** File containing the miaoda openclaw secrets JSON. */
2633
+ const SECRETS_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-openclaw-secrets.json";
2634
+ /** Absolute path to the openclaw config JSON. */
2635
+ const CONFIG_PATH = `${WORKSPACE_DIR}/openclaw.json`;
2636
+ /**
2637
+ * upgrade-lark 每次运行的日志文件路径,含时间戳便于按时间排序定位。
2638
+ * checkOnly=true 时文件名含 "-check" 后缀,便于与正式安装日志区分。
2639
+ */
2640
+ function upgradeLarkLogFile(runId, checkOnly = false) {
2641
+ const ts = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/:/g, "-");
2642
+ return `${DIAGNOSE_DIR}/upgrade-lark${checkOnly ? "-check" : ""}-${ts}-${runId.slice(0, 8)}.log`;
2554
2643
  }
2555
- let MiaodaOfficialPluginsInstallSpecUnlockRule = class MiaodaOfficialPluginsInstallSpecUnlockRule extends DiagnoseRule {
2556
- validate(ctx) {
2557
- const locked = [...iterLockedOfficialInstalls(ctx.config)].map(([k]) => k);
2558
- if (locked.length === 0) return { pass: true };
2559
- return {
2560
- pass: false,
2561
- message: "plugins.installs 中官方插件存在锁版本的 spec: " + locked.sort().join(",")
2562
- };
2563
- }
2564
- repair(ctx) {
2565
- for (const [key, spec] of iterLockedOfficialInstalls(ctx.config)) setNestedValue(ctx.config, [
2566
- "plugins",
2567
- "installs",
2568
- key,
2569
- "spec"
2570
- ], unlockSpec(spec));
2571
- }
2572
- };
2573
- MiaodaOfficialPluginsInstallSpecUnlockRule = __decorate([Rule({
2574
- key: "miaoda_official_plugins_install_spec_unlock",
2575
- description: "移除官方妙搭插件安装条目中的锁版本 npm spec,使其跟随最新 manifest 版本",
2576
- dependsOn: ["config_syntax_check"],
2577
- repairMode: "standard",
2578
- level: "silent"
2579
- })], MiaodaOfficialPluginsInstallSpecUnlockRule);
2580
2644
  //#endregion
2581
- //#region src/rules/miaoda-plugin-allow.ts
2582
- const MIAODA_PLUGIN = "openclaw-extension-miaoda";
2583
- let MiaodaPluginAllowRule = class MiaodaPluginAllowRule extends DiagnoseRule {
2584
- validate(ctx) {
2585
- if (!isPluginInstalledOnDisk(getExtensionsDir(ctx.configPath), MIAODA_PLUGIN)) return { pass: true };
2586
- if (getAllow$1(ctx.config).includes(MIAODA_PLUGIN)) return { pass: true };
2587
- return {
2588
- pass: false,
2589
- message: `plugins.allow 缺少 ${MIAODA_PLUGIN}(已在 extensions/ 下装但未启用)`
2590
- };
2591
- }
2592
- repair(ctx) {
2593
- if (!isPluginInstalledOnDisk(getExtensionsDir(ctx.configPath), MIAODA_PLUGIN)) return;
2594
- const plugins = ctx.config.plugins;
2595
- if (plugins == null || typeof plugins !== "object" || Array.isArray(plugins)) {
2596
- ctx.config.plugins = { allow: [MIAODA_PLUGIN] };
2597
- return;
2645
+ //#region src/lark-cli-init.ts
2646
+ const LARK_PLUGIN_NAMES$1 = ["openclaw-lark", "feishu-openclaw-plugin"];
2647
+ const PE_XML_TAG = "lark-cli-pe";
2648
+ const PE_PLACEHOLDER = `
2649
+ <${PE_XML_TAG}>
2650
+ **【强制要求 - 无例外】飞书所有操作必须先读 feishu-lark-cli skill。未读skill不得调用任何飞书工具,违者视为操作失误。**
2651
+ </${PE_XML_TAG}>
2652
+ `;
2653
+ function isLarkPluginInstalled(configPath) {
2654
+ const extDir = getExtensionsDir(configPath);
2655
+ return LARK_PLUGIN_NAMES$1.some((name) => {
2656
+ try {
2657
+ return node_fs.default.existsSync(node_path.default.join(extDir, name, "package.json"));
2658
+ } catch {
2659
+ return false;
2598
2660
  }
2599
- const pluginsMap = plugins;
2600
- const rawAllow = pluginsMap.allow;
2601
- const allow = Array.isArray(rawAllow) ? rawAllow : [];
2602
- if (allow.includes(MIAODA_PLUGIN)) return;
2603
- allow.push(MIAODA_PLUGIN);
2604
- pluginsMap.allow = allow;
2605
- }
2606
- };
2607
- MiaodaPluginAllowRule = __decorate([Rule({
2608
- key: "miaoda_plugin_allow",
2609
- description: "当 openclaw-extension-miaoda 已在磁盘安装但未在 allow 列表中时,将其添加到 plugins.allow(实验性)",
2610
- dependsOn: ["config_syntax_check"],
2611
- repairMode: "standard",
2612
- level: "critical",
2613
- profile: "standard"
2614
- })], MiaodaPluginAllowRule);
2615
- function getAllow$1(config) {
2616
- const plugins = config.plugins;
2617
- if (plugins == null || typeof plugins !== "object" || Array.isArray(plugins)) return [];
2618
- const allow = plugins.allow;
2619
- if (!Array.isArray(allow)) return [];
2620
- return allow.filter((e) => typeof e === "string");
2661
+ });
2621
2662
  }
2622
- //#endregion
2623
- //#region src/rules/lark-plugin-allow.ts
2624
- const LARK_PLUGIN = "openclaw-lark";
2625
- const LEGACY_LARK_PLUGIN = "feishu-openclaw-plugin";
2626
- const LARK_PLUGIN_NAMES$1 = [LARK_PLUGIN, LEGACY_LARK_PLUGIN];
2627
- let LarkPluginAllowRule = class LarkPluginAllowRule extends DiagnoseRule {
2628
- validate(ctx) {
2629
- const allow = getAllow(ctx.config);
2630
- if (LARK_PLUGIN_NAMES$1.some((name) => allow.includes(name))) return { pass: true };
2631
- const installed = detectInstalledLarkPlugin(getExtensionsDir(ctx.configPath));
2632
- if (installed == null) return { pass: true };
2633
- return {
2634
- pass: false,
2635
- message: `plugins.allow 缺少飞书插件 ${installed}(已在 extensions/ 下装但未启用)`
2636
- };
2663
+ function isLarkCliAvailable$2() {
2664
+ try {
2665
+ return (0, node_child_process.spawnSync)("lark-cli", ["--version"], {
2666
+ encoding: "utf-8",
2667
+ timeout: 5e3,
2668
+ stdio: [
2669
+ "ignore",
2670
+ "pipe",
2671
+ "ignore"
2672
+ ]
2673
+ }).status === 0;
2674
+ } catch {
2675
+ return false;
2637
2676
  }
2638
- repair(ctx) {
2639
- const installed = detectInstalledLarkPlugin(getExtensionsDir(ctx.configPath));
2640
- if (installed == null) return;
2641
- if (ctx.config.plugins == null || typeof ctx.config.plugins !== "object" || Array.isArray(ctx.config.plugins)) {
2642
- ctx.config.plugins = { allow: [installed] };
2643
- return;
2644
- }
2645
- const pluginsMap = ctx.config.plugins;
2646
- const rawAllow = pluginsMap.allow;
2647
- const original = Array.isArray(rawAllow) ? rawAllow : [];
2648
- const stringAllow = original.filter((e) => typeof e === "string");
2649
- if (LARK_PLUGIN_NAMES$1.some((name) => stringAllow.includes(name))) return;
2650
- original.push(installed);
2651
- pluginsMap.allow = original;
2677
+ }
2678
+ function readConfig(configPath) {
2679
+ try {
2680
+ const raw = node_fs.default.readFileSync(configPath, "utf-8");
2681
+ const parsed = loadJSON5().parse(raw);
2682
+ return parsed != null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
2683
+ } catch {
2684
+ return null;
2652
2685
  }
2653
- };
2654
- LarkPluginAllowRule = __decorate([Rule({
2655
- key: "lark_plugin_allow",
2656
- description: "当飞书插件(openclaw-lark 或旧版名)已在磁盘安装但未加入 plugins.allow 时,自动添加",
2657
- dependsOn: ["config_syntax_check"],
2658
- repairMode: "standard",
2659
- level: "critical"
2660
- })], LarkPluginAllowRule);
2661
- function getAllow(config) {
2662
- const plugins = config.plugins;
2663
- if (plugins == null || typeof plugins !== "object" || Array.isArray(plugins)) return [];
2664
- const allow = plugins.allow;
2665
- if (!Array.isArray(allow)) return [];
2666
- return allow.filter((e) => typeof e === "string");
2667
2686
  }
2668
2687
  /**
2669
- * fs-only 检测:`<extDir>/<name>/package.json` 存在即视为已装。
2670
- * 优先级 openclaw-lark(新版)> feishu-openclaw-plugin(legacy)。
2671
- * 不读 package.json 内容,只判存在性,避开 JSON 损坏。
2688
+ * Resolve the feishu app secret for the given appId.
2689
+ *
2690
+ * Lookup order:
2691
+ * 1. channels.feishu.appSecret (single-agent: feishu.appId === appId)
2692
+ * 2. channels.feishu.accounts[key].appSecret (multi-agent: account.appId === appId)
2693
+ *
2694
+ * Value interpretation:
2695
+ * - string → use directly
2696
+ * - object → secret is managed by a provider; use `feishuAppSecret` param instead
2697
+ *
2698
+ * Returns null when the secret cannot be determined.
2672
2699
  */
2673
- function detectInstalledLarkPlugin(extDir) {
2674
- for (const name of [LARK_PLUGIN, LEGACY_LARK_PLUGIN]) if (pluginPackageJsonExists(extDir, name)) return name;
2700
+ function resolveAppSecret(appId, config, feishuAppSecret) {
2701
+ const feishu = getNestedMap(config, "channels", "feishu");
2702
+ if (!feishu) return null;
2703
+ let rawSecret;
2704
+ if (typeof feishu.appId === "string" && feishu.appId === appId) rawSecret = feishu.appSecret;
2705
+ else {
2706
+ const accounts = asRecord(feishu.accounts);
2707
+ if (accounts) for (const [, val] of Object.entries(accounts)) {
2708
+ const account = asRecord(val);
2709
+ if (account?.appId === appId) {
2710
+ rawSecret = account.appSecret ?? feishu.appSecret;
2711
+ break;
2712
+ }
2713
+ }
2714
+ }
2715
+ if (typeof rawSecret === "string" && rawSecret) return rawSecret;
2716
+ if (rawSecret != null && typeof rawSecret === "object") return feishuAppSecret ?? null;
2675
2717
  return null;
2676
2718
  }
2677
- function pluginPackageJsonExists(extDir, pluginDir) {
2678
- try {
2679
- return node_fs.default.existsSync(node_path.default.join(extDir, pluginDir, "package.json"));
2680
- } catch {
2681
- return false;
2719
+ /**
2720
+ * Resolve the agents.md path for the given appId from the openclaw config.
2721
+ *
2722
+ * Case 1: appId matches channels.feishu.appId (single-agent path)
2723
+ * → WORKSPACE_DIR/AGENTS.md
2724
+ *
2725
+ * Case 2: appId found in channels.feishu.accounts (multi-agent path)
2726
+ * → find account key where account.appId === appId
2727
+ * → find binding where match.channel=feishu && match.accountId=that key
2728
+ * → if agentId === 'main' → WORKSPACE_DIR/agents.md
2729
+ * → else find agent in agents.list by id → agent.workspace/agents.md
2730
+ *
2731
+ * Returns null when the path cannot be determined.
2732
+ */
2733
+ function resolveAgentsMdPath(appId, config) {
2734
+ const feishu = getNestedMap(config, "channels", "feishu");
2735
+ if (!feishu) {
2736
+ console.error("resolveAgentsMdPath: channels.feishu not found");
2737
+ return null;
2738
+ }
2739
+ if (typeof feishu.appId === "string" && feishu.appId === appId) {
2740
+ console.error(`resolveAgentsMdPath: case=single-agent feishu.appId=${feishu.appId}`);
2741
+ return node_path.default.join(WORKSPACE_DIR, "workspace", "AGENTS.md");
2742
+ }
2743
+ const accounts = asRecord(feishu.accounts);
2744
+ if (!accounts) {
2745
+ console.error("resolveAgentsMdPath: feishu.accounts not found");
2746
+ return null;
2747
+ }
2748
+ let accountId;
2749
+ for (const [key, val] of Object.entries(accounts)) if (asRecord(val)?.appId === appId) {
2750
+ accountId = key;
2751
+ break;
2752
+ }
2753
+ if (!accountId) {
2754
+ console.error(`resolveAgentsMdPath: no account found with appId=${appId} in feishu.accounts keys=[${Object.keys(accounts).join(",")}]`);
2755
+ return null;
2756
+ }
2757
+ console.error(`resolveAgentsMdPath: found accountId=${accountId}`);
2758
+ const bindings = Array.isArray(config.bindings) ? config.bindings : [];
2759
+ let agentId;
2760
+ for (const b of bindings) {
2761
+ const binding = asRecord(b);
2762
+ if (!binding) continue;
2763
+ const match = asRecord(binding.match);
2764
+ if (match?.channel === "feishu" && match?.accountId === accountId) {
2765
+ if (typeof binding.agentId === "string") {
2766
+ agentId = binding.agentId;
2767
+ break;
2768
+ }
2769
+ }
2770
+ }
2771
+ if (!agentId) {
2772
+ console.error(`resolveAgentsMdPath: no binding found for accountId=${accountId} in bindings(count=${bindings.length})`);
2773
+ return null;
2774
+ }
2775
+ console.error(`resolveAgentsMdPath: found agentId=${agentId}`);
2776
+ if (agentId === "main") {
2777
+ console.error("resolveAgentsMdPath: case=multi-agent-main");
2778
+ return node_path.default.join(WORKSPACE_DIR, "workspace", "AGENTS.md");
2779
+ }
2780
+ const agentsObj = asRecord(config.agents);
2781
+ const list = Array.isArray(agentsObj?.list) ? agentsObj.list : [];
2782
+ for (const a of list) {
2783
+ const agent = asRecord(a);
2784
+ if (agent?.id === agentId) {
2785
+ const ws = typeof agent.workspace === "string" ? agent.workspace : node_path.default.join(WORKSPACE_DIR, "workspace");
2786
+ console.error(`resolveAgentsMdPath: case=multi-agent-custom agentId=${agentId} workspace=${ws}`);
2787
+ return node_path.default.join(ws, "AGENTS.md");
2788
+ }
2682
2789
  }
2790
+ console.error(`resolveAgentsMdPath: agentId=${agentId} not found in agents.list(count=${list.length})`);
2791
+ return null;
2683
2792
  }
2684
- //#endregion
2685
- //#region src/rules/old-miaoda-plugins-cleanup.ts
2686
- const NEW_MIAODA = "openclaw-extension-miaoda";
2687
- const OLD_PLUGIN_NAMES = Object.freeze([
2688
- "openclaw-feishu-greeting",
2689
- "openclaw-miaoda-keepalive",
2690
- "feishu-greeting",
2691
- "miaoda-keepalive"
2692
- ]);
2693
- function getPluginMaps(config) {
2694
- const rawAllow = asRecord(config.plugins)?.allow;
2793
+ function appendPeToAgentsMd(agentsMdPath) {
2794
+ const dir = node_path.default.dirname(agentsMdPath);
2795
+ if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
2796
+ const existing = node_fs.default.existsSync(agentsMdPath) ? node_fs.default.readFileSync(agentsMdPath, "utf-8") : "";
2797
+ if (existing.includes(`<lark-cli-pe>`)) {
2798
+ console.error(`lark-cli-init: <${PE_XML_TAG}> already present in ${agentsMdPath}, skipping`);
2799
+ return;
2800
+ }
2801
+ const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
2802
+ node_fs.default.appendFileSync(agentsMdPath, `${sep}${PE_PLACEHOLDER}`, "utf-8");
2803
+ console.error(`lark-cli-init: appended PE placeholder to ${agentsMdPath}`);
2804
+ }
2805
+ /**
2806
+ * Collect every Feishu bot appId declared in the openclaw config.
2807
+ * Covers both single-agent (channels.feishu.appId) and multi-agent
2808
+ * (channels.feishu.accounts[*].appId) layouts. Returns a deduplicated list.
2809
+ */
2810
+ function collectFeishuAppIds(configPath) {
2811
+ const config = readConfig(configPath ?? CONFIG_PATH);
2812
+ if (!config) return [];
2813
+ const feishu = getNestedMap(config, "channels", "feishu");
2814
+ if (!feishu) return [];
2815
+ const appIds = /* @__PURE__ */ new Set();
2816
+ const topAppId = feishu.appId;
2817
+ if (typeof topAppId === "string" && topAppId.trim()) appIds.add(topAppId.trim());
2818
+ const accounts = asRecord(feishu.accounts);
2819
+ if (accounts) for (const val of Object.values(accounts)) {
2820
+ const appId = asRecord(val)?.appId;
2821
+ if (typeof appId === "string" && appId.trim()) appIds.add(appId.trim());
2822
+ }
2823
+ return [...appIds];
2824
+ }
2825
+ function runLarkCliInit(opts) {
2826
+ const configPath = opts.configPath ?? CONFIG_PATH;
2827
+ if (!isLarkPluginInstalled(configPath)) {
2828
+ console.error("lark-cli-init: skipping — openclaw-lark plugin not installed");
2829
+ return {
2830
+ ok: true,
2831
+ skipped: true,
2832
+ skipReason: "openclaw-lark plugin not installed"
2833
+ };
2834
+ }
2835
+ if (!isLarkCliAvailable$2()) {
2836
+ console.error("lark-cli-init: skipping — lark-cli command not found");
2837
+ return {
2838
+ ok: true,
2839
+ skipped: true,
2840
+ skipReason: "lark-cli command not found"
2841
+ };
2842
+ }
2843
+ const config = readConfig(configPath);
2844
+ if (!config) return {
2845
+ ok: false,
2846
+ error: `could not read config at ${configPath}`
2847
+ };
2848
+ const agentsMdPath = resolveAgentsMdPath(opts.appId, config);
2849
+ console.error(`lark-cli-init: resolved agents.md path=${agentsMdPath ?? "(null)"}`);
2850
+ if (!agentsMdPath) return {
2851
+ ok: false,
2852
+ error: `could not resolve agents.md path for appId=${opts.appId}`
2853
+ };
2854
+ const appSecret = resolveAppSecret(opts.appId, config, opts.feishuAppSecret);
2855
+ if (!appSecret) return {
2856
+ ok: false,
2857
+ error: `could not resolve appSecret for appId=${opts.appId}`
2858
+ };
2859
+ console.error(`lark-cli-init: running lark-cli config init --name ${opts.appId} --app-id ${opts.appId} --brand feishu --app-secret-stdin --force-init`);
2860
+ const initRes = (0, node_child_process.spawnSync)("lark-cli", [
2861
+ "config",
2862
+ "init",
2863
+ "--name",
2864
+ opts.appId,
2865
+ "--app-id",
2866
+ opts.appId,
2867
+ "--brand",
2868
+ "feishu",
2869
+ "--app-secret-stdin",
2870
+ "--force-init"
2871
+ ], {
2872
+ stdio: [
2873
+ "pipe",
2874
+ "pipe",
2875
+ "pipe"
2876
+ ],
2877
+ encoding: "utf-8",
2878
+ input: appSecret
2879
+ });
2880
+ const configInitStdout = initRes.stdout?.trim() || void 0;
2881
+ const configInitStderr = initRes.stderr?.trim() || void 0;
2882
+ if (configInitStdout) console.error(`lark-cli config init stdout: ${configInitStdout}`);
2883
+ if (configInitStderr) console.error(`lark-cli config init stderr: ${configInitStderr}`);
2884
+ if (initRes.error) return {
2885
+ ok: false,
2886
+ configInitStdout,
2887
+ configInitStderr,
2888
+ error: `lark-cli config init spawn error: ${initRes.error.message}`
2889
+ };
2890
+ if (initRes.status !== 0) return {
2891
+ ok: false,
2892
+ configInitExitCode: initRes.status ?? void 0,
2893
+ configInitStdout,
2894
+ configInitStderr,
2895
+ error: `lark-cli config init exited with code ${initRes.status}`
2896
+ };
2897
+ appendPeToAgentsMd(agentsMdPath);
2695
2898
  return {
2696
- entries: getNestedMap(config, "plugins", "entries"),
2697
- installs: getNestedMap(config, "plugins", "installs"),
2698
- allow: Array.isArray(rawAllow) ? rawAllow : void 0
2899
+ ok: true,
2900
+ configInitExitCode: 0,
2901
+ agentsMdPath
2699
2902
  };
2700
2903
  }
2701
- function hasNewMiaoda({ entries, installs, allow }) {
2702
- return asRecord(entries?.[NEW_MIAODA]) != null || asRecord(installs?.[NEW_MIAODA]) != null || (allow?.includes(NEW_MIAODA) ?? false);
2703
- }
2704
- function findResiduals({ entries, installs, allow }, extensionsDir) {
2705
- return OLD_PLUGIN_NAMES.filter((name) => entries?.[name] != null || installs?.[name] != null || (allow?.includes(name) ?? false) || node_fs.default.existsSync(node_path.default.join(extensionsDir, name)));
2904
+ //#endregion
2905
+ //#region src/rules/agents-md-lark-cli-pe.ts
2906
+ function isLarkCliAvailable$1() {
2907
+ try {
2908
+ return (0, node_child_process.spawnSync)("lark-cli", ["--version"], {
2909
+ encoding: "utf-8",
2910
+ timeout: 5e3,
2911
+ stdio: [
2912
+ "ignore",
2913
+ "pipe",
2914
+ "ignore"
2915
+ ]
2916
+ }).status === 0;
2917
+ } catch {
2918
+ return false;
2919
+ }
2706
2920
  }
2707
- let OldMiaodaPluginsCleanupRule = class OldMiaodaPluginsCleanupRule extends DiagnoseRule {
2921
+ let AgentsMdLarkCliPeRule = class AgentsMdLarkCliPeRule extends DiagnoseRule {
2708
2922
  validate(ctx) {
2709
- const maps = getPluginMaps(ctx.config);
2710
- if (!hasNewMiaoda(maps)) return { pass: true };
2711
- const residuals = findResiduals(maps, getExtensionsDir(ctx.configPath));
2712
- if (residuals.length === 0) return { pass: true };
2923
+ if (!isLarkCliAvailable$1()) return { pass: true };
2924
+ const missingPath = collectExistingAgentsMdPaths(ctx).find((filePath) => {
2925
+ return !node_fs.default.readFileSync(filePath, "utf-8").includes(`<${PE_XML_TAG}>`);
2926
+ });
2927
+ if (!missingPath) return { pass: true };
2713
2928
  return {
2714
2929
  pass: false,
2715
- message: "旧 miaoda 插件残留: " + residuals.sort().join(",")
2930
+ message: `${missingPath} 中缺少 lark-cli-pe PE 内容,需要追加`
2716
2931
  };
2717
2932
  }
2718
2933
  repair(ctx) {
2719
- const maps = getPluginMaps(ctx.config);
2720
- if (!hasNewMiaoda(maps)) return;
2721
- const extensionsDir = getExtensionsDir(ctx.configPath);
2722
- const { entries, installs, allow } = maps;
2723
- const oldSet = new Set(OLD_PLUGIN_NAMES);
2724
- if (allow) for (let i = allow.length - 1; i >= 0; i--) {
2725
- const v = allow[i];
2726
- if (typeof v === "string" && oldSet.has(v)) allow.splice(i, 1);
2727
- }
2728
- for (const name of OLD_PLUGIN_NAMES) {
2729
- if (entries && name in entries) delete entries[name];
2730
- if (installs && name in installs) delete installs[name];
2731
- const target = node_path.default.join(extensionsDir, name);
2732
- const rel = node_path.default.relative(extensionsDir, target);
2733
- if (!rel || rel.startsWith("..") || node_path.default.isAbsolute(rel)) continue;
2734
- try {
2735
- node_fs.default.rmSync(target, {
2736
- recursive: true,
2737
- force: true
2738
- });
2739
- } catch (e) {
2740
- console.error(`[old_miaoda_plugins_cleanup] rmSync ${target} failed: ${e.message}`);
2741
- }
2934
+ if (!isLarkCliAvailable$1()) return;
2935
+ for (const filePath of collectExistingAgentsMdPaths(ctx)) {
2936
+ const content = node_fs.default.readFileSync(filePath, "utf-8");
2937
+ if (content.includes(`<lark-cli-pe>`)) continue;
2938
+ const sep = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
2939
+ node_fs.default.appendFileSync(filePath, `${sep}${PE_PLACEHOLDER}`, "utf-8");
2940
+ console.error(`agents-md-lark-cli-pe: appended PE to ${filePath}`);
2742
2941
  }
2743
2942
  }
2744
2943
  };
2745
- OldMiaodaPluginsCleanupRule = __decorate([Rule({
2746
- key: "old_miaoda_plugins_cleanup",
2747
- description: "当新版 openclaw-extension-miaoda 已存在时,清理过时插件引用(openclaw-feishu-greeting、openclaw-miaoda-keepalive 等)",
2944
+ AgentsMdLarkCliPeRule = __decorate([Rule({
2945
+ key: "agents_md_lark_cli_pe",
2946
+ description: "检测各智能体 AGENTS.md 中是否缺失 lark-cli-pe PE 内容,lark-cli 存在时自动追加",
2748
2947
  dependsOn: ["config_syntax_check"],
2749
2948
  repairMode: "standard",
2750
2949
  level: "silent"
2751
- })], OldMiaodaPluginsCleanupRule);
2950
+ })], AgentsMdLarkCliPeRule);
2752
2951
  //#endregion
2753
- //#region src/rules/builtin-plugin-missing.ts
2952
+ //#region src/rules/miaoda-official-plugins-install-spec-unlock.ts
2754
2953
  /**
2755
- * install-extension --all 安装的内置扩展插件列表(manifest role=extension)。
2756
- * miaoda-official-plugins-install-spec-unlock.ts 中的 OFFICIAL_PLUGIN_NAMES 保持一致。
2954
+ * Official miaoda-side plugins that must track manifest — version-locked specs
2955
+ * here block upgrades. Third-party / user-installed plugins are intentionally
2956
+ * out of scope (users may pin them deliberately).
2757
2957
  */
2758
- const BUILTIN_EXTENSION_PLUGINS = new Set([
2958
+ const OFFICIAL_PLUGIN_NAMES = new Set([
2959
+ "openclaw-extension-miaoda",
2960
+ "openclaw-extension-miaoda-coding",
2961
+ "openclaw-guardian-plugin",
2962
+ "openclaw-mem0-plugin",
2963
+ "openclaw-lark"
2964
+ ]);
2965
+ const LOCKED_NPM_SPEC = /^(@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*@[^@/:#\s]+$/i;
2966
+ function isLockedNpmSpec(spec) {
2967
+ return typeof spec === "string" && LOCKED_NPM_SPEC.test(spec);
2968
+ }
2969
+ function unlockSpec(spec) {
2970
+ const slash = spec.indexOf("/");
2971
+ const cut = slash === -1 ? spec.indexOf("@") : spec.indexOf("@", slash + 1);
2972
+ return spec.slice(0, cut);
2973
+ }
2974
+ /** Yield `[key, lockedSpec]` for every official-plugin install whose `spec` is locked. */
2975
+ function* iterLockedOfficialInstalls(config) {
2976
+ const installs = getNestedMap(config, "plugins", "installs");
2977
+ if (!installs) return;
2978
+ for (const [key, entry] of Object.entries(installs)) {
2979
+ if (!OFFICIAL_PLUGIN_NAMES.has(key)) continue;
2980
+ const spec = asRecord(entry)?.spec;
2981
+ if (isLockedNpmSpec(spec)) yield [key, spec];
2982
+ }
2983
+ }
2984
+ let MiaodaOfficialPluginsInstallSpecUnlockRule = class MiaodaOfficialPluginsInstallSpecUnlockRule extends DiagnoseRule {
2985
+ validate(ctx) {
2986
+ const locked = [...iterLockedOfficialInstalls(ctx.config)].map(([k]) => k);
2987
+ if (locked.length === 0) return { pass: true };
2988
+ return {
2989
+ pass: false,
2990
+ message: "plugins.installs 中官方插件存在锁版本的 spec: " + locked.sort().join(",")
2991
+ };
2992
+ }
2993
+ repair(ctx) {
2994
+ for (const [key, spec] of iterLockedOfficialInstalls(ctx.config)) setNestedValue(ctx.config, [
2995
+ "plugins",
2996
+ "installs",
2997
+ key,
2998
+ "spec"
2999
+ ], unlockSpec(spec));
3000
+ }
3001
+ };
3002
+ MiaodaOfficialPluginsInstallSpecUnlockRule = __decorate([Rule({
3003
+ key: "miaoda_official_plugins_install_spec_unlock",
3004
+ description: "移除官方妙搭插件安装条目中的锁版本 npm spec,使其跟随最新 manifest 版本",
3005
+ dependsOn: ["config_syntax_check"],
3006
+ repairMode: "standard",
3007
+ level: "silent"
3008
+ })], MiaodaOfficialPluginsInstallSpecUnlockRule);
3009
+ //#endregion
3010
+ //#region src/rules/miaoda-plugin-allow.ts
3011
+ const MIAODA_PLUGIN = "openclaw-extension-miaoda";
3012
+ let MiaodaPluginAllowRule = class MiaodaPluginAllowRule extends DiagnoseRule {
3013
+ validate(ctx) {
3014
+ if (!isPluginInstalledOnDisk(getExtensionsDir(ctx.configPath), MIAODA_PLUGIN)) return { pass: true };
3015
+ if (getAllow$1(ctx.config).includes(MIAODA_PLUGIN)) return { pass: true };
3016
+ return {
3017
+ pass: false,
3018
+ message: `plugins.allow 缺少 ${MIAODA_PLUGIN}(已在 extensions/ 下装但未启用)`
3019
+ };
3020
+ }
3021
+ repair(ctx) {
3022
+ if (!isPluginInstalledOnDisk(getExtensionsDir(ctx.configPath), MIAODA_PLUGIN)) return;
3023
+ const plugins = ctx.config.plugins;
3024
+ if (plugins == null || typeof plugins !== "object" || Array.isArray(plugins)) {
3025
+ ctx.config.plugins = { allow: [MIAODA_PLUGIN] };
3026
+ return;
3027
+ }
3028
+ const pluginsMap = plugins;
3029
+ const rawAllow = pluginsMap.allow;
3030
+ const allow = Array.isArray(rawAllow) ? rawAllow : [];
3031
+ if (allow.includes(MIAODA_PLUGIN)) return;
3032
+ allow.push(MIAODA_PLUGIN);
3033
+ pluginsMap.allow = allow;
3034
+ }
3035
+ };
3036
+ MiaodaPluginAllowRule = __decorate([Rule({
3037
+ key: "miaoda_plugin_allow",
3038
+ description: "当 openclaw-extension-miaoda 已在磁盘安装但未在 allow 列表中时,将其添加到 plugins.allow(实验性)",
3039
+ dependsOn: ["config_syntax_check"],
3040
+ repairMode: "standard",
3041
+ level: "critical",
3042
+ profile: "standard"
3043
+ })], MiaodaPluginAllowRule);
3044
+ function getAllow$1(config) {
3045
+ const plugins = config.plugins;
3046
+ if (plugins == null || typeof plugins !== "object" || Array.isArray(plugins)) return [];
3047
+ const allow = plugins.allow;
3048
+ if (!Array.isArray(allow)) return [];
3049
+ return allow.filter((e) => typeof e === "string");
3050
+ }
3051
+ //#endregion
3052
+ //#region src/rules/lark-plugin-allow.ts
3053
+ const LARK_PLUGIN = "openclaw-lark";
3054
+ const LEGACY_LARK_PLUGIN = "feishu-openclaw-plugin";
3055
+ const LARK_PLUGIN_NAMES = [LARK_PLUGIN, LEGACY_LARK_PLUGIN];
3056
+ let LarkPluginAllowRule = class LarkPluginAllowRule extends DiagnoseRule {
3057
+ validate(ctx) {
3058
+ const allow = getAllow(ctx.config);
3059
+ if (LARK_PLUGIN_NAMES.some((name) => allow.includes(name))) return { pass: true };
3060
+ const installed = detectInstalledLarkPlugin(getExtensionsDir(ctx.configPath));
3061
+ if (installed == null) return { pass: true };
3062
+ return {
3063
+ pass: false,
3064
+ message: `plugins.allow 缺少飞书插件 ${installed}(已在 extensions/ 下装但未启用)`
3065
+ };
3066
+ }
3067
+ repair(ctx) {
3068
+ const installed = detectInstalledLarkPlugin(getExtensionsDir(ctx.configPath));
3069
+ if (installed == null) return;
3070
+ if (ctx.config.plugins == null || typeof ctx.config.plugins !== "object" || Array.isArray(ctx.config.plugins)) {
3071
+ ctx.config.plugins = { allow: [installed] };
3072
+ return;
3073
+ }
3074
+ const pluginsMap = ctx.config.plugins;
3075
+ const rawAllow = pluginsMap.allow;
3076
+ const original = Array.isArray(rawAllow) ? rawAllow : [];
3077
+ const stringAllow = original.filter((e) => typeof e === "string");
3078
+ if (LARK_PLUGIN_NAMES.some((name) => stringAllow.includes(name))) return;
3079
+ original.push(installed);
3080
+ pluginsMap.allow = original;
3081
+ }
3082
+ };
3083
+ LarkPluginAllowRule = __decorate([Rule({
3084
+ key: "lark_plugin_allow",
3085
+ description: "当飞书插件(openclaw-lark 或旧版名)已在磁盘安装但未加入 plugins.allow 时,自动添加",
3086
+ dependsOn: ["config_syntax_check"],
3087
+ repairMode: "standard",
3088
+ level: "critical"
3089
+ })], LarkPluginAllowRule);
3090
+ function getAllow(config) {
3091
+ const plugins = config.plugins;
3092
+ if (plugins == null || typeof plugins !== "object" || Array.isArray(plugins)) return [];
3093
+ const allow = plugins.allow;
3094
+ if (!Array.isArray(allow)) return [];
3095
+ return allow.filter((e) => typeof e === "string");
3096
+ }
3097
+ /**
3098
+ * fs-only 检测:`<extDir>/<name>/package.json` 存在即视为已装。
3099
+ * 优先级 openclaw-lark(新版)> feishu-openclaw-plugin(legacy)。
3100
+ * 不读 package.json 内容,只判存在性,避开 JSON 损坏。
3101
+ */
3102
+ function detectInstalledLarkPlugin(extDir) {
3103
+ for (const name of [LARK_PLUGIN, LEGACY_LARK_PLUGIN]) if (pluginPackageJsonExists(extDir, name)) return name;
3104
+ return null;
3105
+ }
3106
+ function pluginPackageJsonExists(extDir, pluginDir) {
3107
+ try {
3108
+ return node_fs.default.existsSync(node_path.default.join(extDir, pluginDir, "package.json"));
3109
+ } catch {
3110
+ return false;
3111
+ }
3112
+ }
3113
+ //#endregion
3114
+ //#region src/rules/old-miaoda-plugins-cleanup.ts
3115
+ const NEW_MIAODA = "openclaw-extension-miaoda";
3116
+ const OLD_PLUGIN_NAMES = Object.freeze([
3117
+ "openclaw-feishu-greeting",
3118
+ "openclaw-miaoda-keepalive",
3119
+ "feishu-greeting",
3120
+ "miaoda-keepalive"
3121
+ ]);
3122
+ function getPluginMaps(config) {
3123
+ const rawAllow = asRecord(config.plugins)?.allow;
3124
+ return {
3125
+ entries: getNestedMap(config, "plugins", "entries"),
3126
+ installs: getNestedMap(config, "plugins", "installs"),
3127
+ allow: Array.isArray(rawAllow) ? rawAllow : void 0
3128
+ };
3129
+ }
3130
+ function hasNewMiaoda({ entries, installs, allow }) {
3131
+ return asRecord(entries?.[NEW_MIAODA]) != null || asRecord(installs?.[NEW_MIAODA]) != null || (allow?.includes(NEW_MIAODA) ?? false);
3132
+ }
3133
+ function findResiduals({ entries, installs, allow }, extensionsDir) {
3134
+ return OLD_PLUGIN_NAMES.filter((name) => entries?.[name] != null || installs?.[name] != null || (allow?.includes(name) ?? false) || node_fs.default.existsSync(node_path.default.join(extensionsDir, name)));
3135
+ }
3136
+ let OldMiaodaPluginsCleanupRule = class OldMiaodaPluginsCleanupRule extends DiagnoseRule {
3137
+ validate(ctx) {
3138
+ const maps = getPluginMaps(ctx.config);
3139
+ if (!hasNewMiaoda(maps)) return { pass: true };
3140
+ const residuals = findResiduals(maps, getExtensionsDir(ctx.configPath));
3141
+ if (residuals.length === 0) return { pass: true };
3142
+ return {
3143
+ pass: false,
3144
+ message: "旧 miaoda 插件残留: " + residuals.sort().join(",")
3145
+ };
3146
+ }
3147
+ repair(ctx) {
3148
+ const maps = getPluginMaps(ctx.config);
3149
+ if (!hasNewMiaoda(maps)) return;
3150
+ const extensionsDir = getExtensionsDir(ctx.configPath);
3151
+ const { entries, installs, allow } = maps;
3152
+ const oldSet = new Set(OLD_PLUGIN_NAMES);
3153
+ if (allow) for (let i = allow.length - 1; i >= 0; i--) {
3154
+ const v = allow[i];
3155
+ if (typeof v === "string" && oldSet.has(v)) allow.splice(i, 1);
3156
+ }
3157
+ for (const name of OLD_PLUGIN_NAMES) {
3158
+ if (entries && name in entries) delete entries[name];
3159
+ if (installs && name in installs) delete installs[name];
3160
+ const target = node_path.default.join(extensionsDir, name);
3161
+ const rel = node_path.default.relative(extensionsDir, target);
3162
+ if (!rel || rel.startsWith("..") || node_path.default.isAbsolute(rel)) continue;
3163
+ try {
3164
+ node_fs.default.rmSync(target, {
3165
+ recursive: true,
3166
+ force: true
3167
+ });
3168
+ } catch (e) {
3169
+ console.error(`[old_miaoda_plugins_cleanup] rmSync ${target} failed: ${e.message}`);
3170
+ }
3171
+ }
3172
+ }
3173
+ };
3174
+ OldMiaodaPluginsCleanupRule = __decorate([Rule({
3175
+ key: "old_miaoda_plugins_cleanup",
3176
+ description: "当新版 openclaw-extension-miaoda 已存在时,清理过时插件引用(openclaw-feishu-greeting、openclaw-miaoda-keepalive 等)",
3177
+ dependsOn: ["config_syntax_check"],
3178
+ repairMode: "standard",
3179
+ level: "silent"
3180
+ })], OldMiaodaPluginsCleanupRule);
3181
+ //#endregion
3182
+ //#region src/rules/builtin-plugin-missing.ts
3183
+ /**
3184
+ * install-extension --all 安装的内置扩展插件列表(manifest role=extension)。
3185
+ * 与 miaoda-official-plugins-install-spec-unlock.ts 中的 OFFICIAL_PLUGIN_NAMES 保持一致。
3186
+ */
3187
+ const BUILTIN_EXTENSION_PLUGINS = new Set([
2759
3188
  "openclaw-lark",
2760
3189
  "openclaw-extension-miaoda",
2761
3190
  "openclaw-extension-miaoda-coding",
@@ -3420,11 +3849,14 @@ FeishuPluginLarkUpgradeRule = __decorate([Rule({
3420
3849
  usesVars: ["recommendedOpenclawTag"]
3421
3850
  })], FeishuPluginLarkUpgradeRule);
3422
3851
  /**
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.
3852
+ * 核心判断:非 fork 插件是否需要升级 lark,基于当前 openclaw 版本的兼容性。
3853
+ *
3854
+ * 被 FeishuPluginLarkUpgradeRule.validate 和 needsLarkUpgrade 共用。
3855
+ * 调用方需在调用前自行处理 fork 插件的情况(fork 插件不走本函数)。
3425
3856
  *
3426
- * Shared by FeishuPluginLarkUpgradeRule.validate and needsLarkUpgrade.
3427
- * Callers must handle fork plugin cases before invoking this function.
3857
+ * - recommendedOc:走 resolveUpgradeDirection 判断方向是否为 'lark'
3858
+ * - recommendedOc(doctor 无推荐版本):legacy 插件直接需要升级;
3859
+ * 非 legacy 则检查当前版本是否在兼容表内
3428
3860
  */
3429
3861
  function isLarkUpgradeNeededFromCC(cc) {
3430
3862
  const { ocCur, recommendedOc, installed, isLegacy } = cc;
@@ -3526,9 +3958,8 @@ function extractScopedNameFromSpec$1(spec) {
3526
3958
  return at === -1 ? spec : spec.slice(0, at);
3527
3959
  }
3528
3960
  /**
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.
3961
+ * 判断已安装的飞书插件是否与当前 openclaw 版本不兼容(或为需要替换的 legacy 插件)。
3962
+ * upgrade-lark 前置检测门控(--check-only 和正式安装模式)调用。
3532
3963
  */
3533
3964
  function needsLarkUpgrade(ctx) {
3534
3965
  const cc = resolveCompatContext({
@@ -3547,177 +3978,6 @@ function needsLarkUpgrade(ctx) {
3547
3978
  return isLarkUpgradeNeededFromCC(cc);
3548
3979
  }
3549
3980
  //#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);
3720
- //#endregion
3721
3981
  //#region src/rules/cleanup-install-backup-dirs.ts
3722
3982
  const DIR_PREFIX = ".openclaw-install-";
3723
3983
  function resolveExtensionsDir(configPath) {
@@ -3800,7 +4060,7 @@ function extractScopedNameFromSpec(spec) {
3800
4060
  const at = spec.indexOf("@", 1);
3801
4061
  return at === -1 ? spec : spec.slice(0, at);
3802
4062
  }
3803
- function isLarkCliAvailable$1() {
4063
+ function isLarkCliAvailable() {
3804
4064
  try {
3805
4065
  return (0, node_child_process.spawnSync)(LARK_CLI_NAME$1, ["--version"], {
3806
4066
  encoding: "utf-8",
@@ -3841,7 +4101,7 @@ function installLarkCliOnce(tag) {
3841
4101
  let LarkCliMissingForInstalledLarkPluginRule = class LarkCliMissingForInstalledLarkPluginRule extends DiagnoseRule {
3842
4102
  validate(ctx) {
3843
4103
  if (!isTargetForkPlugin(readInstalledLarkPlugin(ctx))) return { pass: true };
3844
- if (isLarkCliAvailable$1()) return { pass: true };
4104
+ if (isLarkCliAvailable()) return { pass: true };
3845
4105
  return {
3846
4106
  pass: false,
3847
4107
  message: `${FORK_PACKAGE_NAME}@${TARGET_VERSION} 已安装,但 lark-cli 不可用;将执行一次 lark-cli 安装`
@@ -3849,7 +4109,7 @@ let LarkCliMissingForInstalledLarkPluginRule = class LarkCliMissingForInstalledL
3849
4109
  }
3850
4110
  repair(ctx) {
3851
4111
  if (!isTargetForkPlugin(readInstalledLarkPlugin(ctx))) return;
3852
- if (isLarkCliAvailable$1()) return;
4112
+ if (isLarkCliAvailable()) return;
3853
4113
  installLarkCliOnce(ctx.vars.recommendedOpenclawTag ?? TARGET_VERSION);
3854
4114
  }
3855
4115
  };
@@ -3862,6 +4122,117 @@ LarkCliMissingForInstalledLarkPluginRule = __decorate([Rule({
3862
4122
  usesVars: ["recommendedOpenclawTag"]
3863
4123
  })], LarkCliMissingForInstalledLarkPluginRule);
3864
4124
  //#endregion
4125
+ //#region src/rules/feishu-bot-channel-config.ts
4126
+ /**
4127
+ * Ensures each bot account's channel config is correct:
4128
+ * 1. `allowFrom` contains its own `creatorOpenID` from larkApps
4129
+ * 2. `appSecret` is either the canonical provider-ref or matches larkApps plaintext
4130
+ *
4131
+ * Covers both multi-account (channels.feishu.accounts) and single-account
4132
+ * (channels.feishu.appId + allowFrom at top level) layouts.
4133
+ */
4134
+ let FeishuBotChannelConfigRule = class FeishuBotChannelConfigRule extends DiagnoseRule {
4135
+ validate(ctx) {
4136
+ const larkApps = ctx.vars.larkApps;
4137
+ if (!larkApps || larkApps.length === 0) return { pass: true };
4138
+ const feishu = asRecord(getNestedMap(ctx.config, "channels", "feishu"));
4139
+ if (!feishu) return { pass: true };
4140
+ const issues = [];
4141
+ const accounts = asRecord(feishu.accounts);
4142
+ if (accounts) for (const [accountId, account] of Object.entries(accounts)) {
4143
+ const bot = asRecord(account);
4144
+ if (!bot) continue;
4145
+ const appId = bot.appId;
4146
+ if (typeof appId !== "string" || !appId.startsWith("cli_")) continue;
4147
+ const larkApp = larkApps.find((e) => e.larkAppID === appId);
4148
+ if (!larkApp) continue;
4149
+ this.checkBot(accountId, bot, larkApp, issues);
4150
+ }
4151
+ const singleAppId = feishu.appId;
4152
+ if (typeof singleAppId === "string" && singleAppId.startsWith("cli_") && !accounts) {
4153
+ const larkApp = larkApps.find((e) => e.larkAppID === singleAppId);
4154
+ if (larkApp) this.checkBot("feishu", feishu, larkApp, issues);
4155
+ }
4156
+ if (issues.length === 0) return { pass: true };
4157
+ return {
4158
+ pass: false,
4159
+ message: issues.join("; ")
4160
+ };
4161
+ }
4162
+ /** Check a single bot entry (either an account object or the feishu channel itself).
4163
+ * appSecret is validated based on its current type:
4164
+ * - object → must match canonical provider-ref
4165
+ * - string → must match larkApps plaintext
4166
+ */
4167
+ checkBot(label, bot, larkApp, issues) {
4168
+ const creatorOpenID = larkApp.creatorOpenID;
4169
+ const allowFrom = Array.isArray(bot.allowFrom) ? bot.allowFrom : [];
4170
+ if (typeof creatorOpenID === "string" && creatorOpenID !== "") {
4171
+ if (!allowFrom.includes(creatorOpenID)) issues.push(`${label} allowFrom missing creatorOpenID ${creatorOpenID.length > 8 ? creatorOpenID.slice(0, 4) + "***" + creatorOpenID.slice(-4) : "***"}`);
4172
+ } else if (allowFrom.length === 0) issues.push(`${label} allowFrom is empty (creatorOpenID unavailable, cannot auto-fix)`);
4173
+ const secret = bot.appSecret;
4174
+ if (typeof secret === "object" && secret !== null && !Array.isArray(secret)) {
4175
+ if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) issues.push(`${label} appSecret is a provider-ref but not the canonical one`);
4176
+ } else if (typeof secret === "string") {
4177
+ if (secret !== larkApp.appSecret) issues.push(`${label} appSecret plaintext mismatch`);
4178
+ } else issues.push(`${label} appSecret has unexpected type ${typeof secret}`);
4179
+ }
4180
+ repair(ctx) {
4181
+ const larkApps = ctx.vars.larkApps;
4182
+ if (!larkApps || larkApps.length === 0) return;
4183
+ const feishu = asRecord(getNestedMap(ctx.config, "channels", "feishu"));
4184
+ if (!feishu) return;
4185
+ const accounts = asRecord(feishu.accounts);
4186
+ if (accounts) for (const [, account] of Object.entries(accounts)) {
4187
+ const bot = asRecord(account);
4188
+ if (!bot) continue;
4189
+ const appId = bot.appId;
4190
+ if (typeof appId !== "string" || !appId.startsWith("cli_")) continue;
4191
+ const larkApp = larkApps.find((e) => e.larkAppID === appId);
4192
+ if (!larkApp) continue;
4193
+ this.fixBot(bot, larkApp);
4194
+ }
4195
+ const singleAppId = feishu.appId;
4196
+ if (typeof singleAppId === "string" && singleAppId.startsWith("cli_") && !accounts) {
4197
+ const larkApp = larkApps.find((e) => e.larkAppID === singleAppId);
4198
+ if (larkApp) this.fixBot(feishu, larkApp);
4199
+ }
4200
+ }
4201
+ /** Fix a single bot entry in-place.
4202
+ * appSecret is repaired based on its current type:
4203
+ * - object → fix to canonical provider-ref
4204
+ * - string → fix to larkApps plaintext
4205
+ */
4206
+ fixBot(bot, larkApp) {
4207
+ const creatorOpenID = larkApp.creatorOpenID;
4208
+ if (typeof creatorOpenID === "string" && creatorOpenID !== "") {
4209
+ const allowFrom = Array.isArray(bot.allowFrom) ? [...bot.allowFrom] : [];
4210
+ if (!allowFrom.includes(creatorOpenID)) {
4211
+ allowFrom.push(creatorOpenID);
4212
+ bot.allowFrom = allowFrom;
4213
+ }
4214
+ }
4215
+ const secret = bot.appSecret;
4216
+ if (typeof secret === "object" && secret !== null && !Array.isArray(secret)) {
4217
+ if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) bot.appSecret = { ...DEFAULT_FEISHU_APP_SECRET };
4218
+ } else if (typeof secret === "string") {
4219
+ if (secret !== larkApp.appSecret) bot.appSecret = larkApp.appSecret;
4220
+ }
4221
+ }
4222
+ };
4223
+ FeishuBotChannelConfigRule = __decorate([Rule({
4224
+ key: "feishu_bot_channel_config",
4225
+ description: "确保飞书配置中 bot 账号的 allowFrom 包含其创建者 openID 且 appSecret 值正确",
4226
+ dependsOn: [
4227
+ "config_syntax_check",
4228
+ "feishu_default_account",
4229
+ "feishu_bot_id"
4230
+ ],
4231
+ repairMode: "standard",
4232
+ usesVars: ["larkApps"],
4233
+ level: "critical"
4234
+ })], FeishuBotChannelConfigRule);
4235
+ //#endregion
3865
4236
  //#region src/check.ts
3866
4237
  /** Telemetry-aware entry: returns both the legacy CheckResult (for stdout)
3867
4238
  * AND a DoctorReport-shape payload (for `openclaw.report_cli_run`). The
@@ -4302,33 +4673,6 @@ function finalize$1(results, aborted) {
4302
4673
  };
4303
4674
  }
4304
4675
  //#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
4332
4676
  //#region src/run-log.ts
4333
4677
  let currentRunContext;
4334
4678
  /**
@@ -4464,9 +4808,10 @@ function makeLogger(logFile) {
4464
4808
  /**
4465
4809
  * Start an async reset task: spawn a detached child process and return the taskId.
4466
4810
  *
4467
- * The child process runs: node cli.js reset --worker --task-id=xxx --ctx=base64
4811
+ * The child process runs: node cli.js reset --worker --task-id=xxx
4812
+ * The worker fetches ctx from innerApi itself — no --ctx passthrough.
4468
4813
  */
4469
- function startAsyncReset(ctxBase64) {
4814
+ function startAsyncReset() {
4470
4815
  const taskId = (0, node_crypto.randomUUID)();
4471
4816
  const resultFile = resetResultFile(taskId);
4472
4817
  const log = makeLogger(resetLogFile(taskId));
@@ -4490,8 +4835,7 @@ function startAsyncReset(ctxBase64) {
4490
4835
  process.argv[1],
4491
4836
  "reset",
4492
4837
  "--worker",
4493
- `--task-id=${taskId}`,
4494
- `--ctx=${ctxBase64}`
4838
+ `--task-id=${taskId}`
4495
4839
  ], {
4496
4840
  detached: true,
4497
4841
  stdio: "ignore",
@@ -4858,308 +5202,48 @@ function updatePluginInstalls(configPath, installedPkgs) {
4858
5202
  if (!PLUGINS_TO_AUTO_ENABLE.includes(pkg.name)) continue;
4859
5203
  if (!Array.isArray(plugins.allow)) plugins.allow = [];
4860
5204
  const allow = plugins.allow;
4861
- if (!allow.includes(pkg.name)) allow.push(pkg.name);
4862
- if (!plugins.entries || typeof plugins.entries !== "object" || Array.isArray(plugins.entries)) plugins.entries = {};
4863
- const entries = plugins.entries;
4864
- entries[pkg.name] = {
4865
- ...asRecord(entries[pkg.name]) ?? {},
4866
- enabled: true
4867
- };
4868
- }
4869
- const tmpPath = configPath + ".installs-tmp";
4870
- node_fs.default.writeFileSync(tmpPath, JSON.stringify(config, null, 2), "utf-8");
4871
- moveSafe(tmpPath, configPath);
4872
- console.error(`[install-extension] plugins.installs updated: ${updated} entry(ies) in ${configPath}` + (skipped > 0 ? ` (${skipped} package(s) without installMetadata skipped)` : ""));
4873
- }
4874
- function installOne$1(pkg, tarball, homeBase) {
4875
- const destDir = node_path.default.join(homeBase, pkg.installPath);
4876
- const stagingDir = destDir + ".new";
4877
- const oldDir = destDir + ".old";
4878
- node_fs.default.mkdirSync(node_path.default.dirname(destDir), { recursive: true });
4879
- if (node_fs.default.existsSync(stagingDir)) node_fs.default.rmSync(stagingDir, {
4880
- recursive: true,
4881
- force: true
4882
- });
4883
- node_fs.default.mkdirSync(stagingDir);
4884
- try {
4885
- extractTarballTolerant(tarball, stagingDir, { stripComponents: 1 });
4886
- if (!node_fs.default.existsSync(node_path.default.join(stagingDir, "package.json"))) throw new Error(`extension tarball missing package.json: ${pkg.name}`);
4887
- } catch (e) {
4888
- try {
4889
- node_fs.default.rmSync(stagingDir, {
4890
- recursive: true,
4891
- force: true
4892
- });
4893
- } catch {}
4894
- throw e;
4895
- }
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
5205
+ if (!allow.includes(pkg.name)) allow.push(pkg.name);
5206
+ if (!plugins.entries || typeof plugins.entries !== "object" || Array.isArray(plugins.entries)) plugins.entries = {};
5207
+ const entries = plugins.entries;
5208
+ entries[pkg.name] = {
5209
+ ...asRecord(entries[pkg.name]) ?? {},
5210
+ enabled: true
5211
+ };
5212
+ }
5213
+ const tmpPath = configPath + ".installs-tmp";
5214
+ node_fs.default.writeFileSync(tmpPath, JSON.stringify(config, null, 2), "utf-8");
5215
+ moveSafe(tmpPath, configPath);
5216
+ console.error(`[install-extension] plugins.installs updated: ${updated} entry(ies) in ${configPath}` + (skipped > 0 ? ` (${skipped} package(s) without installMetadata skipped)` : ""));
5217
+ }
5218
+ function installOne$1(pkg, tarball, homeBase) {
5219
+ const destDir = node_path.default.join(homeBase, pkg.installPath);
5220
+ const stagingDir = destDir + ".new";
5221
+ const oldDir = destDir + ".old";
5222
+ node_fs.default.mkdirSync(node_path.default.dirname(destDir), { recursive: true });
5223
+ if (node_fs.default.existsSync(stagingDir)) node_fs.default.rmSync(stagingDir, {
5224
+ recursive: true,
5225
+ force: true
5226
+ });
5227
+ node_fs.default.mkdirSync(stagingDir);
5228
+ try {
5229
+ extractTarballTolerant(tarball, stagingDir, { stripComponents: 1 });
5230
+ if (!node_fs.default.existsSync(node_path.default.join(stagingDir, "package.json"))) throw new Error(`extension tarball missing package.json: ${pkg.name}`);
5231
+ } catch (e) {
5232
+ try {
5233
+ node_fs.default.rmSync(stagingDir, {
5234
+ recursive: true,
5235
+ force: true
5236
+ });
5237
+ } catch {}
5238
+ throw e;
5239
+ }
5240
+ const hadOld = node_fs.default.existsSync(destDir);
5241
+ if (hadOld) moveSafe(destDir, oldDir);
5242
+ moveSafe(stagingDir, destDir);
5243
+ if (hadOld && node_fs.default.existsSync(oldDir)) node_fs.default.rmSync(oldDir, {
5244
+ recursive: true,
5245
+ force: true
5139
5246
  });
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
- };
5163
5247
  }
5164
5248
  //#endregion
5165
5249
  //#region ../../openclaw-slardar/lib/client.js
@@ -7005,6 +7089,60 @@ function mergeCoreBackupAndOrigins(configPath, vars, resetData, log) {
7005
7089
  log(`allowedOrigins: added ${added.length} (${JSON.stringify(added)}), total now ${mergedOrigins.length}`);
7006
7090
  }
7007
7091
  /**
7092
+ * Fix bot account allowFrom and appSecret using larkApps from innerApi.
7093
+ *
7094
+ * For each bot account (key starts with `bot-cli_`):
7095
+ * - allowFrom must contain the bot's own creatorOpenID from larkApps
7096
+ * - appSecret must be either the canonical provider-ref or match larkApps plaintext
7097
+ *
7098
+ * Runs after mergeCoreBackupAndOrigins so it operates on the final config state.
7099
+ */
7100
+ function fixBotChannelConfig(configPath, larkApps, log) {
7101
+ if (!larkApps || larkApps.length === 0) {
7102
+ log("no larkApps data, skip bot channel config fix");
7103
+ return;
7104
+ }
7105
+ const config = loadJSON5().parse(node_fs.default.readFileSync(configPath, "utf-8"));
7106
+ const accounts = asRecord(getNestedMap(config, "channels", "feishu")?.accounts);
7107
+ if (!accounts) {
7108
+ log("no feishu accounts in config, skip bot channel config fix");
7109
+ return;
7110
+ }
7111
+ let fixCount = 0;
7112
+ for (const [, account] of Object.entries(accounts)) {
7113
+ const bot = asRecord(account);
7114
+ if (!bot) continue;
7115
+ const appId = bot.appId;
7116
+ if (typeof appId !== "string" || !appId.startsWith("cli_")) continue;
7117
+ const larkApp = larkApps.find((e) => e.larkAppID === appId);
7118
+ if (!larkApp) continue;
7119
+ const creatorOpenID = larkApp.creatorOpenID;
7120
+ if (typeof creatorOpenID === "string" && creatorOpenID !== "") {
7121
+ const allowFrom = Array.isArray(bot.allowFrom) ? [...bot.allowFrom] : [];
7122
+ if (!allowFrom.includes(creatorOpenID)) {
7123
+ allowFrom.push(creatorOpenID);
7124
+ bot.allowFrom = allowFrom;
7125
+ fixCount++;
7126
+ }
7127
+ }
7128
+ const secret = bot.appSecret;
7129
+ let needsFix = false;
7130
+ if (typeof secret === "object" && secret !== null && !Array.isArray(secret)) {
7131
+ if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) needsFix = true;
7132
+ } else if (typeof secret === "string") {
7133
+ if (secret !== larkApp.appSecret) needsFix = true;
7134
+ } else needsFix = true;
7135
+ if (needsFix) {
7136
+ bot.appSecret = { ...DEFAULT_FEISHU_APP_SECRET };
7137
+ fixCount++;
7138
+ }
7139
+ }
7140
+ if (fixCount > 0) {
7141
+ node_fs.default.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
7142
+ log(`fixed ${fixCount} bot channel config issue(s) (allowFrom/appSecret)`);
7143
+ } else log("bot channel config ok, no fixes needed");
7144
+ }
7145
+ /**
7008
7146
  * Step 7: Verify startup scripts landed in configDir/scripts/.
7009
7147
  *
7010
7148
  * Scripts are extracted directly to configDir/scripts/ during stageTemplate —
@@ -7149,6 +7287,7 @@ async function runReset(input, taskId, resultFile) {
7149
7287
  await step5InstallOpenclaw(openclawTag, ossFileMap, log);
7150
7288
  step(6);
7151
7289
  mergeCoreBackupAndOrigins(configPath, vars, resetData, log);
7290
+ fixBotChannelConfig(configPath, vars.larkApps, log);
7152
7291
  step(7);
7153
7292
  verifyStartupScripts(configDir, log);
7154
7293
  step(8);
@@ -7947,7 +8086,8 @@ function normalizeCtx(raw) {
7947
8086
  reset: {
7948
8087
  templateVars: r.reset.templateVars ?? {},
7949
8088
  coreBackup: r.reset.coreBackup
7950
- }
8089
+ },
8090
+ larkApps: Array.isArray(r.larkApps) ? r.larkApps : []
7951
8091
  };
7952
8092
  }
7953
8093
  const vars = r.vars ?? {};
@@ -7972,7 +8112,8 @@ function normalizeCtx(raw) {
7972
8112
  reset: {
7973
8113
  templateVars: resetData.templateVars ?? {},
7974
8114
  coreBackup: resetData.coreBackup
7975
- }
8115
+ },
8116
+ larkApps: Array.isArray(r.larkApps) ? r.larkApps : []
7976
8117
  };
7977
8118
  }
7978
8119
  function fillApp(src) {
@@ -8037,7 +8178,8 @@ function buildCheckInput(raw, configPathOverride) {
8037
8178
  providerFilePath: PROVIDER_FILE_PATH,
8038
8179
  secretsFilePath: SECRETS_FILE_PATH,
8039
8180
  templateVars: ctx.app.templateVars,
8040
- recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
8181
+ recommendedOpenclawTag: ctx.app.recommendedOpenclawTag,
8182
+ larkApps: ctx.larkApps
8041
8183
  },
8042
8184
  templateVars: ctx.app.templateVars
8043
8185
  };
@@ -8069,7 +8211,8 @@ function buildRepairInput(raw, configPathOverride) {
8069
8211
  providerFilePath: PROVIDER_FILE_PATH,
8070
8212
  secretsFilePath: SECRETS_FILE_PATH,
8071
8213
  templateVars: ctx.app.templateVars,
8072
- recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
8214
+ recommendedOpenclawTag: ctx.app.recommendedOpenclawTag,
8215
+ larkApps: ctx.larkApps
8073
8216
  },
8074
8217
  repairData: {
8075
8218
  secretsContent: ctx.secrets.secretsContent,
@@ -8105,7 +8248,8 @@ function buildResetInput(raw, configPathOverride) {
8105
8248
  providerFilePath: PROVIDER_FILE_PATH,
8106
8249
  secretsFilePath: SECRETS_FILE_PATH,
8107
8250
  templateVars: ctx.app.templateVars,
8108
- recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
8251
+ recommendedOpenclawTag: ctx.app.recommendedOpenclawTag,
8252
+ larkApps: ctx.larkApps
8109
8253
  },
8110
8254
  resetData: {
8111
8255
  templateVars: ctx.reset.templateVars,
@@ -10336,6 +10480,139 @@ function finalize(results, aborted) {
10336
10480
  };
10337
10481
  }
10338
10482
  //#endregion
10483
+ //#region src/channels-probe.ts
10484
+ const FEISHU_INVALID_CONFIG_MSG = "channels.feishu: invalid config: must NOT have additional properties";
10485
+ const CHANNEL_LINE_RE = /^-\s+(?:Feishu|openclaw-lark|@larksuite\/openclaw-lark)\s+([^:]+):\s+(.+)$/i;
10486
+ /**
10487
+ * 判断单个飞书账号是否处于"可用"状态。
10488
+ * 移植自 feishu-channel-success-rate skill 的 Python `_account_is_working`。
10489
+ *
10490
+ * 会先剥离 "key:value" 形式的 bit(dm:、bot:、in:、out:、token:、allow:、
10491
+ * intents:、groups:、health: 等),再按以下公式判断:
10492
+ *
10493
+ * @param ignoreProbeFailed 为 true 时忽略 "probe failed" bit,不视为失败。
10494
+ * 升级前置检测中传 true,因为 probe 失败通常反映网络状况而非配置问题。
10495
+ * @param gatewayReachable 为 false 时(gateway 尚未启动),只要 enabled+configured
10496
+ * 即视为可用;为 true 时还需要 running/works 且无 error: bit。
10497
+ */
10498
+ function accountIsWorking(bits, ignoreProbeFailed = true, gatewayReachable = true) {
10499
+ const bitTokens = /* @__PURE__ */ new Set();
10500
+ let hasError = false;
10501
+ let hasProbeFailed = false;
10502
+ for (const raw of bits) {
10503
+ const b = raw.trim();
10504
+ if (!b) continue;
10505
+ if (b.startsWith("error:")) {
10506
+ hasError = true;
10507
+ continue;
10508
+ }
10509
+ if (b === "probe failed") {
10510
+ hasProbeFailed = true;
10511
+ continue;
10512
+ }
10513
+ bitTokens.add(b.split(":")[0]);
10514
+ }
10515
+ if (!bitTokens.has("enabled") || !bitTokens.has("configured")) return false;
10516
+ if (!gatewayReachable) return true;
10517
+ if (bitTokens.has("works")) return true;
10518
+ if (bitTokens.has("running") && !hasError && (ignoreProbeFailed || !hasProbeFailed)) return true;
10519
+ return false;
10520
+ }
10521
+ /**
10522
+ * 解析 `openclaw channels status --probe` 的原始 stdout。
10523
+ * 移植自 feishu-channel-success-rate skill 的 Python `extract_channels_probe`。
10524
+ */
10525
+ function parseChannelsProbeOutput(text, { ignoreProbeFailed = true } = {}) {
10526
+ const gatewayReachable = text.includes("Gateway reachable");
10527
+ const feishuConfigInvalid = text.includes(FEISHU_INVALID_CONFIG_MSG);
10528
+ const accounts = [];
10529
+ let anyAccountWorking = false;
10530
+ for (const line of text.split("\n")) {
10531
+ const m = CHANNEL_LINE_RE.exec(line.trim());
10532
+ if (!m) continue;
10533
+ const [, acct, rest] = m;
10534
+ const bits = rest.split(",").map((b) => b.trim());
10535
+ const isWorking = accountIsWorking(bits, ignoreProbeFailed, gatewayReachable);
10536
+ if (isWorking) anyAccountWorking = true;
10537
+ accounts.push({
10538
+ id: acct.trim(),
10539
+ bits,
10540
+ isWorking,
10541
+ raw: line.trim()
10542
+ });
10543
+ }
10544
+ return {
10545
+ gatewayReachable,
10546
+ feishuConfigInvalid,
10547
+ accounts,
10548
+ anyAccountWorking
10549
+ };
10550
+ }
10551
+ /**
10552
+ * 执行 `openclaw channels status --probe`,返回结构化结果。
10553
+ *
10554
+ * 部分 bot 账号 probe 失败时命令会以非零退出码退出,但 stdout 仍有可用内容。
10555
+ * 因此即使退出码非零,也尝试解析 stdout;只有真正没有任何输出时才返回 unavailable。
10556
+ *
10557
+ * @param timeoutMs 最长等待时长,默认 60 s。v2026.4.x 缺少单请求 HTTP 超时,
10558
+ * 可能无限阻塞,此超时是唯一保护。
10559
+ * @param ignoreProbeFailed 为 true 时,"probe failed" 账号仍计入"可用"。
10560
+ * 升级前置检测中应传 true,避免网络抖动导致误判为不可用。
10561
+ */
10562
+ function runChannelsProbe(timeoutMs = 6e4, { ignoreProbeFailed = true } = {}) {
10563
+ let stdout = "";
10564
+ let stderrText = "";
10565
+ let execError;
10566
+ try {
10567
+ stdout = (0, node_child_process.execSync)("openclaw channels status --probe", {
10568
+ encoding: "utf-8",
10569
+ timeout: timeoutMs,
10570
+ stdio: [
10571
+ "ignore",
10572
+ "pipe",
10573
+ "pipe"
10574
+ ]
10575
+ });
10576
+ } catch (e) {
10577
+ const err = e;
10578
+ const stdoutRaw = err.stdout;
10579
+ stdout = typeof stdoutRaw === "string" ? stdoutRaw : stdoutRaw?.toString("utf-8") ?? "";
10580
+ execError = err.message;
10581
+ const stderrRaw = err.stderr;
10582
+ stderrText = (typeof stderrRaw === "string" ? stderrRaw : stderrRaw?.toString("utf-8") ?? "").trim();
10583
+ if (stderrText) console.error(`channels-probe: stderr from CLI: ${stderrText}`);
10584
+ }
10585
+ if (stdout.trim()) return {
10586
+ available: true,
10587
+ rawOutput: stdout.trim(),
10588
+ ...parseChannelsProbeOutput(stdout, { ignoreProbeFailed })
10589
+ };
10590
+ return {
10591
+ available: false,
10592
+ gatewayReachable: false,
10593
+ feishuConfigInvalid: stderrText.includes(FEISHU_INVALID_CONFIG_MSG),
10594
+ accounts: [],
10595
+ anyAccountWorking: false,
10596
+ error: execError ?? "no output from openclaw channels status --probe"
10597
+ };
10598
+ }
10599
+ /**
10600
+ * 判断 channels probe 结果是否处于"仅一个 Feishu 默认账号、enabled 但未配置"的状态。
10601
+ *
10602
+ * 这是插件全新安装后的初始状态:npx 工具创建了一个默认账号占位,但用户尚未
10603
+ * 填写 AppID / Secret,因此账号显示为 enabled 但 not configured。
10604
+ * 此时 anyAccountWorking=false(configured 缺失),但安装本身是成功的。
10605
+ *
10606
+ * 用于安装后校验的补充分支:当安装前 channels 不可用,且安装后恰好处于此状态时,
10607
+ * 视为安装成功,不触发回滚。
10608
+ */
10609
+ function isDefaultOnlyState(result) {
10610
+ if (result.accounts.length !== 1) return false;
10611
+ const acct = result.accounts[0];
10612
+ const bitTokens = new Set(acct.bits.map((b) => b.trim().split(":")[0]));
10613
+ return bitTokens.has("enabled") && !bitTokens.has("configured");
10614
+ }
10615
+ //#endregion
10339
10616
  //#region src/innerapi/reportCliRun.ts
10340
10617
  /**
10341
10618
  * CLI-side client for studio_server's `openclaw.report_cli_run` inner
@@ -10415,7 +10692,7 @@ async function reportCliRun(opts) {
10415
10692
  //#region src/help.ts
10416
10693
  const BIN = "mclaw-diagnose";
10417
10694
  function versionBanner() {
10418
- return `v0.1.15-alpha.0`;
10695
+ return `v0.1.15-alpha.10`;
10419
10696
  }
10420
10697
  const COMMANDS = [
10421
10698
  {
@@ -10519,16 +10796,12 @@ EXIT CODES
10519
10796
  hidden: true,
10520
10797
  summary: "Run rule-engine check only",
10521
10798
  help: `USAGE
10522
- ${BIN} check [--ctx=<base64>]
10799
+ ${BIN} check
10523
10800
 
10524
10801
  DESCRIPTION
10525
10802
  Runs the rule engine against the sandbox's current openclaw config and
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).
10803
+ returns { failedRules }. Ctx is fetched from innerapi automatically.
10804
+ End-users should prefer \`doctor\`.
10532
10805
  `
10533
10806
  },
10534
10807
  {
@@ -10536,16 +10809,11 @@ OPTIONS
10536
10809
  hidden: true,
10537
10810
  summary: "Apply standard-mode repairs",
10538
10811
  help: `USAGE
10539
- ${BIN} repair [--ctx=<base64>]
10812
+ ${BIN} repair
10540
10813
 
10541
10814
  DESCRIPTION
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.
10815
+ Runs repair for the failing rules. Ctx is fetched from innerapi
10816
+ automatically. End-users should use \`doctor --fix\` instead.
10549
10817
  `
10550
10818
  },
10551
10819
  {
@@ -10553,14 +10821,15 @@ OPTIONS
10553
10821
  hidden: true,
10554
10822
  summary: "Re-initialize sandbox via the 9-step reset pipeline",
10555
10823
  help: `USAGE
10556
- ${BIN} reset --async [--ctx=<base64>]
10557
- ${BIN} reset --worker --task-id=<id> [--ctx=<base64>]
10824
+ ${BIN} reset --async
10825
+ ${BIN} reset --worker --task-id=<id>
10558
10826
 
10559
10827
  DESCRIPTION
10560
10828
  Two-phase pipeline driven asynchronously: the --async invocation spawns
10561
10829
  a detached worker and returns { taskId } immediately; the --worker
10562
10830
  invocation (spawned by --async) runs the actual 9 steps and writes
10563
10831
  progress to /tmp/openclaw-diagnose/reset-<taskId>.json.
10832
+ Ctx is fetched from innerapi automatically.
10564
10833
 
10565
10834
  Poll progress with \`${BIN} get_reset_task --task-id=<id>\`.
10566
10835
 
@@ -10568,7 +10837,6 @@ OPTIONS
10568
10837
  --async Start a detached worker and return taskId on stdout.
10569
10838
  --worker Internal — run the 9-step pipeline (launched by --async).
10570
10839
  --task-id=<id> Required with --worker; identifies the progress file.
10571
- --ctx=<base64> Opaque ctx JSON; fetched from innerapi when absent.
10572
10840
  `
10573
10841
  },
10574
10842
  {
@@ -10591,7 +10859,7 @@ OPTIONS
10591
10859
  hidden: true,
10592
10860
  summary: "Download + install the openclaw tarball",
10593
10861
  help: `USAGE
10594
- ${BIN} install-openclaw <tag> [--ctx=<base64> | --oss_file_map=<base64>]
10862
+ ${BIN} install-openclaw <tag> [--oss_file_map=<base64>]
10595
10863
 
10596
10864
  DESCRIPTION
10597
10865
  Downloads the openclaw@<tag> tgz via the signed OSS URL found in the
@@ -10603,9 +10871,9 @@ ARGUMENTS
10603
10871
  <tag> Openclaw version tag, e.g. 2026.4.11.
10604
10872
 
10605
10873
  OPTIONS
10606
- --ctx=<base64> Opaque ctx; ossFileMap is extracted from it.
10607
10874
  --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi
10608
- entirely. Wins over --ctx when both provided.
10875
+ entirely. When absent, ossFileMap is fetched from
10876
+ innerapi automatically.
10609
10877
  `
10610
10878
  },
10611
10879
  {
@@ -10631,8 +10899,7 @@ OPTIONS
10631
10899
  --home_base=<dir> Override the /home/gem base (tests).
10632
10900
  --config_path=<p> Override the openclaw.json path (tests).
10633
10901
  --skip-config-update Leave plugins.installs in openclaw.json untouched.
10634
- --ctx=<base64> Opaque ctx; see install-openclaw for semantics.
10635
- --oss_file_map=... Pre-built OSS URL map (base64 JSON).
10902
+ --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi.
10636
10903
  `
10637
10904
  },
10638
10905
  {
@@ -10659,7 +10926,6 @@ OPTIONS
10659
10926
  --cli=<name> CLI package to install by short name or scoped
10660
10927
  packageName (repeatable, at least one required).
10661
10928
  --home_base=<dir> Override the /home/gem base (tests).
10662
- --ctx=<base64> Opaque ctx; ossFileMap is extracted from it.
10663
10929
  --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi.
10664
10930
 
10665
10931
  EXAMPLES
@@ -10720,12 +10986,17 @@ EXIT CODES
10720
10986
  hidden: false,
10721
10987
  summary: "Upgrade the Feishu/Lark plugin via @larksuite/openclaw-lark-tools",
10722
10988
  help: `USAGE
10723
- ${BIN} upgrade-lark [--scene=<scene>] [--caller=<n>] [--trace-id=<id>]
10989
+ ${BIN} upgrade-lark [--check] [--skip-restart] [--scene=<scene>] [--caller=<n>] [--trace-id=<id>]
10724
10990
 
10725
10991
  DESCRIPTION
10726
10992
  Upgrades the Feishu/Lark plugin by running:
10727
10993
  npx -y @larksuite/openclaw-lark-tools update --use-existing
10728
10994
 
10995
+ Before the upgrade, a pre-check gate runs to verify the upgrade is needed:
10996
+ - version incompatible OR feishu channel config invalid, AND
10997
+ - no feishu account is currently working
10998
+ If the gate is not triggered, the command skips with exit code 0.
10999
+
10729
11000
  Before the upgrade, the following files are backed up:
10730
11001
  - openclaw.json
10731
11002
  - extensions/openclaw-lark/ (if present)
@@ -10736,6 +11007,10 @@ DESCRIPTION
10736
11007
  If the upgrade command fails, or validation fails, the backed-up files are
10737
11008
  restored to roll back the changes.
10738
11009
 
11010
+ After a successful upgrade, the openclaw service is restarted via
11011
+ /opt/force/bin/openclaw_scripts/restart.sh
11012
+ Pass --skip-restart to skip this step (e.g. when restart is handled externally).
11013
+
10739
11014
  Execution is logged to /tmp/openclaw-diagnose/upgrade-lark-<runId>.log.
10740
11015
 
10741
11016
  Output is a single JSON object on stdout:
@@ -10743,16 +11018,24 @@ DESCRIPTION
10743
11018
  { "ok": false, "error": "...", "stderr": "...", "exitCode": 1,
10744
11019
  "rollbackOk": true, "validationError": "...", "logFile": "..." }
10745
11020
 
11021
+ With --check:
11022
+ { "ok": true, "skipped": true, "upgradeNeeded": false, "logFile": "..." }
11023
+ { "ok": true, "skipped": true, "upgradeNeeded": true, "logFile": "..." } ← exit 1
11024
+
10746
11025
  OPTIONS
11026
+ --check Diagnose only: run the pre-check gate and report whether
11027
+ upgrade is needed without installing. Exit 1 if needed.
11028
+ --skip-restart Skip the post-install service restart (default: restart).
10747
11029
  --scene=<scene> Telemetry label forwarded to Slardar only.
10748
11030
  Known values: PageUpgradeLark, etc. Custom strings accepted.
10749
11031
  --caller=<name> Optional metadata passed to innerapi.
10750
11032
  --trace-id=<id> Optional log-correlation id.
10751
11033
 
10752
11034
  EXIT CODES
10753
- 0 Success: upgrade ran and all validations passed.
11035
+ 0 Success: upgrade ran and all validations passed; or gate skipped upgrade.
10754
11036
  1 Failure: npx error, validation failed, or git commit failed.
10755
11037
  File rollback was attempted (see rollbackOk in the JSON output).
11038
+ With --check: exit 1 means upgrade IS needed.
10756
11039
  `
10757
11040
  },
10758
11041
  {
@@ -10841,8 +11124,7 @@ OPTIONS
10841
11124
  --role=<role> Package role (e.g. template, config).
10842
11125
  --name=<name> Package name within the role.
10843
11126
  --dir=<dir> Target dir (defaults to dirname(pkg.installPath)).
10844
- --ctx=<base64> Opaque ctx; ossFileMap is extracted from it.
10845
- --oss_file_map=... Pre-built OSS URL map (base64 JSON).
11127
+ --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi.
10846
11128
  `
10847
11129
  }
10848
11130
  ];
@@ -10918,31 +11200,31 @@ function planVarsFields(opts = {}) {
10918
11200
  *
10919
11201
  * Per-command group needs:
10920
11202
  *
10921
- * doctor / check app (rule-driven)
10922
- * repair app + secrets (writes secretsContent / providerKeyContent)
10923
- * reset app + secrets + install + reset (the works)
11203
+ * doctor / check app + larkApps
11204
+ * repair app + secrets + larkApps
11205
+ * reset app + secrets + install + reset + larkApps
10924
11206
  * install-* install only
10925
11207
  *
10926
11208
  * Empty result (`{}`) means "no group needed" — the CLI can skip the
10927
11209
  * `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`.
10930
11210
  */
10931
11211
  function planCtxPopulate(opts) {
10932
11212
  if (opts.command === "install") return { install: true };
10933
11213
  const populate = {};
10934
- const appFields = planVarsFields({
11214
+ if (planVarsFields({
10935
11215
  disabled: opts.disabled,
10936
11216
  onlyRules: opts.onlyRules,
10937
11217
  profile: opts.profile
10938
- });
10939
- if (appFields.length > 0) populate.app = appFields;
10940
- if (opts.command === "repair") populate.secrets = true;
10941
- else if (opts.command === "reset") {
11218
+ }).length > 0) populate.app = true;
11219
+ if (opts.command === "repair") {
11220
+ populate.secrets = true;
11221
+ populate.larkApps = true;
11222
+ } else if (opts.command === "reset") {
10942
11223
  populate.secrets = true;
10943
11224
  populate.install = true;
10944
11225
  populate.reset = true;
10945
- }
11226
+ populate.larkApps = true;
11227
+ } else if (opts.command === "doctor" || opts.command === "check") populate.larkApps = true;
10946
11228
  return populate;
10947
11229
  }
10948
11230
  //#endregion
@@ -10996,33 +11278,84 @@ function reportDoctorRunToSlardar(opts) {
10996
11278
  }
10997
11279
  });
10998
11280
  }
10999
- function readLogFile(filePath) {
11000
- try {
11001
- return node_fs.default.readFileSync(filePath, "utf-8");
11002
- } catch {
11003
- return "";
11004
- }
11005
- }
11281
+ /**
11282
+ * 向 Slardar 上报 upgrade-lark 运行结果(upgrade_lark_run 事件)。
11283
+ *
11284
+ * ## 标准字段
11285
+ * - durationMs:整条命令的总耗时(从 CLI 入口到 runUpgradeLark 返回),含重启阶段。
11286
+ * - status:success / failed
11287
+ *
11288
+ * ## extraCategories(字符串维度)
11289
+ * - scene:调用方标识(如 PageUpgradeLark)
11290
+ * - check_only:是否为 --check 仅诊断模式
11291
+ * - skipped:"true" 表示前置门控未触发跳过安装(含 --check 模式),"false" 表示执行了安装
11292
+ * - skip_reason:跳过原因描述(skipped=true 时有值,"check" 表示 --check 模式)
11293
+ * - exit_code:npx 子进程退出码(跳过安装时为空)
11294
+ * - rollback_ok:回滚是否成功(未触发回滚时为空)
11295
+ * - validation_error:安装后校验失败的错误信息
11296
+ * - error_msg:命令级错误信息
11297
+ * - result:执行结果一行摘要("success: ..."/"failed: ..."/"skipped: ..."/"check: ...")
11298
+ * - log_file:日志文件绝对路径,便于在沙箱中定位完整日志
11299
+ *
11300
+ * ## extraMetrics(数值指标,单位毫秒)
11301
+ * 未执行的阶段上报 -1 作为哨兵值,便于与"运行了 0ms"区分。
11302
+ * - pre_probe_ms:[Pre-check A] 升级前 channels probe 耗时
11303
+ * - version_check_ms:[Pre-check B] 版本兼容性检测耗时
11304
+ * - backup_ms:[1/6] 文件备份耗时
11305
+ * - npx_install_ms:[3/6] npx install 耗时(不含安装后 5s 等待)
11306
+ * - post_probe_ms:[4/5] 安装后 channels probe 耗时
11307
+ * - doctor_fix_ms:[6/6] doctor --fix 耗时
11308
+ * 注:[7/7] 重启耗时写入日志但未单独上报,包含在 durationMs 总耗时中。
11309
+ */
11006
11310
  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);
11311
+ console.error(`[slardar] upgrade_lark_run scene=${opts.scene ?? ""} checkOnly=${opts.checkOnly} skipped=${opts.skipped ?? false} success=${opts.success} exitCode=${opts.exitCode ?? ""} rollbackOk=${opts.rollbackOk ?? ""}`);
11312
+ const t = opts.timing ?? {};
11009
11313
  reportTask({
11010
11314
  eventName: "upgrade_lark_run",
11011
11315
  durationMs: opts.durationMs,
11012
11316
  status: opts.success ? "success" : "failed",
11013
11317
  extraCategories: {
11014
11318
  scene: opts.scene ?? "",
11319
+ check_only: String(opts.checkOnly),
11320
+ skipped: String(opts.skipped ?? false),
11321
+ skip_reason: opts.skipReason ?? "",
11015
11322
  exit_code: String(opts.exitCode ?? ""),
11016
11323
  rollback_ok: opts.rollbackOk != null ? String(opts.rollbackOk) : "",
11017
11324
  validation_error: opts.validationError ?? "",
11018
11325
  error_msg: opts.error ?? "",
11019
- log_content: logContent
11326
+ result: opts.resultSummary,
11327
+ log_file: opts.logFile
11328
+ },
11329
+ extraMetrics: {
11330
+ pre_probe_ms: t.preProbeMs ?? -1,
11331
+ version_check_ms: t.versionCheckMs ?? -1,
11332
+ backup_ms: t.backupMs ?? -1,
11333
+ npx_install_ms: t.npxInstallMs ?? -1,
11334
+ post_probe_ms: t.postProbeMs ?? -1,
11335
+ doctor_fix_ms: t.doctorFixMs ?? -1
11020
11336
  }
11021
11337
  });
11022
11338
  }
11339
+ /**
11340
+ * 将 upgrade-lark 运行结果归纳为一行摘要字符串,用于 Slardar result 字段。
11341
+ *
11342
+ * 格式:
11343
+ * "check: upgrade needed"
11344
+ * "check: no upgrade needed"
11345
+ * "skipped: <skipReason>"
11346
+ * "success: upgrade installed"
11347
+ * "success: new default account (plugin installed, awaiting config)"
11348
+ * "failed: <error>"
11349
+ */
11350
+ function buildUpgradeLarkResultSummary(opts) {
11351
+ if (opts.checkOnly && opts.skipped) return opts.upgradeNeeded ? "check: upgrade needed" : "check: no upgrade needed";
11352
+ if (opts.skipped) return `skipped: ${opts.skipReason ?? "pre-check gate"}`;
11353
+ if (opts.ok) return "success: upgrade installed";
11354
+ return `failed: ${opts.error ?? opts.validationError ?? "unknown error"}${opts.rollbackOk === false ? " (rollback FAILED)" : opts.rollbackOk ? " (rolled back)" : ""}`;
11355
+ }
11023
11356
  //#endregion
11024
11357
  //#region src/upgrade-lark.ts
11025
- /** Plugin directories under extensions/ that are backed up before upgrade */
11358
+ /** 升级前需备份的 extensions/ 下的插件目录 */
11026
11359
  const FEISHU_PLUGIN_DIRS = ["openclaw-lark", "feishu-openclaw-plugin"];
11027
11360
  function backupFiles(opts) {
11028
11361
  const { workspaceDir, configPath, backupDir, log } = opts;
@@ -11034,6 +11367,7 @@ function backupFiles(opts) {
11034
11367
  node_fs.default.copyFileSync(configPath, node_path.default.join(backupDir, "openclaw.json"));
11035
11368
  log(` backed up: openclaw.json (${stat.size} bytes)`);
11036
11369
  } else log(` skipped: openclaw.json (not found)`);
11370
+ node_fs.default.mkdirSync(node_path.default.join(backupDir, "extensions"), { recursive: true });
11037
11371
  const extSrc = node_path.default.join(workspaceDir, "extensions");
11038
11372
  for (const pluginDir of FEISHU_PLUGIN_DIRS) {
11039
11373
  const src = node_path.default.join(extSrc, pluginDir);
@@ -11046,32 +11380,43 @@ function backupFiles(opts) {
11046
11380
  }
11047
11381
  return { ok: true };
11048
11382
  } catch (e) {
11383
+ const msg = `backup failed: ${e.message}`;
11384
+ log(`ERROR: ${msg}`);
11049
11385
  return {
11050
11386
  ok: false,
11051
- error: `backup failed: ${e.message}`
11387
+ error: msg
11052
11388
  };
11053
11389
  }
11054
11390
  }
11055
11391
  function restoreFiles(opts) {
11056
11392
  const { workspaceDir, configPath, backupDir, log } = opts;
11057
11393
  try {
11394
+ if (node_fs.default.existsSync(configPath)) {
11395
+ node_fs.default.rmSync(configPath, { force: true });
11396
+ log(` deleted: openclaw.json`);
11397
+ }
11398
+ const extDst = node_path.default.join(workspaceDir, "extensions");
11399
+ for (const pluginDir of FEISHU_PLUGIN_DIRS) {
11400
+ const dst = node_path.default.join(extDst, pluginDir);
11401
+ if (node_fs.default.existsSync(dst)) {
11402
+ node_fs.default.rmSync(dst, {
11403
+ recursive: true,
11404
+ force: true
11405
+ });
11406
+ log(` deleted: extensions/${pluginDir}`);
11407
+ }
11408
+ }
11058
11409
  const configBackup = node_path.default.join(backupDir, "openclaw.json");
11059
11410
  if (node_fs.default.existsSync(configBackup)) {
11060
11411
  node_fs.default.copyFileSync(configBackup, configPath);
11061
11412
  log(` restored: openclaw.json`);
11062
- }
11063
- const extDst = node_path.default.join(workspaceDir, "extensions");
11413
+ } else log(` skipped restore: openclaw.json (not in backup — was not present before upgrade)`);
11064
11414
  for (const pluginDir of FEISHU_PLUGIN_DIRS) {
11065
11415
  const backupSrc = node_path.default.join(backupDir, "extensions", pluginDir);
11066
11416
  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 });
11417
+ node_fs.default.cpSync(backupSrc, node_path.default.join(extDst, pluginDir), { recursive: true });
11073
11418
  log(` restored: extensions/${pluginDir}`);
11074
- }
11419
+ } else log(` skipped restore: extensions/${pluginDir} (not in backup — was not present before upgrade)`);
11075
11420
  }
11076
11421
  return true;
11077
11422
  } catch (e) {
@@ -11125,7 +11470,7 @@ function countFeishuBots(configPath) {
11125
11470
  return 0;
11126
11471
  }
11127
11472
  }
11128
- /** Run channels probe, log results, and return the result. Never throws. */
11473
+ /** 执行 channels probe 并将结果写入日志,从不抛出异常(异常时返回全零结果)。 */
11129
11474
  function probeChannels(label, log, timeoutMs) {
11130
11475
  try {
11131
11476
  const r = runChannelsProbe(timeoutMs);
@@ -11133,6 +11478,7 @@ function probeChannels(label, log, timeoutMs) {
11133
11478
  if (r.error) log(` ${label} error: ${r.error}`);
11134
11479
  if (r.gatewayReachable != null) log(` ${label} gatewayReachable: ${r.gatewayReachable}`);
11135
11480
  for (const acct of r.accounts ?? []) log(` ${label} account ${acct.id}: isWorking=${acct.isWorking} bits=[${acct.bits.join(",")}]`);
11481
+ if (r.rawOutput) log(` ${label} raw output:\n${r.rawOutput}`);
11136
11482
  return r;
11137
11483
  } catch (e) {
11138
11484
  log(` ${label} channels probe threw: ${e.message}`);
@@ -11148,7 +11494,7 @@ function probeChannels(label, log, timeoutMs) {
11148
11494
  function runUpgradeLark(opts) {
11149
11495
  const cwd = opts.cwd ?? "/home/gem/workspace/agent";
11150
11496
  const configPath = opts.configPath ?? CONFIG_PATH;
11151
- const logFile = upgradeLarkLogFile(opts.runId);
11497
+ const logFile = upgradeLarkLogFile(opts.runId, opts.checkOnly);
11152
11498
  const log = makeLogger(logFile);
11153
11499
  const fsOpts = {
11154
11500
  workspaceDir: cwd,
@@ -11163,12 +11509,16 @@ function runUpgradeLark(opts) {
11163
11509
  log(` cwd : ${cwd}`);
11164
11510
  log(` configPath : ${configPath}`);
11165
11511
  log(`${"=".repeat(60)}`);
11512
+ const timing = {};
11166
11513
  log("");
11167
11514
  log("── [Pre-check A] channels probe(升级前)────────────────");
11168
- const beforeChannels = probeChannels("before", log, 6e4);
11515
+ const t_preProbeStart = Date.now();
11516
+ const beforeChannels = probeChannels("before", log, 3e5);
11517
+ timing.preProbeMs = Date.now() - t_preProbeStart;
11169
11518
  log("");
11170
11519
  log("── [Pre-check B] 版本兼容预检 ───────────────────────────");
11171
11520
  let versionIncompatible = false;
11521
+ const t_versionCheckStart = Date.now();
11172
11522
  try {
11173
11523
  const rawConfig = node_fs.default.readFileSync(configPath, "utf-8");
11174
11524
  versionIncompatible = needsLarkUpgrade({
@@ -11184,6 +11534,7 @@ function runUpgradeLark(opts) {
11184
11534
  } catch (e) {
11185
11535
  log(` version-compat pre-check error: ${e.message} — version signal unavailable`);
11186
11536
  }
11537
+ timing.versionCheckMs = Date.now() - t_versionCheckStart;
11187
11538
  const feishuConfigInvalid = beforeChannels.feishuConfigInvalid;
11188
11539
  log(` feishu config invalid : ${feishuConfigInvalid}`);
11189
11540
  log("");
@@ -11201,6 +11552,8 @@ function runUpgradeLark(opts) {
11201
11552
  ok: true,
11202
11553
  skipped: true,
11203
11554
  skipReason: reason,
11555
+ upgradeNeeded: false,
11556
+ timing,
11204
11557
  logFile
11205
11558
  };
11206
11559
  }
@@ -11214,19 +11567,38 @@ function runUpgradeLark(opts) {
11214
11567
  ok: true,
11215
11568
  skipped: true,
11216
11569
  skipReason: reason,
11570
+ upgradeNeeded: false,
11571
+ timing,
11217
11572
  logFile
11218
11573
  };
11219
11574
  }
11220
11575
  log(` PROCEED: requiresLarkUpgrade=true (version=${versionIncompatible}, feishuConfig=${feishuConfigInvalid}) AND channels not working → running upgrade`);
11576
+ if (opts.checkOnly) {
11577
+ log(` --check 模式:需要升级 — 不执行安装,直接返回`);
11578
+ log(`${"=".repeat(60)}`);
11579
+ log("upgrade-lark check complete");
11580
+ log(`${"=".repeat(60)}`);
11581
+ return {
11582
+ ok: true,
11583
+ skipped: true,
11584
+ skipReason: "check",
11585
+ upgradeNeeded: true,
11586
+ timing,
11587
+ logFile
11588
+ };
11589
+ }
11221
11590
  log("");
11222
11591
  log("── [1/6] 文件备份 ────────────────────────────────────────");
11223
11592
  log(`before-state: botCount=${countFeishuBots(configPath)}`);
11593
+ const t_backupStart = Date.now();
11224
11594
  const backup = backupFiles(fsOpts);
11595
+ timing.backupMs = Date.now() - t_backupStart;
11225
11596
  if (!backup.ok) {
11226
11597
  log(`ERROR: ${backup.error}`);
11227
11598
  return {
11228
11599
  ok: false,
11229
11600
  error: backup.error,
11601
+ timing,
11230
11602
  logFile
11231
11603
  };
11232
11604
  }
@@ -11244,6 +11616,7 @@ function runUpgradeLark(opts) {
11244
11616
  else log(` skipped: ${localOpenclawBin} (not found)`);
11245
11617
  log("");
11246
11618
  log("── [3/6] npx install (@larksuite/openclaw-lark-tools update) ──");
11619
+ const t_npxStart = Date.now();
11247
11620
  const npxResult = (0, node_child_process.spawnSync)("npx", [
11248
11621
  "-y",
11249
11622
  "@larksuite/openclaw-lark-tools",
@@ -11256,8 +11629,9 @@ function runUpgradeLark(opts) {
11256
11629
  "pipe",
11257
11630
  "pipe"
11258
11631
  ],
11259
- timeout: 12e4
11632
+ timeout: 6e5
11260
11633
  });
11634
+ timing.npxInstallMs = Date.now() - t_npxStart;
11261
11635
  const npxStdout = npxResult.stdout?.trim() ?? "";
11262
11636
  const npxStderr = npxResult.stderr?.trim() ?? "";
11263
11637
  const npxExitCode = npxResult.status ?? 1;
@@ -11282,6 +11656,7 @@ function runUpgradeLark(opts) {
11282
11656
  stderr: npxStderr,
11283
11657
  exitCode: npxExitCode,
11284
11658
  rollbackOk,
11659
+ timing,
11285
11660
  logFile
11286
11661
  };
11287
11662
  };
@@ -11304,16 +11679,21 @@ function runUpgradeLark(opts) {
11304
11679
  } catch (e) {
11305
11680
  log(` version-compat post-check error: ${e.message} — version signal unavailable`);
11306
11681
  }
11307
- const afterChannels = probeChannels("after", log, 6e4);
11682
+ const t_postProbeStart = Date.now();
11683
+ const afterChannels = probeChannels("after", log, 3e5);
11684
+ timing.postProbeMs = Date.now() - t_postProbeStart;
11308
11685
  log(` feishu config invalid after: ${afterChannels.feishuConfigInvalid}`);
11309
11686
  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)");
11687
+ const isNewDefaultOnly = !afterVersionIncompatible && !afterChannels.feishuConfigInvalid && !beforeChannels.anyAccountWorking && isDefaultOnlyState(afterChannels);
11688
+ log(` post-check: stillNeedsUpgrade=${stillNeedsUpgrade} (version=${afterVersionIncompatible}, feishuConfig=${afterChannels.feishuConfigInvalid}, channelsWorking=${afterChannels.anyAccountWorking}) isNewDefaultOnly=${isNewDefaultOnly}`);
11689
+ if (stillNeedsUpgrade && !isNewDefaultOnly) return doRollback(`post-install diagnosis still shows anomaly: versionIncompatible=${afterVersionIncompatible}, feishuConfigInvalid=${afterChannels.feishuConfigInvalid}, anyAccountWorking=${afterChannels.anyAccountWorking}`);
11690
+ if (isNewDefaultOnly) log(" post-install diagnosis: ok (new default account — plugin installed, awaiting configuration)");
11691
+ else log(" post-install diagnosis: ok (upgrade conditions resolved)");
11313
11692
  log("");
11314
11693
  log("── [6/6] doctor --fix ────────────────────────────────────");
11315
11694
  const fixArgs = ["doctor", "--fix"];
11316
11695
  if (opts.scene) fixArgs.push(`--scene=${opts.scene}`);
11696
+ const t_doctorFixStart = Date.now();
11317
11697
  const fixResult = (0, node_child_process.spawnSync)(process.execPath, [cliScript, ...fixArgs], {
11318
11698
  cwd,
11319
11699
  encoding: "utf-8",
@@ -11325,10 +11705,31 @@ function runUpgradeLark(opts) {
11325
11705
  timeout: 6e4,
11326
11706
  env: process.env
11327
11707
  });
11708
+ timing.doctorFixMs = Date.now() - t_doctorFixStart;
11328
11709
  if (fixResult.stdout?.trim()) log(`doctor(fix) stdout:\n${fixResult.stdout.trim()}`);
11329
11710
  if (fixResult.stderr?.trim()) log(`doctor(fix) stderr:\n${fixResult.stderr.trim()}`);
11330
11711
  log(`doctor(fix) exit: ${fixResult.status ?? "null"}${fixResult.error ? ` error: ${fixResult.error.message}` : ""}`);
11331
11712
  log("");
11713
+ log("── [7/7] 重启 openclaw 服务 ──────────────────────────────");
11714
+ const restartScript = "/opt/force/bin/openclaw_scripts/restart.sh";
11715
+ if (opts.skipRestart) log(" skipped: --skip-restart");
11716
+ else if (node_fs.default.existsSync(restartScript)) {
11717
+ const t_restart = Date.now();
11718
+ const restartResult = (0, node_child_process.spawnSync)("bash", [restartScript], {
11719
+ encoding: "utf-8",
11720
+ stdio: [
11721
+ "ignore",
11722
+ "pipe",
11723
+ "pipe"
11724
+ ],
11725
+ timeout: 3e4
11726
+ });
11727
+ const restartMs = Date.now() - t_restart;
11728
+ if (restartResult.stdout?.trim()) log(` restart stdout:\n${restartResult.stdout.trim()}`);
11729
+ if (restartResult.stderr?.trim()) log(` restart stderr:\n${restartResult.stderr.trim()}`);
11730
+ log(` restart.sh exit: ${restartResult.status ?? "null"} (${restartMs}ms)${restartResult.error ? ` error: ${restartResult.error.message}` : ""}`);
11731
+ } else log(` skipped: ${restartScript} not found`);
11732
+ log("");
11332
11733
  log(`${"=".repeat(60)}`);
11333
11734
  log("upgrade-lark completed successfully");
11334
11735
  log(`${"=".repeat(60)}`);
@@ -11337,6 +11738,7 @@ function runUpgradeLark(opts) {
11337
11738
  stdout: npxStdout,
11338
11739
  stderr: npxStderr,
11339
11740
  exitCode: npxExitCode,
11741
+ timing,
11340
11742
  logFile
11341
11743
  };
11342
11744
  }
@@ -11345,21 +11747,6 @@ function runUpgradeLark(opts) {
11345
11747
  const args = node_process.default.argv.slice(2);
11346
11748
  const mode = args.find((a) => !a.startsWith("-"));
11347
11749
  /**
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
- /**
11363
11750
  * Pull the first non-flag positional after the mode name.
11364
11751
  * (The mode itself is args[0] in the filtered set, so we skip index 0.)
11365
11752
  */
@@ -11387,8 +11774,8 @@ function getMultiFlag(args, name) {
11387
11774
  * case but is no longer consulted.
11388
11775
  */
11389
11776
  async function reportRun(command, rc, _raw, invocation, durationMs, outcome, slardar = {
11390
- scene,
11391
- profile,
11777
+ scene: void 0,
11778
+ profile: "standard",
11392
11779
  fix: false
11393
11780
  }) {
11394
11781
  console.error(`${command}: telemetry calling report_cli_run`);
@@ -11452,7 +11839,7 @@ async function main() {
11452
11839
  console.error(`${mode}: begin argv=[${args.join(" ")}] version=${getVersion()} traceId=${traceId ?? "-"} caller=${caller ?? "-"} runIdGenerated=${rc.generated}`);
11453
11840
  switch (mode) {
11454
11841
  case "check": {
11455
- const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11842
+ const raw = await fetchCtxViaInnerApi({
11456
11843
  populate: planCtxPopulate({
11457
11844
  command: "check",
11458
11845
  profile
@@ -11477,7 +11864,7 @@ async function main() {
11477
11864
  break;
11478
11865
  }
11479
11866
  case "repair": {
11480
- const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11867
+ const raw = await fetchCtxViaInnerApi({
11481
11868
  populate: planCtxPopulate({
11482
11869
  command: "repair",
11483
11870
  profile
@@ -11548,27 +11935,15 @@ async function main() {
11548
11935
  break;
11549
11936
  }
11550
11937
  case "reset":
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")) {
11938
+ if (args.includes("--async")) console.log(JSON.stringify(startAsyncReset()));
11939
+ else if (args.includes("--worker")) {
11565
11940
  const taskId = args.find((a) => a.startsWith("--task-id="))?.slice(10);
11566
11941
  if (!taskId) {
11567
11942
  console.error("Error: --task-id=<id> is required for worker");
11568
11943
  node_process.default.exit(1);
11569
11944
  }
11570
11945
  const resultFile = resetResultFile(taskId);
11571
- const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11946
+ const raw = await fetchCtxViaInnerApi({
11572
11947
  populate: planCtxPopulate({ command: "reset" }),
11573
11948
  caller,
11574
11949
  traceId
@@ -11592,7 +11967,7 @@ async function main() {
11592
11967
  return;
11593
11968
  }
11594
11969
  } else {
11595
- console.error("Usage: reset --async [--ctx=<base64>] | reset --worker --task-id=<id> [--ctx=<base64>]");
11970
+ console.error("Usage: reset --async | reset --worker --task-id=<id>");
11596
11971
  node_process.default.exit(1);
11597
11972
  }
11598
11973
  break;
@@ -11608,14 +11983,14 @@ async function main() {
11608
11983
  case "install-openclaw": {
11609
11984
  const tag = getPositionalTag(args, "install-openclaw");
11610
11985
  if (!tag) {
11611
- console.error("Usage: install-openclaw <tag> [--ctx=<base64> | --oss_file_map=<base64>]");
11986
+ console.error("Usage: install-openclaw <tag> [--oss_file_map=<base64>]");
11612
11987
  node_process.default.exit(1);
11613
11988
  }
11614
11989
  const ossFileMapFlag = getFlag(args, "oss_file_map");
11615
11990
  let installOssFileMap;
11616
11991
  let rawForTelemetry;
11617
11992
  if (!ossFileMapFlag) {
11618
- rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
11993
+ rawForTelemetry = await fetchCtxViaInnerApi({
11619
11994
  populate: planCtxPopulate({ command: "install" }),
11620
11995
  caller,
11621
11996
  traceId
@@ -11650,7 +12025,7 @@ async function main() {
11650
12025
  case "install-extension": {
11651
12026
  const tag = getPositionalTag(args, "install-extension");
11652
12027
  if (!tag) {
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>]");
12028
+ console.error("Usage: install-extension <tag> (--all | --extension=<name>...) [--home_base=<dir>] [--config_path=<path>] [--skip-config-update] [--oss_file_map=<base64>]");
11654
12029
  node_process.default.exit(1);
11655
12030
  }
11656
12031
  const all = args.includes("--all");
@@ -11662,7 +12037,7 @@ async function main() {
11662
12037
  let installOssFileMap;
11663
12038
  let rawForTelemetry;
11664
12039
  if (!ossFileMapFlag) {
11665
- rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
12040
+ rawForTelemetry = await fetchCtxViaInnerApi({
11666
12041
  populate: planCtxPopulate({ command: "install" }),
11667
12042
  caller,
11668
12043
  traceId
@@ -11708,12 +12083,12 @@ async function main() {
11708
12083
  case "install-cli": {
11709
12084
  const tag = getPositionalTag(args, "install-cli");
11710
12085
  if (!tag) {
11711
- console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--ctx=<base64> | --oss_file_map=<base64>]");
12086
+ console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--oss_file_map=<base64>]");
11712
12087
  node_process.default.exit(1);
11713
12088
  }
11714
12089
  const names = getMultiFlag(args, "cli");
11715
12090
  if (names.length === 0) {
11716
- console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--ctx=<base64> | --oss_file_map=<base64>]");
12091
+ console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--oss_file_map=<base64>]");
11717
12092
  node_process.default.exit(1);
11718
12093
  }
11719
12094
  const homeBase = getFlag(args, "home_base");
@@ -11721,7 +12096,7 @@ async function main() {
11721
12096
  let installOssFileMap;
11722
12097
  let rawForTelemetry;
11723
12098
  if (!ossFileMapFlag) {
11724
- rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
12099
+ rawForTelemetry = await fetchCtxViaInnerApi({
11725
12100
  populate: planCtxPopulate({ command: "install" }),
11726
12101
  caller,
11727
12102
  traceId
@@ -11769,7 +12144,7 @@ async function main() {
11769
12144
  case "download-resource": {
11770
12145
  const tag = getPositionalTag(args, "download-resource");
11771
12146
  if (!tag) {
11772
- console.error("Usage: download-resource <tag> --role=<role> --name=<name> [--dir=<dir>] [--ctx=<base64> | --oss_file_map=<base64>]");
12147
+ console.error("Usage: download-resource <tag> --role=<role> --name=<name> [--dir=<dir>] [--oss_file_map=<base64>]");
11773
12148
  node_process.default.exit(1);
11774
12149
  }
11775
12150
  const role = getFlag(args, "role");
@@ -11783,7 +12158,7 @@ async function main() {
11783
12158
  let installOssFileMap;
11784
12159
  let rawForTelemetry;
11785
12160
  if (!ossFileMapFlag) {
11786
- rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
12161
+ rawForTelemetry = await fetchCtxViaInnerApi({
11787
12162
  populate: planCtxPopulate({ command: "install" }),
11788
12163
  caller,
11789
12164
  traceId
@@ -11858,21 +12233,39 @@ async function main() {
11858
12233
  break;
11859
12234
  }
11860
12235
  case "upgrade-lark": {
12236
+ const checkOnly = args.includes("--check");
12237
+ const skipRestart = args.includes("--skip-restart");
11861
12238
  const result = runUpgradeLark({
11862
12239
  runId: rc.runId,
11863
- scene
12240
+ scene,
12241
+ checkOnly,
12242
+ skipRestart
11864
12243
  });
11865
12244
  const upgradeDurationMs = Date.now() - t0;
11866
12245
  console.log(JSON.stringify(result));
11867
12246
  reportUpgradeLarkToSlardar({
11868
12247
  scene,
12248
+ checkOnly,
11869
12249
  durationMs: upgradeDurationMs,
11870
12250
  success: result.ok,
12251
+ skipped: result.skipped,
12252
+ skipReason: result.skipReason,
11871
12253
  logFile: result.logFile,
12254
+ resultSummary: buildUpgradeLarkResultSummary({
12255
+ ok: result.ok,
12256
+ checkOnly,
12257
+ skipped: result.skipped,
12258
+ skipReason: result.skipReason,
12259
+ upgradeNeeded: result.upgradeNeeded,
12260
+ error: result.error,
12261
+ validationError: result.validationError,
12262
+ rollbackOk: result.rollbackOk
12263
+ }),
11872
12264
  exitCode: result.exitCode,
11873
12265
  rollbackOk: result.rollbackOk,
11874
12266
  validationError: result.validationError,
11875
- error: result.error
12267
+ error: result.error,
12268
+ timing: result.timing
11876
12269
  });
11877
12270
  try {
11878
12271
  await reportCliRun({
@@ -11890,14 +12283,16 @@ async function main() {
11890
12283
  } catch (e) {
11891
12284
  console.error(`[telemetry] reportCliRun failed: ${e.message}`);
11892
12285
  }
11893
- if (!result.ok) {
12286
+ if (!result.ok || checkOnly && result.upgradeNeeded) {
11894
12287
  node_process.default.exitCode = 1;
11895
12288
  return;
11896
12289
  }
11897
12290
  break;
11898
12291
  }
11899
12292
  case "channels-probe": {
11900
- const result = runChannelsProbe(getFlag(args, "timeout") ? Number(getFlag(args, "timeout")) : void 0);
12293
+ const timeoutRaw = getFlag(args, "timeout");
12294
+ const parsed = timeoutRaw != null ? Number(timeoutRaw) : NaN;
12295
+ const result = runChannelsProbe(timeoutRaw != null ? Number.isNaN(parsed) ? 6e4 : Math.max(1e3, parsed) : void 0);
11901
12296
  console.log(JSON.stringify(result));
11902
12297
  break;
11903
12298
  }