@lark-apaas/openclaw-scripts-diagnose-cli 0.1.18-alpha.2 → 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 +1078 -964
  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.2";
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,603 +2622,285 @@ 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
2625
+ //#region src/constants.ts
2614
2626
  /**
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.
2627
+ * 全局共享常量(插件名 / CLI / 路径 / 插件集合)。
2628
+ *
2629
+ * 历史上这些字面量散落在 install-*、upgrade-lark、各 rule 等十余个文件里重复定义,
2630
+ * 易漂移。统一收口到此处,按用途分组,单一来源。
2620
2631
  */
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
2632
  /**
2637
- * upgrade-lark 场景专属修复状态的信号文件目录。
2638
- * fixStatus 有值时在此目录下创建同名文件(如 /tmp/event/PORT_FIX_READY),
2639
- * 文件内容为完整的 UpgradeLarkResult JSON,供外部进程轮询感知升级结果。
2633
+ * 官方插件:`openclaw-lark`(plugins.allow 中的短名;npm 发布名 `@larksuite/openclaw-lark`)。
2634
+ * VERSION_COMPAT_MAP 按版本判定与 openclaw 的兼容性。
2640
2635
  */
2641
- const FIX_EVENT_DIR = "/tmp/event";
2636
+ const LARK_PLUGIN_NAME = "openclaw-lark";
2642
2637
  /**
2643
- * 安装指令互斥锁文件路径。
2644
- * upgrade-lark / install-openclaw / install-extension / install-cli / reset --worker
2645
- * 共享此锁,同一时刻只允许一个安装指令运行。
2646
- * 锁文件内容:{ pid, command, startedAt }。
2638
+ * fork 插件:`@lark-apaas/openclaw-lark`(@lark-apaas 内部 fork,全名带 scope)。
2639
+ * 版本号自成体系、不在 VERSION_COMPAT_MAP 内,按对标的官方版本判定兼容性。
2647
2640
  */
2648
- const INSTALL_LOCK_FILE = `${DIAGNOSE_DIR}/install.lock`;
2641
+ const FORK_LARK_PLUGIN_FULL_NAME = "@lark-apaas/openclaw-lark";
2649
2642
  /**
2650
- * upgrade-lark 每次运行的日志文件路径,含时间戳便于按时间排序定位。
2651
- * checkOnly=true 时文件名含 "-check" 后缀,便于与正式安装日志区分。
2643
+ * 社区插件:openclaw **内置** 的 `feishu` 插件(非独立扩展)。
2644
+ * 与官方/ fork openclaw-lark 互斥,规范化时应禁用以让位 openclaw-lark。
2652
2645
  */
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
- }
2646
+ const BUILTIN_FEISHU_PLUGIN_NAME = "feishu";
2647
+ /**
2648
+ * 旧版插件:已弃用的 `feishu-openclaw-plugin`,始终不兼容,需替换为官方 openclaw-lark。
2649
+ */
2650
+ const LEGACY_LARK_PLUGIN_NAME = "feishu-openclaw-plugin";
2651
+ const MIAODA_PLUGIN_NAME = "openclaw-extension-miaoda";
2652
+ const MIAODA_CODING_PLUGIN_NAME = "openclaw-extension-miaoda-coding";
2653
+ const GUARDIAN_PLUGIN_NAME = "openclaw-guardian-plugin";
2654
+ const MEM0_PLUGIN_NAME = "openclaw-mem0-plugin";
2655
+ /** 飞书 lark-cli 命令名(独立 CLI,与 openclaw-lark 插件配套)。 */
2656
+ const LARK_CLI_NAME = "lark-cli";
2657
+ /** agent-skills 模板包名(manifest role=template,随 lark-cli 一起安装)。 */
2658
+ const AGENT_SKILLS_NAME = "agent-skills";
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];
2665
+ /**
2666
+ * install-extension --all 安装的官方扩展插件集合(manifest role=extension)。
2667
+ * builtin-plugin-missing 与 miaoda-official-plugins-install-spec-unlock 共用此单一来源,
2668
+ * 二者此前各维护一份且注释明示"必须保持一致"。
2669
+ */
2670
+ const OFFICIAL_EXTENSION_PLUGIN_NAMES = new Set([
2671
+ LARK_PLUGIN_NAME,
2672
+ MIAODA_PLUGIN_NAME,
2673
+ MIAODA_CODING_PLUGIN_NAME,
2674
+ GUARDIAN_PLUGIN_NAME,
2675
+ MEM0_PLUGIN_NAME
2676
+ ]);
2677
+ /** agent 工作区相对路径。 */
2678
+ const WORKSPACE_AGENT_REL = "workspace/agent";
2679
+ /** openclaw.json 默认相对路径。 */
2680
+ const DEFAULT_CONFIG_REL = `${WORKSPACE_AGENT_REL}/openclaw.json`;
2657
2681
  //#endregion
2658
- //#region src/lark-cli-init.ts
2659
- const LARK_PLUGIN_NAMES$1 = ["openclaw-lark", "feishu-openclaw-plugin"];
2660
- const PE_XML_TAG = "lark-cli-pe";
2661
- const PE_PLACEHOLDER = `
2662
- <${PE_XML_TAG}>
2663
- **【强制要求 - 无例外】飞书所有操作必须先读 feishu-lark-cli skill。未读skill不得调用任何飞书工具,违者视为操作失误。**
2664
- </${PE_XML_TAG}>
2665
- `;
2666
- function isLarkPluginInstalled(configPath) {
2667
- const extDir = getExtensionsDir(configPath);
2668
- return LARK_PLUGIN_NAMES$1.some((name) => {
2669
- try {
2670
- return node_fs.default.existsSync(node_path.default.join(extDir, name, "package.json"));
2671
- } catch {
2672
- return false;
2673
- }
2674
- });
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);
2675
2691
  }
2676
- function isLarkCliAvailable$2() {
2677
- try {
2678
- return (0, node_child_process.spawnSync)("lark-cli", ["--version"], {
2679
- encoding: "utf-8",
2680
- timeout: 5e3,
2681
- stdio: [
2682
- "ignore",
2683
- "pipe",
2684
- "ignore"
2685
- ]
2686
- }).status === 0;
2687
- } catch {
2688
- return false;
2689
- }
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);
2690
2696
  }
2691
- function readConfig(configPath) {
2692
- try {
2693
- const raw = node_fs.default.readFileSync(configPath, "utf-8");
2694
- const parsed = loadJSON5().parse(raw);
2695
- return parsed != null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
2696
- } catch {
2697
- return null;
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];
2698
2705
  }
2699
2706
  }
2700
- /**
2701
- * Resolve the feishu app secret for the given appId.
2702
- *
2703
- * Lookup order:
2704
- * 1. channels.feishu.appSecret (single-agent: feishu.appId === appId)
2705
- * 2. channels.feishu.accounts[key].appSecret (multi-agent: account.appId === appId)
2706
- *
2707
- * Value interpretation:
2708
- * - string → use directly
2709
- * - object → secret is managed by a provider; use `feishuAppSecret` param instead
2710
- *
2711
- * Returns null when the secret cannot be determined.
2712
- */
2713
- function resolveAppSecret(appId, config, feishuAppSecret) {
2714
- const feishu = getNestedMap(config, "channels", "feishu");
2715
- if (!feishu) return null;
2716
- let rawSecret;
2717
- if (typeof feishu.appId === "string" && feishu.appId === appId) rawSecret = feishu.appSecret;
2718
- else {
2719
- const accounts = asRecord(feishu.accounts);
2720
- if (accounts) for (const [, val] of Object.entries(accounts)) {
2721
- const account = asRecord(val);
2722
- if (account?.appId === appId) {
2723
- rawSecret = account.appSecret ?? feishu.appSecret;
2724
- break;
2725
- }
2726
- }
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
+ };
2727
2715
  }
2728
- if (typeof rawSecret === "string" && rawSecret) return rawSecret;
2729
- if (rawSecret != null && typeof rawSecret === "object") return feishuAppSecret ?? null;
2730
- return null;
2731
- }
2732
- /**
2733
- * Resolve the agents.md path for the given appId from the openclaw config.
2734
- *
2735
- * Case 1: appId matches channels.feishu.appId (single-agent path)
2736
- * → WORKSPACE_DIR/AGENTS.md
2737
- *
2738
- * Case 2: appId found in channels.feishu.accounts (multi-agent path)
2739
- * → find account key where account.appId === appId
2740
- * → find binding where match.channel=feishu && match.accountId=that key
2741
- * → if agentId === 'main' → WORKSPACE_DIR/agents.md
2742
- * → else find agent in agents.list by id → agent.workspace/agents.md
2743
- *
2744
- * Returns null when the path cannot be determined.
2745
- */
2746
- function resolveAgentsMdPath(appId, config) {
2747
- const feishu = getNestedMap(config, "channels", "feishu");
2748
- if (!feishu) {
2749
- console.error("resolveAgentsMdPath: channels.feishu not found");
2750
- return null;
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));
2751
2723
  }
2752
- if (typeof feishu.appId === "string" && feishu.appId === appId) {
2753
- console.error(`resolveAgentsMdPath: case=single-agent feishu.appId=${feishu.appId}`);
2754
- return node_path.default.join(WORKSPACE_DIR, "workspace", "AGENTS.md");
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
+ };
2755
2742
  }
2756
- const accounts = asRecord(feishu.accounts);
2757
- if (!accounts) {
2758
- console.error("resolveAgentsMdPath: feishu.accounts not found");
2759
- return null;
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;
2760
2756
  }
2761
- let accountId;
2762
- for (const [key, val] of Object.entries(accounts)) if (asRecord(val)?.appId === appId) {
2763
- accountId = key;
2764
- break;
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");
2772
+ }
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
+ };
2765
2785
  }
2766
- if (!accountId) {
2767
- console.error(`resolveAgentsMdPath: no account found with appId=${appId} in feishu.accounts keys=[${Object.keys(accounts).join(",")}]`);
2768
- return null;
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;
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;
2769
2800
  }
2770
- console.error(`resolveAgentsMdPath: found accountId=${accountId}`);
2771
- const bindings = Array.isArray(config.bindings) ? config.bindings : [];
2772
- let agentId;
2773
- for (const b of bindings) {
2774
- const binding = asRecord(b);
2775
- if (!binding) continue;
2776
- const match = asRecord(binding.match);
2777
- if (match?.channel === "feishu" && match?.accountId === accountId) {
2778
- if (typeof binding.agentId === "string") {
2779
- agentId = binding.agentId;
2780
- break;
2781
- }
2782
- }
2783
- }
2784
- if (!agentId) {
2785
- console.error(`resolveAgentsMdPath: no binding found for accountId=${accountId} in bindings(count=${bindings.length})`);
2786
- return null;
2787
- }
2788
- console.error(`resolveAgentsMdPath: found agentId=${agentId}`);
2789
- if (agentId === "main") {
2790
- console.error("resolveAgentsMdPath: case=multi-agent-main");
2791
- return node_path.default.join(WORKSPACE_DIR, "workspace", "AGENTS.md");
2792
- }
2793
- const agentsObj = asRecord(config.agents);
2794
- const list = Array.isArray(agentsObj?.list) ? agentsObj.list : [];
2795
- for (const a of list) {
2796
- const agent = asRecord(a);
2797
- if (agent?.id === agentId) {
2798
- const ws = typeof agent.workspace === "string" ? agent.workspace : node_path.default.join(WORKSPACE_DIR, "workspace");
2799
- console.error(`resolveAgentsMdPath: case=multi-agent-custom agentId=${agentId} workspace=${ws}`);
2800
- return node_path.default.join(ws, "AGENTS.md");
2801
- }
2802
- }
2803
- console.error(`resolveAgentsMdPath: agentId=${agentId} not found in agents.list(count=${list.length})`);
2804
- return null;
2805
- }
2806
- function appendPeToAgentsMd(agentsMdPath) {
2807
- const dir = node_path.default.dirname(agentsMdPath);
2808
- if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
2809
- const existing = node_fs.default.existsSync(agentsMdPath) ? node_fs.default.readFileSync(agentsMdPath, "utf-8") : "";
2810
- if (existing.includes(`<lark-cli-pe>`)) {
2811
- console.error(`lark-cli-init: <${PE_XML_TAG}> already present in ${agentsMdPath}, skipping`);
2812
- return;
2813
- }
2814
- const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
2815
- node_fs.default.appendFileSync(agentsMdPath, `${sep}${PE_PLACEHOLDER}`, "utf-8");
2816
- console.error(`lark-cli-init: appended PE placeholder to ${agentsMdPath}`);
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");
2817
2815
  }
