@lark-apaas/openclaw-scripts-diagnose-cli 0.1.18-alpha.3 → 0.1.18-alpha.4

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 +916 -910
  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.18-alpha.3";
55
+ return "0.1.18-alpha.4";
56
56
  }
57
57
  //#endregion
58
58
  //#region src/rule-engine/base.ts
@@ -1155,6 +1155,18 @@ function isValidJWT(token) {
1155
1155
  function asRecord(val) {
1156
1156
  return val != null && typeof val === "object" && !Array.isArray(val) ? val : void 0;
1157
1157
  }
1158
+ /**
1159
+ * Get-or-create a nested record at `obj[key]`: returns the existing plain object,
1160
+ * or replaces a missing / non-object / array value with a fresh `{}` and returns it.
1161
+ * Mutates `obj`. Chainable for deep paths: `ensureRecord(ensureRecord(o,'a'),'b')`.
1162
+ */
1163
+ function ensureRecord(obj, key) {
1164
+ const cur = obj[key];
1165
+ if (cur != null && typeof cur === "object" && !Array.isArray(cur)) return cur;
1166
+ const fresh = {};
1167
+ obj[key] = fresh;
1168
+ return fresh;
1169
+ }
1158
1170
  /** Collect existing AGENTS.md files from the default workspace and configured agent workspaces. */
1159
1171
  function collectExistingAgentsMdPaths(ctx) {
1160
1172
  const paths = /* @__PURE__ */ new Set();
@@ -2610,51 +2622,6 @@ function upsertResourceConstrainedToolsBlock(content) {
2610
2622
  return `${content}${content.length > 0 && !content.endsWith("\n") ? "\n\n" : "\n"}${RESOURCE_CONSTRAINED_TOOLS_BLOCK}\n`;
2611
2623
  }
2612
2624
  //#endregion
2613
- //#region src/paths.ts
2614
- /**
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.
2620
- */
2621
- const DIAGNOSE_DIR = "/tmp/openclaw-diagnose";
2622
- function resetResultFile(taskId) {
2623
- return `${DIAGNOSE_DIR}/reset-${taskId}.json`;
2624
- }
2625
- function resetLogFile(taskId) {
2626
- return `${DIAGNOSE_DIR}/reset-${taskId}.log`;
2627
- }
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
- * fixStatus 有值时在此目录下创建同名文件(如 /tmp/event/PORT_FIX_READY),
2639
- * 文件内容为完整的 UpgradeLarkResult JSON,供外部进程轮询感知升级结果。
2640
- */
2641
- const FIX_EVENT_DIR = "/tmp/event";
2642
- /**
2643
- * 安装指令互斥锁文件路径。
2644
- * upgrade-lark / install-openclaw / install-extension / install-cli / reset --worker
2645
- * 共享此锁,同一时刻只允许一个安装指令运行。
2646
- * 锁文件内容:{ pid, command, startedAt }。
2647
- */
2648
- const INSTALL_LOCK_FILE = `${DIAGNOSE_DIR}/install.lock`;
2649
- /**
2650
- * upgrade-lark 每次运行的日志文件路径,含时间戳便于按时间排序定位。
2651
- * checkOnly=true 时文件名含 "-check" 后缀,便于与正式安装日志区分。
2652
- */
2653
- function upgradeLarkLogFile(runId, checkOnly = false) {
2654
- const ts = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/:/g, "-");
2655
- return `${DIAGNOSE_DIR}/upgrade-lark${checkOnly ? "-check" : ""}-${ts}-${runId.slice(0, 8)}.log`;
2656
- }
2657
- //#endregion
2658
2625
  //#region src/constants.ts
2659
2626
  /**
2660
2627
  * 全局共享常量(插件名 / CLI 名 / 路径 / 插件集合)。
@@ -2672,8 +2639,6 @@ const LARK_PLUGIN_NAME = "openclaw-lark";
2672
2639
  * 版本号自成体系、不在 VERSION_COMPAT_MAP 内,按对标的官方版本判定兼容性。
2673
2640
  */
2674
2641
  const FORK_LARK_PLUGIN_FULL_NAME = "@lark-apaas/openclaw-lark";
2675
- /** fork 插件的 scope 前缀。 */
2676
- const FORK_LARK_PLUGIN_SCOPE = "@lark-apaas";
2677
2642
  /**
2678
2643
  * 社区插件:openclaw **内置** 的 `feishu` 插件(非独立扩展)。
2679
2644
  * 与官方/ fork openclaw-lark 互斥,规范化时应禁用以让位 openclaw-lark。
@@ -2691,8 +2656,12 @@ const MEM0_PLUGIN_NAME = "openclaw-mem0-plugin";
2691
2656
  const LARK_CLI_NAME = "lark-cli";
2692
2657
  /** agent-skills 模板包名(manifest role=template,随 lark-cli 一起安装)。 */
2693
2658
  const AGENT_SKILLS_NAME = "agent-skills";
2694
- /** 飞书插件目录集合(官方短名 + 旧版名):备份 / 清理 / 安装时迭代用。 */
2695
- const FEISHU_PLUGIN_DIR_NAMES = [LARK_PLUGIN_NAME, LEGACY_LARK_PLUGIN_NAME];
2659
+ /**
2660
+ * lark 系**有 extension 目录**的插件名(官方 openclaw-lark + 旧版 feishu-openclaw-plugin):
2661
+ * 备份 / 清理 / 安装、以及 plugins.allow 成员判定时迭代用。
2662
+ * 注意与「可启用的飞书插件集」区分——后者还含**无目录**的内置 feishu(见各规则局部定义)。
2663
+ */
2664
+ const LARK_PLUGIN_DIR_NAMES = [LARK_PLUGIN_NAME, LEGACY_LARK_PLUGIN_NAME];
2696
2665
  /**
2697
2666
  * install-extension --all 安装的官方扩展插件集合(manifest role=extension)。
2698
2667
  * builtin-plugin-missing 与 miaoda-official-plugins-install-spec-unlock 共用此单一来源,
@@ -2710,535 +2679,147 @@ const WORKSPACE_AGENT_REL = "workspace/agent";
2710
2679
  /** openclaw.json 默认相对路径。 */
2711
2680
  const DEFAULT_CONFIG_REL = `${WORKSPACE_AGENT_REL}/openclaw.json`;
2712
2681
  //#endregion
2713
- //#region src/lark-cli-init.ts
2714
- const PE_XML_TAG = "lark-cli-pe";
2715
- const PE_PLACEHOLDER = `
2716
- <${PE_XML_TAG}>
2717
- **【强制要求 - 无例外】飞书所有操作必须先读 feishu-lark-cli skill。未读skill不得调用任何飞书工具,违者视为操作失误。**
2718
- </${PE_XML_TAG}>
2719
- `;
2720
- function isLarkPluginInstalled$1(configPath) {
2721
- const extDir = getExtensionsDir(configPath);
2722
- return FEISHU_PLUGIN_DIR_NAMES.some((name) => {
2723
- try {
2724
- return node_fs.default.existsSync(node_path.default.join(extDir, name, "package.json"));
2725
- } catch {
2726
- return false;
2727
- }
2728
- });
2682
+ //#region src/rules/miaoda-official-plugins-install-spec-unlock.ts
2683
+ /**
2684
+ * Official miaoda-side plugins that must track manifest — version-locked specs
2685
+ * here block upgrades. Third-party / user-installed plugins are intentionally
2686
+ * out of scope (users may pin them deliberately). 集合见 constants.OFFICIAL_EXTENSION_PLUGIN_NAMES。
2687
+ */
2688
+ const LOCKED_NPM_SPEC = /^(@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*@[^@/:#\s]+$/i;
2689
+ function isLockedNpmSpec(spec) {
2690
+ return typeof spec === "string" && LOCKED_NPM_SPEC.test(spec);
2729
2691
  }
2730
- function isLarkCliAvailable$2() {
2731
- try {
2732
- return (0, node_child_process.spawnSync)("lark-cli", ["--version"], {
2733
- encoding: "utf-8",
2734
- timeout: 5e3,
2735
- stdio: [
2736
- "ignore",
2737
- "pipe",
2738
- "ignore"
2739
- ]
2740
- }).status === 0;
2741
- } catch {
2742
- return false;
2692
+ function unlockSpec(spec) {
2693
+ const slash = spec.indexOf("/");
2694
+ const cut = slash === -1 ? spec.indexOf("@") : spec.indexOf("@", slash + 1);
2695
+ return spec.slice(0, cut);
2696
+ }
2697
+ /** Yield `[key, lockedSpec]` for every official-plugin install whose `spec` is locked. */
2698
+ function* iterLockedOfficialInstalls(config) {
2699
+ const installs = getNestedMap(config, "plugins", "installs");
2700
+ if (!installs) return;
2701
+ for (const [key, entry] of Object.entries(installs)) {
2702
+ if (!OFFICIAL_EXTENSION_PLUGIN_NAMES.has(key)) continue;
2703
+ const spec = asRecord(entry)?.spec;
2704
+ if (isLockedNpmSpec(spec)) yield [key, spec];
2743
2705
  }
2744
2706
  }
2745
- function readConfig(configPath) {
2746
- try {
2747
- const raw = node_fs.default.readFileSync(configPath, "utf-8");
2748
- const parsed = loadJSON5().parse(raw);
2749
- return parsed != null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
2750
- } catch {
2751
- return null;
2707
+ let MiaodaOfficialPluginsInstallSpecUnlockRule = class MiaodaOfficialPluginsInstallSpecUnlockRule extends DiagnoseRule {
2708
+ validate(ctx) {
2709
+ const locked = [...iterLockedOfficialInstalls(ctx.config)].map(([k]) => k);
2710
+ if (locked.length === 0) return { pass: true };
2711
+ return {
2712
+ pass: false,
2713
+ message: "plugins.installs 中官方插件存在锁版本的 spec: " + locked.sort().join(",")
2714
+ };
2715
+ }
2716
+ repair(ctx) {
2717
+ for (const [key, spec] of iterLockedOfficialInstalls(ctx.config)) setNestedValue(ctx.config, [
2718
+ "plugins",
2719
+ "installs",
2720
+ key,
2721
+ "spec"
2722
+ ], unlockSpec(spec));
2752
2723
  }
2724
+ };
2725
+ MiaodaOfficialPluginsInstallSpecUnlockRule = __decorate([Rule({
2726
+ key: "miaoda_official_plugins_install_spec_unlock",
2727
+ description: "移除官方妙搭插件安装条目中的锁版本 npm spec,使其跟随最新 manifest 版本",
2728
+ dependsOn: ["config_syntax_check"],
2729
+ repairMode: "standard",
2730
+ level: "silent"
2731
+ })], MiaodaOfficialPluginsInstallSpecUnlockRule);
2732
+ //#endregion
2733
+ //#region src/rules/miaoda-plugin-allow.ts
2734
+ let MiaodaPluginAllowRule = class MiaodaPluginAllowRule extends DiagnoseRule {
2735
+ validate(ctx) {
2736
+ if (!isPluginInstalledOnDisk(getExtensionsDir(ctx.configPath), "openclaw-extension-miaoda")) return { pass: true };
2737
+ if (getAllow$1(ctx.config).includes("openclaw-extension-miaoda")) return { pass: true };
2738
+ return {
2739
+ pass: false,
2740
+ message: `plugins.allow 缺少 ${MIAODA_PLUGIN_NAME}(已在 extensions/ 下装但未启用)`
2741
+ };
2742
+ }
2743
+ repair(ctx) {
2744
+ if (!isPluginInstalledOnDisk(getExtensionsDir(ctx.configPath), "openclaw-extension-miaoda")) return;
2745
+ const plugins = ctx.config.plugins;
2746
+ if (plugins == null || typeof plugins !== "object" || Array.isArray(plugins)) {
2747
+ ctx.config.plugins = { allow: [MIAODA_PLUGIN_NAME] };
2748
+ return;
2749
+ }
2750
+ const pluginsMap = plugins;
2751
+ const rawAllow = pluginsMap.allow;
2752
+ const allow = Array.isArray(rawAllow) ? rawAllow : [];
2753
+ if (allow.includes("openclaw-extension-miaoda")) return;
2754
+ allow.push(MIAODA_PLUGIN_NAME);
2755
+ pluginsMap.allow = allow;
2756
+ }
2757
+ };
2758
+ MiaodaPluginAllowRule = __decorate([Rule({
2759
+ key: "miaoda_plugin_allow",
2760
+ description: "当 openclaw-extension-miaoda 已在磁盘安装但未在 allow 列表中时,将其添加到 plugins.allow(实验性)",
2761
+ dependsOn: ["config_syntax_check"],
2762
+ repairMode: "standard",
2763
+ level: "critical",
2764
+ profile: "standard"
2765
+ })], MiaodaPluginAllowRule);
2766
+ function getAllow$1(config) {
2767
+ const plugins = config.plugins;
2768
+ if (plugins == null || typeof plugins !== "object" || Array.isArray(plugins)) return [];
2769
+ const allow = plugins.allow;
2770
+ if (!Array.isArray(allow)) return [];
2771
+ return allow.filter((e) => typeof e === "string");
2753
2772
  }
2754
- /**
2755
- * Resolve the feishu app secret for the given appId.
2756
- *
2757
- * Lookup order:
2758
- * 1. channels.feishu.appSecret (single-agent: feishu.appId === appId)
2759
- * 2. channels.feishu.accounts[key].appSecret (multi-agent: account.appId === appId)
2760
- *
2761
- * Value interpretation:
2762
- * - string → use directly
2763
- * - object → secret is managed by a provider; use `feishuAppSecret` param instead
2764
- *
2765
- * Returns null when the secret cannot be determined.
2766
- */
2767
- function resolveAppSecret(appId, config, feishuAppSecret) {
2768
- const feishu = getNestedMap(config, "channels", "feishu");
2769
- if (!feishu) return null;
2770
- let rawSecret;
2771
- if (typeof feishu.appId === "string" && feishu.appId === appId) rawSecret = feishu.appSecret;
2772
- else {
2773
- const accounts = asRecord(feishu.accounts);
2774
- if (accounts) for (const [, val] of Object.entries(accounts)) {
2775
- const account = asRecord(val);
2776
- if (account?.appId === appId) {
2777
- rawSecret = account.appSecret ?? feishu.appSecret;
2778
- break;
2779
- }
2773
+ //#endregion
2774
+ //#region src/rules/lark-plugin-allow.ts
2775
+ let LarkPluginAllowRule = class LarkPluginAllowRule extends DiagnoseRule {
2776
+ validate(ctx) {
2777
+ const allow = getAllow(ctx.config);
2778
+ if (LARK_PLUGIN_DIR_NAMES.some((name) => allow.includes(name))) return { pass: true };
2779
+ const installed = detectInstalledLarkPlugin$1(getExtensionsDir(ctx.configPath));
2780
+ if (installed == null) return { pass: true };
2781
+ return {
2782
+ pass: false,
2783
+ message: `plugins.allow 缺少飞书插件 ${installed}(已在 extensions/ 下装但未启用)`
2784
+ };
2785
+ }
2786
+ repair(ctx) {
2787
+ const installed = detectInstalledLarkPlugin$1(getExtensionsDir(ctx.configPath));
2788
+ if (installed == null) return;
2789
+ if (ctx.config.plugins == null || typeof ctx.config.plugins !== "object" || Array.isArray(ctx.config.plugins)) {
2790
+ ctx.config.plugins = { allow: [installed] };
2791
+ return;
2780
2792
  }
2793
+ const pluginsMap = ctx.config.plugins;
2794
+ const rawAllow = pluginsMap.allow;
2795
+ const original = Array.isArray(rawAllow) ? rawAllow : [];
2796
+ const stringAllow = original.filter((e) => typeof e === "string");
2797
+ if (LARK_PLUGIN_DIR_NAMES.some((name) => stringAllow.includes(name))) return;
2798
+ original.push(installed);
2799
+ pluginsMap.allow = original;
2781
2800
  }
2782
- if (typeof rawSecret === "string" && rawSecret) return rawSecret;
2783
- if (rawSecret != null && typeof rawSecret === "object") return feishuAppSecret ?? null;
2784
- return null;
2801
+ };
2802
+ LarkPluginAllowRule = __decorate([Rule({
2803
+ key: "lark_plugin_allow",
2804
+ description: "当飞书插件(openclaw-lark 或旧版名)已在磁盘安装但未加入 plugins.allow 时,自动添加",
2805
+ dependsOn: ["config_syntax_check"],
2806
+ repairMode: "standard",
2807
+ level: "critical"
2808
+ })], LarkPluginAllowRule);
2809
+ function getAllow(config) {
2810
+ const plugins = config.plugins;
2811
+ if (plugins == null || typeof plugins !== "object" || Array.isArray(plugins)) return [];
2812
+ const allow = plugins.allow;
2813
+ if (!Array.isArray(allow)) return [];
2814
+ return allow.filter((e) => typeof e === "string");
2785
2815
  }
2786
2816
  /**
2787
- * Resolve the agents.md path for the given appId from the openclaw config.
2788
- *
2789
- * Case 1: appId matches channels.feishu.appId (single-agent path)
2790
- * → WORKSPACE_DIR/AGENTS.md
2791
- *
2792
- * Case 2: appId found in channels.feishu.accounts (multi-agent path)
2793
- * → find account key where account.appId === appId
2794
- * → find binding where match.channel=feishu && match.accountId=that key
2795
- * → if agentId === 'main' → WORKSPACE_DIR/agents.md
2796
- * → else find agent in agents.list by id → agent.workspace/agents.md
2797
- *
2798
- * Returns null when the path cannot be determined.
2817
+ * fs-only 检测:`<extDir>/<name>/package.json` 存在即视为已装。
2818
+ * 优先级 openclaw-lark(新版)> feishu-openclaw-plugin(legacy)。
2819
+ * 不读 package.json 内容,只判存在性,避开 JSON 损坏。
2799
2820
  */
2800
- function resolveAgentsMdPath(appId, config) {
2801
- const feishu = getNestedMap(config, "channels", "feishu");
2802
- if (!feishu) {
2803
- console.error("resolveAgentsMdPath: channels.feishu not found");
2804
- return null;
2805
- }
2806
- if (typeof feishu.appId === "string" && feishu.appId === appId) {
2807
- console.error(`resolveAgentsMdPath: case=single-agent feishu.appId=${feishu.appId}`);
2808
- return node_path.default.join(WORKSPACE_DIR, "workspace", "AGENTS.md");
2809
- }
2810
- const accounts = asRecord(feishu.accounts);
2811
- if (!accounts) {
2812
- console.error("resolveAgentsMdPath: feishu.accounts not found");
2813
- return null;
2814
- }
2815
- let accountId;
2816
- for (const [key, val] of Object.entries(accounts)) if (asRecord(val)?.appId === appId) {
2817
- accountId = key;
2818
- break;
2819
- }
2820
- if (!accountId) {
2821
- console.error(`resolveAgentsMdPath: no account found with appId=${appId} in feishu.accounts keys=[${Object.keys(accounts).join(",")}]`);
2822
- return null;
2823
- }
2824
- console.error(`resolveAgentsMdPath: found accountId=${accountId}`);
2825
- const bindings = Array.isArray(config.bindings) ? config.bindings : [];
2826
- let agentId;
2827
- for (const b of bindings) {
2828
- const binding = asRecord(b);
2829
- if (!binding) continue;
2830
- const match = asRecord(binding.match);
2831
- if (match?.channel === "feishu" && match?.accountId === accountId) {
2832
- if (typeof binding.agentId === "string") {
2833
- agentId = binding.agentId;
2834
- break;
2835
- }
2836
- }
2837
- }
2838
- if (!agentId) {
2839
- console.error(`resolveAgentsMdPath: no binding found for accountId=${accountId} in bindings(count=${bindings.length})`);
2840
- return null;
2841
- }
2842
- console.error(`resolveAgentsMdPath: found agentId=${agentId}`);
2843
- if (agentId === "main") {
2844
- console.error("resolveAgentsMdPath: case=multi-agent-main");
2845
- return node_path.default.join(WORKSPACE_DIR, "workspace", "AGENTS.md");
2846
- }
2847
- const agentsObj = asRecord(config.agents);
2848
- const list = Array.isArray(agentsObj?.list) ? agentsObj.list : [];
2849
- for (const a of list) {
2850
- const agent = asRecord(a);
2851
- if (agent?.id === agentId) {
2852
- const ws = typeof agent.workspace === "string" ? agent.workspace : node_path.default.join(WORKSPACE_DIR, "workspace");
2853
- console.error(`resolveAgentsMdPath: case=multi-agent-custom agentId=${agentId} workspace=${ws}`);
2854
- return node_path.default.join(ws, "AGENTS.md");
2855
- }
2856
- }
2857
- console.error(`resolveAgentsMdPath: agentId=${agentId} not found in agents.list(count=${list.length})`);
2858
- return null;
2859
- }
2860
- function appendPeToAgentsMd(agentsMdPath) {
2861
- const dir = node_path.default.dirname(agentsMdPath);
2862
- if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
2863
- const existing = node_fs.default.existsSync(agentsMdPath) ? node_fs.default.readFileSync(agentsMdPath, "utf-8") : "";
2864
- if (existing.includes(`<lark-cli-pe>`)) {
2865
- console.error(`lark-cli-init: <${PE_XML_TAG}> already present in ${agentsMdPath}, skipping`);
2866
- return;
2867
- }
2868
- const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
2869
- node_fs.default.appendFileSync(agentsMdPath, `${sep}${PE_PLACEHOLDER}`, "utf-8");
2870
- console.error(`lark-cli-init: appended PE placeholder to ${agentsMdPath}`);
2871
- }
2872
- /**
2873
- * Collect every Feishu bot appId declared in the openclaw config.
2874
- * Covers both single-agent (channels.feishu.appId) and multi-agent
2875
- * (channels.feishu.accounts[*].appId) layouts. Returns a deduplicated list.
2876
- */
2877
- function collectFeishuAppIds(configPath) {
2878
- const config = readConfig(configPath ?? CONFIG_PATH);
2879
- if (!config) return [];
2880
- const feishu = getNestedMap(config, "channels", "feishu");
2881
- if (!feishu) return [];
2882
- const appIds = /* @__PURE__ */ new Set();
2883
- const topAppId = feishu.appId;
2884
- if (typeof topAppId === "string" && topAppId.trim()) appIds.add(topAppId.trim());
2885
- const accounts = asRecord(feishu.accounts);
2886
- if (accounts) for (const val of Object.values(accounts)) {
2887
- const appId = asRecord(val)?.appId;
2888
- if (typeof appId === "string" && appId.trim()) appIds.add(appId.trim());
2889
- }
2890
- return [...appIds];
2891
- }
2892
- function runLarkCliInit(opts) {
2893
- const configPath = opts.configPath ?? CONFIG_PATH;
2894
- if (!isLarkPluginInstalled$1(configPath)) {
2895
- console.error("lark-cli-init: skipping — openclaw-lark plugin not installed");
2896
- return {
2897
- ok: true,
2898
- skipped: true,
2899
- skipReason: "openclaw-lark plugin not installed"
2900
- };
2901
- }
2902
- if (!isLarkCliAvailable$2()) {
2903
- console.error("lark-cli-init: skipping — lark-cli command not found");
2904
- return {
2905
- ok: true,
2906
- skipped: true,
2907
- skipReason: "lark-cli command not found"
2908
- };
2909
- }
2910
- const config = readConfig(configPath);
2911
- if (!config) return {
2912
- ok: false,
2913
- error: `could not read config at ${configPath}`
2914
- };
2915
- const agentsMdPath = resolveAgentsMdPath(opts.appId, config);
2916
- console.error(`lark-cli-init: resolved agents.md path=${agentsMdPath ?? "(null)"}`);
2917
- if (!agentsMdPath) return {
2918
- ok: false,
2919
- error: `could not resolve agents.md path for appId=${opts.appId}`
2920
- };
2921
- const appSecret = resolveAppSecret(opts.appId, config, opts.feishuAppSecret);
2922
- if (!appSecret) return {
2923
- ok: false,
2924
- error: `could not resolve appSecret for appId=${opts.appId}`
2925
- };
2926
- console.error(`lark-cli-init: running lark-cli config init --name ${opts.appId} --app-id ${opts.appId} --brand feishu --app-secret-stdin --force-init`);
2927
- const initRes = (0, node_child_process.spawnSync)("lark-cli", [
2928
- "config",
2929
- "init",
2930
- "--name",
2931
- opts.appId,
2932
- "--app-id",
2933
- opts.appId,
2934
- "--brand",
2935
- "feishu",
2936
- "--app-secret-stdin",
2937
- "--force-init"
2938
- ], {
2939
- stdio: [
2940
- "pipe",
2941
- "pipe",
2942
- "pipe"
2943
- ],
2944
- encoding: "utf-8",
2945
- input: appSecret
2946
- });
2947
- const configInitStdout = initRes.stdout?.trim() || void 0;
2948
- const configInitStderr = initRes.stderr?.trim() || void 0;
2949
- if (configInitStdout) console.error(`lark-cli config init stdout: ${configInitStdout}`);
2950
- if (configInitStderr) console.error(`lark-cli config init stderr: ${configInitStderr}`);
2951
- if (initRes.error) return {
2952
- ok: false,
2953
- configInitStdout,
2954
- configInitStderr,
2955
- error: `lark-cli config init spawn error: ${initRes.error.message}`
2956
- };
2957
- if (initRes.status !== 0) return {
2958
- ok: false,
2959
- configInitExitCode: initRes.status ?? void 0,
2960
- configInitStdout,
2961
- configInitStderr,
2962
- error: `lark-cli config init exited with code ${initRes.status}`
2963
- };
2964
- appendPeToAgentsMd(agentsMdPath);
2965
- return {
2966
- ok: true,
2967
- configInitExitCode: 0,
2968
- agentsMdPath
2969
- };
2970
- }
2971
- //#endregion
2972
- //#region src/utils/feishu-tools-deny.ts
2973
- /**
2974
- * openclaw-lark 中与 lark-cli 功能重叠的工具,写入 channels.feishu.tools.deny 后
2975
- * 由 openclaw-lark 插件自身的 shouldRegisterTool() 跳过注册,避免与 lark-cli 重复。
2976
- * 支持尾部 `*` 通配(openclaw-lark matchesAnyPattern 识别)。
2977
- *
2978
- * 保留(IM / 鉴权 / 诊断类,与 lark-cli 无重叠):feishu_chat*、feishu_get_user、
2979
- * feishu_search_user、feishu_im_*、feishu_oauth*、feishu_auth、feishu_diagnose、feishu_doctor。
2980
- *
2981
- * install-cli(安装 lark-cli 后即时写)与 agents_md_lark_cli_pe 规则(doctor --fix
2982
- * 自愈)共用本常量与合并逻辑,单一来源。
2983
- */
2984
- const LARK_CLI_OVERLAP_TOOL_DENY = Object.freeze([
2985
- "feishu_create_doc",
2986
- "feishu_fetch_doc",
2987
- "feishu_update_doc",
2988
- "feishu_doc_comments",
2989
- "feishu_doc_media",
2990
- "feishu_drive_file",
2991
- "feishu_wiki_space",
2992
- "feishu_wiki_space_node",
2993
- "feishu_search_doc_wiki",
2994
- "feishu_bitable_*",
2995
- "feishu_calendar_*",
2996
- "feishu_task_*",
2997
- "feishu_sheet"
2998
- ]);
2999
- /** 读取 channels.feishu.tools.deny 的字符串数组(缺失/非法时返回空数组)。 */
3000
- function readDenyList(config) {
3001
- const deny = asRecord(asRecord(asRecord(config.channels)?.feishu)?.tools)?.deny;
3002
- return Array.isArray(deny) ? deny.filter((e) => typeof e === "string") : [];
3003
- }
3004
- /** 返回 deny 列表中尚缺的重叠工具(用于 validate 判定)。 */
3005
- function larkCliToolDenyMissing(config) {
3006
- const existing = new Set(readDenyList(config));
3007
- return LARK_CLI_OVERLAP_TOOL_DENY.filter((t) => !existing.has(t));
3008
- }
3009
- /**
3010
- * 确保 channels.feishu.tools.deny 含全部重叠工具(只增不删,幂等合并)。
3011
- * 直接 mutate 传入的 config 对象;返回是否发生了变更。
3012
- */
3013
- function ensureLarkCliToolDeny(config) {
3014
- if (larkCliToolDenyMissing(config).length === 0) return false;
3015
- if (!config.channels || typeof config.channels !== "object" || Array.isArray(config.channels)) config.channels = {};
3016
- const channels = config.channels;
3017
- if (!channels.feishu || typeof channels.feishu !== "object" || Array.isArray(channels.feishu)) channels.feishu = {};
3018
- const feishu = channels.feishu;
3019
- if (!feishu.tools || typeof feishu.tools !== "object" || Array.isArray(feishu.tools)) feishu.tools = {};
3020
- const tools = feishu.tools;
3021
- const existing = readDenyList(config);
3022
- tools.deny = [...new Set([...existing, ...LARK_CLI_OVERLAP_TOOL_DENY])];
3023
- return true;
3024
- }
3025
- //#endregion
3026
- //#region src/utils/lark-cli-detect.ts
3027
- /**
3028
- * 探测 lark-cli 是否可用(`lark-cli --version` 退出码为 0)。
3029
- * 多条规则共用同一判定:feishu_plugin_state_normalize(是否补 feishu_* 授权)、
3030
- * agents_md_lark_cli_pe(是否补 PE / deny)等。
3031
- */
3032
- function isLarkCliAvailable$1() {
3033
- try {
3034
- return (0, node_child_process.spawnSync)("lark-cli", ["--version"], {
3035
- encoding: "utf-8",
3036
- timeout: 5e3,
3037
- stdio: [
3038
- "ignore",
3039
- "pipe",
3040
- "ignore"
3041
- ]
3042
- }).status === 0;
3043
- } catch {
3044
- return false;
3045
- }
3046
- }
3047
- //#endregion
3048
- //#region src/rules/agents-md-lark-cli-pe.ts
3049
- /** openclaw-lark 插件是否已落盘安装(deny 仅在插件存在时有意义)。 */
3050
- function isLarkPluginInstalled(ctx) {
3051
- return node_fs.default.existsSync(node_path.default.join(getExtensionsDir(ctx.configPath), LARK_PLUGIN_NAME));
3052
- }
3053
- /** 第一个缺失 PE 内容的 AGENTS.md 路径;无则 undefined。 */
3054
- function findAgentsMdMissingPe(ctx) {
3055
- return collectExistingAgentsMdPaths(ctx).find((filePath) => {
3056
- return !node_fs.default.readFileSync(filePath, "utf-8").includes(`<${PE_XML_TAG}>`);
3057
- });
3058
- }
3059
- /**
3060
- * lark-cli 存在时统一规范化环境:
3061
- * 1. 给各智能体 AGENTS.md 追加 lark-cli-pe PE 内容
3062
- * 2. openclaw-lark 已装时,把与 lark-cli 重叠的工具写入 channels.feishu.tools.deny
3063
- * (只增不删,避免与 lark-cli 重复注册)
3064
- * 两者门禁同为「lark-cli 可用」,故合并为一条规则。
3065
- */
3066
- let AgentsMdLarkCliPeRule = class AgentsMdLarkCliPeRule extends DiagnoseRule {
3067
- validate(ctx) {
3068
- if (!isLarkCliAvailable$1()) return { pass: true };
3069
- const missingPath = findAgentsMdMissingPe(ctx);
3070
- if (missingPath) return {
3071
- pass: false,
3072
- message: `${missingPath} 中缺少 lark-cli-pe PE 内容,需要追加`
3073
- };
3074
- if (isLarkPluginInstalled(ctx) && larkCliToolDenyMissing(ctx.config).length > 0) return {
3075
- pass: false,
3076
- message: "channels.feishu.tools.deny 缺少与 lark-cli 重叠的 openclaw-lark 工具,需要补充"
3077
- };
3078
- return { pass: true };
3079
- }
3080
- repair(ctx) {
3081
- if (!isLarkCliAvailable$1()) return;
3082
- for (const filePath of collectExistingAgentsMdPaths(ctx)) {
3083
- const content = node_fs.default.readFileSync(filePath, "utf-8");
3084
- if (content.includes(`<lark-cli-pe>`)) continue;
3085
- const sep = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
3086
- node_fs.default.appendFileSync(filePath, `${sep}${PE_PLACEHOLDER}`, "utf-8");
3087
- console.error(`agents-md-lark-cli-pe: appended PE to ${filePath}`);
3088
- }
3089
- if (isLarkPluginInstalled(ctx) && ensureLarkCliToolDeny(ctx.config)) console.error("agents-md-lark-cli-pe: merged lark-cli overlap tools into channels.feishu.tools.deny");
3090
- }
3091
- };
3092
- AgentsMdLarkCliPeRule = __decorate([Rule({
3093
- key: "agents_md_lark_cli_pe",
3094
- description: "lark-cli 存在时规范化环境:AGENTS.md 追加 lark-cli-pe PE + openclaw-lark 重叠工具写入 channels.feishu.tools.deny",
3095
- dependsOn: ["config_syntax_check"],
3096
- repairMode: "standard",
3097
- level: "silent"
3098
- })], AgentsMdLarkCliPeRule);
3099
- //#endregion
3100
- //#region src/rules/miaoda-official-plugins-install-spec-unlock.ts
3101
- /**
3102
- * Official miaoda-side plugins that must track manifest — version-locked specs
3103
- * here block upgrades. Third-party / user-installed plugins are intentionally
3104
- * out of scope (users may pin them deliberately). 集合见 constants.OFFICIAL_EXTENSION_PLUGIN_NAMES。
3105
- */
3106
- const LOCKED_NPM_SPEC = /^(@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*@[^@/:#\s]+$/i;
3107
- function isLockedNpmSpec(spec) {
3108
- return typeof spec === "string" && LOCKED_NPM_SPEC.test(spec);
3109
- }
3110
- function unlockSpec(spec) {
3111
- const slash = spec.indexOf("/");
3112
- const cut = slash === -1 ? spec.indexOf("@") : spec.indexOf("@", slash + 1);
3113
- return spec.slice(0, cut);
3114
- }
3115
- /** Yield `[key, lockedSpec]` for every official-plugin install whose `spec` is locked. */
3116
- function* iterLockedOfficialInstalls(config) {
3117
- const installs = getNestedMap(config, "plugins", "installs");
3118
- if (!installs) return;
3119
- for (const [key, entry] of Object.entries(installs)) {
3120
- if (!OFFICIAL_EXTENSION_PLUGIN_NAMES.has(key)) continue;
3121
- const spec = asRecord(entry)?.spec;
3122
- if (isLockedNpmSpec(spec)) yield [key, spec];
3123
- }
3124
- }
3125
- let MiaodaOfficialPluginsInstallSpecUnlockRule = class MiaodaOfficialPluginsInstallSpecUnlockRule extends DiagnoseRule {
3126
- validate(ctx) {
3127
- const locked = [...iterLockedOfficialInstalls(ctx.config)].map(([k]) => k);
3128
- if (locked.length === 0) return { pass: true };
3129
- return {
3130
- pass: false,
3131
- message: "plugins.installs 中官方插件存在锁版本的 spec: " + locked.sort().join(",")
3132
- };
3133
- }
3134
- repair(ctx) {
3135
- for (const [key, spec] of iterLockedOfficialInstalls(ctx.config)) setNestedValue(ctx.config, [
3136
- "plugins",
3137
- "installs",
3138
- key,
3139
- "spec"
3140
- ], unlockSpec(spec));
3141
- }
3142
- };
3143
- MiaodaOfficialPluginsInstallSpecUnlockRule = __decorate([Rule({
3144
- key: "miaoda_official_plugins_install_spec_unlock",
3145
- description: "移除官方妙搭插件安装条目中的锁版本 npm spec,使其跟随最新 manifest 版本",
3146
- dependsOn: ["config_syntax_check"],
3147
- repairMode: "standard",
3148
- level: "silent"
3149
- })], MiaodaOfficialPluginsInstallSpecUnlockRule);
3150
- //#endregion
3151
- //#region src/rules/miaoda-plugin-allow.ts
3152
- let MiaodaPluginAllowRule = class MiaodaPluginAllowRule extends DiagnoseRule {
3153
- validate(ctx) {
3154
- if (!isPluginInstalledOnDisk(getExtensionsDir(ctx.configPath), "openclaw-extension-miaoda")) return { pass: true };
3155
- if (getAllow$1(ctx.config).includes("openclaw-extension-miaoda")) return { pass: true };
3156
- return {
3157
- pass: false,
3158
- message: `plugins.allow 缺少 ${MIAODA_PLUGIN_NAME}(已在 extensions/ 下装但未启用)`
3159
- };
3160
- }
3161
- repair(ctx) {
3162
- if (!isPluginInstalledOnDisk(getExtensionsDir(ctx.configPath), "openclaw-extension-miaoda")) return;
3163
- const plugins = ctx.config.plugins;
3164
- if (plugins == null || typeof plugins !== "object" || Array.isArray(plugins)) {
3165
- ctx.config.plugins = { allow: [MIAODA_PLUGIN_NAME] };
3166
- return;
3167
- }
3168
- const pluginsMap = plugins;
3169
- const rawAllow = pluginsMap.allow;
3170
- const allow = Array.isArray(rawAllow) ? rawAllow : [];
3171
- if (allow.includes("openclaw-extension-miaoda")) return;
3172
- allow.push(MIAODA_PLUGIN_NAME);
3173
- pluginsMap.allow = allow;
3174
- }
3175
- };
3176
- MiaodaPluginAllowRule = __decorate([Rule({
3177
- key: "miaoda_plugin_allow",
3178
- description: "当 openclaw-extension-miaoda 已在磁盘安装但未在 allow 列表中时,将其添加到 plugins.allow(实验性)",
3179
- dependsOn: ["config_syntax_check"],
3180
- repairMode: "standard",
3181
- level: "critical",
3182
- profile: "standard"
3183
- })], MiaodaPluginAllowRule);
3184
- function getAllow$1(config) {
3185
- const plugins = config.plugins;
3186
- if (plugins == null || typeof plugins !== "object" || Array.isArray(plugins)) return [];
3187
- const allow = plugins.allow;
3188
- if (!Array.isArray(allow)) return [];
3189
- return allow.filter((e) => typeof e === "string");
3190
- }
3191
- //#endregion
3192
- //#region src/rules/lark-plugin-allow.ts
3193
- const LARK_PLUGIN_NAMES = FEISHU_PLUGIN_DIR_NAMES;
3194
- let LarkPluginAllowRule = class LarkPluginAllowRule extends DiagnoseRule {
3195
- validate(ctx) {
3196
- const allow = getAllow(ctx.config);
3197
- if (LARK_PLUGIN_NAMES.some((name) => allow.includes(name))) return { pass: true };
3198
- const installed = detectInstalledLarkPlugin(getExtensionsDir(ctx.configPath));
3199
- if (installed == null) return { pass: true };
3200
- return {
3201
- pass: false,
3202
- message: `plugins.allow 缺少飞书插件 ${installed}(已在 extensions/ 下装但未启用)`
3203
- };
3204
- }
3205
- repair(ctx) {
3206
- const installed = detectInstalledLarkPlugin(getExtensionsDir(ctx.configPath));
3207
- if (installed == null) return;
3208
- if (ctx.config.plugins == null || typeof ctx.config.plugins !== "object" || Array.isArray(ctx.config.plugins)) {
3209
- ctx.config.plugins = { allow: [installed] };
3210
- return;
3211
- }
3212
- const pluginsMap = ctx.config.plugins;
3213
- const rawAllow = pluginsMap.allow;
3214
- const original = Array.isArray(rawAllow) ? rawAllow : [];
3215
- const stringAllow = original.filter((e) => typeof e === "string");
3216
- if (LARK_PLUGIN_NAMES.some((name) => stringAllow.includes(name))) return;
3217
- original.push(installed);
3218
- pluginsMap.allow = original;
3219
- }
3220
- };
3221
- LarkPluginAllowRule = __decorate([Rule({
3222
- key: "lark_plugin_allow",
3223
- description: "当飞书插件(openclaw-lark 或旧版名)已在磁盘安装但未加入 plugins.allow 时,自动添加",
3224
- dependsOn: ["config_syntax_check"],
3225
- repairMode: "standard",
3226
- level: "critical"
3227
- })], LarkPluginAllowRule);
3228
- function getAllow(config) {
3229
- const plugins = config.plugins;
3230
- if (plugins == null || typeof plugins !== "object" || Array.isArray(plugins)) return [];
3231
- const allow = plugins.allow;
3232
- if (!Array.isArray(allow)) return [];
3233
- return allow.filter((e) => typeof e === "string");
3234
- }
3235
- /**
3236
- * fs-only 检测:`<extDir>/<name>/package.json` 存在即视为已装。
3237
- * 优先级 openclaw-lark(新版)> feishu-openclaw-plugin(legacy)。
3238
- * 不读 package.json 内容,只判存在性,避开 JSON 损坏。
3239
- */
3240
- function detectInstalledLarkPlugin(extDir) {
3241
- for (const name of LARK_PLUGIN_NAMES) if (pluginPackageJsonExists(extDir, name)) return name;
2821
+ function detectInstalledLarkPlugin$1(extDir) {
2822
+ for (const name of LARK_PLUGIN_DIR_NAMES) if (pluginPackageJsonExists(extDir, name)) return name;
3242
2823
  return null;
3243
2824
  }
3244
2825
  function pluginPackageJsonExists(extDir, pluginDir) {
@@ -3756,19 +3337,12 @@ function cleanupLegacyResiduals(ctx) {
3756
3337
  const target = node_path.default.join(extDir, name);
3757
3338
  const rel = node_path.default.relative(extDir, target);
3758
3339
  if (!rel || rel.startsWith("..") || node_path.default.isAbsolute(rel)) continue;
3759
- try {
3760
- rmrfTolerant(target);
3761
- } catch (e) {
3762
- console.error(`[feishu_plugin_state_normalize] rmrf ${target} failed: ${e.message}`);
3763
- }
3764
- }
3765
- }
3766
- function ensureRecord(obj, key) {
3767
- const cur = obj[key];
3768
- if (cur != null && typeof cur === "object" && !Array.isArray(cur)) return cur;
3769
- const fresh = {};
3770
- obj[key] = fresh;
3771
- return fresh;
3340
+ try {
3341
+ rmrfTolerant(target);
3342
+ } catch (e) {
3343
+ console.error(`[feishu_plugin_state_normalize] rmrf ${target} failed: ${e.message}`);
3344
+ }
3345
+ }
3772
3346
  }
3773
3347
  //#endregion
3774
3348
  //#region src/version-compat.ts
@@ -3873,10 +3447,6 @@ function coerceCalVer(v) {
3873
3447
  function compareCalVer(a, b) {
3874
3448
  return semver.default.compare(coerceCalVer(a), coerceCalVer(b));
3875
3449
  }
3876
- /** Look up an entry by exact plugin version; undefined if not in the table. */
3877
- function findEntry(pluginVersion) {
3878
- return VERSION_COMPAT_MAP.find((e) => e.openclawLarkVersion === pluginVersion);
3879
- }
3880
3450
  /**
3881
3451
  * Infer the effective upper bound for a compat entry that has no explicit maxOpenclawVersion.
3882
3452
  *
@@ -3899,316 +3469,765 @@ function inferEffectiveMax(index) {
3899
3469
  };
3900
3470
  }
3901
3471
  /**
3902
- * Full version compatibility check that infers the effective maxOpenclawVersion for
3903
- * entries that only define minOpenclawVersion.
3472
+ * 对一个**已对标到 VERSION_COMPAT_MAP 键**的版本评估其与 openclaw 的兼容性,
3473
+ * 并在不兼容时给出方向。fork pin、legacy 分类等在 `checkPluginCompat` 处理,
3474
+ * 本函数只负责「版本 × openclaw 区间」这一段唯一逻辑。
3475
+ *
3476
+ * - index === -1(插件版本比全表都旧,无 floor 条目)→ 无区间可判,视为插件过旧 → 'lark'
3477
+ * - oc < entry.minOpenclawVersion → 'openclaw'
3478
+ * - oc ≥ 推断/显式上界 → 'lark'
3479
+ * - 否则兼容
3480
+ *
3481
+ * 上界语义:显式 maxOpenclawVersion 为 INCLUSIVE;推断上界为 EXCLUSIVE。
3482
+ */
3483
+ function evaluateVersionRange(pluginVersion, openclawVersion) {
3484
+ const index = VERSION_COMPAT_MAP.findIndex((e) => compareCalVer(e.openclawLarkVersion, pluginVersion) <= 0);
3485
+ if (index === -1) return {
3486
+ compatible: false,
3487
+ direction: "lark",
3488
+ entry: void 0
3489
+ };
3490
+ const entry = VERSION_COMPAT_MAP[index];
3491
+ const oc = coerceCalVer(openclawVersion);
3492
+ if (semver.default.lt(oc, coerceCalVer(entry.minOpenclawVersion))) return {
3493
+ compatible: false,
3494
+ direction: "openclaw",
3495
+ entry
3496
+ };
3497
+ const maxInfo = inferEffectiveMax(index);
3498
+ if (maxInfo) {
3499
+ const max = coerceCalVer(maxInfo.max);
3500
+ if (maxInfo.exclusive ? !semver.default.lt(oc, max) : semver.default.gt(oc, max)) return {
3501
+ compatible: false,
3502
+ direction: "lark",
3503
+ entry
3504
+ };
3505
+ }
3506
+ return {
3507
+ compatible: true,
3508
+ direction: null,
3509
+ entry
3510
+ };
3511
+ }
3512
+ /**
3513
+ * Fork plugin handling.
3514
+ *
3515
+ * `@lark-apaas/openclaw-lark` is an internal fork whose own version numbers
3516
+ * (e.g. 2026.4.3, 2026.4.4) are NOT keys in VERSION_COMPAT_MAP. Floor-matching
3517
+ * a fork version against the official-keyed map only works by coincidence of
3518
+ * numbering and breaks once the fork version drifts past an official entry.
3519
+ * Both the install-time compat check and the diagnose rule must therefore pin
3520
+ * the fork to its official-equivalent version before consulting the map.
3521
+ */
3522
+ /** fork 对标的官方 openclaw-lark 版本(与 VERSION_COMPAT_MAP 强耦合,故留在此处)。 */
3523
+ const FORK_LARK_PLUGIN_PINNED_VERSION = "2026.4.1";
3524
+ /**
3525
+ * Floor match: find the entry with the largest openclawLarkVersion that is
3526
+ * ≤ pluginVersion ("closest lower-or-equal version").
3527
+ *
3528
+ * The table is sorted descending, so the first entry whose version ≤
3529
+ * pluginVersion is the answer. Returns undefined only when pluginVersion
3530
+ * is older than every entry in the table.
3531
+ */
3532
+ function findClosestEntry(pluginVersion) {
3533
+ return VERSION_COMPAT_MAP.find((e) => compareCalVer(e.openclawLarkVersion, pluginVersion) <= 0);
3534
+ }
3535
+ /** "@scope/name" → "name";无 scope 原样返回。 */
3536
+ function extractBaseName(name) {
3537
+ if (!name) return void 0;
3538
+ return name.startsWith("@") ? name.split("/")[1] : name;
3539
+ }
3540
+ function reasonFromRange(r) {
3541
+ if (r.compatible) return "compatible";
3542
+ if (r.direction === "openclaw") return "oc-below-min";
3543
+ return r.entry ? "oc-above-max" : "version-not-in-map";
3544
+ }
3545
+ /**
3546
+ * 统一的飞书插件兼容判定入口——唯一的判定真相源。
3547
+ *
3548
+ * 入参只有三个:插件包名、插件版本、当前 openclaw 版本(**不涉及 recommendedOpenclawTag**,
3549
+ * 推荐版本的处理交由各业务点)。判定算法:
3550
+ *
3551
+ * 1. fork `@lark-apaas/openclaw-lark`:先 pin 到对标官方版本(FORK_LARK_PLUGIN_PINNED_VERSION)
3552
+ * 再按区间判定上下界。
3553
+ * 2. 其他 `@lark-apaas/*` fork:无条件豁免(兼容)。
3554
+ * 3. legacy `feishu-openclaw-plugin`(含带 scope 形式):一律不兼容,方向 = 换飞书插件。
3555
+ * 4. 官方版及其余包:用自身版本按 VERSION_COMPAT_MAP 区间判定;读不到版本号 → 不兼容(换插件)。
3556
+ *
3557
+ * 所有消费方(install-extension 预检、feishu_plugin_version_compat 两条规则、needsLarkUpgrade)
3558
+ * 都应消费本方法,不再各自实现兼容/方向逻辑,避免口径漂移。
3559
+ */
3560
+ function checkPluginCompat(packageName, pluginVersion, openclawVersion) {
3561
+ if (packageName === "@lark-apaas/openclaw-lark") {
3562
+ const resolvedVersion = FORK_LARK_PLUGIN_PINNED_VERSION;
3563
+ const r = evaluateVersionRange(resolvedVersion, openclawVersion);
3564
+ return {
3565
+ ...r,
3566
+ reason: reasonFromRange(r),
3567
+ resolvedVersion
3568
+ };
3569
+ }
3570
+ if ((packageName && packageName.startsWith("@") ? packageName.split("/")[0] : void 0) === "@lark-apaas") return {
3571
+ compatible: true,
3572
+ direction: null,
3573
+ reason: "fork-exempt",
3574
+ resolvedVersion: pluginVersion,
3575
+ entry: void 0
3576
+ };
3577
+ if (extractBaseName(packageName) === "feishu-openclaw-plugin") return {
3578
+ compatible: false,
3579
+ direction: "lark",
3580
+ reason: "legacy",
3581
+ resolvedVersion: pluginVersion,
3582
+ entry: void 0
3583
+ };
3584
+ if (!pluginVersion) return {
3585
+ compatible: false,
3586
+ direction: "lark",
3587
+ reason: "version-unknown",
3588
+ resolvedVersion: void 0,
3589
+ entry: void 0
3590
+ };
3591
+ const r = evaluateVersionRange(pluginVersion, openclawVersion);
3592
+ return {
3593
+ ...r,
3594
+ reason: reasonFromRange(r),
3595
+ resolvedVersion: pluginVersion
3596
+ };
3597
+ }
3598
+ //#endregion
3599
+ //#region src/rules/feishu-plugin-version-compat.ts
3600
+ const LEGACY_SHORT_NAMES = [LEGACY_LARK_PLUGIN_NAME];
3601
+ let _ocVersion = void 0;
3602
+ function getOcVersion() {
3603
+ if (_ocVersion === void 0) _ocVersion = readOpenclawRuntimeVersion();
3604
+ return _ocVersion;
3605
+ }
3606
+ const _installedCache = /* @__PURE__ */ new Map();
3607
+ function getInstalledPlugin(ctx) {
3608
+ if (!_installedCache.has(ctx.configPath)) _installedCache.set(ctx.configPath, detectInstalledLarkPlugin(ctx));
3609
+ return _installedCache.get(ctx.configPath);
3610
+ }
3611
+ /** 提取公共前置上下文;任何前置条件不满足时返回 null(规则 pass)。 */
3612
+ function resolveCompatContext(ctx) {
3613
+ const ocCur = getOcVersion();
3614
+ if (!ocCur) return null;
3615
+ const installed = getInstalledPlugin(ctx);
3616
+ if (installed == null) return null;
3617
+ const compatResult = checkPluginCompat(pluginIdentifier(installed), installed.version, ocCur);
3618
+ return {
3619
+ ocCur,
3620
+ installed,
3621
+ isLegacy: isLegacyPlugin(installed),
3622
+ compatResult
3623
+ };
3624
+ }
3625
+ /**
3626
+ * 飞书插件与当前 openclaw 不兼容,且「升级 openclaw 有望转兼容」时 → 提示调整 openclaw。
3627
+ *
3628
+ * 判定顺序:
3629
+ * 1. 无 recommendedOpenclawTag(如 doctor 模式无目标)→ 前置短路 pass,连兼容性都不判
3630
+ * (upgrade_openclaw 语义是「升到推荐目标」,没有目标就无从建议)。
3631
+ * 2. checkPluginCompat 判为兼容 → pass。
3632
+ * 3. 不兼容时只比较 ocCur 与 recommendedOc:仅当 ocCur < recommendedOc(升 openclaw 能拉到
3633
+ * 更高版本、有望满足插件要求)才报 upgrade_openclaw;ocCur ≥ recommendedOc 说明升 openclaw
3634
+ * 无益,交由 Rule 2 报 upgrade_lark。
3635
+ *
3636
+ */
3637
+ let FeishuPluginOpenclawUpgradeRule = class FeishuPluginOpenclawUpgradeRule extends DiagnoseRule {
3638
+ validate(ctx) {
3639
+ const recommendedOc = ctx.vars.recommendedOpenclawTag;
3640
+ if (!recommendedOc) return { pass: true };
3641
+ const cc = resolveCompatContext(ctx);
3642
+ if (!cc) return { pass: true };
3643
+ const { ocCur, installed, isLegacy, compatResult } = cc;
3644
+ if (compatResult.compatible) return { pass: true };
3645
+ if (compareCalVer(ocCur, recommendedOc) >= 0) return { pass: true };
3646
+ return {
3647
+ pass: false,
3648
+ action: "upgrade_openclaw",
3649
+ message: `${buildCompatPrefix(installed, ocCur, isLegacy)};将 openclaw 升级到 ${recommendedOc} 即可满足(飞书插件随之同步)`
3650
+ };
3651
+ }
3652
+ };
3653
+ FeishuPluginOpenclawUpgradeRule = __decorate([Rule({
3654
+ key: "feishu_plugin_version_compat_openclaw",
3655
+ description: "检查飞书插件是否要求更高版本的 openclaw;是则提示升级 openclaw",
3656
+ dependsOn: ["config_syntax_check"],
3657
+ repairMode: "user-confirm",
3658
+ level: "critical",
3659
+ usesVars: ["recommendedOpenclawTag"]
3660
+ })], FeishuPluginOpenclawUpgradeRule);
3661
+ /**
3662
+ * 飞书插件与当前 openclaw 不兼容 → 提示调整飞书插件(重装 / 换版本,含 legacy 旧包替换)。
3663
+ *
3664
+ * 仅判断兼容性,不依赖 recommendedOc:compatResult.compatible 为真 → pass,否则 → upgrade_lark。
3665
+ *
3666
+ * 与 Rule 1 的协作靠 dependsOn(LOAD-BEARING,非仅排序):当「不兼容且 ocCur < recommendedOc」时
3667
+ * Rule 1 先报 upgrade_openclaw 并失败,dependsOn 使本规则被跳过,避免对同一不兼容同时报两个动作;
3668
+ * 其余不兼容场景(ocCur ≥ recommendedOc / 无推荐目标)Rule 1 pass,本规则如实报 upgrade_lark。
3669
+ * (--rule=lark 单独运行不经 Rule 1,会对任何不兼容直接报 upgrade_lark。)
3670
+ */
3671
+ let FeishuPluginLarkUpgradeRule = class FeishuPluginLarkUpgradeRule extends DiagnoseRule {
3672
+ validate(ctx) {
3673
+ const cc = resolveCompatContext(ctx);
3674
+ if (!cc) return { pass: true };
3675
+ const { ocCur, installed, isLegacy, compatResult } = cc;
3676
+ if (compatResult.compatible) return { pass: true };
3677
+ return {
3678
+ pass: false,
3679
+ action: "upgrade_lark",
3680
+ message: `${buildCompatPrefix(installed, ocCur, isLegacy)};需要将飞书插件安装/升级到与当前 openclaw 兼容的版本`
3681
+ };
3682
+ }
3683
+ };
3684
+ FeishuPluginLarkUpgradeRule = __decorate([Rule({
3685
+ key: "feishu_plugin_version_compat_lark",
3686
+ description: "检查飞书插件版本是否落后于当前 openclaw;是则提示升级飞书插件",
3687
+ dependsOn: ["config_syntax_check", "feishu_plugin_version_compat_openclaw"],
3688
+ repairMode: "user-confirm",
3689
+ level: "critical"
3690
+ })], FeishuPluginLarkUpgradeRule);
3691
+ /**
3692
+ * 传给 checkPluginCompat 的包标识:优先 package.json name(含 scope,用于识别 fork),
3693
+ * 退化到 plugins.allow 短名(legacy 旧包靠短名 feishu-openclaw-plugin 识别)。
3694
+ */
3695
+ function pluginIdentifier(p) {
3696
+ return p.fullName ?? p.allowName;
3697
+ }
3698
+ function isLegacyPlugin(p) {
3699
+ return LEGACY_SHORT_NAMES.includes(p.allowName);
3700
+ }
3701
+ function buildCompatPrefix(installed, ocCur, isLegacy) {
3702
+ const desc = describePlugin(installed);
3703
+ if (isLegacy) return `检测到已弃用的旧包 ${desc}(包名 "${installed.allowName}" 已停止维护,需替换为 "openclaw-lark")`;
3704
+ return `飞书插件 ${desc} 与当前 openclaw@${ocCur} 不兼容(${describeCompatConstraint(installed.version ? findClosestEntry(installed.version) : void 0, installed.version)})`;
3705
+ }
3706
+ function describeCompatConstraint(entry, pluginVersion) {
3707
+ if (!entry) return `插件版本 ${pluginVersion ?? "未知"} 不在版本兼容表中`;
3708
+ if (entry.maxOpenclawVersion) return `该插件版本要求 openclaw ∈ [${entry.minOpenclawVersion}, ${entry.maxOpenclawVersion}]`;
3709
+ return `该插件版本要求 openclaw ≥ ${entry.minOpenclawVersion}`;
3710
+ }
3711
+ function describePlugin(p) {
3712
+ return (p.fullName ?? p.allowName) + (p.version ? `@${p.version}` : "");
3713
+ }
3714
+ function readPluginPackageJson(filePath) {
3715
+ try {
3716
+ if (!node_fs.default.existsSync(filePath)) return null;
3717
+ const raw = node_fs.default.readFileSync(filePath, "utf-8");
3718
+ const parsed = JSON.parse(raw);
3719
+ return {
3720
+ name: typeof parsed.name === "string" ? parsed.name : void 0,
3721
+ version: typeof parsed.version === "string" ? parsed.version : void 0
3722
+ };
3723
+ } catch {
3724
+ return null;
3725
+ }
3726
+ }
3727
+ /** "已装" = plugins.allow 含名 AND extensions/<name>/package.json 真实存在。 */
3728
+ function detectInstalledLarkPlugin(ctx) {
3729
+ const allowRaw = asRecord(ctx.config.plugins)?.allow;
3730
+ const allow = Array.isArray(allowRaw) ? allowRaw.filter((e) => typeof e === "string") : [];
3731
+ const extDir = getExtensionsDir(ctx.configPath);
3732
+ const installs = getNestedMap(ctx.config, "plugins", "installs");
3733
+ for (const name of [LARK_PLUGIN_NAME, ...LEGACY_SHORT_NAMES]) {
3734
+ if (!allow.includes(name)) continue;
3735
+ const pkgPath = node_path.default.join(extDir, name, "package.json");
3736
+ if (!node_fs.default.existsSync(pkgPath)) continue;
3737
+ const pkg = readPluginPackageJson(pkgPath) ?? {};
3738
+ const installEntry = installs && asRecord(installs[name]);
3739
+ return {
3740
+ allowName: name,
3741
+ fullName: pkg.name ?? extractScopedNameFromSpec$1(installEntry?.spec),
3742
+ version: pkg.version ?? (typeof installEntry?.version === "string" ? installEntry.version : void 0)
3743
+ };
3744
+ }
3745
+ return null;
3746
+ }
3747
+ /** "@scope/name@1.2.3" / "name@1.2.3" / "@scope/name" / "name" → 去掉 @version 后缀 */
3748
+ function extractScopedNameFromSpec$1(spec) {
3749
+ if (typeof spec !== "string") return void 0;
3750
+ const at = spec.indexOf("@", 1);
3751
+ return at === -1 ? spec : spec.slice(0, at);
3752
+ }
3753
+ /**
3754
+ * 判断已安装的飞书插件是否与当前 openclaw 版本不兼容(或为需要替换的 legacy 插件)。
3755
+ * 被 upgrade-lark 前置检测门控(--check-only 和正式安装模式)调用。
3756
+ */
3757
+ function needsLarkUpgrade(ctx) {
3758
+ const cc = resolveCompatContext(ctx);
3759
+ if (!cc) return false;
3760
+ return !cc.compatResult.compatible;
3761
+ }
3762
+ //#endregion
3763
+ //#region src/paths.ts
3764
+ /**
3765
+ * Central directory for all ephemeral diagnose/reset artifacts: task status
3766
+ * files (`reset-<taskId>.json`) and human-readable step logs
3767
+ * (`reset-<taskId>.log`). Having everything under one dir makes debugging a
3768
+ * stuck reset much easier — `ls /tmp/openclaw-diagnose/` shows every recent
3769
+ * run, and each run's log is right next to its state.
3770
+ */
3771
+ const DIAGNOSE_DIR = "/tmp/openclaw-diagnose";
3772
+ function resetResultFile(taskId) {
3773
+ return `${DIAGNOSE_DIR}/reset-${taskId}.json`;
3774
+ }
3775
+ function resetLogFile(taskId) {
3776
+ return `${DIAGNOSE_DIR}/reset-${taskId}.log`;
3777
+ }
3778
+ /** Sandbox workspace root where openclaw config + agent state lives. */
3779
+ const WORKSPACE_DIR = "/home/gem/workspace/agent";
3780
+ /** File containing the provider key used by the openclaw miaoda provider. */
3781
+ const PROVIDER_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-provider-key";
3782
+ /** File containing the miaoda openclaw secrets JSON. */
3783
+ const SECRETS_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-openclaw-secrets.json";
3784
+ /** Absolute path to the openclaw config JSON. */
3785
+ const CONFIG_PATH = `${WORKSPACE_DIR}/openclaw.json`;
3786
+ /**
3787
+ * upgrade-lark 场景专属修复状态的信号文件目录。
3788
+ * fixStatus 有值时在此目录下创建同名文件(如 /tmp/event/PORT_FIX_READY),
3789
+ * 文件内容为完整的 UpgradeLarkResult JSON,供外部进程轮询感知升级结果。
3790
+ */
3791
+ const FIX_EVENT_DIR = "/tmp/event";
3792
+ /**
3793
+ * 安装指令互斥锁文件路径。
3794
+ * upgrade-lark / install-openclaw / install-extension / install-cli / reset --worker
3795
+ * 共享此锁,同一时刻只允许一个安装指令运行。
3796
+ * 锁文件内容:{ pid, command, startedAt }。
3797
+ */
3798
+ const INSTALL_LOCK_FILE = `${DIAGNOSE_DIR}/install.lock`;
3799
+ /**
3800
+ * upgrade-lark 每次运行的日志文件路径,含时间戳便于按时间排序定位。
3801
+ * checkOnly=true 时文件名含 "-check" 后缀,便于与正式安装日志区分。
3802
+ */
3803
+ function upgradeLarkLogFile(runId, checkOnly = false) {
3804
+ const ts = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/:/g, "-");
3805
+ return `${DIAGNOSE_DIR}/upgrade-lark${checkOnly ? "-check" : ""}-${ts}-${runId.slice(0, 8)}.log`;
3806
+ }
3807
+ //#endregion
3808
+ //#region src/lark-cli-init.ts
3809
+ const PE_XML_TAG = "lark-cli-pe";
3810
+ const PE_PLACEHOLDER = `
3811
+ <${PE_XML_TAG}>
3812
+ **【强制要求 - 无例外】飞书所有操作必须先读 feishu-lark-cli skill。未读skill不得调用任何飞书工具,违者视为操作失误。**
3813
+ </${PE_XML_TAG}>
3814
+ `;
3815
+ function isLarkPluginInstalled(configPath) {
3816
+ const extDir = getExtensionsDir(configPath);
3817
+ return LARK_PLUGIN_DIR_NAMES.some((name) => {
3818
+ try {
3819
+ return node_fs.default.existsSync(node_path.default.join(extDir, name, "package.json"));
3820
+ } catch {
3821
+ return false;
3822
+ }
3823
+ });
3824
+ }
3825
+ function isLarkCliAvailable$2() {
3826
+ try {
3827
+ return (0, node_child_process.spawnSync)("lark-cli", ["--version"], {
3828
+ encoding: "utf-8",
3829
+ timeout: 5e3,
3830
+ stdio: [
3831
+ "ignore",
3832
+ "pipe",
3833
+ "ignore"
3834
+ ]
3835
+ }).status === 0;
3836
+ } catch {
3837
+ return false;
3838
+ }
3839
+ }
3840
+ function readConfig(configPath) {
3841
+ try {
3842
+ const raw = node_fs.default.readFileSync(configPath, "utf-8");
3843
+ const parsed = loadJSON5().parse(raw);
3844
+ return parsed != null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
3845
+ } catch {
3846
+ return null;
3847
+ }
3848
+ }
3849
+ /**
3850
+ * Resolve the feishu app secret for the given appId.
3904
3851
  *
3905
- * For entries with an explicit maxOpenclawVersion the upper bound is INCLUSIVE (existing
3906
- * semantics). For entries without one the upper bound is EXCLUSIVE — it is derived from
3907
- * the minOpenclawVersion of the next entry group that requires a higher openclaw version.
3852
+ * Lookup order:
3853
+ * 1. channels.feishu.appSecret (single-agent: feishu.appId === appId)
3854
+ * 2. channels.feishu.accounts[key].appSecret (multi-agent: account.appId === appId)
3908
3855
  *
3909
- * Example: openclaw-lark@2026.4.10 (min=2026.4.27, no explicit max) gets an inferred
3910
- * exclusive max of '2026.5.6', so openclaw@2026.5.7 is correctly detected as incompatible
3911
- * whereas the old compatible() call would have returned true.
3912
- */
3913
- function effectiveCompatible(pluginVersion, openclawVersion) {
3914
- const index = VERSION_COMPAT_MAP.findIndex((e) => compareCalVer(e.openclawLarkVersion, pluginVersion) <= 0);
3915
- if (index === -1) return false;
3916
- const entry = VERSION_COMPAT_MAP[index];
3917
- const oc = coerceCalVer(openclawVersion);
3918
- if (semver.default.lt(oc, coerceCalVer(entry.minOpenclawVersion))) return false;
3919
- const maxInfo = inferEffectiveMax(index);
3920
- if (!maxInfo) return true;
3921
- const max = coerceCalVer(maxInfo.max);
3922
- return maxInfo.exclusive ? semver.default.lt(oc, max) : !semver.default.gt(oc, max);
3923
- }
3924
- /**
3925
- * Fork plugin handling.
3856
+ * Value interpretation:
3857
+ * - string → use directly
3858
+ * - object → secret is managed by a provider; use `feishuAppSecret` param instead
3926
3859
  *
3927
- * `@lark-apaas/openclaw-lark` is an internal fork whose own version numbers
3928
- * (e.g. 2026.4.3, 2026.4.4) are NOT keys in VERSION_COMPAT_MAP. Floor-matching
3929
- * a fork version against the official-keyed map only works by coincidence of
3930
- * numbering and breaks once the fork version drifts past an official entry.
3931
- * Both the install-time compat check and the diagnose rule must therefore pin
3932
- * the fork to its official-equivalent version before consulting the map.
3933
- */
3934
- /** fork 对标的官方 openclaw-lark 版本(与 VERSION_COMPAT_MAP 强耦合,故留在此处)。 */
3935
- const FORK_LARK_PLUGIN_PINNED_VERSION = "2026.4.1";
3936
- /**
3937
- * Resolve the version to evaluate against VERSION_COMPAT_MAP for a given plugin.
3938
- * The fork is pinned to FORK_LARK_PLUGIN_PINNED_VERSION regardless of its own
3939
- * version; everything else uses its real version.
3860
+ * Returns null when the secret cannot be determined.
3940
3861
  */
3941
- function resolveCompatVersion(packageName, version) {
3942
- if (packageName === "@lark-apaas/openclaw-lark") return FORK_LARK_PLUGIN_PINNED_VERSION;
3943
- return version;
3862
+ function resolveAppSecret(appId, config, feishuAppSecret) {
3863
+ const feishu = getNestedMap(config, "channels", "feishu");
3864
+ if (!feishu) return null;
3865
+ let rawSecret;
3866
+ if (typeof feishu.appId === "string" && feishu.appId === appId) rawSecret = feishu.appSecret;
3867
+ else {
3868
+ const accounts = asRecord(feishu.accounts);
3869
+ if (accounts) for (const [, val] of Object.entries(accounts)) {
3870
+ const account = asRecord(val);
3871
+ if (account?.appId === appId) {
3872
+ rawSecret = account.appSecret ?? feishu.appSecret;
3873
+ break;
3874
+ }
3875
+ }
3876
+ }
3877
+ if (typeof rawSecret === "string" && rawSecret) return rawSecret;
3878
+ if (rawSecret != null && typeof rawSecret === "object") return feishuAppSecret ?? null;
3879
+ return null;
3944
3880
  }
3945
3881
  /**
3946
- * Floor match: find the entry with the largest openclawLarkVersion that is
3947
- * ≤ pluginVersion ("closest lower-or-equal version").
3882
+ * Resolve the agents.md path for the given appId from the openclaw config.
3948
3883
  *
3949
- * The table is sorted descending, so the first entry whose version ≤
3950
- * pluginVersion is the answer. Returns undefined only when pluginVersion
3951
- * is older than every entry in the table.
3952
- */
3953
- function findClosestEntry(pluginVersion) {
3954
- return VERSION_COMPAT_MAP.find((e) => compareCalVer(e.openclawLarkVersion, pluginVersion) <= 0);
3955
- }
3956
- //#endregion
3957
- //#region src/rules/feishu-plugin-version-compat.ts
3958
- const LEGACY_SHORT_NAMES = [LEGACY_LARK_PLUGIN_NAME];
3959
- const FORK_SCOPES = [FORK_LARK_PLUGIN_SCOPE];
3960
- /**
3961
- * fork 版兼容区间的下界,取自 VERSION_COMPAT_MAP 中 openclawLarkVersion=2026.4.1
3962
- * 的 minOpenclawVersion,避免与映射表脱钩写死。仅用于区分升级方向
3963
- * (oc 低于下界 → 升 openclaw;高于上界 → 升 lark);该条目被移除时回退到已知值。
3884
+ * Case 1: appId matches channels.feishu.appId (single-agent path)
3885
+ * WORKSPACE_DIR/AGENTS.md
3886
+ *
3887
+ * Case 2: appId found in channels.feishu.accounts (multi-agent path)
3888
+ * → find account key where account.appId === appId
3889
+ * → find binding where match.channel=feishu && match.accountId=that key
3890
+ * → if agentId === 'main' → WORKSPACE_DIR/agents.md
3891
+ * → else find agent in agents.list by id → agent.workspace/agents.md
3892
+ *
3893
+ * Returns null when the path cannot be determined.
3964
3894
  */
3965
- const FORK_LARK_PLUGIN_MIN_OC_VERSION = findEntry("2026.4.1")?.minOpenclawVersion ?? "2026.3.28";
3966
- let _ocVersion = void 0;
3967
- function getOcVersion() {
3968
- if (_ocVersion === void 0) _ocVersion = readOpenclawRuntimeVersion();
3969
- return _ocVersion;
3895
+ function resolveAgentsMdPath(appId, config) {
3896
+ const feishu = getNestedMap(config, "channels", "feishu");
3897
+ if (!feishu) {
3898
+ console.error("resolveAgentsMdPath: channels.feishu not found");
3899
+ return null;
3900
+ }
3901
+ if (typeof feishu.appId === "string" && feishu.appId === appId) {
3902
+ console.error(`resolveAgentsMdPath: case=single-agent feishu.appId=${feishu.appId}`);
3903
+ return node_path.default.join(WORKSPACE_DIR, "workspace", "AGENTS.md");
3904
+ }
3905
+ const accounts = asRecord(feishu.accounts);
3906
+ if (!accounts) {
3907
+ console.error("resolveAgentsMdPath: feishu.accounts not found");
3908
+ return null;
3909
+ }
3910
+ let accountId;
3911
+ for (const [key, val] of Object.entries(accounts)) if (asRecord(val)?.appId === appId) {
3912
+ accountId = key;
3913
+ break;
3914
+ }
3915
+ if (!accountId) {
3916
+ console.error(`resolveAgentsMdPath: no account found with appId=${appId} in feishu.accounts keys=[${Object.keys(accounts).join(",")}]`);
3917
+ return null;
3918
+ }
3919
+ console.error(`resolveAgentsMdPath: found accountId=${accountId}`);
3920
+ const bindings = Array.isArray(config.bindings) ? config.bindings : [];
3921
+ let agentId;
3922
+ for (const b of bindings) {
3923
+ const binding = asRecord(b);
3924
+ if (!binding) continue;
3925
+ const match = asRecord(binding.match);
3926
+ if (match?.channel === "feishu" && match?.accountId === accountId) {
3927
+ if (typeof binding.agentId === "string") {
3928
+ agentId = binding.agentId;
3929
+ break;
3930
+ }
3931
+ }
3932
+ }
3933
+ if (!agentId) {
3934
+ console.error(`resolveAgentsMdPath: no binding found for accountId=${accountId} in bindings(count=${bindings.length})`);
3935
+ return null;
3936
+ }
3937
+ console.error(`resolveAgentsMdPath: found agentId=${agentId}`);
3938
+ if (agentId === "main") {
3939
+ console.error("resolveAgentsMdPath: case=multi-agent-main");
3940
+ return node_path.default.join(WORKSPACE_DIR, "workspace", "AGENTS.md");
3941
+ }
3942
+ const agentsObj = asRecord(config.agents);
3943
+ const list = Array.isArray(agentsObj?.list) ? agentsObj.list : [];
3944
+ for (const a of list) {
3945
+ const agent = asRecord(a);
3946
+ if (agent?.id === agentId) {
3947
+ const ws = typeof agent.workspace === "string" ? agent.workspace : node_path.default.join(WORKSPACE_DIR, "workspace");
3948
+ console.error(`resolveAgentsMdPath: case=multi-agent-custom agentId=${agentId} workspace=${ws}`);
3949
+ return node_path.default.join(ws, "AGENTS.md");
3950
+ }
3951
+ }
3952
+ console.error(`resolveAgentsMdPath: agentId=${agentId} not found in agents.list(count=${list.length})`);
3953
+ return null;
3970
3954
  }
3971
- const _installedCache = /* @__PURE__ */ new Map();
3972
- function getInstalledPlugin(ctx) {
3973
- if (!_installedCache.has(ctx.configPath)) _installedCache.set(ctx.configPath, detectInstalledPlugin(ctx));
3974
- return _installedCache.get(ctx.configPath);
3955
+ function appendPeToAgentsMd(agentsMdPath) {
3956
+ const dir = node_path.default.dirname(agentsMdPath);
3957
+ if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
3958
+ const existing = node_fs.default.existsSync(agentsMdPath) ? node_fs.default.readFileSync(agentsMdPath, "utf-8") : "";
3959
+ if (existing.includes(`<lark-cli-pe>`)) {
3960
+ console.error(`lark-cli-init: <${PE_XML_TAG}> already present in ${agentsMdPath}, skipping`);
3961
+ return;
3962
+ }
3963
+ const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
3964
+ node_fs.default.appendFileSync(agentsMdPath, `${sep}${PE_PLACEHOLDER}`, "utf-8");
3965
+ console.error(`lark-cli-init: appended PE placeholder to ${agentsMdPath}`);
3975
3966
  }
3976
3967
  /**
3977
- * 解析升级方向。返回 null 表示当前状态无需任何升级。
3978
- *
3979
- * - isVersionCompatible null(兼容)
3980
- * - isRecommendedBelowEntryMin → null(推荐版本未到位,豁免)
3981
- * - ocCur < recommendedOc → 'openclaw'
3982
- * - ocCur ≥ recommendedOc → 'lark'
3968
+ * Collect every Feishu bot appId declared in the openclaw config.
3969
+ * Covers both single-agent (channels.feishu.appId) and multi-agent
3970
+ * (channels.feishu.accounts[*].appId) layouts. Returns a deduplicated list.
3983
3971
  */
3984
- function resolveUpgradeDirection(installed, ocCur, recommendedOc, isLegacy) {
3985
- if (!isLegacy && isVersionCompatible(installed, ocCur)) return null;
3986
- if (!isLegacy && isRecommendedBelowEntryMin(installed, recommendedOc)) return null;
3987
- return compareCalVer(ocCur, recommendedOc) < 0 ? "openclaw" : "lark";
3972
+ function collectFeishuAppIds(configPath) {
3973
+ const config = readConfig(configPath ?? CONFIG_PATH);
3974
+ if (!config) return [];
3975
+ const feishu = getNestedMap(config, "channels", "feishu");
3976
+ if (!feishu) return [];
3977
+ const appIds = /* @__PURE__ */ new Set();
3978
+ const topAppId = feishu.appId;
3979
+ if (typeof topAppId === "string" && topAppId.trim()) appIds.add(topAppId.trim());
3980
+ const accounts = asRecord(feishu.accounts);
3981
+ if (accounts) for (const val of Object.values(accounts)) {
3982
+ const appId = asRecord(val)?.appId;
3983
+ if (typeof appId === "string" && appId.trim()) appIds.add(appId.trim());
3984
+ }
3985
+ return [...appIds];
3988
3986
  }
3989
- /** 提取公共前置上下文;任何前置条件不满足时返回 null(规则 pass)。 */
3990
- function resolveCompatContext(ctx) {
3991
- const recommendedOc = ctx.vars.recommendedOpenclawTag;
3992
- const ocCur = getOcVersion();
3993
- if (!ocCur) return null;
3994
- const installed = getInstalledPlugin(ctx);
3995
- if (installed == null) return null;
3987
+ function runLarkCliInit(opts) {
3988
+ const configPath = opts.configPath ?? CONFIG_PATH;
3989
+ if (!isLarkPluginInstalled(configPath)) {
3990
+ console.error("lark-cli-init: skipping openclaw-lark plugin not installed");
3991
+ return {
3992
+ ok: true,
3993
+ skipped: true,
3994
+ skipReason: "openclaw-lark plugin not installed"
3995
+ };
3996
+ }
3997
+ if (!isLarkCliAvailable$2()) {
3998
+ console.error("lark-cli-init: skipping — lark-cli command not found");
3999
+ return {
4000
+ ok: true,
4001
+ skipped: true,
4002
+ skipReason: "lark-cli command not found"
4003
+ };
4004
+ }
4005
+ const config = readConfig(configPath);
4006
+ if (!config) return {
4007
+ ok: false,
4008
+ error: `could not read config at ${configPath}`
4009
+ };
4010
+ const agentsMdPath = resolveAgentsMdPath(opts.appId, config);
4011
+ console.error(`lark-cli-init: resolved agents.md path=${agentsMdPath ?? "(null)"}`);
4012
+ if (!agentsMdPath) return {
4013
+ ok: false,
4014
+ error: `could not resolve agents.md path for appId=${opts.appId}`
4015
+ };
4016
+ const appSecret = resolveAppSecret(opts.appId, config, opts.feishuAppSecret);
4017
+ if (!appSecret) return {
4018
+ ok: false,
4019
+ error: `could not resolve appSecret for appId=${opts.appId}`
4020
+ };
4021
+ console.error(`lark-cli-init: running lark-cli config init --name ${opts.appId} --app-id ${opts.appId} --brand feishu --app-secret-stdin --force-init`);
4022
+ const initRes = (0, node_child_process.spawnSync)("lark-cli", [
4023
+ "config",
4024
+ "init",
4025
+ "--name",
4026
+ opts.appId,
4027
+ "--app-id",
4028
+ opts.appId,
4029
+ "--brand",
4030
+ "feishu",
4031
+ "--app-secret-stdin",
4032
+ "--force-init"
4033
+ ], {
4034
+ stdio: [
4035
+ "pipe",
4036
+ "pipe",
4037
+ "pipe"
4038
+ ],
4039
+ encoding: "utf-8",
4040
+ input: appSecret
4041
+ });
4042
+ const configInitStdout = initRes.stdout?.trim() || void 0;
4043
+ const configInitStderr = initRes.stderr?.trim() || void 0;
4044
+ if (configInitStdout) console.error(`lark-cli config init stdout: ${configInitStdout}`);
4045
+ if (configInitStderr) console.error(`lark-cli config init stderr: ${configInitStderr}`);
4046
+ if (initRes.error) return {
4047
+ ok: false,
4048
+ configInitStdout,
4049
+ configInitStderr,
4050
+ error: `lark-cli config init spawn error: ${initRes.error.message}`
4051
+ };
4052
+ if (initRes.status !== 0) return {
4053
+ ok: false,
4054
+ configInitExitCode: initRes.status ?? void 0,
4055
+ configInitStdout,
4056
+ configInitStderr,
4057
+ error: `lark-cli config init exited with code ${initRes.status}`
4058
+ };
4059
+ appendPeToAgentsMd(agentsMdPath);
3996
4060
  return {
3997
- ocCur,
3998
- recommendedOc,
3999
- installed,
4000
- isLegacy: isLegacyPlugin(installed)
4061
+ ok: true,
4062
+ configInitExitCode: 0,
4063
+ agentsMdPath
4001
4064
  };
4002
4065
  }
4066
+ //#endregion
4067
+ //#region src/utils/feishu-tools-deny.ts
4003
4068
  /**
4004
- * 检测是否需要升级 openclaw:
4005
- * - fork 版(@lark-apaas/openclaw-lark)低于最低要求 upgrade_openclaw
4006
- * - legacy / 官方版不兼容且 ocCur < recommendedOc → upgrade_openclaw
4007
- */
4008
- let FeishuPluginOpenclawUpgradeRule = class FeishuPluginOpenclawUpgradeRule extends DiagnoseRule {
4009
- validate(ctx) {
4010
- const cc = resolveCompatContext(ctx);
4011
- if (!cc) return { pass: true };
4012
- const { ocCur, recommendedOc, installed, isLegacy } = cc;
4013
- if (isForkPlugin(installed)) return validateForkPlugin(installed, ocCur, recommendedOc);
4014
- if (!recommendedOc) return { pass: true };
4015
- if (resolveUpgradeDirection(installed, ocCur, recommendedOc, isLegacy) !== "openclaw") return { pass: true };
4016
- return {
4017
- pass: false,
4018
- action: "upgrade_openclaw",
4019
- message: `${buildCompatPrefix(installed, ocCur, isLegacy)};将 openclaw 升级到 ${recommendedOc},飞书插件会随之同步升级`
4020
- };
4021
- }
4022
- };
4023
- FeishuPluginOpenclawUpgradeRule = __decorate([Rule({
4024
- key: "feishu_plugin_version_compat_openclaw",
4025
- description: "检查飞书插件是否要求更高版本的 openclaw;是则提示升级 openclaw",
4026
- dependsOn: ["config_syntax_check"],
4027
- repairMode: "user-confirm",
4028
- level: "critical",
4029
- usesVars: ["recommendedOpenclawTag"]
4030
- })], FeishuPluginOpenclawUpgradeRule);
4031
- /**
4032
- * 检测是否需要升级飞书插件(仅在 Rule 1 pass 后执行):
4033
- * - fork 版已由 Rule 1 处理,这里直接 pass
4034
- * - legacy / 官方版不兼容且 ocCur ≥ recommendedOc → upgrade_lark
4035
- */
4036
- let FeishuPluginLarkUpgradeRule = class FeishuPluginLarkUpgradeRule extends DiagnoseRule {
4037
- validate(ctx) {
4038
- const cc = resolveCompatContext(ctx);
4039
- if (!cc) return { pass: true };
4040
- const { ocCur, recommendedOc, installed, isLegacy } = cc;
4041
- if (isForkPlugin(installed)) {
4042
- if (installed.fullName !== "@lark-apaas/openclaw-lark") return { pass: true };
4043
- if (resolveForkUpgradeDirection(ocCur) !== "lark") return { pass: true };
4044
- return {
4045
- pass: false,
4046
- action: "upgrade_lark",
4047
- message: `飞书插件 ${describePlugin(installed)}(fork 版,对标 openclaw-lark@${FORK_LARK_PLUGIN_PINNED_VERSION})与当前 openclaw@${ocCur} 不兼容;建议升级飞书插件至兼容版本`
4048
- };
4049
- }
4050
- if (!isLarkUpgradeNeededFromCC(cc)) return { pass: true };
4051
- const prefix = buildCompatPrefix(installed, ocCur, isLegacy);
4052
- if (!recommendedOc) return {
4053
- pass: false,
4054
- action: "upgrade_lark",
4055
- message: `${prefix};建议升级飞书插件至兼容版本`
4056
- };
4057
- return {
4058
- pass: false,
4059
- action: "upgrade_lark",
4060
- message: `${prefix};当前 openclaw@${ocCur} 已达推荐版本,可直接升级飞书插件`
4061
- };
4062
- }
4063
- };
4064
- FeishuPluginLarkUpgradeRule = __decorate([Rule({
4065
- key: "feishu_plugin_version_compat_lark",
4066
- description: "检查飞书插件版本是否落后于当前 openclaw;是则提示升级飞书插件",
4067
- dependsOn: ["config_syntax_check", "feishu_plugin_version_compat_openclaw"],
4068
- repairMode: "user-confirm",
4069
- level: "critical",
4070
- usesVars: ["recommendedOpenclawTag"]
4071
- })], FeishuPluginLarkUpgradeRule);
4072
- /**
4073
- * 核心判断:非 fork 插件是否需要升级 lark,基于当前 openclaw 版本的兼容性。
4069
+ * openclaw-lark 中与 lark-cli 功能重叠的工具,写入 channels.feishu.tools.deny 后
4070
+ * openclaw-lark 插件自身的 shouldRegisterTool() 跳过注册,避免与 lark-cli 重复。
4071
+ * 支持尾部 `*` 通配(openclaw-lark matchesAnyPattern 识别)。
4074
4072
  *
4075
- * FeishuPluginLarkUpgradeRule.validate needsLarkUpgrade 共用。
4076
- * 调用方需在调用前自行处理 fork 插件的情况(fork 插件不走本函数)。
4073
+ * 保留(IM / 鉴权 / 诊断类,与 lark-cli 无重叠):feishu_chat*、feishu_get_user、
4074
+ * feishu_search_user、feishu_im_*、feishu_oauth*、feishu_auth、feishu_diagnose、feishu_doctor。
4077
4075
  *
4078
- * - recommendedOc:走 resolveUpgradeDirection 判断方向是否为 'lark'
4079
- * - 无 recommendedOc(doctor 无推荐版本):legacy 插件直接需要升级;
4080
- * 非 legacy 则检查当前版本是否在兼容表内
4076
+ * install-cli(安装 lark-cli 后即时写)与 agents_md_lark_cli_pe 规则(doctor --fix
4077
+ * 自愈)共用本常量与合并逻辑,单一来源。
4081
4078
  */
4082
- function isLarkUpgradeNeededFromCC(cc) {
4083
- const { ocCur, recommendedOc, installed, isLegacy } = cc;
4084
- if (!recommendedOc) return isLegacy || !isVersionCompatible(installed, ocCur);
4085
- return resolveUpgradeDirection(installed, ocCur, recommendedOc, isLegacy) === "lark";
4086
- }
4087
- function isForkPlugin(p) {
4088
- return p.scope != null && FORK_SCOPES.includes(p.scope);
4089
- }
4090
- function isLegacyPlugin(p) {
4091
- return LEGACY_SHORT_NAMES.includes(p.allowName);
4079
+ const LARK_CLI_OVERLAP_TOOL_DENY = Object.freeze([
4080
+ "feishu_create_doc",
4081
+ "feishu_fetch_doc",
4082
+ "feishu_update_doc",
4083
+ "feishu_doc_comments",
4084
+ "feishu_doc_media",
4085
+ "feishu_drive_file",
4086
+ "feishu_wiki_space",
4087
+ "feishu_wiki_space_node",
4088
+ "feishu_search_doc_wiki",
4089
+ "feishu_bitable_*",
4090
+ "feishu_calendar_*",
4091
+ "feishu_task_*",
4092
+ "feishu_sheet"
4093
+ ]);
4094
+ /** 读取 channels.feishu.tools.deny 的字符串数组(缺失/非法时返回空数组)。 */
4095
+ function readDenyList(config) {
4096
+ const deny = asRecord(asRecord(asRecord(config.channels)?.feishu)?.tools)?.deny;
4097
+ return Array.isArray(deny) ? deny.filter((e) => typeof e === "string") : [];
4092
4098
  }
4093
- /**
4094
- * 检查已装插件版本与当前 openclaw 版本是否兼容。
4095
- * 使用 effectiveCompatible() 以正确推断无显式 maxOpenclawVersion 条目的上界。
4096
- */
4097
- function isVersionCompatible(p, ocCur) {
4098
- if (!p.version) return false;
4099
- return effectiveCompatible(p.version, ocCur);
4099
+ /** 返回 deny 列表中尚缺的重叠工具(用于 validate 判定)。 */
4100
+ function larkCliToolDenyMissing(config) {
4101
+ const existing = new Set(readDenyList(config));
4102
+ return LARK_CLI_OVERLAP_TOOL_DENY.filter((t) => !existing.has(t));
4100
4103
  }
4101
4104
  /**
4102
- * 豁免判断:若推荐的 openclaw 版本本身低于最近条目要求的最低版本,
4103
- * 说明兼容表超前于推荐版本,不应由用户承担升级责任,视为通过。
4105
+ * 确保 channels.feishu.tools.deny 含全部重叠工具(只增不删,幂等合并)。
4106
+ * 直接 mutate 传入的 config 对象;返回是否发生了变更。
4104
4107
  */
4105
- function isRecommendedBelowEntryMin(p, recommendedOc) {
4106
- if (!p.version) return false;
4107
- const entry = findClosestEntry(p.version);
4108
- if (!entry) return false;
4109
- return compareCalVer(recommendedOc, entry.minOpenclawVersion) < 0;
4110
- }
4111
- function buildCompatPrefix(installed, ocCur, isLegacy) {
4112
- const desc = describePlugin(installed);
4113
- if (isLegacy) return `检测到已弃用的旧包 ${desc}(包名 "${installed.allowName}" 已停止维护,需替换为 "openclaw-lark")`;
4114
- return `飞书插件 ${desc} 与当前 openclaw@${ocCur} 不兼容(${describeCompatConstraint(installed.version ? findClosestEntry(installed.version) : void 0, installed.version)})`;
4115
- }
4116
- function describeCompatConstraint(entry, pluginVersion) {
4117
- if (!entry) return `插件版本 ${pluginVersion ?? "未知"} 不在版本兼容表中`;
4118
- if (entry.maxOpenclawVersion) return `该插件版本要求 openclaw ∈ [${entry.minOpenclawVersion}, ${entry.maxOpenclawVersion}]`;
4119
- return `该插件版本要求 openclaw ≥ ${entry.minOpenclawVersion}`;
4108
+ function ensureLarkCliToolDeny(config) {
4109
+ if (larkCliToolDenyMissing(config).length === 0) return false;
4110
+ const tools = ensureRecord(ensureRecord(ensureRecord(config, "channels"), "feishu"), "tools");
4111
+ const existing = readDenyList(config);
4112
+ tools.deny = [...new Set([...existing, ...LARK_CLI_OVERLAP_TOOL_DENY])];
4113
+ return true;
4120
4114
  }
4121
4115
  /**
4122
- * fork @lark-apaas/openclaw-lark 的升级方向:复用官方 effectiveCompatible,按对标版本
4123
- * FORK_LARK_PLUGIN_PINNED_VERSION (2026.4.1) 取完整兼容区间。
4124
- * null → 兼容
4125
- * 'openclaw' oc 低于区间下界,需升级 openclaw
4126
- * 'lark' → oc 高于区间上界(插件相对当前 openclaw 过旧),需升级飞书插件
4116
+ * 文件级:读 openclaw.json 合并 deny(ensureLarkCliToolDeny)→ 原子回写。
4117
+ * 仅当确实有变更时才写盘;config 不存在或解析失败仅记日志、不抛。
4118
+ *
4119
+ * 调用方负责门控「何时该写 deny」(即 lark-cli 存在时)——本函数不做 lark-cli 探测,
4120
+ * 只做「把 deny 合并进配置文件」这一件事,供 install-cli / install-extension /
4121
+ * upgrade-lark 在装好 lark-cli 或 openclaw-lark 后调用。
4122
+ *
4123
+ * @returns 是否发生了写盘
4127
4124
  */
4128
- function resolveForkUpgradeDirection(ocCur) {
4129
- if (effectiveCompatible("2026.4.1", ocCur)) return null;
4130
- return compareCalVer(ocCur, FORK_LARK_PLUGIN_MIN_OC_VERSION) < 0 ? "openclaw" : "lark";
4125
+ function ensureLarkCliToolDenyInFile(configPath, log = () => {}) {
4126
+ if (!node_fs.default.existsSync(configPath)) {
4127
+ log(`tools.deny: config not found at ${configPath} skip`);
4128
+ return false;
4129
+ }
4130
+ try {
4131
+ const config = loadJSON5().parse(node_fs.default.readFileSync(configPath, "utf-8"));
4132
+ if (!ensureLarkCliToolDeny(config)) {
4133
+ log("tools.deny: already up to date");
4134
+ return false;
4135
+ }
4136
+ const tmp = configPath + ".lark-cli-deny-tmp";
4137
+ node_fs.default.writeFileSync(tmp, JSON.stringify(config, null, 2), "utf-8");
4138
+ moveSafe(tmp, configPath);
4139
+ log("channels.feishu.tools.deny updated");
4140
+ return true;
4141
+ } catch (e) {
4142
+ log(`WARN: ensure tools.deny failed: ${e.message}`);
4143
+ return false;
4144
+ }
4131
4145
  }
4146
+ //#endregion
4147
+ //#region src/utils/lark-cli-detect.ts
4132
4148
  /**
4133
- * @lark-apaas/openclaw-lark 复用官方 effectiveCompatible(按对标版本 2026.4.1 完整区间)。
4134
- * 本函数(Rule 1)只负责 openclaw 方向(oc 低于下界);oc 高于上界由 Rule 2 报 upgrade_lark。
4135
- * 其他 @lark-apaas scope 的 fork 插件继续无条件 pass。
4136
- * recommendedOc 可为 undefined(doctor 模式),此时不指定目标升级版本。
4149
+ * 探测 lark-cli 是否可用(`lark-cli --version` 退出码为 0)。
4150
+ * 多条规则共用同一判定:feishu_plugin_state_normalize(是否补 feishu_* 授权)、
4151
+ * agents_md_lark_cli_pe(是否补 PE / deny)等。
4137
4152
  */
4138
- function validateForkPlugin(installed, ocCur, recommendedOc) {
4139
- if (installed.fullName !== "@lark-apaas/openclaw-lark") return { pass: true };
4140
- if (resolveForkUpgradeDirection(ocCur) !== "openclaw") return { pass: true };
4141
- const recommendation = recommendedOc ? `;将 openclaw 升级到 ${recommendedOc} 即可满足` : `;请升级 openclaw 至 ${FORK_LARK_PLUGIN_MIN_OC_VERSION} 或更高版本`;
4142
- return {
4143
- pass: false,
4144
- action: "upgrade_openclaw",
4145
- message: `飞书插件 ${describePlugin(installed)}(fork 版)要求 openclaw ≥ ${FORK_LARK_PLUGIN_MIN_OC_VERSION},当前 openclaw@${ocCur} 低于此要求${recommendation}`
4146
- };
4147
- }
4148
- function describePlugin(p) {
4149
- return (p.fullName ?? p.allowName) + (p.version ? `@${p.version}` : "");
4150
- }
4151
- function readPluginPackageJson(filePath) {
4153
+ function isLarkCliAvailable$1() {
4152
4154
  try {
4153
- if (!node_fs.default.existsSync(filePath)) return null;
4154
- const raw = node_fs.default.readFileSync(filePath, "utf-8");
4155
- const parsed = JSON.parse(raw);
4156
- return {
4157
- name: typeof parsed.name === "string" ? parsed.name : void 0,
4158
- version: typeof parsed.version === "string" ? parsed.version : void 0
4159
- };
4155
+ return (0, node_child_process.spawnSync)("lark-cli", ["--version"], {
4156
+ encoding: "utf-8",
4157
+ timeout: 5e3,
4158
+ stdio: [
4159
+ "ignore",
4160
+ "pipe",
4161
+ "ignore"
4162
+ ]
4163
+ }).status === 0;
4160
4164
  } catch {
4161
- return null;
4165
+ return false;
4162
4166
  }
4163
4167
  }
4164
- /** "已装" = plugins.allow 含名 AND extensions/<name>/package.json 真实存在。 */
4165
- function detectInstalledPlugin(ctx) {
4166
- const allowRaw = asRecord(ctx.config.plugins)?.allow;
4167
- const allow = Array.isArray(allowRaw) ? allowRaw.filter((e) => typeof e === "string") : [];
4168
- const extDir = getExtensionsDir(ctx.configPath);
4169
- const installs = getNestedMap(ctx.config, "plugins", "installs");
4170
- for (const name of [LARK_PLUGIN_NAME, ...LEGACY_SHORT_NAMES]) {
4171
- if (!allow.includes(name)) continue;
4172
- const pkgPath = node_path.default.join(extDir, name, "package.json");
4173
- if (!node_fs.default.existsSync(pkgPath)) continue;
4174
- const pkg = readPluginPackageJson(pkgPath) ?? {};
4175
- const installEntry = installs && asRecord(installs[name]);
4176
- const fullName = pkg.name ?? extractScopedNameFromSpec$1(installEntry?.spec);
4177
- return {
4178
- allowName: name,
4179
- fullName,
4180
- scope: fullName?.startsWith("@") ? fullName.split("/")[0] : void 0,
4181
- version: pkg.version ?? (typeof installEntry?.version === "string" ? installEntry.version : void 0)
4182
- };
4183
- }
4184
- return null;
4168
+ //#endregion
4169
+ //#region src/rules/agents-md-lark-cli-pe.ts
4170
+ /** 会注册 feishu_* 工具的三种飞书插件:官方 / 旧版 / 内置。 */
4171
+ const FEISHU_PLUGIN_NAMES = [
4172
+ LARK_PLUGIN_NAME,
4173
+ LEGACY_LARK_PLUGIN_NAME,
4174
+ BUILTIN_FEISHU_PLUGIN_NAME
4175
+ ];
4176
+ /**
4177
+ * 任一飞书插件是否在 config 中**启用**(plugins.entries[name].enabled === true)。
4178
+ * deny 的意义在于「有飞书插件会注册 feishu_* 工具」,故按启用判定——不看是否落盘安装:
4179
+ * 启用的可能是官方 openclaw-lark、旧版 feishu-openclaw-plugin 或内置 feishu 中的任意一个。
4180
+ */
4181
+ function isAnyFeishuPluginEnabled(config) {
4182
+ const entries = getNestedMap(config, "plugins", "entries");
4183
+ return FEISHU_PLUGIN_NAMES.some((name) => asRecord(entries?.[name])?.enabled === true);
4185
4184
  }
4186
- /** "@scope/name@1.2.3" / "name@1.2.3" / "@scope/name" / "name" → 去掉 @version 后缀 */
4187
- function extractScopedNameFromSpec$1(spec) {
4188
- if (typeof spec !== "string") return void 0;
4189
- const at = spec.indexOf("@", 1);
4190
- return at === -1 ? spec : spec.slice(0, at);
4185
+ /** 第一个缺失 PE 内容的 AGENTS.md 路径;无则 undefined。 */
4186
+ function findAgentsMdMissingPe(ctx) {
4187
+ return collectExistingAgentsMdPaths(ctx).find((filePath) => {
4188
+ return !node_fs.default.readFileSync(filePath, "utf-8").includes(`<${PE_XML_TAG}>`);
4189
+ });
4191
4190
  }
4192
4191
  /**
4193
- * 判断已安装的飞书插件是否与当前 openclaw 版本不兼容(或为需要替换的 legacy 插件)。
4194
- * upgrade-lark 前置检测门控(--check-only 和正式安装模式)调用。
4192
+ * lark-cli 存在时统一规范化环境:
4193
+ * 1. 给各智能体 AGENTS.md 追加 lark-cli-pe PE 内容
4194
+ * 2. 有飞书插件启用时,把与 lark-cli 重叠的工具写入 channels.feishu.tools.deny
4195
+ * (只增不删,避免与 lark-cli 重复注册)
4196
+ * 两者门禁同为「lark-cli 可用」,故合并为一条规则。
4195
4197
  */
4196
- function needsLarkUpgrade(ctx) {
4197
- const cc = resolveCompatContext({
4198
- ...ctx,
4199
- vars: {
4200
- ...ctx.vars,
4201
- recommendedOpenclawTag: void 0
4198
+ let AgentsMdLarkCliPeRule = class AgentsMdLarkCliPeRule extends DiagnoseRule {
4199
+ validate(ctx) {
4200
+ if (!isLarkCliAvailable$1()) return { pass: true };
4201
+ const missingPath = findAgentsMdMissingPe(ctx);
4202
+ if (missingPath) return {
4203
+ pass: false,
4204
+ message: `${missingPath} 中缺少 lark-cli-pe PE 内容,需要追加`
4205
+ };
4206
+ if (isAnyFeishuPluginEnabled(ctx.config) && larkCliToolDenyMissing(ctx.config).length > 0) return {
4207
+ pass: false,
4208
+ message: "channels.feishu.tools.deny 缺少与 lark-cli 重叠的飞书工具,需要补充"
4209
+ };
4210
+ return { pass: true };
4211
+ }
4212
+ repair(ctx) {
4213
+ if (!isLarkCliAvailable$1()) return;
4214
+ for (const filePath of collectExistingAgentsMdPaths(ctx)) {
4215
+ const content = node_fs.default.readFileSync(filePath, "utf-8");
4216
+ if (content.includes(`<lark-cli-pe>`)) continue;
4217
+ const sep = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
4218
+ node_fs.default.appendFileSync(filePath, `${sep}${PE_PLACEHOLDER}`, "utf-8");
4219
+ console.error(`agents-md-lark-cli-pe: appended PE to ${filePath}`);
4202
4220
  }
4203
- });
4204
- if (!cc) return false;
4205
- const { ocCur, installed } = cc;
4206
- if (isForkPlugin(installed)) {
4207
- if (installed.fullName === "@lark-apaas/openclaw-lark") return !effectiveCompatible(FORK_LARK_PLUGIN_PINNED_VERSION, ocCur);
4208
- return false;
4221
+ if (isAnyFeishuPluginEnabled(ctx.config) && ensureLarkCliToolDeny(ctx.config)) console.error("agents-md-lark-cli-pe: merged lark-cli overlap tools into channels.feishu.tools.deny");
4209
4222
  }
4210
- return isLarkUpgradeNeededFromCC(cc);
4211
- }
4223
+ };
4224
+ AgentsMdLarkCliPeRule = __decorate([Rule({
4225
+ key: "agents_md_lark_cli_pe",
4226
+ description: "lark-cli 存在时规范化环境:AGENTS.md 追加 lark-cli-pe PE + 飞书插件重叠工具写入 channels.feishu.tools.deny",
4227
+ dependsOn: ["config_syntax_check"],
4228
+ repairMode: "standard",
4229
+ level: "silent"
4230
+ })], AgentsMdLarkCliPeRule);
4212
4231
  //#endregion
4213
4232
  //#region src/rules/cleanup-install-backup-dirs.ts
4214
4233
  const DIR_PREFIX = ".openclaw-install-";
@@ -5359,8 +5378,6 @@ async function installOpenclaw(openclawTag, ossFileMap, opts = {}) {
5359
5378
  * `console.error`,独立调用方不传则静默。
5360
5379
  */
5361
5380
  const NOOP_LOG = () => {};
5362
- /** 升级前需备份的 extensions/ 下的插件目录(官方短名 + legacy)。 */
5363
- const FEISHU_PLUGIN_DIRS = FEISHU_PLUGIN_DIR_NAMES;
5364
5381
  /** 读取 package.json 的 version 字段,失败返回 null(仅用于日志) */
5365
5382
  function readPkgVersion(pkgPath) {
5366
5383
  try {
@@ -5371,13 +5388,20 @@ function readPkgVersion(pkgPath) {
5371
5388
  }
5372
5389
  }
5373
5390
  /**
5374
- * 备份 openclaw.json + FEISHU_PLUGIN_DIRS 下存在的插件目录到 backupDir。
5391
+ * 备份 openclaw.json + LARK_PLUGIN_DIR_NAMES 下存在的插件目录到 backupDir。
5375
5392
  * 只备份当前存在的文件;不存在的记日志后跳过(恢复时据此判断「升级前是否存在」)。
5376
5393
  */
5377
5394
  function backupFeishuPlugins(opts) {
5378
5395
  const { workspaceDir, configPath, backupDir } = opts;
5379
5396
  const log = opts.log ?? NOOP_LOG;
5380
5397
  try {
5398
+ if (node_fs.default.existsSync(backupDir)) {
5399
+ node_fs.default.rmSync(backupDir, {
5400
+ recursive: true,
5401
+ force: true
5402
+ });
5403
+ log(`cleaned stale backup dir: ${backupDir}`);
5404
+ }
5381
5405
  node_fs.default.mkdirSync(backupDir, { recursive: true });
5382
5406
  log(`backup dir: ${backupDir}`);
5383
5407
  if (node_fs.default.existsSync(configPath)) {
@@ -5387,7 +5411,7 @@ function backupFeishuPlugins(opts) {
5387
5411
  } else log(` skipped: openclaw.json (not found)`);
5388
5412
  node_fs.default.mkdirSync(node_path.default.join(backupDir, "extensions"), { recursive: true });
5389
5413
  const extSrc = node_path.default.join(workspaceDir, "extensions");
5390
- for (const pluginDir of FEISHU_PLUGIN_DIRS) {
5414
+ for (const pluginDir of LARK_PLUGIN_DIR_NAMES) {
5391
5415
  const src = node_path.default.join(extSrc, pluginDir);
5392
5416
  if (node_fs.default.existsSync(src)) {
5393
5417
  const dst = node_path.default.join(backupDir, "extensions", pluginDir);
@@ -5419,7 +5443,7 @@ function restoreFeishuPlugins(opts) {
5419
5443
  log(` deleted: openclaw.json`);
5420
5444
  }
5421
5445
  const extDst = node_path.default.join(workspaceDir, "extensions");
5422
- for (const pluginDir of FEISHU_PLUGIN_DIRS) {
5446
+ for (const pluginDir of LARK_PLUGIN_DIR_NAMES) {
5423
5447
  const dst = node_path.default.join(extDst, pluginDir);
5424
5448
  if (node_fs.default.existsSync(dst)) {
5425
5449
  node_fs.default.rmSync(dst, {
@@ -5434,7 +5458,7 @@ function restoreFeishuPlugins(opts) {
5434
5458
  node_fs.default.copyFileSync(configBackup, configPath);
5435
5459
  log(` restored: openclaw.json`);
5436
5460
  } else log(` skipped restore: openclaw.json (not in backup — was not present before upgrade)`);
5437
- for (const pluginDir of FEISHU_PLUGIN_DIRS) {
5461
+ for (const pluginDir of LARK_PLUGIN_DIR_NAMES) {
5438
5462
  const backupSrc = node_path.default.join(backupDir, "extensions", pluginDir);
5439
5463
  if (node_fs.default.existsSync(backupSrc)) {
5440
5464
  node_fs.default.cpSync(backupSrc, node_path.default.join(extDst, pluginDir), { recursive: true });
@@ -5476,8 +5500,7 @@ function runLarkToolsUpdate(opts) {
5476
5500
  return {
5477
5501
  exitCode,
5478
5502
  stdout,
5479
- stderr,
5480
- spawnError: r.error?.message
5503
+ stderr
5481
5504
  };
5482
5505
  }
5483
5506
  //#endregion
@@ -5534,8 +5557,11 @@ async function installExtension(tag, ossFileMap, opts = {}) {
5534
5557
  installOne$1(pkg, tarball, homeBase);
5535
5558
  console.error(`[install-extension] ${pkg.name}: installed`);
5536
5559
  }
5537
- if (!opts.skipConfigUpdate) updatePluginInstalls(opts.configPath ?? node_path.default.join(homeBase, DEFAULT_CONFIG_REL), targets);
5538
- else console.error(`[install-extension] skipConfigUpdate=true not touching openclaw.json`);
5560
+ if (!opts.skipConfigUpdate) {
5561
+ const configPath = opts.configPath ?? node_path.default.join(homeBase, DEFAULT_CONFIG_REL);
5562
+ updatePluginInstalls(configPath, targets);
5563
+ if (targets.some((p) => p.name === "openclaw-lark") && isLarkCliAvailable$1()) ensureLarkCliToolDenyInFile(configPath, (msg) => console.error(`[install-extension] ${msg}`));
5564
+ } else console.error(`[install-extension] skipConfigUpdate=true — not touching openclaw.json`);
5539
5565
  console.error(`[install-extension] done ${targets.length}/${targets.length} in ${Date.now() - t0}ms`);
5540
5566
  }
5541
5567
  const PLUGINS_TO_AUTO_ENABLE = [LARK_PLUGIN_NAME, MIAODA_PLUGIN_NAME];
@@ -5644,9 +5670,9 @@ async function installLarkPluginWithCompatCheck(opts) {
5644
5670
  console.error("[install-extension] WARN: cannot read openclaw version — falling back to direct tarball install for openclaw-lark");
5645
5671
  return false;
5646
5672
  }
5647
- const compatVersion = resolveCompatVersion(pkg.packageName, pkg.version);
5648
- const compatible = compatVersion ? effectiveCompatible(compatVersion, ocCur) : true;
5649
- console.error(`[install-extension] ${pkg.packageName ?? pkg.name}@${pkg.version ?? "unknown"} (compat-as ${compatVersion ?? "unknown"}) vs openclaw@${ocCur}: ${compatible}`);
5673
+ const compatResult = checkPluginCompat(pkg.packageName, pkg.version, ocCur);
5674
+ const compatible = compatResult.resolvedVersion ? compatResult.compatible : true;
5675
+ console.error(`[install-extension] ${pkg.packageName ?? pkg.name}@${pkg.version ?? "unknown"} (compat-as ${compatResult.resolvedVersion ?? "unknown"}) vs openclaw@${ocCur}: ${compatible}`);
5650
5676
  if (compatible) return false;
5651
5677
  console.error(`[install-extension] openclaw-lark@${pkg.version} incompatible with openclaw@${ocCur} — using openclaw-lark-tools update`);
5652
5678
  const log = (msg) => console.error(`[install-extension] ${msg}`);
@@ -5687,6 +5713,7 @@ async function installLarkPluginWithCompatCheck(opts) {
5687
5713
  });
5688
5714
  throw new Error(`openclaw-lark post-install validation failed: ${validation.error}`);
5689
5715
  }
5716
+ ensureLarkCliToolDenyInFile(configPath, (msg) => console.error(`[install-extension] ${msg}`));
5690
5717
  console.error("[install-extension] openclaw-lark-tools update succeeded");
5691
5718
  return true;
5692
5719
  } finally {
@@ -7034,7 +7061,15 @@ async function installClis(tag, ossFileMap, opts) {
7034
7061
  }
7035
7062
  console.error(`[install-cli] tag=${tag} targets=${targets.length}`);
7036
7063
  const t0 = Date.now();
7064
+ const larkCliAlreadyAvailable = targets.some((p) => p.name === "lark-cli") && isLarkCliAvailable$1();
7037
7065
  const tarballs = await Promise.all(targets.map(async (p) => {
7066
+ if (p.name === "lark-cli" && larkCliAlreadyAvailable) {
7067
+ console.error(`[install-cli] ${p.name}: already available (lark-cli --version) — skip download/extract/link`);
7068
+ return {
7069
+ pkg: p,
7070
+ tarball: null
7071
+ };
7072
+ }
7038
7073
  const tb = await downloadWithCache(p, ossFileMap, opts);
7039
7074
  console.error(`[install-cli] ${p.name}: downloaded`);
7040
7075
  return {
@@ -7043,6 +7078,7 @@ async function installClis(tag, ossFileMap, opts) {
7043
7078
  };
7044
7079
  }));
7045
7080
  for (const { pkg, tarball } of tarballs) {
7081
+ if (tarball === null) continue;
7046
7082
  installOne(pkg, tarball, homeBase, opts.tmpRoot);
7047
7083
  linkBins(pkg, homeBase);
7048
7084
  console.error(`[install-cli] ${pkg.name}: installed`);
@@ -7072,7 +7108,9 @@ async function installClis(tag, ossFileMap, opts) {
7072
7108
  }
7073
7109
  });
7074
7110
  }
7075
- disableLarkCliOverlapTools(node_path.default.join(homeBase, WORKSPACE_AGENT_REL, "openclaw.json"), homeBase);
7111
+ const configPath = node_path.default.join(homeBase, WORKSPACE_AGENT_REL, "openclaw.json");
7112
+ if (node_fs.default.existsSync(node_path.default.join(homeBase, "workspace/agent", "extensions", "openclaw-lark"))) ensureLarkCliToolDenyInFile(configPath, (msg) => console.error(`[install-cli] ${msg}`));
7113
+ else console.error(`[install-cli] ${LARK_PLUGIN_NAME} not installed — skip tools.deny`);
7076
7114
  }
7077
7115
  console.error(`[install-cli] done ${targets.length}/${targets.length} in ${Date.now() - t0}ms`);
7078
7116
  }
@@ -7247,38 +7285,6 @@ function installOne(pkg, tarball, homeBase, tmpRoot) {
7247
7285
  } catch {}
7248
7286
  }
7249
7287
  }
7250
- /**
7251
- * Write overlapping tool names to channels.feishu.tools.deny in openclaw.json so that
7252
- * openclaw-lark's shouldRegisterTool() skips them when lark-cli is present.
7253
- *
7254
- * Only runs when openclaw-lark is installed (extensions/openclaw-lark/ exists).
7255
- * Idempotent: merges with any existing deny entries.
7256
- * Failures are non-fatal (logged as warnings).
7257
- */
7258
- function disableLarkCliOverlapTools(configPath, homeBase) {
7259
- const larkPluginDir = node_path.default.join(homeBase, WORKSPACE_AGENT_REL, "extensions", LARK_PLUGIN_NAME);
7260
- if (!node_fs.default.existsSync(larkPluginDir)) {
7261
- console.error(`[install-cli] disableLarkCliOverlapTools: ${LARK_PLUGIN_NAME} not installed — skipping`);
7262
- return;
7263
- }
7264
- if (!node_fs.default.existsSync(configPath)) {
7265
- console.error(`[install-cli] disableLarkCliOverlapTools: config not found at ${configPath} — skipping`);
7266
- return;
7267
- }
7268
- try {
7269
- const config = loadJSON5().parse(node_fs.default.readFileSync(configPath, "utf-8"));
7270
- if (!ensureLarkCliToolDeny(config)) {
7271
- console.error("[install-cli] disableLarkCliOverlapTools: deny already up to date");
7272
- return;
7273
- }
7274
- const tmp = configPath + ".lark-cli-deny-tmp";
7275
- node_fs.default.writeFileSync(tmp, JSON.stringify(config, null, 2), "utf-8");
7276
- moveSafe(tmp, configPath);
7277
- console.error("[install-cli] disableLarkCliOverlapTools: channels.feishu.tools.deny updated");
7278
- } catch (e) {
7279
- console.error(`[install-cli] WARN: disableLarkCliOverlapTools failed: ${e.message}`);
7280
- }
7281
- }
7282
7288
  //#endregion
7283
7289
  //#region src/download-resource.ts
7284
7290
  /**
@@ -11214,7 +11220,7 @@ async function reportCliRun(opts) {
11214
11220
  //#region src/help.ts
11215
11221
  const BIN = "mclaw-diagnose";
11216
11222
  function versionBanner() {
11217
- return `v0.1.18-alpha.3`;
11223
+ return `v0.1.18-alpha.4`;
11218
11224
  }
11219
11225
  const COMMANDS = [
11220
11226
  {
@@ -12152,7 +12158,7 @@ function runUpgradeLark(opts) {
12152
12158
  });
12153
12159
  }
12154
12160
  log("");
12155
- log("── [1/6] 文件备份 ────────────────────────────────────────");
12161
+ log("── [1/7] 文件备份 ────────────────────────────────────────");
12156
12162
  log(`before-state: botCount=${countFeishuBots(configPath)}`);
12157
12163
  const t_backupStart = Date.now();
12158
12164
  const backup = backupFeishuPlugins(fsOpts);
@@ -12170,7 +12176,7 @@ function runUpgradeLark(opts) {
12170
12176
  log("backup: ok");
12171
12177
  logVersionSnapshot("before-versions", snapshotVersions(cwd, log), log);
12172
12178
  log("");
12173
- log("── [2/6] 清理本地 openclaw shim ─────────────────────────");
12179
+ log("── [2/7] 清理本地 openclaw shim ─────────────────────────");
12174
12180
  const localOpenclawBin = node_path.default.join(cwd, "node_modules", ".bin", "openclaw");
12175
12181
  if (node_fs.default.existsSync(localOpenclawBin)) try {
12176
12182
  node_fs.default.rmSync(localOpenclawBin);
@@ -12180,7 +12186,7 @@ function runUpgradeLark(opts) {
12180
12186
  }
12181
12187
  else log(` skipped: ${localOpenclawBin} (not found)`);
12182
12188
  log("");
12183
- log("── [3/6] npx install (@larksuite/openclaw-lark-tools update) ──");
12189
+ log("── [3/7] npx install (@larksuite/openclaw-lark-tools update) ──");
12184
12190
  const t_npxStart = Date.now();
12185
12191
  const { exitCode: npxExitCode, stdout: npxStdout, stderr: npxStderr } = runLarkToolsUpdate({
12186
12192
  cwd,
@@ -12211,7 +12217,7 @@ function runUpgradeLark(opts) {
12211
12217
  });
12212
12218
  };
12213
12219
  log("");
12214
- log("── [4/5] 安装后诊断校验 ─────────────────────────────────");
12220
+ log("── [4/7] 安装后诊断校验 ─────────────────────────────────");
12215
12221
  logVersionSnapshot("after-versions", snapshotVersions(cwd, log), log);
12216
12222
  let afterVersionIncompatible = false;
12217
12223
  try {
@@ -12240,7 +12246,7 @@ function runUpgradeLark(opts) {
12240
12246
  if (isNewDefaultOnly) log(" post-install diagnosis: ok (new default account — plugin installed, awaiting configuration)");
12241
12247
  else log(" post-install diagnosis: ok (upgrade conditions resolved)");
12242
12248
  log("");
12243
- log("── [6/6] doctor --fix ────────────────────────────────────");
12249
+ log("── [5/7] doctor --fix ────────────────────────────────────");
12244
12250
  const fixArgs = ["doctor", "--fix"];
12245
12251
  if (opts.scene) fixArgs.push(`--scene=${opts.scene}`);
12246
12252
  const t_doctorFixStart = Date.now();
@@ -12260,7 +12266,7 @@ function runUpgradeLark(opts) {
12260
12266
  if (fixResult.stderr?.trim()) log(`doctor(fix) stderr:\n${fixResult.stderr.trim()}`);
12261
12267
  log(`doctor(fix) exit: ${fixResult.status ?? "null"}${fixResult.error ? ` error: ${fixResult.error.message}` : ""}`);
12262
12268
  log("");
12263
- log("── [7/8] 重启 openclaw 服务 ──────────────────────────────");
12269
+ log("── [6/7] 重启 openclaw 服务 ──────────────────────────────");
12264
12270
  const restartScript = "/opt/force/bin/openclaw_scripts/restart.sh";
12265
12271
  let restartExecuted = false;
12266
12272
  if (opts.skipRestart) log(" skipped: --skip-restart");
@@ -12282,7 +12288,7 @@ function runUpgradeLark(opts) {
12282
12288
  restartExecuted = true;
12283
12289
  } else log(` skipped: ${restartScript} not found`);
12284
12290
  log("");
12285
- log("── [8/8] 端口存活检测 ────────────────────────────────────");
12291
+ log("── [7/7] 端口存活检测 ────────────────────────────────────");
12286
12292
  let portCheckOk;
12287
12293
  if (!restartExecuted) log(" skipped: restart was not executed");
12288
12294
  else {