2818
2816
  /**
2819
- * Collect every Feishu bot appId declared in the openclaw config.
2820
- * Covers both single-agent (channels.feishu.appId) and multi-agent
2821
- * (channels.feishu.accounts[*].appId) layouts. Returns a deduplicated list.
2817
+ * fs-only 检测:`<extDir>/<name>/package.json` 存在即视为已装。
2818
+ * 优先级 openclaw-lark(新版)> feishu-openclaw-plugin(legacy)。
2819
+ * 不读 package.json 内容,只判存在性,避开 JSON 损坏。
2822
2820
  */
2823
- function collectFeishuAppIds(configPath) {
2824
- const config = readConfig(configPath ?? CONFIG_PATH);
2825
- if (!config) return [];
2826
- const feishu = getNestedMap(config, "channels", "feishu");
2827
- if (!feishu) return [];
2828
- const appIds = /* @__PURE__ */ new Set();
2829
- const topAppId = feishu.appId;
2830
- if (typeof topAppId === "string" && topAppId.trim()) appIds.add(topAppId.trim());
2831
- const accounts = asRecord(feishu.accounts);
2832
- if (accounts) for (const val of Object.values(accounts)) {
2833
- const appId = asRecord(val)?.appId;
2834
- if (typeof appId === "string" && appId.trim()) appIds.add(appId.trim());
2835
- }
2836
- return [...appIds];
2837
- }
2838
- function runLarkCliInit(opts) {
2839
- const configPath = opts.configPath ?? CONFIG_PATH;
2840
- if (!isLarkPluginInstalled(configPath)) {
2841
- console.error("lark-cli-init: skipping — openclaw-lark plugin not installed");
2842
- return {
2843
- ok: true,
2844
- skipped: true,
2845
- skipReason: "openclaw-lark plugin not installed"
2846
- };
2847
- }
2848
- if (!isLarkCliAvailable$2()) {
2849
- console.error("lark-cli-init: skipping — lark-cli command not found");
2850
- return {
2851
- ok: true,
2852
- skipped: true,
2853
- skipReason: "lark-cli command not found"
2854
- };
2855
- }
2856
- const config = readConfig(configPath);
2857
- if (!config) return {
2858
- ok: false,
2859
- error: `could not read config at ${configPath}`
2860
- };
2861
- const agentsMdPath = resolveAgentsMdPath(opts.appId, config);
2862
- console.error(`lark-cli-init: resolved agents.md path=${agentsMdPath ?? "(null)"}`);
2863
- if (!agentsMdPath) return {
2864
- ok: false,
2865
- error: `could not resolve agents.md path for appId=${opts.appId}`
2866
- };
2867
- const appSecret = resolveAppSecret(opts.appId, config, opts.feishuAppSecret);
2868
- if (!appSecret) return {
2869
- ok: false,
2870
- error: `could not resolve appSecret for appId=${opts.appId}`
2871
- };
2872
- console.error(`lark-cli-init: running lark-cli config init --name ${opts.appId} --app-id ${opts.appId} --brand feishu --app-secret-stdin --force-init`);
2873
- const initRes = (0, node_child_process.spawnSync)("lark-cli", [
2874
- "config",
2875
- "init",
2876
- "--name",
2877
- opts.appId,
2878
- "--app-id",
2879
- opts.appId,
2880
- "--brand",
2881
- "feishu",
2882
- "--app-secret-stdin",
2883
- "--force-init"
2884
- ], {
2885
- stdio: [
2886
- "pipe",
2887
- "pipe",
2888
- "pipe"
2889
- ],
2890
- encoding: "utf-8",
2891
- input: appSecret
2892
- });
2893
- const configInitStdout = initRes.stdout?.trim() || void 0;
2894
- const configInitStderr = initRes.stderr?.trim() || void 0;
2895
- if (configInitStdout) console.error(`lark-cli config init stdout: ${configInitStdout}`);
2896
- if (configInitStderr) console.error(`lark-cli config init stderr: ${configInitStderr}`);
2897
- if (initRes.error) return {
2898
- ok: false,
2899
- configInitStdout,
2900
- configInitStderr,
2901
- error: `lark-cli config init spawn error: ${initRes.error.message}`
2902
- };
2903
- if (initRes.status !== 0) return {
2904
- ok: false,
2905
- configInitExitCode: initRes.status ?? void 0,
2906
- configInitStdout,
2907
- configInitStderr,
2908
- error: `lark-cli config init exited with code ${initRes.status}`
2909
- };
2910
- appendPeToAgentsMd(agentsMdPath);
2911
- return {
2912
- ok: true,
2913
- configInitExitCode: 0,
2914
- agentsMdPath
2915
- };
2821
+ function detectInstalledLarkPlugin$1(extDir) {
2822
+ for (const name of LARK_PLUGIN_DIR_NAMES) if (pluginPackageJsonExists(extDir, name)) return name;
2823
+ return null;
2916
2824
  }
2917
- //#endregion
2918
- //#region src/rules/agents-md-lark-cli-pe.ts
2919
- function isLarkCliAvailable$1() {
2825
+ function pluginPackageJsonExists(extDir, pluginDir) {
2920
2826
  try {
2921
- return (0, node_child_process.spawnSync)("lark-cli", ["--version"], {
2922
- encoding: "utf-8",
2923
- timeout: 5e3,
2924
- stdio: [
2925
- "ignore",
2926
- "pipe",
2927
- "ignore"
2928
- ]
2929
- }).status === 0;
2827
+ return node_fs.default.existsSync(node_path.default.join(extDir, pluginDir, "package.json"));
2930
2828
  } catch {
2931
2829
  return false;
2932
2830
  }
2933
2831
  }
2934
- let AgentsMdLarkCliPeRule = class AgentsMdLarkCliPeRule extends DiagnoseRule {
2832
+ //#endregion
2833
+ //#region src/rules/old-miaoda-plugins-cleanup.ts
2834
+ const OLD_PLUGIN_NAMES = Object.freeze([
2835
+ "openclaw-feishu-greeting",
2836
+ "openclaw-miaoda-keepalive",
2837
+ "feishu-greeting",
2838
+ "miaoda-keepalive"
2839
+ ]);
2840
+ function getPluginMaps(config) {
2841
+ const rawAllow = asRecord(config.plugins)?.allow;
2842
+ return {
2843
+ entries: getNestedMap(config, "plugins", "entries"),
2844
+ installs: getNestedMap(config, "plugins", "installs"),
2845
+ allow: Array.isArray(rawAllow) ? rawAllow : void 0
2846
+ };
2847
+ }
2848
+ function hasNewMiaoda({ entries, installs, allow }) {
2849
+ return asRecord(entries?.["openclaw-extension-miaoda"]) != null || asRecord(installs?.["openclaw-extension-miaoda"]) != null || (allow?.includes("openclaw-extension-miaoda") ?? false);
2850
+ }
2851
+ function findResiduals({ entries, installs, allow }, extensionsDir) {
2852
+ return OLD_PLUGIN_NAMES.filter((name) => entries?.[name] != null || installs?.[name] != null || (allow?.includes(name) ?? false) || node_fs.default.existsSync(node_path.default.join(extensionsDir, name)));
2853
+ }
2854
+ let OldMiaodaPluginsCleanupRule = class OldMiaodaPluginsCleanupRule extends DiagnoseRule {
2935
2855
  validate(ctx) {
2936
- if (!isLarkCliAvailable$1()) return { pass: true };
2937
- const missingPath = collectExistingAgentsMdPaths(ctx).find((filePath) => {
2938
- return !node_fs.default.readFileSync(filePath, "utf-8").includes(`<${PE_XML_TAG}>`);
2939
- });
2940
- if (!missingPath) return { pass: true };
2856
+ const maps = getPluginMaps(ctx.config);
2857
+ if (!hasNewMiaoda(maps)) return { pass: true };
2858
+ const residuals = findResiduals(maps, getExtensionsDir(ctx.configPath));
2859
+ if (residuals.length === 0) return { pass: true };
2941
2860
  return {
2942
2861
  pass: false,
2943
- message: `${missingPath} 中缺少 lark-cli-pe PE 内容,需要追加`
2862
+ message: "旧 miaoda 插件残留: " + residuals.sort().join(",")
2944
2863
  };
2945
2864
  }
2946
2865
  repair(ctx) {
2947
- if (!isLarkCliAvailable$1()) return;
2948
- for (const filePath of collectExistingAgentsMdPaths(ctx)) {
2949
- const content = node_fs.default.readFileSync(filePath, "utf-8");
2950
- if (content.includes(`<lark-cli-pe>`)) continue;
2951
- const sep = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
2952
- node_fs.default.appendFileSync(filePath, `${sep}${PE_PLACEHOLDER}`, "utf-8");
2953
- console.error(`agents-md-lark-cli-pe: appended PE to ${filePath}`);
2866
+ const maps = getPluginMaps(ctx.config);
2867
+ if (!hasNewMiaoda(maps)) return;
2868
+ const extensionsDir = getExtensionsDir(ctx.configPath);
2869
+ const { entries, installs, allow } = maps;
2870
+ const oldSet = new Set(OLD_PLUGIN_NAMES);
2871
+ if (allow) for (let i = allow.length - 1; i >= 0; i--) {
2872
+ const v = allow[i];
2873
+ if (typeof v === "string" && oldSet.has(v)) allow.splice(i, 1);
2874
+ }
2875
+ for (const name of OLD_PLUGIN_NAMES) {
2876
+ if (entries && name in entries) delete entries[name];
2877
+ if (installs && name in installs) delete installs[name];
2878
+ const target = node_path.default.join(extensionsDir, name);
2879
+ const rel = node_path.default.relative(extensionsDir, target);
2880
+ if (!rel || rel.startsWith("..") || node_path.default.isAbsolute(rel)) continue;
2881
+ try {
2882
+ node_fs.default.rmSync(target, {
2883
+ recursive: true,
2884
+ force: true
2885
+ });
2886
+ } catch (e) {
2887
+ console.error(`[old_miaoda_plugins_cleanup] rmSync ${target} failed: ${e.message}`);
2888
+ }
2954
2889
  }
2955
2890
  }
2956
2891
  };
2957
- AgentsMdLarkCliPeRule = __decorate([Rule({
2958
- key: "agents_md_lark_cli_pe",
2959
- description: "检测各智能体 AGENTS.md 中是否缺失 lark-cli-pe PE 内容,lark-cli 存在时自动追加",
2892
+ OldMiaodaPluginsCleanupRule = __decorate([Rule({
2893
+ key: "old_miaoda_plugins_cleanup",
2894
+ description: "当新版 openclaw-extension-miaoda 已存在时,清理过时插件引用(openclaw-feishu-greeting、openclaw-miaoda-keepalive 等)",
2960
2895
  dependsOn: ["config_syntax_check"],
2961
2896
  repairMode: "standard",
2962
2897
  level: "silent"
2963
- })], AgentsMdLarkCliPeRule);
2898
+ })], OldMiaodaPluginsCleanupRule);
2964
2899
  //#endregion
2965
- //#region src/rules/miaoda-official-plugins-install-spec-unlock.ts
2966
- /**
2967
- * Official miaoda-side plugins that must track manifest — version-locked specs
2968
- * here block upgrades. Third-party / user-installed plugins are intentionally
2969
- * out of scope (users may pin them deliberately).
2970
- */
2971
- const OFFICIAL_PLUGIN_NAMES = new Set([
2972
- "openclaw-extension-miaoda",
2973
- "openclaw-extension-miaoda-coding",
2974
- "openclaw-guardian-plugin",
2975
- "openclaw-mem0-plugin",
2976
- "openclaw-lark"
2977
- ]);
2978
- const LOCKED_NPM_SPEC = /^(@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*@[^@/:#\s]+$/i;
2979
- function isLockedNpmSpec(spec) {
2980
- return typeof spec === "string" && LOCKED_NPM_SPEC.test(spec);
2981
- }
2982
- function unlockSpec(spec) {
2983
- const slash = spec.indexOf("/");
2984
- const cut = slash === -1 ? spec.indexOf("@") : spec.indexOf("@", slash + 1);
2985
- return spec.slice(0, cut);
2986
- }
2987
- /** Yield `[key, lockedSpec]` for every official-plugin install whose `spec` is locked. */
2988
- function* iterLockedOfficialInstalls(config) {
2989
- const installs = getNestedMap(config, "plugins", "installs");
2990
- if (!installs) return;
2991
- for (const [key, entry] of Object.entries(installs)) {
2992
- if (!OFFICIAL_PLUGIN_NAMES.has(key)) continue;
2993
- const spec = asRecord(entry)?.spec;
2994
- if (isLockedNpmSpec(spec)) yield [key, spec];
2995
- }
2996
- }
2997
- let MiaodaOfficialPluginsInstallSpecUnlockRule = class MiaodaOfficialPluginsInstallSpecUnlockRule extends DiagnoseRule {
2998
- validate(ctx) {
2999
- const locked = [...iterLockedOfficialInstalls(ctx.config)].map(([k]) => k);
3000
- if (locked.length === 0) return { pass: true };
3001
- return {
3002
- pass: false,
3003
- message: "plugins.installs 中官方插件存在锁版本的 spec: " + locked.sort().join(",")
3004
- };
3005
- }
3006
- repair(ctx) {
3007
- for (const [key, spec] of iterLockedOfficialInstalls(ctx.config)) setNestedValue(ctx.config, [
3008
- "plugins",
3009
- "installs",
3010
- key,
3011
- "spec"
3012
- ], unlockSpec(spec));
3013
- }
3014
- };
3015
- MiaodaOfficialPluginsInstallSpecUnlockRule = __decorate([Rule({
3016
- key: "miaoda_official_plugins_install_spec_unlock",
3017
- description: "移除官方妙搭插件安装条目中的锁版本 npm spec,使其跟随最新 manifest 版本",
3018
- dependsOn: ["config_syntax_check"],
3019
- repairMode: "standard",
3020
- level: "silent"
3021
- })], MiaodaOfficialPluginsInstallSpecUnlockRule);
3022
- //#endregion
3023
- //#region src/rules/miaoda-plugin-allow.ts
3024
- const MIAODA_PLUGIN = "openclaw-extension-miaoda";
3025
- let MiaodaPluginAllowRule = class MiaodaPluginAllowRule extends DiagnoseRule {
3026
- validate(ctx) {
3027
- if (!isPluginInstalledOnDisk(getExtensionsDir(ctx.configPath), MIAODA_PLUGIN)) return { pass: true };
3028
- if (getAllow$1(ctx.config).includes(MIAODA_PLUGIN)) return { pass: true };
3029
- return {
3030
- pass: false,
3031
- message: `plugins.allow 缺少 ${MIAODA_PLUGIN}(已在 extensions/ 下装但未启用)`
3032
- };
3033
- }
3034
- repair(ctx) {
3035
- if (!isPluginInstalledOnDisk(getExtensionsDir(ctx.configPath), MIAODA_PLUGIN)) return;
3036
- const plugins = ctx.config.plugins;
3037
- if (plugins == null || typeof plugins !== "object" || Array.isArray(plugins)) {
3038
- ctx.config.plugins = { allow: [MIAODA_PLUGIN] };
3039
- return;
3040
- }
3041
- const pluginsMap = plugins;
3042
- const rawAllow = pluginsMap.allow;
3043
- const allow = Array.isArray(rawAllow) ? rawAllow : [];
3044
- if (allow.includes(MIAODA_PLUGIN)) return;
3045
- allow.push(MIAODA_PLUGIN);
3046
- pluginsMap.allow = allow;
3047
- }
3048
- };
3049
- MiaodaPluginAllowRule = __decorate([Rule({
3050
- key: "miaoda_plugin_allow",
3051
- description: "当 openclaw-extension-miaoda 已在磁盘安装但未在 allow 列表中时,将其添加到 plugins.allow(实验性)",
3052
- dependsOn: ["config_syntax_check"],
3053
- repairMode: "standard",
3054
- level: "critical",
3055
- profile: "standard"
3056
- })], MiaodaPluginAllowRule);
3057
- function getAllow$1(config) {
3058
- const plugins = config.plugins;
3059
- if (plugins == null || typeof plugins !== "object" || Array.isArray(plugins)) return [];
3060
- const allow = plugins.allow;
3061
- if (!Array.isArray(allow)) return [];
3062
- return allow.filter((e) => typeof e === "string");
3063
- }
3064
- //#endregion
3065
- //#region src/rules/lark-plugin-allow.ts
3066
- const LARK_PLUGIN = "openclaw-lark";
3067
- const LEGACY_LARK_PLUGIN = "feishu-openclaw-plugin";
3068
- const LARK_PLUGIN_NAMES = [LARK_PLUGIN, LEGACY_LARK_PLUGIN];
3069
- let LarkPluginAllowRule = class LarkPluginAllowRule extends DiagnoseRule {
3070
- validate(ctx) {
3071
- const allow = getAllow(ctx.config);
3072
- if (LARK_PLUGIN_NAMES.some((name) => allow.includes(name))) return { pass: true };
3073
- const installed = detectInstalledLarkPlugin(getExtensionsDir(ctx.configPath));
3074
- if (installed == null) return { pass: true };
3075
- return {
3076
- pass: false,
3077
- message: `plugins.allow 缺少飞书插件 ${installed}(已在 extensions/ 下装但未启用)`
3078
- };
3079
- }
3080
- repair(ctx) {
3081
- const installed = detectInstalledLarkPlugin(getExtensionsDir(ctx.configPath));
3082
- if (installed == null) return;
3083
- if (ctx.config.plugins == null || typeof ctx.config.plugins !== "object" || Array.isArray(ctx.config.plugins)) {
3084
- ctx.config.plugins = { allow: [installed] };
3085
- return;
3086
- }
3087
- const pluginsMap = ctx.config.plugins;
3088
- const rawAllow = pluginsMap.allow;
3089
- const original = Array.isArray(rawAllow) ? rawAllow : [];
3090
- const stringAllow = original.filter((e) => typeof e === "string");
3091
- if (LARK_PLUGIN_NAMES.some((name) => stringAllow.includes(name))) return;
3092
- original.push(installed);
3093
- pluginsMap.allow = original;
3094
- }
3095
- };
3096
- LarkPluginAllowRule = __decorate([Rule({
3097
- key: "lark_plugin_allow",
3098
- description: "当飞书插件(openclaw-lark 或旧版名)已在磁盘安装但未加入 plugins.allow 时,自动添加",
3099
- dependsOn: ["config_syntax_check"],
3100
- repairMode: "standard",
3101
- level: "critical"
3102
- })], LarkPluginAllowRule);
3103
- function getAllow(config) {
3104
- const plugins = config.plugins;
3105
- if (plugins == null || typeof plugins !== "object" || Array.isArray(plugins)) return [];
3106
- const allow = plugins.allow;
3107
- if (!Array.isArray(allow)) return [];
3108
- return allow.filter((e) => typeof e === "string");
3109
- }
3110
- /**
3111
- * fs-only 检测:`<extDir>/<name>/package.json` 存在即视为已装。
3112
- * 优先级 openclaw-lark(新版)> feishu-openclaw-plugin(legacy)。
3113
- * 不读 package.json 内容,只判存在性,避开 JSON 损坏。
3114
- */
3115
- function detectInstalledLarkPlugin(extDir) {
3116
- for (const name of [LARK_PLUGIN, LEGACY_LARK_PLUGIN]) if (pluginPackageJsonExists(extDir, name)) return name;
3117
- return null;
3118
- }
3119
- function pluginPackageJsonExists(extDir, pluginDir) {
3120
- try {
3121
- return node_fs.default.existsSync(node_path.default.join(extDir, pluginDir, "package.json"));
3122
- } catch {
3123
- return false;
3124
- }
3125
- }
3126
- //#endregion
3127
- //#region src/rules/old-miaoda-plugins-cleanup.ts
3128
- const NEW_MIAODA = "openclaw-extension-miaoda";
3129
- const OLD_PLUGIN_NAMES = Object.freeze([
3130
- "openclaw-feishu-greeting",
3131
- "openclaw-miaoda-keepalive",
3132
- "feishu-greeting",
3133
- "miaoda-keepalive"
3134
- ]);
3135
- function getPluginMaps(config) {
3136
- const rawAllow = asRecord(config.plugins)?.allow;
3137
- return {
3138
- entries: getNestedMap(config, "plugins", "entries"),
3139
- installs: getNestedMap(config, "plugins", "installs"),
3140
- allow: Array.isArray(rawAllow) ? rawAllow : void 0
3141
- };
3142
- }
3143
- function hasNewMiaoda({ entries, installs, allow }) {
3144
- return asRecord(entries?.[NEW_MIAODA]) != null || asRecord(installs?.[NEW_MIAODA]) != null || (allow?.includes(NEW_MIAODA) ?? false);
3145
- }
3146
- function findResiduals({ entries, installs, allow }, extensionsDir) {
3147
- return OLD_PLUGIN_NAMES.filter((name) => entries?.[name] != null || installs?.[name] != null || (allow?.includes(name) ?? false) || node_fs.default.existsSync(node_path.default.join(extensionsDir, name)));
3148
- }
3149
- let OldMiaodaPluginsCleanupRule = class OldMiaodaPluginsCleanupRule extends DiagnoseRule {
3150
- validate(ctx) {
3151
- const maps = getPluginMaps(ctx.config);
3152
- if (!hasNewMiaoda(maps)) return { pass: true };
3153
- const residuals = findResiduals(maps, getExtensionsDir(ctx.configPath));
3154
- if (residuals.length === 0) return { pass: true };
3155
- return {
3156
- pass: false,
3157
- message: "旧 miaoda 插件残留: " + residuals.sort().join(",")
3158
- };
3159
- }
3160
- repair(ctx) {
3161
- const maps = getPluginMaps(ctx.config);
3162
- if (!hasNewMiaoda(maps)) return;
3163
- const extensionsDir = getExtensionsDir(ctx.configPath);
3164
- const { entries, installs, allow } = maps;
3165
- const oldSet = new Set(OLD_PLUGIN_NAMES);
3166
- if (allow) for (let i = allow.length - 1; i >= 0; i--) {
3167
- const v = allow[i];
3168
- if (typeof v === "string" && oldSet.has(v)) allow.splice(i, 1);
3169
- }
3170
- for (const name of OLD_PLUGIN_NAMES) {
3171
- if (entries && name in entries) delete entries[name];
3172
- if (installs && name in installs) delete installs[name];
3173
- const target = node_path.default.join(extensionsDir, name);
3174
- const rel = node_path.default.relative(extensionsDir, target);
3175
- if (!rel || rel.startsWith("..") || node_path.default.isAbsolute(rel)) continue;
3176
- try {
3177
- node_fs.default.rmSync(target, {
3178
- recursive: true,
3179
- force: true
3180
- });
3181
- } catch (e) {
3182
- console.error(`[old_miaoda_plugins_cleanup] rmSync ${target} failed: ${e.message}`);
3183
- }
3184
- }
3185
- }
3186
- };
3187
- OldMiaodaPluginsCleanupRule = __decorate([Rule({
3188
- key: "old_miaoda_plugins_cleanup",
3189
- description: "当新版 openclaw-extension-miaoda 已存在时,清理过时插件引用(openclaw-feishu-greeting、openclaw-miaoda-keepalive 等)",
3190
- dependsOn: ["config_syntax_check"],
3191
- repairMode: "standard",
3192
- level: "silent"
3193
- })], OldMiaodaPluginsCleanupRule);
3194
- //#endregion
3195
- //#region src/rules/builtin-plugin-missing.ts
3196
- /**
3197
- * install-extension --all 安装的内置扩展插件列表(manifest role=extension)。
3198
- * 与 miaoda-official-plugins-install-spec-unlock.ts 中的 OFFICIAL_PLUGIN_NAMES 保持一致。
3199
- */
3200
- const BUILTIN_EXTENSION_PLUGINS = new Set([
3201
- "openclaw-lark",
3202
- "openclaw-extension-miaoda",
3203
- "openclaw-extension-miaoda-coding",
3204
- "openclaw-guardian-plugin",
3205
- "openclaw-mem0-plugin"
3206
- ]);
3207
- function findMissingBuiltinPlugins(ctx) {
3208
- const extDir = getExtensionsDir(ctx.configPath);
3209
- return [...BUILTIN_EXTENSION_PLUGINS].filter((name) => !isPluginInstalledOnDisk(extDir, name)).sort();
2900
+ //#region src/rules/builtin-plugin-missing.ts
2901
+ function findMissingBuiltinPlugins(ctx) {
2902
+ const extDir = getExtensionsDir(ctx.configPath);
2903
+ return [...OFFICIAL_EXTENSION_PLUGIN_NAMES].filter((name) => !isPluginInstalledOnDisk(extDir, name)).sort();
3210
2904
  }
3211
2905
  let BuiltinPluginMissingRule = class BuiltinPluginMissingRule extends DiagnoseRule {
3212
2906
  validate(ctx) {
@@ -3487,10 +3181,7 @@ function extractTarballTolerant(tarball, destDir, opts = {}) {
3487
3181
  }
3488
3182
  //#endregion
3489
3183
  //#region src/rules/feishu-plugin-state-normalize.ts
3490
- const PLUGIN_NAME$2 = "openclaw-lark";
3491
- const BUILTIN_FEISHU = "feishu";
3492
- const LEGACY_PLUGIN_NAME = "feishu-openclaw-plugin";
3493
- const LEGACY_DIRS_TO_REMOVE = [LEGACY_PLUGIN_NAME, BUILTIN_FEISHU];
3184
+ const LEGACY_DIRS_TO_REMOVE = [LEGACY_LARK_PLUGIN_NAME, BUILTIN_FEISHU_PLUGIN_NAME];
3494
3185
  const FEISHU_TOOLS = Object.freeze([
3495
3186
  "feishu_bitable_app",
3496
3187
  "feishu_bitable_app_table",
@@ -3536,9 +3227,10 @@ let FeishuPluginStateNormalizeRule = class FeishuPluginStateNormalizeRule extend
3536
3227
  validate(ctx) {
3537
3228
  if (!isPluginInstalled(ctx)) return { pass: true };
3538
3229
  const fails = [];
3539
- if (!isNewPluginEnabled(ctx.config)) fails.push(`plugins.entries["${PLUGIN_NAME$2}"].enabled !== true(应启用)`);
3230
+ if (!isNewPluginEnabled(ctx.config)) fails.push(`plugins.entries["${LARK_PLUGIN_NAME}"].enabled !== true(应启用)`);
3540
3231
  if (isBuiltinFeishuEnabled(ctx.config)) fails.push("plugins.entries.feishu.enabled === true(应禁用)");
3541
- if (isTopLevelMissingFeishuTools(ctx.config)) fails.push("tools.alsoAllow 缺 feishu_* tools");
3232
+ const grantGap = missingFeishuToolsGrant(ctx.config);
3233
+ if (grantGap) fails.push(`tools.${grantGap} 缺 feishu_* tools`);
3542
3234
  const legacyResiduals = findLegacyResiduals(ctx);
3543
3235
  if (legacyResiduals.length > 0) fails.push(`legacy 飞书插件残留:${legacyResiduals.join(", ")}`);
3544
3236
  if (fails.length === 0) return { pass: true };
@@ -3548,8 +3240,8 @@ let FeishuPluginStateNormalizeRule = class FeishuPluginStateNormalizeRule extend
3548
3240
  };
3549
3241
  }
3550
3242
  repair(ctx) {
3551
- setEntryEnabled(ctx.config, PLUGIN_NAME$2, true);
3552
- setEntryEnabled(ctx.config, BUILTIN_FEISHU, false);
3243
+ setEntryEnabled(ctx.config, LARK_PLUGIN_NAME, true);
3244
+ setEntryEnabled(ctx.config, BUILTIN_FEISHU_PLUGIN_NAME, false);
3553
3245
  ensureFeishuTools(ctx.config);
3554
3246
  cleanupLegacyResiduals(ctx);
3555
3247
  }
@@ -3562,36 +3254,54 @@ FeishuPluginStateNormalizeRule = __decorate([Rule({
3562
3254
  level: "critical"
3563
3255
  })], FeishuPluginStateNormalizeRule);
3564
3256
  function isPluginInstalled(ctx) {
3565
- return node_fs.default.existsSync(node_path.default.join(getExtensionsDir(ctx.configPath), PLUGIN_NAME$2));
3257
+ return node_fs.default.existsSync(node_path.default.join(getExtensionsDir(ctx.configPath), LARK_PLUGIN_NAME));
3566
3258
  }
3567
3259
  function isNewPluginEnabled(config) {
3568
- return asRecord(getNestedMap(config, "plugins", "entries")?.[PLUGIN_NAME$2])?.enabled === true;
3260
+ return asRecord(getNestedMap(config, "plugins", "entries")?.[LARK_PLUGIN_NAME])?.enabled === true;
3569
3261
  }
3570
3262
  function isBuiltinFeishuEnabled(config) {
3571
- return asRecord(getNestedMap(config, "plugins", "entries")?.[BUILTIN_FEISHU])?.enabled === true;
3572
- }
3573
- /** 仅看顶层 tools.alsoAllow——agent 级 alsoAllow 是用户对单 agent 的精细化授权,doctor 不动。
3574
- * 含 "*" 或任一 feishu_* 即视为已配。 */
3575
- function isTopLevelMissingFeishuTools(config) {
3576
- return !hasFeishuTool(readAlsoAllow(config));
3263
+ return asRecord(getNestedMap(config, "plugins", "entries")?.[BUILTIN_FEISHU_PLUGIN_NAME])?.enabled === true;
3577
3264
  }
3578
- function readAlsoAllow(host) {
3579
- const tools = asRecord(host)?.tools;
3580
- const arr = asRecord(tools)?.alsoAllow;
3265
+ /** 读取顶层 tools.<key>(allow / alsoAllow)的字符串数组。 */
3266
+ function readTopLevelToolsList(config, key) {
3267
+ const arr = asRecord(asRecord(config)?.tools)?.[key];
3581
3268
  if (!Array.isArray(arr)) return [];
3582
3269
  return arr.filter((e) => typeof e === "string");
3583
3270
  }
3584
- function hasFeishuTool(alsoAllow) {
3585
- if (alsoAllow.includes("*")) return true;
3586
- return alsoAllow.some((t) => FEISHU_TOOLS.includes(t));
3271
+ function hasFeishuTool(list) {
3272
+ if (list.includes("*")) return true;
3273
+ return list.some((t) => FEISHU_TOOLS.includes(t));
3274
+ }
3275
+ /**
3276
+ * 决定 feishu_* 应补充到哪个授权键:
3277
+ * - 已有非空 tools.allow(限制式白名单)→ 'allow'(合并进 allow,避免与 alsoAllow 冲突)
3278
+ * - 否则 → 'alsoAllow'(追加式,叠加在 profile 基线上)
3279
+ * openclaw schema 禁止同一 scope 同时设 allow + alsoAllow,故有 allow 时必须并入 allow,
3280
+ * 否则会触发 tools_allow_also_allow_conflict 规则反复来回改写。
3281
+ */
3282
+ function feishuGrantTarget(config) {
3283
+ return readTopLevelToolsList(config, "allow").length > 0 ? "allow" : "alsoAllow";
3284
+ }
3285
+ /**
3286
+ * 仅看顶层 tools——agent 级授权是用户对单 agent 的精细化配置,doctor 不动。
3287
+ * 返回缺失的目标键名('allow' / 'alsoAllow'),不缺时返回 null。
3288
+ *
3289
+ * 不关心 lark-cli:tools.alsoAllow(授权层)与 channels.feishu.tools.deny(插件注册层)
3290
+ * 是两个正交的 config key,可并存——授权一个被 deny 的工具是无害空操作。因此本规则始终
3291
+ * 补全 FEISHU_TOOLS(让 openclaw-lark 实际注册的工具都可用),重叠工具的"不可用"由独立的
3292
+ * deny 规则在注册层处理,互不干扰。
3293
+ */
3294
+ function missingFeishuToolsGrant(config) {
3295
+ const target = feishuGrantTarget(config);
3296
+ return hasFeishuTool(readTopLevelToolsList(config, target)) ? null : target;
3587
3297
  }
3588
3298
  function findLegacyResiduals(ctx) {
3589
3299
  const found = [];
3590
3300
  const plugins = asRecord(ctx.config.plugins);
3591
- if (asRecord(plugins?.entries)?.[LEGACY_PLUGIN_NAME] != null) found.push("entries[legacy]");
3301
+ if (asRecord(plugins?.entries)?.["feishu-openclaw-plugin"] != null) found.push("entries[legacy]");
3592
3302
  const allow = plugins?.allow;
3593
- if (Array.isArray(allow) && allow.includes(LEGACY_PLUGIN_NAME)) found.push("allow[legacy]");
3594
- if (asRecord(plugins?.installs)?.[LEGACY_PLUGIN_NAME] != null) found.push("installs[legacy]");
3303
+ if (Array.isArray(allow) && allow.includes("feishu-openclaw-plugin")) found.push("allow[legacy]");
3304
+ if (asRecord(plugins?.installs)?.["feishu-openclaw-plugin"] != null) found.push("installs[legacy]");
3595
3305
  const extDir = getExtensionsDir(ctx.configPath);
3596
3306
  for (const name of LEGACY_DIRS_TO_REMOVE) if (node_fs.default.existsSync(node_path.default.join(extDir, name))) found.push(`fs/${name}`);
3597
3307
  return found;
@@ -3604,21 +3314,22 @@ function setEntryEnabled(config, key, enabled) {
3604
3314
  };
3605
3315
  }
3606
3316
  function ensureFeishuTools(config) {
3607
- const alsoAllow = readAlsoAllow(config);
3608
- if (hasFeishuTool(alsoAllow)) return;
3609
- ensureRecord(config, "tools").alsoAllow = [...new Set([...alsoAllow, ...FEISHU_TOOLS])];
3317
+ const target = feishuGrantTarget(config);
3318
+ const current = readTopLevelToolsList(config, target);
3319
+ if (hasFeishuTool(current)) return;
3320
+ ensureRecord(config, "tools")[target] = [...new Set([...current, ...FEISHU_TOOLS])];
3610
3321
  }
3611
3322
  function cleanupLegacyResiduals(ctx) {
3612
3323
  const plugins = asRecord(ctx.config.plugins);
3613
3324
  if (plugins) {
3614
3325
  const entries = asRecord(plugins.entries);
3615
- if (entries && LEGACY_PLUGIN_NAME in entries) delete entries[LEGACY_PLUGIN_NAME];
3326
+ if (entries && "feishu-openclaw-plugin" in entries) delete entries[LEGACY_LARK_PLUGIN_NAME];
3616
3327
  const installs = asRecord(plugins.installs);
3617
- if (installs && LEGACY_PLUGIN_NAME in installs) delete installs[LEGACY_PLUGIN_NAME];
3328
+ if (installs && "feishu-openclaw-plugin" in installs) delete installs[LEGACY_LARK_PLUGIN_NAME];
3618
3329
  const allow = plugins.allow;
3619
3330
  if (Array.isArray(allow)) {
3620
- for (let i = allow.length - 1; i >= 0; i--) if (allow[i] === LEGACY_PLUGIN_NAME) allow.splice(i, 1);
3621
- if (!allow.includes(PLUGIN_NAME$2)) allow.push(PLUGIN_NAME$2);
3331
+ for (let i = allow.length - 1; i >= 0; i--) if (allow[i] === "feishu-openclaw-plugin") allow.splice(i, 1);
3332
+ if (!allow.includes("openclaw-lark")) allow.push(LARK_PLUGIN_NAME);
3622
3333
  }
3623
3334
  }
3624
3335
  const extDir = getExtensionsDir(ctx.configPath);
@@ -3633,13 +3344,6 @@ function cleanupLegacyResiduals(ctx) {
3633
3344
  }
3634
3345
  }
3635
3346
  }
3636
- function ensureRecord(obj, key) {
3637
- const cur = obj[key];
3638
- if (cur != null && typeof cur === "object" && !Array.isArray(cur)) return cur;
3639
- const fresh = {};
3640
- obj[key] = fresh;
3641
- return fresh;
3642
- }
3643
3347
  //#endregion
3644
3348
  //#region src/version-compat.ts
3645
3349
  const VERSION_COMPAT_MAP = Object.freeze([
@@ -3743,10 +3447,6 @@ function coerceCalVer(v) {
3743
3447
  function compareCalVer(a, b) {
3744
3448
  return semver.default.compare(coerceCalVer(a), coerceCalVer(b));
3745
3449
  }
3746
- /** Look up an entry by exact plugin version; undefined if not in the table. */
3747
- function findEntry(pluginVersion) {
3748
- return VERSION_COMPAT_MAP.find((e) => e.openclawLarkVersion === pluginVersion);
3749
- }
3750
3450
  /**
3751
3451
  * Infer the effective upper bound for a compat entry that has no explicit maxOpenclawVersion.
3752
3452
  *
@@ -3769,38 +3469,58 @@ function inferEffectiveMax(index) {
3769
3469
  };
3770
3470
  }
3771
3471
  /**
3772
- * Full version compatibility check that infers the effective maxOpenclawVersion for
3773
- * entries that only define minOpenclawVersion.
3472
+ * 对一个**已对标到 VERSION_COMPAT_MAP 键**的版本评估其与 openclaw 的兼容性,
3473
+ * 并在不兼容时给出方向。fork pin、legacy 分类等在 `checkPluginCompat` 处理,
3474
+ * 本函数只负责「版本 × openclaw 区间」这一段唯一逻辑。
3774
3475
  *
3775
- * For entries with an explicit maxOpenclawVersion the upper bound is INCLUSIVE (existing
3776
- * semantics). For entries without one the upper bound is EXCLUSIVE — it is derived from
3777
- * the minOpenclawVersion of the next entry group that requires a higher openclaw version.
3476
+ * - index === -1(插件版本比全表都旧,无 floor 条目)→ 无区间可判,视为插件过旧 'lark'
3477
+ * - oc < entry.minOpenclawVersion 'openclaw'
3478
+ * - oc 推断/显式上界 'lark'
3479
+ * - 否则兼容
3778
3480
  *
3779
- * Example: openclaw-lark@2026.4.10 (min=2026.4.27, no explicit max) gets an inferred
3780
- * exclusive max of '2026.5.6', so openclaw@2026.5.7 is correctly detected as incompatible
3781
- * whereas the old compatible() call would have returned true.
3481
+ * 上界语义:显式 maxOpenclawVersion INCLUSIVE;推断上界为 EXCLUSIVE。
3782
3482
  */
3783
- function effectiveCompatible(pluginVersion, openclawVersion) {
3483
+ function evaluateVersionRange(pluginVersion, openclawVersion) {
3784
3484
  const index = VERSION_COMPAT_MAP.findIndex((e) => compareCalVer(e.openclawLarkVersion, pluginVersion) <= 0);
3785
- if (index === -1) return false;
3485
+ if (index === -1) return {
3486
+ compatible: false,
3487
+ direction: "lark",
3488
+ entry: void 0
3489
+ };
3786
3490
  const entry = VERSION_COMPAT_MAP[index];
3787
3491
  const oc = coerceCalVer(openclawVersion);
3788
- if (semver.default.lt(oc, coerceCalVer(entry.minOpenclawVersion))) return false;
3492
+ if (semver.default.lt(oc, coerceCalVer(entry.minOpenclawVersion))) return {
3493
+ compatible: false,
3494
+ direction: "openclaw",
3495
+ entry
3496
+ };
3789
3497
  const maxInfo = inferEffectiveMax(index);
3790
- if (!maxInfo) return true;
3791
- const max = coerceCalVer(maxInfo.max);
3792
- return maxInfo.exclusive ? semver.default.lt(oc, max) : !semver.default.gt(oc, max);
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
+ };
3793
3511
  }
3794
- const FORK_LARK_PLUGIN_PINNED_VERSION = "2026.4.1";
3795
3512
  /**
3796
- * Resolve the version to evaluate against VERSION_COMPAT_MAP for a given plugin.
3797
- * The fork is pinned to FORK_LARK_PLUGIN_PINNED_VERSION regardless of its own
3798
- * version; everything else uses its real version.
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.
3799
3521
  */
3800
- function resolveCompatVersion(packageName, version) {
3801
- if (packageName === "@lark-apaas/openclaw-lark") return FORK_LARK_PLUGIN_PINNED_VERSION;
3802
- return version;
3803
- }
3522
+ /** fork 对标的官方 openclaw-lark 版本(与 VERSION_COMPAT_MAP 强耦合,故留在此处)。 */
3523
+ const FORK_LARK_PLUGIN_PINNED_VERSION = "2026.4.1";
3804
3524
  /**
3805
3525
  * Floor match: find the entry with the largest openclawLarkVersion that is
3806
3526
  * ≤ pluginVersion ("closest lower-or-equal version").
@@ -3812,263 +3532,702 @@ function resolveCompatVersion(packageName, version) {
3812
3532
  function findClosestEntry(pluginVersion) {
3813
3533
  return VERSION_COMPAT_MAP.find((e) => compareCalVer(e.openclawLarkVersion, pluginVersion) <= 0);
3814
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.
3851
+ *
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)
3855
+ *
3856
+ * Value interpretation:
3857
+ * - string → use directly
3858
+ * - object → secret is managed by a provider; use `feishuAppSecret` param instead
3859
+ *
3860
+ * Returns null when the secret cannot be determined.
3861
+ */
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;
3880
+ }
3881
+ /**
3882
+ * Resolve the agents.md path for the given appId from the openclaw config.
3883
+ *
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.
3894
+ */
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;
3954
+ }
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}`);
3966
+ }
3967
+ /**
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.
3971
+ */
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];
3986
+ }
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);
4060
+ return {
4061
+ ok: true,
4062
+ configInitExitCode: 0,
4063
+ agentsMdPath
4064
+ };
4065
+ }
3815
4066
  //#endregion
3816
- //#region src/rules/feishu-plugin-version-compat.ts
3817
- const PLUGIN_NAME$1 = "openclaw-lark";
3818
- const LEGACY_SHORT_NAMES = ["feishu-openclaw-plugin"];
3819
- const FORK_SCOPES = ["@lark-apaas"];
4067
+ //#region src/utils/feishu-tools-deny.ts
3820
4068
  /**
3821
- * fork 版兼容区间的下界,取自 VERSION_COMPAT_MAP openclawLarkVersion=2026.4.1
3822
- * minOpenclawVersion,避免与映射表脱钩写死。仅用于区分升级方向
3823
- * (oc 低于下界 → 升 openclaw;高于上界 升 lark);该条目被移除时回退到已知值。
4069
+ * openclaw-lark 中与 lark-cli 功能重叠的工具,写入 channels.feishu.tools.deny 后
4070
+ * openclaw-lark 插件自身的 shouldRegisterTool() 跳过注册,避免与 lark-cli 重复。
4071
+ * 支持尾部 `*` 通配(openclaw-lark matchesAnyPattern 识别)。
4072
+ *
4073
+ * 保留(IM / 鉴权 / 诊断类,与 lark-cli 无重叠):feishu_chat*、feishu_get_user、
4074
+ * feishu_search_user、feishu_im_*、feishu_oauth*、feishu_auth、feishu_diagnose、feishu_doctor。
4075
+ *
4076
+ * install-cli(安装 lark-cli 后即时写)与 agents_md_lark_cli_pe 规则(doctor --fix
4077
+ * 自愈)共用本常量与合并逻辑,单一来源。
3824
4078
  */
3825
- const FORK_LARK_PLUGIN_MIN_OC_VERSION = findEntry("2026.4.1")?.minOpenclawVersion ?? "2026.3.28";
3826
- let _ocVersion = void 0;
3827
- function getOcVersion() {
3828
- if (_ocVersion === void 0) _ocVersion = readOpenclawRuntimeVersion();
3829
- return _ocVersion;
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") : [];
3830
4098
  }
3831
- const _installedCache = /* @__PURE__ */ new Map();
3832
- function getInstalledPlugin(ctx) {
3833
- if (!_installedCache.has(ctx.configPath)) _installedCache.set(ctx.configPath, detectInstalledPlugin(ctx));
3834
- return _installedCache.get(ctx.configPath);
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));
3835
4103
  }
3836
4104
  /**
3837
- * 解析升级方向。返回 null 表示当前状态无需任何升级。
3838
- *
3839
- * - isVersionCompatible → null(兼容)
3840
- * - isRecommendedBelowEntryMin → null(推荐版本未到位,豁免)
3841
- * - ocCur < recommendedOc → 'openclaw'
3842
- * - ocCur ≥ recommendedOc → 'lark'
4105
+ * 确保 channels.feishu.tools.deny 含全部重叠工具(只增不删,幂等合并)。
4106
+ * 直接 mutate 传入的 config 对象;返回是否发生了变更。
3843
4107
  */
3844
- function resolveUpgradeDirection(installed, ocCur, recommendedOc, isLegacy) {
3845
- if (!isLegacy && isVersionCompatible(installed, ocCur)) return null;
3846
- if (!isLegacy && isRecommendedBelowEntryMin(installed, recommendedOc)) return null;
3847
- return compareCalVer(ocCur, recommendedOc) < 0 ? "openclaw" : "lark";
3848
- }
3849
- /** 提取公共前置上下文;任何前置条件不满足时返回 null(规则 pass)。 */
3850
- function resolveCompatContext(ctx) {
3851
- const recommendedOc = ctx.vars.recommendedOpenclawTag;
3852
- const ocCur = getOcVersion();
3853
- if (!ocCur) return null;
3854
- const installed = getInstalledPlugin(ctx);
3855
- if (installed == null) return null;
3856
- return {
3857
- ocCur,
3858
- recommendedOc,
3859
- installed,
3860
- isLegacy: isLegacyPlugin(installed)
3861
- };
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;
3862
4114
  }
3863
4115
  /**
3864
- * 检测是否需要升级 openclaw
3865
- * - fork 版(@lark-apaas/openclaw-lark)低于最低要求 → upgrade_openclaw
3866
- * - legacy / 官方版不兼容且 ocCur < recommendedOc → upgrade_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 是否发生了写盘
3867
4124
  */
3868
- let FeishuPluginOpenclawUpgradeRule = class FeishuPluginOpenclawUpgradeRule extends DiagnoseRule {
3869
- validate(ctx) {
3870
- const cc = resolveCompatContext(ctx);
3871
- if (!cc) return { pass: true };
3872
- const { ocCur, recommendedOc, installed, isLegacy } = cc;
3873
- if (isForkPlugin(installed)) return validateForkPlugin(installed, ocCur, recommendedOc);
3874
- if (!recommendedOc) return { pass: true };
3875
- if (resolveUpgradeDirection(installed, ocCur, recommendedOc, isLegacy) !== "openclaw") return { pass: true };
3876
- return {
3877
- pass: false,
3878
- action: "upgrade_openclaw",
3879
- message: `${buildCompatPrefix(installed, ocCur, isLegacy)};将 openclaw 升级到 ${recommendedOc},飞书插件会随之同步升级`
3880
- };
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;
3881
4129
  }
3882
- };
3883
- FeishuPluginOpenclawUpgradeRule = __decorate([Rule({
3884
- key: "feishu_plugin_version_compat_openclaw",
3885
- description: "检查飞书插件是否要求更高版本的 openclaw;是则提示升级 openclaw",
3886
- dependsOn: ["config_syntax_check"],
3887
- repairMode: "user-confirm",
3888
- level: "critical",
3889
- usesVars: ["recommendedOpenclawTag"]
3890
- })], FeishuPluginOpenclawUpgradeRule);
3891
- /**
3892
- * 检测是否需要升级飞书插件(仅在 Rule 1 pass 后执行):
3893
- * - fork 版已由 Rule 1 处理,这里直接 pass
3894
- * - legacy / 官方版不兼容且 ocCur ≥ recommendedOc → upgrade_lark
3895
- */
3896
- let FeishuPluginLarkUpgradeRule = class FeishuPluginLarkUpgradeRule extends DiagnoseRule {
3897
- validate(ctx) {
3898
- const cc = resolveCompatContext(ctx);
3899
- if (!cc) return { pass: true };
3900
- const { ocCur, recommendedOc, installed, isLegacy } = cc;
3901
- if (isForkPlugin(installed)) {
3902
- if (installed.fullName !== "@lark-apaas/openclaw-lark") return { pass: true };
3903
- if (resolveForkUpgradeDirection(ocCur) !== "lark") return { pass: true };
3904
- return {
3905
- pass: false,
3906
- action: "upgrade_lark",
3907
- message: `飞书插件 ${describePlugin(installed)}(fork 版,对标 openclaw-lark@${FORK_LARK_PLUGIN_PINNED_VERSION})与当前 openclaw@${ocCur} 不兼容;建议升级飞书插件至兼容版本`
3908
- };
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;
3909
4135
  }
3910
- if (!isLarkUpgradeNeededFromCC(cc)) return { pass: true };
3911
- const prefix = buildCompatPrefix(installed, ocCur, isLegacy);
3912
- if (!recommendedOc) return {
3913
- pass: false,
3914
- action: "upgrade_lark",
3915
- message: `${prefix};建议升级飞书插件至兼容版本`
3916
- };
3917
- return {
3918
- pass: false,
3919
- action: "upgrade_lark",
3920
- message: `${prefix};当前 openclaw@${ocCur} 已达推荐版本,可直接升级飞书插件`
3921
- };
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;
3922
4144
  }
3923
- };
3924
- FeishuPluginLarkUpgradeRule = __decorate([Rule({
3925
- key: "feishu_plugin_version_compat_lark",
3926
- description: "检查飞书插件版本是否落后于当前 openclaw;是则提示升级飞书插件",
3927
- dependsOn: ["config_syntax_check", "feishu_plugin_version_compat_openclaw"],
3928
- repairMode: "user-confirm",
3929
- level: "critical",
3930
- usesVars: ["recommendedOpenclawTag"]
3931
- })], FeishuPluginLarkUpgradeRule);
3932
- /**
3933
- * 核心判断:非 fork 插件是否需要升级 lark,基于当前 openclaw 版本的兼容性。
3934
- *
3935
- * 被 FeishuPluginLarkUpgradeRule.validate 和 needsLarkUpgrade 共用。
3936
- * 调用方需在调用前自行处理 fork 插件的情况(fork 插件不走本函数)。
3937
- *
3938
- * - 有 recommendedOc:走 resolveUpgradeDirection 判断方向是否为 'lark'
3939
- * - 无 recommendedOc(doctor 无推荐版本):legacy 插件直接需要升级;
3940
- * 非 legacy 则检查当前版本是否在兼容表内
3941
- */
3942
- function isLarkUpgradeNeededFromCC(cc) {
3943
- const { ocCur, recommendedOc, installed, isLegacy } = cc;
3944
- if (!recommendedOc) return isLegacy || !isVersionCompatible(installed, ocCur);
3945
- return resolveUpgradeDirection(installed, ocCur, recommendedOc, isLegacy) === "lark";
3946
- }
3947
- function isForkPlugin(p) {
3948
- return p.scope != null && FORK_SCOPES.includes(p.scope);
3949
- }
3950
- function isLegacyPlugin(p) {
3951
- return LEGACY_SHORT_NAMES.includes(p.allowName);
3952
- }
3953
- /**
3954
- * 检查已装插件版本与当前 openclaw 版本是否兼容。
3955
- * 使用 effectiveCompatible() 以正确推断无显式 maxOpenclawVersion 条目的上界。
3956
- */
3957
- function isVersionCompatible(p, ocCur) {
3958
- if (!p.version) return false;
3959
- return effectiveCompatible(p.version, ocCur);
3960
4145
  }
4146
+ //#endregion
4147
+ //#region src/utils/lark-cli-detect.ts
3961
4148
  /**
3962
- * 豁免判断:若推荐的 openclaw 版本本身低于最近条目要求的最低版本,
3963
- * 说明兼容表超前于推荐版本,不应由用户承担升级责任,视为通过。
4149
+ * 探测 lark-cli 是否可用(`lark-cli --version` 退出码为 0)。
4150
+ * 多条规则共用同一判定:feishu_plugin_state_normalize(是否补 feishu_* 授权)、
4151
+ * agents_md_lark_cli_pe(是否补 PE / deny)等。
3964
4152
  */
3965
- function isRecommendedBelowEntryMin(p, recommendedOc) {
3966
- if (!p.version) return false;
3967
- const entry = findClosestEntry(p.version);
3968
- if (!entry) return false;
3969
- return compareCalVer(recommendedOc, entry.minOpenclawVersion) < 0;
3970
- }
3971
- function buildCompatPrefix(installed, ocCur, isLegacy) {
3972
- const desc = describePlugin(installed);
3973
- if (isLegacy) return `检测到已弃用的旧包 ${desc}(包名 "${installed.allowName}" 已停止维护,需替换为 "openclaw-lark")`;
3974
- return `飞书插件 ${desc} 与当前 openclaw@${ocCur} 不兼容(${describeCompatConstraint(installed.version ? findClosestEntry(installed.version) : void 0, installed.version)})`;
3975
- }
3976
- function describeCompatConstraint(entry, pluginVersion) {
3977
- if (!entry) return `插件版本 ${pluginVersion ?? "未知"} 不在版本兼容表中`;
3978
- if (entry.maxOpenclawVersion) return `该插件版本要求 openclaw ∈ [${entry.minOpenclawVersion}, ${entry.maxOpenclawVersion}]`;
3979
- return `该插件版本要求 openclaw ≥ ${entry.minOpenclawVersion}`;
4153
+ function isLarkCliAvailable$1() {
4154
+ try {
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;
4164
+ } catch {
4165
+ return false;
4166
+ }
3980
4167
  }
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
+ ];
3981
4176
  /**
3982
- * fork @lark-apaas/openclaw-lark 的升级方向:复用官方 effectiveCompatible,按对标版本
3983
- * FORK_LARK_PLUGIN_PINNED_VERSION (2026.4.1) 取完整兼容区间。
3984
- * null → 兼容
3985
- * 'openclaw' → oc 低于区间下界,需升级 openclaw
3986
- * 'lark' → oc 高于区间上界(插件相对当前 openclaw 过旧),需升级飞书插件
4177
+ * 任一飞书插件是否在 config 中**启用**(plugins.entries[name].enabled === true)。
4178
+ * deny 的意义在于「有飞书插件会注册 feishu_* 工具」,故按启用判定——不看是否落盘安装:
4179
+ * 启用的可能是官方 openclaw-lark、旧版 feishu-openclaw-plugin 或内置 feishu 中的任意一个。
3987
4180
  */
3988
- function resolveForkUpgradeDirection(ocCur) {
3989
- if (effectiveCompatible("2026.4.1", ocCur)) return null;
3990
- return compareCalVer(ocCur, FORK_LARK_PLUGIN_MIN_OC_VERSION) < 0 ? "openclaw" : "lark";
4181
+ function isAnyFeishuPluginEnabled(config) {
4182
+ const entries = getNestedMap(config, "plugins", "entries");
4183
+ return FEISHU_PLUGIN_NAMES.some((name) => asRecord(entries?.[name])?.enabled === true);
4184
+ }
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
+ });
3991
4190
  }
3992
4191
  /**
3993
- * @lark-apaas/openclaw-lark 复用官方 effectiveCompatible(按对标版本 2026.4.1 完整区间)。
3994
- * 本函数(Rule 1)只负责 openclaw 方向(oc 低于下界);oc 高于上界由 Rule 2 报 upgrade_lark。
3995
- * 其他 @lark-apaas scope 的 fork 插件继续无条件 pass。
3996
- * recommendedOc 可为 undefined(doctor 模式),此时不指定目标升级版本。
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 可用」,故合并为一条规则。
3997
4197
  */
3998
- function validateForkPlugin(installed, ocCur, recommendedOc) {
3999
- if (installed.fullName !== "@lark-apaas/openclaw-lark") return { pass: true };
4000
- if (resolveForkUpgradeDirection(ocCur) !== "openclaw") return { pass: true };
4001
- const recommendation = recommendedOc ? `;将 openclaw 升级到 ${recommendedOc} 即可满足` : `;请升级 openclaw 至 ${FORK_LARK_PLUGIN_MIN_OC_VERSION} 或更高版本`;
4002
- return {
4003
- pass: false,
4004
- action: "upgrade_openclaw",
4005
- message: `飞书插件 ${describePlugin(installed)}(fork 版)要求 openclaw ≥ ${FORK_LARK_PLUGIN_MIN_OC_VERSION},当前 openclaw@${ocCur} 低于此要求${recommendation}`
4006
- };
4007
- }
4008
- function describePlugin(p) {
4009
- return (p.fullName ?? p.allowName) + (p.version ? `@${p.version}` : "");
4010
- }
4011
- function readPluginPackageJson(filePath) {
4012
- try {
4013
- if (!node_fs.default.existsSync(filePath)) return null;
4014
- const raw = node_fs.default.readFileSync(filePath, "utf-8");
4015
- const parsed = JSON.parse(raw);
4016
- return {
4017
- name: typeof parsed.name === "string" ? parsed.name : void 0,
4018
- version: typeof parsed.version === "string" ? parsed.version : 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 内容,需要追加`
4019
4205
  };
4020
- } catch {
4021
- return null;
4022
- }
4023
- }
4024
- /** "已装" = plugins.allow 含名 AND extensions/<name>/package.json 真实存在。 */
4025
- function detectInstalledPlugin(ctx) {
4026
- const allowRaw = asRecord(ctx.config.plugins)?.allow;
4027
- const allow = Array.isArray(allowRaw) ? allowRaw.filter((e) => typeof e === "string") : [];
4028
- const extDir = getExtensionsDir(ctx.configPath);
4029
- const installs = getNestedMap(ctx.config, "plugins", "installs");
4030
- for (const name of [PLUGIN_NAME$1, ...LEGACY_SHORT_NAMES]) {
4031
- if (!allow.includes(name)) continue;
4032
- const pkgPath = node_path.default.join(extDir, name, "package.json");
4033
- if (!node_fs.default.existsSync(pkgPath)) continue;
4034
- const pkg = readPluginPackageJson(pkgPath) ?? {};
4035
- const installEntry = installs && asRecord(installs[name]);
4036
- const fullName = pkg.name ?? extractScopedNameFromSpec$1(installEntry?.spec);
4037
- return {
4038
- allowName: name,
4039
- fullName,
4040
- scope: fullName?.startsWith("@") ? fullName.split("/")[0] : void 0,
4041
- version: pkg.version ?? (typeof installEntry?.version === "string" ? installEntry.version : void 0)
4206
+ if (isAnyFeishuPluginEnabled(ctx.config) && larkCliToolDenyMissing(ctx.config).length > 0) return {
4207
+ pass: false,
4208
+ message: "channels.feishu.tools.deny 缺少与 lark-cli 重叠的飞书工具,需要补充"
4042
4209
  };
4210
+ return { pass: true };
4043
4211
  }
4044
- return null;
4045
- }
4046
- /** "@scope/name@1.2.3" / "name@1.2.3" / "@scope/name" / "name" → 去掉 @version 后缀 */
4047
- function extractScopedNameFromSpec$1(spec) {
4048
- if (typeof spec !== "string") return void 0;
4049
- const at = spec.indexOf("@", 1);
4050
- return at === -1 ? spec : spec.slice(0, at);
4051
- }
4052
- /**
4053
- * 判断已安装的飞书插件是否与当前 openclaw 版本不兼容(或为需要替换的 legacy 插件)。
4054
- * 被 upgrade-lark 前置检测门控(--check-only 和正式安装模式)调用。
4055
- */
4056
- function needsLarkUpgrade(ctx) {
4057
- const cc = resolveCompatContext({
4058
- ...ctx,
4059
- vars: {
4060
- ...ctx.vars,
4061
- recommendedOpenclawTag: void 0
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}`);
4062
4220
  }
4063
- });
4064
- if (!cc) return false;
4065
- const { ocCur, installed } = cc;
4066
- if (isForkPlugin(installed)) {
4067
- if (installed.fullName === "@lark-apaas/openclaw-lark") return !effectiveCompatible(FORK_LARK_PLUGIN_PINNED_VERSION, ocCur);
4068
- 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");
4069
4222
  }
4070
- return isLarkUpgradeNeededFromCC(cc);
4071
- }
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);
4072
4231
  //#endregion
4073
4232
  //#region src/rules/cleanup-install-backup-dirs.ts
4074
4233
  const DIR_PREFIX = ".openclaw-install-";
@@ -4120,12 +4279,10 @@ CleanupInstallBackupDirsRule = __decorate([Rule({
4120
4279
  })], CleanupInstallBackupDirsRule);
4121
4280
  //#endregion
4122
4281
  //#region src/rules/lark-cli-missing-for-installed-lark-plugin.ts
4123
- const PLUGIN_NAME = "openclaw-lark";
4124
- const FORK_PACKAGE_NAME = "@lark-apaas/openclaw-lark";
4282
+ /** 触发本规则自动安装 lark-cli 的特定 fork 版本(与对标的官方兼容版本无关)。 */
4125
4283
  const TARGET_VERSION = "2026.4.4";
4126
- const LARK_CLI_NAME$1 = "lark-cli";
4127
4284
  function readInstalledLarkPlugin(ctx) {
4128
- const pkgPath = node_path.default.join(getExtensionsDir(ctx.configPath), PLUGIN_NAME, "package.json");
4285
+ const pkgPath = node_path.default.join(getExtensionsDir(ctx.configPath), LARK_PLUGIN_NAME, "package.json");
4129
4286
  if (!node_fs.default.existsSync(pkgPath)) return null;
4130
4287
  let pkg = {};
4131
4288
  try {
@@ -4138,14 +4295,14 @@ function readInstalledLarkPlugin(ctx) {
4138
4295
  pkg = {};
4139
4296
  }
4140
4297
  const installs = getNestedMap(ctx.config, "plugins", "installs");
4141
- const installEntry = installs ? asRecord(installs[PLUGIN_NAME]) : void 0;
4298
+ const installEntry = installs ? asRecord(installs[LARK_PLUGIN_NAME]) : void 0;
4142
4299
  return {
4143
4300
  name: pkg.name ?? extractScopedNameFromSpec(installEntry?.spec),
4144
4301
  version: pkg.version ?? (typeof installEntry?.version === "string" ? installEntry.version : void 0)
4145
4302
  };
4146
4303
  }
4147
4304
  function isTargetForkPlugin(plugin) {
4148
- return plugin?.name === FORK_PACKAGE_NAME && plugin.version === TARGET_VERSION;
4305
+ return plugin?.name === "@lark-apaas/openclaw-lark" && plugin.version === TARGET_VERSION;
4149
4306
  }
4150
4307
  function extractScopedNameFromSpec(spec) {
4151
4308
  if (typeof spec !== "string") return void 0;
@@ -4154,7 +4311,7 @@ function extractScopedNameFromSpec(spec) {
4154
4311
  }
4155
4312
  function isLarkCliAvailable() {
4156
4313
  try {
4157
- return (0, node_child_process.spawnSync)(LARK_CLI_NAME$1, ["--version"], {
4314
+ return (0, node_child_process.spawnSync)(LARK_CLI_NAME, ["--version"], {
4158
4315
  encoding: "utf-8",
4159
4316
  timeout: 5e3,
4160
4317
  stdio: [
@@ -4196,7 +4353,7 @@ let LarkCliMissingForInstalledLarkPluginRule = class LarkCliMissingForInstalledL
4196
4353
  if (isLarkCliAvailable()) return { pass: true };
4197
4354
  return {
4198
4355
  pass: false,
4199
- message: `${FORK_PACKAGE_NAME}@${TARGET_VERSION} 已安装,但 lark-cli 不可用;将执行一次 lark-cli 安装`
4356
+ message: `${FORK_LARK_PLUGIN_FULL_NAME}@${TARGET_VERSION} 已安装,但 lark-cli 不可用;将执行一次 lark-cli 安装`
4200
4357
  };
4201
4358
  }
4202
4359
  repair(ctx) {
@@ -5210,7 +5367,7 @@ async function installOpenclaw(openclawTag, ossFileMap, opts = {}) {
5210
5367
  console.error(`[install-openclaw] done in ${Date.now() - t0}ms`);
5211
5368
  }
5212
5369
  //#endregion
5213
- //#region src/lark-tools-update.ts
5370
+ //#region src/utils/lark-tools-update.ts
5214
5371
  /**
5215
5372
  * 共享的飞书插件备份 / 恢复 / `openclaw-lark-tools update` 调用。
5216
5373
  *
@@ -5221,8 +5378,6 @@ async function installOpenclaw(openclawTag, ossFileMap, opts = {}) {
5221
5378
  * `console.error`,独立调用方不传则静默。
5222
5379
  */
5223
5380
  const NOOP_LOG = () => {};
5224
- /** 升级前需备份的 extensions/ 下的插件目录(含 legacy) */
5225
- const FEISHU_PLUGIN_DIRS = ["openclaw-lark", "feishu-openclaw-plugin"];
5226
5381
  /** 读取 package.json 的 version 字段,失败返回 null(仅用于日志) */
5227
5382
  function readPkgVersion(pkgPath) {
5228
5383
  try {
@@ -5233,13 +5388,20 @@ function readPkgVersion(pkgPath) {
5233
5388
  }
5234
5389
  }
5235
5390
  /**
5236
- * 备份 openclaw.json + FEISHU_PLUGIN_DIRS 下存在的插件目录到 backupDir。
5391
+ * 备份 openclaw.json + LARK_PLUGIN_DIR_NAMES 下存在的插件目录到 backupDir。
5237
5392
  * 只备份当前存在的文件;不存在的记日志后跳过(恢复时据此判断「升级前是否存在」)。
5238
5393
  */
5239
5394
  function backupFeishuPlugins(opts) {
5240
5395
  const { workspaceDir, configPath, backupDir } = opts;
5241
5396
  const log = opts.log ?? NOOP_LOG;
5242
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
+ }
5243
5405
  node_fs.default.mkdirSync(backupDir, { recursive: true });
5244
5406
  log(`backup dir: ${backupDir}`);
5245
5407
  if (node_fs.default.existsSync(configPath)) {
@@ -5249,7 +5411,7 @@ function backupFeishuPlugins(opts) {
5249
5411
  } else log(` skipped: openclaw.json (not found)`);
5250
5412
  node_fs.default.mkdirSync(node_path.default.join(backupDir, "extensions"), { recursive: true });
5251
5413
  const extSrc = node_path.default.join(workspaceDir, "extensions");
5252
- for (const pluginDir of FEISHU_PLUGIN_DIRS) {
5414
+ for (const pluginDir of LARK_PLUGIN_DIR_NAMES) {
5253
5415
  const src = node_path.default.join(extSrc, pluginDir);
5254
5416
  if (node_fs.default.existsSync(src)) {
5255
5417
  const dst = node_path.default.join(backupDir, "extensions", pluginDir);
@@ -5281,7 +5443,7 @@ function restoreFeishuPlugins(opts) {
5281
5443
  log(` deleted: openclaw.json`);
5282
5444
  }
5283
5445
  const extDst = node_path.default.join(workspaceDir, "extensions");
5284
- for (const pluginDir of FEISHU_PLUGIN_DIRS) {
5446
+ for (const pluginDir of LARK_PLUGIN_DIR_NAMES) {
5285
5447
  const dst = node_path.default.join(extDst, pluginDir);
5286
5448
  if (node_fs.default.existsSync(dst)) {
5287
5449
  node_fs.default.rmSync(dst, {
@@ -5296,7 +5458,7 @@ function restoreFeishuPlugins(opts) {
5296
5458
  node_fs.default.copyFileSync(configBackup, configPath);
5297
5459
  log(` restored: openclaw.json`);
5298
5460
  } else log(` skipped restore: openclaw.json (not in backup — was not present before upgrade)`);
5299
- for (const pluginDir of FEISHU_PLUGIN_DIRS) {
5461
+ for (const pluginDir of LARK_PLUGIN_DIR_NAMES) {
5300
5462
  const backupSrc = node_path.default.join(backupDir, "extensions", pluginDir);
5301
5463
  if (node_fs.default.existsSync(backupSrc)) {
5302
5464
  node_fs.default.cpSync(backupSrc, node_path.default.join(extDst, pluginDir), { recursive: true });
@@ -5338,10 +5500,11 @@ function runLarkToolsUpdate(opts) {
5338
5500
  return {
5339
5501
  exitCode,
5340
5502
  stdout,
5341
- stderr,
5342
- spawnError: r.error?.message
5503
+ stderr
5343
5504
  };
5344
5505
  }
5506
+ //#endregion
5507
+ //#region src/install-extension.ts
5345
5508
  async function installExtension(tag, ossFileMap, opts = {}) {
5346
5509
  const homeBase = resolveHomeBase(opts.homeBase);
5347
5510
  const hasAll = !!opts.all;
@@ -5364,18 +5527,18 @@ async function installExtension(tag, ossFileMap, opts = {}) {
5364
5527
  }
5365
5528
  console.error(`[install-extension] tag=${tag} targets=${targets.length}`);
5366
5529
  const t0 = Date.now();
5367
- const larkTarget = opts.skipConfigUpdate ? void 0 : targets.find((p) => p.name === LARK_PLUGIN_NAME$1);
5530
+ const larkTarget = opts.skipConfigUpdate ? void 0 : targets.find((p) => p.name === LARK_PLUGIN_NAME);
5368
5531
  if (larkTarget) {
5369
5532
  if (await installLarkPluginWithCompatCheck({
5370
5533
  tag,
5371
5534
  pkg: larkTarget,
5372
5535
  homeBase,
5373
- configPath: opts.configPath ?? node_path.default.join(homeBase, "workspace/agent/openclaw.json"),
5536
+ configPath: opts.configPath ?? node_path.default.join(homeBase, DEFAULT_CONFIG_REL),
5374
5537
  workspaceDir: node_path.default.join(homeBase, "workspace", "agent"),
5375
5538
  ossFileMap,
5376
5539
  downloadOpts: opts
5377
5540
  })) {
5378
- targets = targets.filter((p) => p.name !== LARK_PLUGIN_NAME$1);
5541
+ targets = targets.filter((p) => p.name !== LARK_PLUGIN_NAME);
5379
5542
  if (targets.length === 0) {
5380
5543
  console.error(`[install-extension] done in ${Date.now() - t0}ms`);
5381
5544
  return;
@@ -5394,13 +5557,14 @@ async function installExtension(tag, ossFileMap, opts = {}) {
5394
5557
  installOne$1(pkg, tarball, homeBase);
5395
5558
  console.error(`[install-extension] ${pkg.name}: installed`);
5396
5559
  }
5397
- if (!opts.skipConfigUpdate) updatePluginInstalls(opts.configPath ?? node_path.default.join(homeBase, "workspace/agent/openclaw.json"), targets);
5398
- 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`);
5399
5565
  console.error(`[install-extension] done ${targets.length}/${targets.length} in ${Date.now() - t0}ms`);
5400
5566
  }
5401
- const LARK_PLUGIN_NAME$1 = "openclaw-lark";
5402
- const MEM0_PLUGIN_NAME = "openclaw-mem0-plugin";
5403
- const PLUGINS_TO_AUTO_ENABLE = ["openclaw-lark", "openclaw-extension-miaoda"];
5567
+ const PLUGINS_TO_AUTO_ENABLE = [LARK_PLUGIN_NAME, MIAODA_PLUGIN_NAME];
5404
5568
  /**
5405
5569
  * Merge each installed extension's installMetadata into openclaw.json's
5406
5570
  * plugins.installs[<pkg.name>]. Atomic write via tmp + rename.
@@ -5432,14 +5596,14 @@ function updatePluginInstalls(configPath, installedPkgs) {
5432
5596
  installs[pkg.name] = pkg.installMetadata;
5433
5597
  updated++;
5434
5598
  } else skipped++;
5435
- if (installedPkgs.some((p) => p.name === MEM0_PLUGIN_NAME)) {
5599
+ if (installedPkgs.some((p) => p.name === "openclaw-mem0-plugin")) {
5436
5600
  const allowRaw = plugins.allow;
5437
5601
  const allow = Array.isArray(allowRaw) ? allowRaw : [];
5438
- if (!allow.filter((e) => typeof e === "string").includes(MEM0_PLUGIN_NAME)) allow.push(MEM0_PLUGIN_NAME);
5602
+ if (!allow.filter((e) => typeof e === "string").includes("openclaw-mem0-plugin")) allow.push(MEM0_PLUGIN_NAME);
5439
5603
  plugins.allow = allow;
5440
5604
  if (!plugins.entries || typeof plugins.entries !== "object" || Array.isArray(plugins.entries)) plugins.entries = {};
5441
5605
  const entries = plugins.entries;
5442
- if (!(MEM0_PLUGIN_NAME in entries)) entries[MEM0_PLUGIN_NAME] = { enabled: false };
5606
+ if (!("openclaw-mem0-plugin" in entries)) entries[MEM0_PLUGIN_NAME] = { enabled: false };
5443
5607
  }
5444
5608
  for (const pkg of installedPkgs) {
5445
5609
  if (!PLUGINS_TO_AUTO_ENABLE.includes(pkg.name)) continue;
@@ -5506,9 +5670,9 @@ async function installLarkPluginWithCompatCheck(opts) {
5506
5670
  console.error("[install-extension] WARN: cannot read openclaw version — falling back to direct tarball install for openclaw-lark");
5507
5671
  return false;
5508
5672
  }
5509
- const compatVersion = resolveCompatVersion(pkg.packageName, pkg.version);
5510
- const compatible = compatVersion ? effectiveCompatible(compatVersion, ocCur) : true;
5511
- 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}`);
5512
5676
  if (compatible) return false;
5513
5677
  console.error(`[install-extension] openclaw-lark@${pkg.version} incompatible with openclaw@${ocCur} — using openclaw-lark-tools update`);
5514
5678
  const log = (msg) => console.error(`[install-extension] ${msg}`);
@@ -5549,6 +5713,7 @@ async function installLarkPluginWithCompatCheck(opts) {
5549
5713
  });
5550
5714
  throw new Error(`openclaw-lark post-install validation failed: ${validation.error}`);
5551
5715
  }
5716
+ ensureLarkCliToolDenyInFile(configPath, (msg) => console.error(`[install-extension] ${msg}`));
5552
5717
  console.error("[install-extension] openclaw-lark-tools update succeeded");
5553
5718
  return true;
5554
5719
  } finally {
@@ -5557,7 +5722,9 @@ async function installLarkPluginWithCompatCheck(opts) {
5557
5722
  recursive: true,
5558
5723
  force: true
5559
5724
  });
5560
- } catch {}
5725
+ } catch (e) {
5726
+ console.error(`[install-extension] WARN: failed to clean up backup dir ${backupDir}: ${e.message}`);
5727
+ }
5561
5728
  }
5562
5729
  }
5563
5730
  /**
@@ -5566,7 +5733,7 @@ async function installLarkPluginWithCompatCheck(opts) {
5566
5733
  */
5567
5734
  function validateLarkPluginInstall(opts) {
5568
5735
  const { homeBase, configPath } = opts;
5569
- const pkgJson = node_path.default.join(homeBase, "workspace", "agent", "extensions", LARK_PLUGIN_NAME$1, "package.json");
5736
+ const pkgJson = node_path.default.join(homeBase, "workspace", "agent", "extensions", LARK_PLUGIN_NAME, "package.json");
5570
5737
  if (!node_fs.default.existsSync(pkgJson)) return {
5571
5738
  ok: false,
5572
5739
  error: `plugin directory missing: ${pkgJson}`
@@ -5576,9 +5743,9 @@ function validateLarkPluginInstall(opts) {
5576
5743
  error: `openclaw.json not found at ${configPath}`
5577
5744
  };
5578
5745
  try {
5579
- if (asRecord(getNestedMap(loadJSON5().parse(node_fs.default.readFileSync(configPath, "utf-8")), "plugins", "entries")?.[LARK_PLUGIN_NAME$1])?.enabled !== true) return {
5746
+ if (asRecord(getNestedMap(loadJSON5().parse(node_fs.default.readFileSync(configPath, "utf-8")), "plugins", "entries")?.["openclaw-lark"])?.enabled !== true) return {
5580
5747
  ok: false,
5581
- error: `plugins.entries["${LARK_PLUGIN_NAME$1}"].enabled is not true`
5748
+ error: `plugins.entries["${LARK_PLUGIN_NAME}"].enabled is not true`
5582
5749
  };
5583
5750
  } catch (e) {
5584
5751
  return {
@@ -6868,34 +7035,6 @@ function reportError(params) {
6868
7035
  }
6869
7036
  //#endregion
6870
7037
  //#region src/install-cli.ts
6871
- const LARK_CLI_NAME = "lark-cli";
6872
- const AGENT_SKILLS_NAME = "agent-skills";
6873
- const WORKSPACE_AGENT_REL = "workspace/agent";
6874
- const LARK_PLUGIN_NAME = "openclaw-lark";
6875
- /**
6876
- * openclaw-lark tools that overlap with lark-cli functionality.
6877
- * Written to channels.feishu.tools.deny after lark-cli is installed so that
6878
- * openclaw-lark's shouldRegisterTool() skips them, avoiding duplicate tools.
6879
- * Supports trailing-* wildcards (handled by openclaw-lark's matchesAnyPattern).
6880
- *
6881
- * Kept: feishu_chat*, feishu_get_user, feishu_search_user, feishu_im_*,
6882
- * feishu_oauth*, feishu_auth, feishu_diagnose, feishu_doctor
6883
- */
6884
- const LARK_CLI_OVERLAP_TOOL_DENY = Object.freeze([
6885
- "feishu_create_doc",
6886
- "feishu_fetch_doc",
6887
- "feishu_update_doc",
6888
- "feishu_doc_comments",
6889
- "feishu_doc_media",
6890
- "feishu_drive_file",
6891
- "feishu_wiki_space",
6892
- "feishu_wiki_space_node",
6893
- "feishu_search_doc_wiki",
6894
- "feishu_bitable_*",
6895
- "feishu_calendar_*",
6896
- "feishu_task_*",
6897
- "feishu_sheet"
6898
- ]);
6899
7038
  async function installClis(tag, ossFileMap, opts) {
6900
7039
  const homeBase = resolveHomeBase(opts.homeBase);
6901
7040
  if (opts.names.length === 0) throw new Error("install-cli: must provide at least one --cli=<name>");
@@ -6922,7 +7061,15 @@ async function installClis(tag, ossFileMap, opts) {
6922
7061
  }
6923
7062
  console.error(`[install-cli] tag=${tag} targets=${targets.length}`);
6924
7063
  const t0 = Date.now();
7064
+ const larkCliAlreadyAvailable = targets.some((p) => p.name === "lark-cli") && isLarkCliAvailable$1();
6925
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
+ }
6926
7073
  const tb = await downloadWithCache(p, ossFileMap, opts);
6927
7074
  console.error(`[install-cli] ${p.name}: downloaded`);
6928
7075
  return {
@@ -6931,11 +7078,12 @@ async function installClis(tag, ossFileMap, opts) {
6931
7078
  };
6932
7079
  }));
6933
7080
  for (const { pkg, tarball } of tarballs) {
7081
+ if (tarball === null) continue;
6934
7082
  installOne(pkg, tarball, homeBase, opts.tmpRoot);
6935
7083
  linkBins(pkg, homeBase);
6936
7084
  console.error(`[install-cli] ${pkg.name}: installed`);
6937
7085
  }
6938
- if (targets.some((p) => p.name === LARK_CLI_NAME)) {
7086
+ if (targets.some((p) => p.name === "lark-cli")) {
6939
7087
  const skillsInstallPath = await installAgentSkills(manifest, ossFileMap, opts, homeBase, opts.tmpRoot);
6940
7088
  if (skillsInstallPath) linkAgentSkills(homeBase, skillsInstallPath);
6941
7089
  const appIds = collectFeishuAppIds();
@@ -6960,7 +7108,9 @@ async function installClis(tag, ossFileMap, opts) {
6960
7108
  }
6961
7109
  });
6962
7110
  }
6963
- 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`);
6964
7114
  }
6965
7115
  console.error(`[install-cli] done ${targets.length}/${targets.length} in ${Date.now() - t0}ms`);
6966
7116
  }
@@ -6997,7 +7147,7 @@ function linkBins(pkg, homeBase) {
6997
7147
  }
6998
7148
  }
6999
7149
  async function installAgentSkills(manifest, ossFileMap, downloadOpts, homeBase, tmpRoot) {
7000
- const pkg = manifest.packages.find((p) => p.role === "template" && p.name === AGENT_SKILLS_NAME);
7150
+ const pkg = manifest.packages.find((p) => p.role === "template" && p.name === "agent-skills");
7001
7151
  if (!pkg) {
7002
7152
  console.error(`[install-cli] installAgentSkills: ${AGENT_SKILLS_NAME} not found in manifest (tag may not bundle it) — skipping skill install`);
7003
7153
  return null;
@@ -7135,42 +7285,6 @@ function installOne(pkg, tarball, homeBase, tmpRoot) {
7135
7285
  } catch {}
7136
7286
  }
7137
7287
  }
7138
- /**
7139
- * Write overlapping tool names to channels.feishu.tools.deny in openclaw.json so that
7140
- * openclaw-lark's shouldRegisterTool() skips them when lark-cli is present.
7141
- *
7142
- * Only runs when openclaw-lark is installed (extensions/openclaw-lark/ exists).
7143
- * Idempotent: merges with any existing deny entries.
7144
- * Failures are non-fatal (logged as warnings).
7145
- */
7146
- function disableLarkCliOverlapTools(configPath, homeBase) {
7147
- const larkPluginDir = node_path.default.join(homeBase, WORKSPACE_AGENT_REL, "extensions", LARK_PLUGIN_NAME);
7148
- if (!node_fs.default.existsSync(larkPluginDir)) {
7149
- console.error(`[install-cli] disableLarkCliOverlapTools: ${LARK_PLUGIN_NAME} not installed — skipping`);
7150
- return;
7151
- }
7152
- if (!node_fs.default.existsSync(configPath)) {
7153
- console.error(`[install-cli] disableLarkCliOverlapTools: config not found at ${configPath} — skipping`);
7154
- return;
7155
- }
7156
- try {
7157
- const config = loadJSON5().parse(node_fs.default.readFileSync(configPath, "utf-8"));
7158
- if (!config.channels || typeof config.channels !== "object") config.channels = {};
7159
- const channels = config.channels;
7160
- if (!channels.feishu || typeof channels.feishu !== "object") channels.feishu = {};
7161
- const feishu = channels.feishu;
7162
- if (!feishu.tools || typeof feishu.tools !== "object") feishu.tools = {};
7163
- const tools = feishu.tools;
7164
- const existing = Array.isArray(tools.deny) ? tools.deny : [];
7165
- tools.deny = [...new Set([...existing, ...LARK_CLI_OVERLAP_TOOL_DENY])];
7166
- const tmp = configPath + ".lark-cli-deny-tmp";
7167
- node_fs.default.writeFileSync(tmp, JSON.stringify(config, null, 2), "utf-8");
7168
- moveSafe(tmp, configPath);
7169
- console.error(`[install-cli] disableLarkCliOverlapTools: channels.feishu.tools.deny updated (${LARK_CLI_OVERLAP_TOOL_DENY.length} patterns)`);
7170
- } catch (e) {
7171
- console.error(`[install-cli] WARN: disableLarkCliOverlapTools failed: ${e.message}`);
7172
- }
7173
- }
7174
7288
  //#endregion
7175
7289
  //#region src/download-resource.ts
7176
7290
  /**
@@ -11106,7 +11220,7 @@ async function reportCliRun(opts) {
11106
11220
  //#region src/help.ts
11107
11221
  const BIN = "mclaw-diagnose";
11108
11222
  function versionBanner() {
11109
- return `v0.1.18-alpha.2`;
11223
+ return `v0.1.18-alpha.4`;
11110
11224
  }
11111
11225
  const COMMANDS = [
11112
11226
  {
@@ -11805,8 +11919,8 @@ function snapshotVersions(cwd, log) {
11805
11919
  });
11806
11920
  const ocRaw = (ocResult.stdout ?? "").trim() || (ocResult.stderr ?? "").trim();
11807
11921
  const extDir = node_path.default.join(cwd, "extensions");
11808
- const larkPkg = node_path.default.join(extDir, "openclaw-lark", "package.json");
11809
- const feishuPkg = node_path.default.join(extDir, "feishu-openclaw-plugin", "package.json");
11922
+ const larkPkg = node_path.default.join(extDir, LARK_PLUGIN_NAME, "package.json");
11923
+ const feishuPkg = node_path.default.join(extDir, LEGACY_LARK_PLUGIN_NAME, "package.json");
11810
11924
  log(` version-check paths: ${larkPkg} [${node_fs.default.existsSync(larkPkg) ? "exists" : "missing"}]`);
11811
11925
  log(` version-check paths: ${feishuPkg} [${node_fs.default.existsSync(feishuPkg) ? "exists" : "missing"}]`);
11812
11926
  return {
@@ -12044,7 +12158,7 @@ function runUpgradeLark(opts) {
12044
12158
  });
12045
12159
  }
12046
12160
  log("");
12047
- log("── [1/6] 文件备份 ────────────────────────────────────────");
12161
+ log("── [1/7] 文件备份 ────────────────────────────────────────");
12048
12162
  log(`before-state: botCount=${countFeishuBots(configPath)}`);
12049
12163
  const t_backupStart = Date.now();
12050
12164
  const backup = backupFeishuPlugins(fsOpts);
@@ -12062,7 +12176,7 @@ function runUpgradeLark(opts) {
12062
12176
  log("backup: ok");
12063
12177
  logVersionSnapshot("before-versions", snapshotVersions(cwd, log), log);
12064
12178
  log("");
12065
- log("── [2/6] 清理本地 openclaw shim ─────────────────────────");
12179
+ log("── [2/7] 清理本地 openclaw shim ─────────────────────────");
12066
12180
  const localOpenclawBin = node_path.default.join(cwd, "node_modules", ".bin", "openclaw");
12067
12181
  if (node_fs.default.existsSync(localOpenclawBin)) try {
12068
12182
  node_fs.default.rmSync(localOpenclawBin);
@@ -12072,7 +12186,7 @@ function runUpgradeLark(opts) {
12072
12186
  }
12073
12187
  else log(` skipped: ${localOpenclawBin} (not found)`);
12074
12188
  log("");
12075
- log("── [3/6] npx install (@larksuite/openclaw-lark-tools update) ──");
12189
+ log("── [3/7] npx install (@larksuite/openclaw-lark-tools update) ──");
12076
12190
  const t_npxStart = Date.now();
12077
12191
  const { exitCode: npxExitCode, stdout: npxStdout, stderr: npxStderr } = runLarkToolsUpdate({
12078
12192
  cwd,
@@ -12103,7 +12217,7 @@ function runUpgradeLark(opts) {
12103
12217
  });
12104
12218
  };
12105
12219
  log("");
12106
- log("── [4/5] 安装后诊断校验 ─────────────────────────────────");
12220
+ log("── [4/7] 安装后诊断校验 ─────────────────────────────────");
12107
12221
  logVersionSnapshot("after-versions", snapshotVersions(cwd, log), log);
12108
12222
  let afterVersionIncompatible = false;
12109
12223
  try {
@@ -12132,7 +12246,7 @@ function runUpgradeLark(opts) {
12132
12246
  if (isNewDefaultOnly) log(" post-install diagnosis: ok (new default account — plugin installed, awaiting configuration)");
12133
12247
  else log(" post-install diagnosis: ok (upgrade conditions resolved)");
12134
12248
  log("");
12135
- log("── [6/6] doctor --fix ────────────────────────────────────");
12249
+ log("── [5/7] doctor --fix ────────────────────────────────────");
12136
12250
  const fixArgs = ["doctor", "--fix"];
12137
12251
  if (opts.scene) fixArgs.push(`--scene=${opts.scene}`);
12138
12252
  const t_doctorFixStart = Date.now();
@@ -12152,7 +12266,7 @@ function runUpgradeLark(opts) {
12152
12266
  if (fixResult.stderr?.trim()) log(`doctor(fix) stderr:\n${fixResult.stderr.trim()}`);
12153
12267
  log(`doctor(fix) exit: ${fixResult.status ?? "null"}${fixResult.error ? ` error: ${fixResult.error.message}` : ""}`);
12154
12268
  log("");
12155
- log("── [7/8] 重启 openclaw 服务 ──────────────────────────────");
12269
+ log("── [6/7] 重启 openclaw 服务 ──────────────────────────────");
12156
12270
  const restartScript = "/opt/force/bin/openclaw_scripts/restart.sh";
12157
12271
  let restartExecuted = false;
12158
12272
  if (opts.skipRestart) log(" skipped: --skip-restart");
@@ -12174,7 +12288,7 @@ function runUpgradeLark(opts) {
12174
12288
  restartExecuted = true;
12175
12289
  } else log(` skipped: ${restartScript} not found`);
12176
12290
  log("");
12177
- log("── [8/8] 端口存活检测 ────────────────────────────────────");
12291
+ log("── [7/7] 端口存活检测 ────────────────────────────────────");
12178
12292
  let portCheckOk;
12179
12293
  if (!restartExecuted) log(" skipped: restart was not executed");
12180
12294
  else {