@lark-apaas/openclaw-scripts-diagnose-cli 0.1.20 → 0.1.21-alpha.1

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 +532 -366
  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.20";
55
+ return "0.1.21-alpha.1";
56
56
  }
57
57
  //#endregion
58
58
  //#region src/rule-engine/base.ts
@@ -2639,9 +2639,11 @@ const LARK_PLUGIN_NAME = "openclaw-lark";
2639
2639
  * 版本号自成体系、不在 VERSION_COMPAT_MAP 内,按对标的官方版本判定兼容性。
2640
2640
  */
2641
2641
  const FORK_LARK_PLUGIN_FULL_NAME = "@lark-apaas/openclaw-lark";
2642
+ /** OpenClaw 从该版本起使用内置飞书插件作为目标态,不再安装外置 openclaw-lark。 */
2643
+ const BUILTIN_FEISHU_MIN_OPENCLAW_VERSION = "2026.6.6";
2642
2644
  /**
2643
- * 社区插件:openclaw **内置** 的 `feishu` 插件(非独立扩展)。
2644
- * 与官方/ fork openclaw-lark 互斥,规范化时应禁用以让位 openclaw-lark。
2645
+ * openclaw **内置** 的 `feishu` 插件(非独立扩展)。
2646
+ * OpenClaw 2026.6.6 及之后以该内置能力作为目标态;更早版本仍使用外置 openclaw-lark。
2645
2647
  */
2646
2648
  const BUILTIN_FEISHU_PLUGIN_NAME = "feishu";
2647
2649
  /**
@@ -2679,6 +2681,54 @@ const WORKSPACE_AGENT_REL = "workspace/agent";
2679
2681
  /** openclaw.json 默认相对路径。 */
2680
2682
  const DEFAULT_CONFIG_REL = `${WORKSPACE_AGENT_REL}/openclaw.json`;
2681
2683
  //#endregion
2684
+ //#region src/plugin-installs-store.ts
2685
+ function getExternalInstallsPath(configPath) {
2686
+ return node_path.default.join(node_path.default.dirname(configPath), "plugins", "installs.json");
2687
+ }
2688
+ function readExternalInstallRecords(filePath) {
2689
+ const raw = node_fs.default.readFileSync(filePath, "utf-8");
2690
+ const records = asRecord(asRecord(JSON.parse(raw))?.installRecords);
2691
+ if (!records) throw new Error(`plugins installs file must contain object installRecords: ${filePath}`);
2692
+ return records;
2693
+ }
2694
+ function readExternalPayload(filePath) {
2695
+ const raw = node_fs.default.readFileSync(filePath, "utf-8");
2696
+ const payload = JSON.parse(raw);
2697
+ if (!asRecord(payload)) throw new Error(`plugins installs file must be an object: ${filePath}`);
2698
+ return payload;
2699
+ }
2700
+ function cloneRecords(records) {
2701
+ return JSON.parse(JSON.stringify(records));
2702
+ }
2703
+ function getPluginInstalls(configPath, config) {
2704
+ const externalPath = getExternalInstallsPath(configPath);
2705
+ if (node_fs.default.existsSync(externalPath)) return {
2706
+ records: readExternalInstallRecords(externalPath),
2707
+ externalPath,
2708
+ hasExternalFile: true
2709
+ };
2710
+ return {
2711
+ records: asRecord(asRecord(config.plugins)?.installs) ?? {},
2712
+ externalPath,
2713
+ hasExternalFile: false
2714
+ };
2715
+ }
2716
+ function getPluginInstallsMap(configPath, config) {
2717
+ return getPluginInstalls(configPath, config).records;
2718
+ }
2719
+ function setPluginInstallsMap(configPath, config, records) {
2720
+ const externalPath = getExternalInstallsPath(configPath);
2721
+ const nextRecords = cloneRecords(records);
2722
+ const plugins = ensureRecord(config, "plugins");
2723
+ plugins.installs = cloneRecords(nextRecords);
2724
+ if (!node_fs.default.existsSync(externalPath)) return;
2725
+ const payload = readExternalPayload(externalPath);
2726
+ payload.installRecords = nextRecords;
2727
+ const tmpPath = `${externalPath}.tmp`;
2728
+ node_fs.default.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), "utf-8");
2729
+ node_fs.default.renameSync(tmpPath, externalPath);
2730
+ }
2731
+ //#endregion
2682
2732
  //#region src/rules/miaoda-official-plugins-install-spec-unlock.ts
2683
2733
  /**
2684
2734
  * Official miaoda-side plugins that must track manifest — version-locked specs
@@ -2695,9 +2745,7 @@ function unlockSpec(spec) {
2695
2745
  return spec.slice(0, cut);
2696
2746
  }
2697
2747
  /** 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;
2748
+ function* iterLockedOfficialInstalls(installs) {
2701
2749
  for (const [key, entry] of Object.entries(installs)) {
2702
2750
  if (!OFFICIAL_EXTENSION_PLUGIN_NAMES.has(key)) continue;
2703
2751
  const spec = asRecord(entry)?.spec;
@@ -2706,7 +2754,7 @@ function* iterLockedOfficialInstalls(config) {
2706
2754
  }
2707
2755
  let MiaodaOfficialPluginsInstallSpecUnlockRule = class MiaodaOfficialPluginsInstallSpecUnlockRule extends DiagnoseRule {
2708
2756
  validate(ctx) {
2709
- const locked = [...iterLockedOfficialInstalls(ctx.config)].map(([k]) => k);
2757
+ const locked = [...iterLockedOfficialInstalls(getPluginInstallsMap(ctx.configPath, ctx.config))].map(([k]) => k);
2710
2758
  if (locked.length === 0) return { pass: true };
2711
2759
  return {
2712
2760
  pass: false,
@@ -2714,12 +2762,16 @@ let MiaodaOfficialPluginsInstallSpecUnlockRule = class MiaodaOfficialPluginsInst
2714
2762
  };
2715
2763
  }
2716
2764
  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));
2765
+ const installs = { ...getPluginInstallsMap(ctx.configPath, ctx.config) };
2766
+ for (const [key, spec] of iterLockedOfficialInstalls(installs)) {
2767
+ const entry = asRecord(installs[key]);
2768
+ if (!entry) continue;
2769
+ installs[key] = {
2770
+ ...entry,
2771
+ spec: unlockSpec(spec)
2772
+ };
2773
+ }
2774
+ setPluginInstallsMap(ctx.configPath, ctx.config, installs);
2723
2775
  }
2724
2776
  };
2725
2777
  MiaodaOfficialPluginsInstallSpecUnlockRule = __decorate([Rule({
@@ -2771,9 +2823,282 @@ function getAllow$1(config) {
2771
2823
  return allow.filter((e) => typeof e === "string");
2772
2824
  }
2773
2825
  //#endregion
2826
+ //#region src/version-compat.ts
2827
+ const VERSION_COMPAT_MAP = Object.freeze([
2828
+ {
2829
+ openclawLarkVersion: "2026.5.20",
2830
+ minOpenclawVersion: "2026.5.7"
2831
+ },
2832
+ {
2833
+ openclawLarkVersion: "2026.5.13",
2834
+ minOpenclawVersion: "2026.5.7"
2835
+ },
2836
+ {
2837
+ openclawLarkVersion: "2026.5.12",
2838
+ minOpenclawVersion: "2026.5.7"
2839
+ },
2840
+ {
2841
+ openclawLarkVersion: "2026.5.7",
2842
+ minOpenclawVersion: "2026.5.6"
2843
+ },
2844
+ {
2845
+ openclawLarkVersion: "2026.4.10",
2846
+ minOpenclawVersion: "2026.4.27"
2847
+ },
2848
+ {
2849
+ openclawLarkVersion: "2026.4.9",
2850
+ minOpenclawVersion: "2026.3.28"
2851
+ },
2852
+ {
2853
+ openclawLarkVersion: "2026.4.8",
2854
+ minOpenclawVersion: "2026.3.28"
2855
+ },
2856
+ {
2857
+ openclawLarkVersion: "2026.4.7",
2858
+ minOpenclawVersion: "2026.3.28"
2859
+ },
2860
+ {
2861
+ openclawLarkVersion: "2026.4.1",
2862
+ minOpenclawVersion: "2026.3.28"
2863
+ },
2864
+ {
2865
+ openclawLarkVersion: "2026.3.31",
2866
+ minOpenclawVersion: "2026.3.28"
2867
+ },
2868
+ {
2869
+ openclawLarkVersion: "2026.3.30",
2870
+ minOpenclawVersion: "2026.3.28"
2871
+ },
2872
+ {
2873
+ openclawLarkVersion: "2026.3.29",
2874
+ minOpenclawVersion: "2026.3.28"
2875
+ },
2876
+ {
2877
+ openclawLarkVersion: "2026.3.26",
2878
+ minOpenclawVersion: "2026.3.22"
2879
+ },
2880
+ {
2881
+ openclawLarkVersion: "2026.3.25",
2882
+ minOpenclawVersion: "2026.3.22"
2883
+ },
2884
+ {
2885
+ openclawLarkVersion: "2026.3.24",
2886
+ minOpenclawVersion: "2026.3.22"
2887
+ },
2888
+ {
2889
+ openclawLarkVersion: "2026.3.18",
2890
+ minOpenclawVersion: "2026.2.26",
2891
+ maxOpenclawVersion: "2026.3.13"
2892
+ },
2893
+ {
2894
+ openclawLarkVersion: "2026.3.17",
2895
+ minOpenclawVersion: "2026.2.26",
2896
+ maxOpenclawVersion: "2026.3.13"
2897
+ },
2898
+ {
2899
+ openclawLarkVersion: "2026.3.15",
2900
+ minOpenclawVersion: "2026.2.26",
2901
+ maxOpenclawVersion: "2026.3.13"
2902
+ },
2903
+ {
2904
+ openclawLarkVersion: "2026.3.12",
2905
+ minOpenclawVersion: "2026.2.26",
2906
+ maxOpenclawVersion: "2026.3.13"
2907
+ },
2908
+ {
2909
+ openclawLarkVersion: "2026.3.10",
2910
+ minOpenclawVersion: "2026.2.26",
2911
+ maxOpenclawVersion: "2026.3.13"
2912
+ },
2913
+ {
2914
+ openclawLarkVersion: "2026.3.9",
2915
+ minOpenclawVersion: "2026.2.26",
2916
+ maxOpenclawVersion: "2026.3.13"
2917
+ }
2918
+ ]);
2919
+ /**
2920
+ * Coerce a CalVer string ("YYYY.M.D[suffix]") to a clean semver string.
2921
+ * semver.coerce strips non-numeric suffixes (e.g. "-rc.1") and fills in
2922
+ * missing components with 0. Returns "0.0.0" for unparseable input.
2923
+ */
2924
+ function coerceCalVer(v) {
2925
+ return semver.default.coerce(v)?.version ?? "0.0.0";
2926
+ }
2927
+ /**
2928
+ * Compare two CalVer strings ("YYYY.M.D") numerically.
2929
+ * Delegates to semver for correct numeric component ordering —
2930
+ * plain string comparison would give wrong results (e.g. '2026.3.18' < '2026.3.9').
2931
+ * Suffixes like "-rc.1" are stripped via coercion before comparison.
2932
+ */
2933
+ function compareCalVer(a, b) {
2934
+ return semver.default.compare(coerceCalVer(a), coerceCalVer(b));
2935
+ }
2936
+ /** Whether this OpenClaw version should use the built-in feishu plugin target state. */
2937
+ function supportsBuiltinFeishu(openclawVersion) {
2938
+ if (!openclawVersion) return false;
2939
+ return semver.default.gte(coerceCalVer(openclawVersion), coerceCalVer(BUILTIN_FEISHU_MIN_OPENCLAW_VERSION));
2940
+ }
2941
+ /**
2942
+ * 该条目的有效上界——开区间(EXCLUSIVE):openclaw ≥ 此值即不兼容。
2943
+ * undefined = 最高档,无上界。
2944
+ *
2945
+ * 上界一律为开区间,两种来源:
2946
+ * - 有显式 maxOpenclawVersion → 直接用它(它本身就是「首个不兼容的 openclaw 版本」)。
2947
+ * - 无显式 max → 向更高插件版本(表降序,index 更小)扫描,取首个「严格更高 min」的
2948
+ * minOpenclawVersion,即下一不兼容区间的起点。
2949
+ *
2950
+ * 只接受严格更高的 min(跳过相同 min 的同档;跳过更低 min 防止非单调表产生空区间
2951
+ * [min, max) 把本条目永久判为不兼容)。
2952
+ */
2953
+ function inferEffectiveMax(index) {
2954
+ const cur = VERSION_COMPAT_MAP[index];
2955
+ if (cur.maxOpenclawVersion != null) return cur.maxOpenclawVersion;
2956
+ for (let i = index - 1; i >= 0; i--) if (compareCalVer(VERSION_COMPAT_MAP[i].minOpenclawVersion, cur.minOpenclawVersion) > 0) return VERSION_COMPAT_MAP[i].minOpenclawVersion;
2957
+ }
2958
+ /**
2959
+ * 单一真相源:floor 匹配 + 有效上界,得到某插件版本的半开兼容区间 [min, max)(含命中条目)。
2960
+ * evaluateVersionRange / findClosestEntry / effectiveCompatRange 全部基于它,
2961
+ * 避免 floor 匹配与 inferEffectiveMax 在多处重复。max 一律为开区间上界(EXCLUSIVE);
2962
+ * 最高档 max 为 undefined(无上界)。undefined = 版本比全表都旧(无 floor 条目)。
2963
+ */
2964
+ function resolveRange(pluginVersion) {
2965
+ const index = VERSION_COMPAT_MAP.findIndex((e) => compareCalVer(e.openclawLarkVersion, pluginVersion) <= 0);
2966
+ if (index === -1) return void 0;
2967
+ const entry = VERSION_COMPAT_MAP[index];
2968
+ return {
2969
+ entry,
2970
+ min: entry.minOpenclawVersion,
2971
+ max: inferEffectiveMax(index)
2972
+ };
2973
+ }
2974
+ /**
2975
+ * 对一个**已对标到 VERSION_COMPAT_MAP 键**的版本评估其与 openclaw 的兼容性,
2976
+ * 并在不兼容时给出方向。fork 的 pin、legacy 分类等在 `checkPluginCompat` 处理,
2977
+ * 本函数只负责「版本 × openclaw 区间」这一段唯一逻辑。
2978
+ *
2979
+ * - index === -1(插件版本比全表都旧,无 floor 条目)→ 无区间可判,视为插件过旧 → 'lark'
2980
+ * - oc < entry.minOpenclawVersion → 'openclaw'
2981
+ * - oc ≥ 有效上界(开区间)→ 'lark'
2982
+ * - 否则兼容
2983
+ *
2984
+ * 区间统一为半开 [min, max):min 闭、max 开(显式与推断上界一律 EXCLUSIVE)。
2985
+ */
2986
+ function evaluateVersionRange(pluginVersion, openclawVersion) {
2987
+ const r = resolveRange(pluginVersion);
2988
+ if (!r) return {
2989
+ compatible: false,
2990
+ direction: "lark",
2991
+ entry: void 0
2992
+ };
2993
+ const oc = coerceCalVer(openclawVersion);
2994
+ if (semver.default.lt(oc, coerceCalVer(r.min))) return {
2995
+ compatible: false,
2996
+ direction: "openclaw",
2997
+ entry: r.entry
2998
+ };
2999
+ if (r.max !== void 0 && semver.default.gte(oc, coerceCalVer(r.max))) return {
3000
+ compatible: false,
3001
+ direction: "lark",
3002
+ entry: r.entry
3003
+ };
3004
+ return {
3005
+ compatible: true,
3006
+ direction: null,
3007
+ entry: r.entry
3008
+ };
3009
+ }
3010
+ /**
3011
+ * Fork plugin handling.
3012
+ *
3013
+ * `@lark-apaas/openclaw-lark` is an internal fork whose own version numbers
3014
+ * (e.g. 2026.4.3, 2026.4.4) are NOT keys in VERSION_COMPAT_MAP. Floor-matching
3015
+ * a fork version against the official-keyed map only works by coincidence of
3016
+ * numbering and breaks once the fork version drifts past an official entry.
3017
+ * Both the install-time compat check and the diagnose rule must therefore pin
3018
+ * the fork to its official-equivalent version before consulting the map.
3019
+ */
3020
+ /** fork 对标的官方 openclaw-lark 版本(与 VERSION_COMPAT_MAP 强耦合,故留在此处)。 */
3021
+ const FORK_LARK_PLUGIN_PINNED_VERSION = "2026.4.1";
3022
+ /**
3023
+ * 返回某插件版本(floor 匹配后)的有效兼容区间 [min, max),含 evaluateVersionRange 所用的
3024
+ * (显式或推断的)开区间上界——区别于仅暴露显式 maxOpenclawVersion 的 findClosestEntry。
3025
+ * undefined = 版本比全表都旧(无 floor 条目)。
3026
+ */
3027
+ function effectiveCompatRange(pluginVersion) {
3028
+ const r = resolveRange(pluginVersion);
3029
+ return r ? {
3030
+ min: r.min,
3031
+ max: r.max
3032
+ } : void 0;
3033
+ }
3034
+ /** "@scope/name" → "name";无 scope 原样返回。 */
3035
+ function extractBaseName(name) {
3036
+ if (!name) return void 0;
3037
+ return name.startsWith("@") ? name.split("/")[1] : name;
3038
+ }
3039
+ function reasonFromRange(r) {
3040
+ if (r.compatible) return "compatible";
3041
+ if (r.direction === "openclaw") return "oc-below-min";
3042
+ return r.entry ? "oc-above-max" : "version-not-in-map";
3043
+ }
3044
+ /**
3045
+ * 统一的飞书插件兼容判定入口——唯一的判定真相源。
3046
+ *
3047
+ * 入参只有三个:插件包名、插件版本、当前 openclaw 版本(**不涉及 recommendedOpenclawTag**,
3048
+ * 推荐版本的处理交由各业务点)。判定算法:
3049
+ *
3050
+ * 1. fork `@lark-apaas/openclaw-lark`:先 pin 到对标官方版本(FORK_LARK_PLUGIN_PINNED_VERSION)
3051
+ * 再按区间判定上下界。
3052
+ * 2. 其他 `@lark-apaas/*` fork:无条件豁免(兼容)。
3053
+ * 3. legacy `feishu-openclaw-plugin`(含带 scope 形式):一律不兼容,方向 = 换飞书插件。
3054
+ * 4. 官方版及其余包:用自身版本按 VERSION_COMPAT_MAP 区间判定;读不到版本号 → 不兼容(换插件)。
3055
+ *
3056
+ * 所有消费方(install-extension 预检、feishu_plugin_version_compat 两条规则、needsLarkUpgrade)
3057
+ * 都应消费本方法,不再各自实现兼容/方向逻辑,避免口径漂移。
3058
+ */
3059
+ function checkPluginCompat(packageName, pluginVersion, openclawVersion) {
3060
+ if (packageName === "@lark-apaas/openclaw-lark") {
3061
+ const resolvedVersion = FORK_LARK_PLUGIN_PINNED_VERSION;
3062
+ const r = evaluateVersionRange(resolvedVersion, openclawVersion);
3063
+ return {
3064
+ ...r,
3065
+ reason: reasonFromRange(r),
3066
+ resolvedVersion
3067
+ };
3068
+ }
3069
+ if ((packageName && packageName.startsWith("@") ? packageName.split("/")[0] : void 0) === "@lark-apaas") return {
3070
+ compatible: true,
3071
+ direction: null,
3072
+ reason: "fork-exempt",
3073
+ resolvedVersion: pluginVersion,
3074
+ entry: void 0
3075
+ };
3076
+ if (extractBaseName(packageName) === "feishu-openclaw-plugin") return {
3077
+ compatible: false,
3078
+ direction: "lark",
3079
+ reason: "legacy",
3080
+ resolvedVersion: pluginVersion,
3081
+ entry: void 0
3082
+ };
3083
+ if (!pluginVersion) return {
3084
+ compatible: false,
3085
+ direction: "lark",
3086
+ reason: "version-unknown",
3087
+ resolvedVersion: void 0,
3088
+ entry: void 0
3089
+ };
3090
+ const r = evaluateVersionRange(pluginVersion, openclawVersion);
3091
+ return {
3092
+ ...r,
3093
+ reason: reasonFromRange(r),
3094
+ resolvedVersion: pluginVersion
3095
+ };
3096
+ }
3097
+ //#endregion
2774
3098
  //#region src/rules/lark-plugin-allow.ts
2775
3099
  let LarkPluginAllowRule = class LarkPluginAllowRule extends DiagnoseRule {
2776
3100
  validate(ctx) {
3101
+ if (supportsBuiltinFeishu(readOpenclawRuntimeVersion())) return { pass: true };
2777
3102
  const allow = getAllow(ctx.config);
2778
3103
  if (LARK_PLUGIN_DIR_NAMES.some((name) => allow.includes(name))) return { pass: true };
2779
3104
  const installed = detectInstalledLarkPlugin$1(getExtensionsDir(ctx.configPath));
@@ -2837,11 +3162,11 @@ const OLD_PLUGIN_NAMES = Object.freeze([
2837
3162
  "feishu-greeting",
2838
3163
  "miaoda-keepalive"
2839
3164
  ]);
2840
- function getPluginMaps(config) {
3165
+ function getPluginMaps(configPath, config) {
2841
3166
  const rawAllow = asRecord(config.plugins)?.allow;
2842
3167
  return {
2843
3168
  entries: getNestedMap(config, "plugins", "entries"),
2844
- installs: getNestedMap(config, "plugins", "installs"),
3169
+ installs: getPluginInstallsMap(configPath, config),
2845
3170
  allow: Array.isArray(rawAllow) ? rawAllow : void 0
2846
3171
  };
2847
3172
  }
@@ -2853,7 +3178,7 @@ function findResiduals({ entries, installs, allow }, extensionsDir) {
2853
3178
  }
2854
3179
  let OldMiaodaPluginsCleanupRule = class OldMiaodaPluginsCleanupRule extends DiagnoseRule {
2855
3180
  validate(ctx) {
2856
- const maps = getPluginMaps(ctx.config);
3181
+ const maps = getPluginMaps(ctx.configPath, ctx.config);
2857
3182
  if (!hasNewMiaoda(maps)) return { pass: true };
2858
3183
  const residuals = findResiduals(maps, getExtensionsDir(ctx.configPath));
2859
3184
  if (residuals.length === 0) return { pass: true };
@@ -2863,7 +3188,7 @@ let OldMiaodaPluginsCleanupRule = class OldMiaodaPluginsCleanupRule extends Diag
2863
3188
  };
2864
3189
  }
2865
3190
  repair(ctx) {
2866
- const maps = getPluginMaps(ctx.config);
3191
+ const maps = getPluginMaps(ctx.configPath, ctx.config);
2867
3192
  if (!hasNewMiaoda(maps)) return;
2868
3193
  const extensionsDir = getExtensionsDir(ctx.configPath);
2869
3194
  const { entries, installs, allow } = maps;
@@ -2887,6 +3212,7 @@ let OldMiaodaPluginsCleanupRule = class OldMiaodaPluginsCleanupRule extends Diag
2887
3212
  console.error(`[old_miaoda_plugins_cleanup] rmSync ${target} failed: ${e.message}`);
2888
3213
  }
2889
3214
  }
3215
+ if (installs) setPluginInstallsMap(ctx.configPath, ctx.config, installs);
2890
3216
  }
2891
3217
  };
2892
3218
  OldMiaodaPluginsCleanupRule = __decorate([Rule({
@@ -2900,7 +3226,8 @@ OldMiaodaPluginsCleanupRule = __decorate([Rule({
2900
3226
  //#region src/rules/builtin-plugin-missing.ts
2901
3227
  function findMissingBuiltinPlugins(ctx) {
2902
3228
  const extDir = getExtensionsDir(ctx.configPath);
2903
- return [...OFFICIAL_EXTENSION_PLUGIN_NAMES].filter((name) => !isPluginInstalledOnDisk(extDir, name)).sort();
3229
+ const supportsBuiltin = supportsBuiltinFeishu(readOpenclawRuntimeVersion());
3230
+ return [...OFFICIAL_EXTENSION_PLUGIN_NAMES].filter((name) => !(supportsBuiltin && name === "openclaw-lark")).filter((name) => !isPluginInstalledOnDisk(extDir, name)).sort();
2904
3231
  }
2905
3232
  let BuiltinPluginMissingRule = class BuiltinPluginMissingRule extends DiagnoseRule {
2906
3233
  validate(ctx) {
@@ -2915,7 +3242,7 @@ let BuiltinPluginMissingRule = class BuiltinPluginMissingRule extends DiagnoseRu
2915
3242
  };
2916
3243
  BuiltinPluginMissingRule = __decorate([Rule({
2917
3244
  key: "builtin_plugin_missing",
2918
- description: "检查所有内置扩展插件(openclaw-lark、openclaw-extension-miaoda 等)是否已在磁盘安装;缺失时提示重新安装(实验性)",
3245
+ description: "检查当前 OpenClaw 版本需要的内置扩展插件是否已在磁盘安装;缺失时提示重新安装(实验性)",
2919
3246
  dependsOn: ["config_syntax_check"],
2920
3247
  repairMode: "check-only",
2921
3248
  level: "critical",
@@ -2929,8 +3256,7 @@ BuiltinPluginMissingRule = __decorate([Rule({
2929
3256
  */
2930
3257
  function findOrphanedInstalls(ctx) {
2931
3258
  const extDir = getExtensionsDir(ctx.configPath);
2932
- const installs = getNestedMap(ctx.config, "plugins", "installs");
2933
- if (!installs) return [];
3259
+ const installs = getPluginInstallsMap(ctx.configPath, ctx.config);
2934
3260
  return Object.keys(installs).filter((name) => !isPluginInstalledOnDisk(extDir, name)).sort();
2935
3261
  }
2936
3262
  let BuiltinPluginInstallsCleanupRule = class BuiltinPluginInstallsCleanupRule extends DiagnoseRule {
@@ -2946,11 +3272,10 @@ let BuiltinPluginInstallsCleanupRule = class BuiltinPluginInstallsCleanupRule ex
2946
3272
  const orphaned = findOrphanedInstalls(ctx);
2947
3273
  if (orphaned.length === 0) return;
2948
3274
  const orphanSet = new Set(orphaned);
2949
- const plugins = asRecord(ctx.config.plugins);
2950
- if (!plugins) return;
2951
- const installs = asRecord(plugins.installs);
2952
- if (installs) for (const name of orphaned) delete installs[name];
2953
- const rawAllow = plugins.allow;
3275
+ const installs = { ...getPluginInstallsMap(ctx.configPath, ctx.config) };
3276
+ for (const name of orphaned) delete installs[name];
3277
+ setPluginInstallsMap(ctx.configPath, ctx.config, installs);
3278
+ const rawAllow = asRecord(ctx.config.plugins)?.allow;
2954
3279
  if (Array.isArray(rawAllow)) for (let i = rawAllow.length - 1; i >= 0; i--) {
2955
3280
  const v = rawAllow[i];
2956
3281
  if (typeof v === "string" && orphanSet.has(v)) rawAllow.splice(i, 1);
@@ -3225,6 +3550,14 @@ const FEISHU_TOOLS = Object.freeze([
3225
3550
  */
3226
3551
  let FeishuPluginStateNormalizeRule = class FeishuPluginStateNormalizeRule extends DiagnoseRule {
3227
3552
  validate(ctx) {
3553
+ if (isBuiltinFeishuTarget()) {
3554
+ const residuals = findExternalLarkResiduals(ctx);
3555
+ if (residuals.length === 0) return { pass: true };
3556
+ return {
3557
+ pass: false,
3558
+ message: `外置飞书插件残留:${residuals.join(", ")}`
3559
+ };
3560
+ }
3228
3561
  if (!isPluginInstalled(ctx)) return { pass: true };
3229
3562
  const fails = [];
3230
3563
  if (!isNewPluginEnabled(ctx.config)) fails.push(`plugins.entries["${LARK_PLUGIN_NAME}"].enabled !== true(应启用)`);
@@ -3240,6 +3573,10 @@ let FeishuPluginStateNormalizeRule = class FeishuPluginStateNormalizeRule extend
3240
3573
  };
3241
3574
  }
3242
3575
  repair(ctx) {
3576
+ if (isBuiltinFeishuTarget()) {
3577
+ cleanupExternalLarkResiduals(ctx);
3578
+ return;
3579
+ }
3243
3580
  setEntryEnabled(ctx.config, LARK_PLUGIN_NAME, true);
3244
3581
  setEntryEnabled(ctx.config, BUILTIN_FEISHU_PLUGIN_NAME, false);
3245
3582
  ensureFeishuTools(ctx.config);
@@ -3253,6 +3590,9 @@ FeishuPluginStateNormalizeRule = __decorate([Rule({
3253
3590
  repairMode: "standard",
3254
3591
  level: "critical"
3255
3592
  })], FeishuPluginStateNormalizeRule);
3593
+ function isBuiltinFeishuTarget() {
3594
+ return supportsBuiltinFeishu(readOpenclawRuntimeVersion());
3595
+ }
3256
3596
  function isPluginInstalled(ctx) {
3257
3597
  return node_fs.default.existsSync(node_path.default.join(getExtensionsDir(ctx.configPath), LARK_PLUGIN_NAME));
3258
3598
  }
@@ -3270,346 +3610,122 @@ function readTopLevelToolsList(config, key) {
3270
3610
  }
3271
3611
  function hasFeishuTool(list) {
3272
3612
  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;
3297
- }
3298
- function findLegacyResiduals(ctx) {
3299
- const found = [];
3300
- const plugins = asRecord(ctx.config.plugins);
3301
- if (asRecord(plugins?.entries)?.["feishu-openclaw-plugin"] != null) found.push("entries[legacy]");
3302
- const allow = plugins?.allow;
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]");
3305
- const extDir = getExtensionsDir(ctx.configPath);
3306
- for (const name of LEGACY_DIRS_TO_REMOVE) if (node_fs.default.existsSync(node_path.default.join(extDir, name))) found.push(`fs/${name}`);
3307
- return found;
3308
- }
3309
- function setEntryEnabled(config, key, enabled) {
3310
- const entries = ensureRecord(ensureRecord(config, "plugins"), "entries");
3311
- entries[key] = {
3312
- ...asRecord(entries[key]) ?? {},
3313
- enabled
3314
- };
3315
- }
3316
- function ensureFeishuTools(config) {
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])];
3321
- }
3322
- function cleanupLegacyResiduals(ctx) {
3323
- const plugins = asRecord(ctx.config.plugins);
3324
- if (plugins) {
3325
- const entries = asRecord(plugins.entries);
3326
- if (entries && "feishu-openclaw-plugin" in entries) delete entries[LEGACY_LARK_PLUGIN_NAME];
3327
- const installs = asRecord(plugins.installs);
3328
- if (installs && "feishu-openclaw-plugin" in installs) delete installs[LEGACY_LARK_PLUGIN_NAME];
3329
- const allow = plugins.allow;
3330
- if (Array.isArray(allow)) {
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);
3333
- }
3334
- }
3335
- const extDir = getExtensionsDir(ctx.configPath);
3336
- for (const name of LEGACY_DIRS_TO_REMOVE) {
3337
- const target = node_path.default.join(extDir, name);
3338
- const rel = node_path.default.relative(extDir, target);
3339
- if (!rel || rel.startsWith("..") || node_path.default.isAbsolute(rel)) continue;
3340
- try {
3341
- rmrfTolerant(target);
3342
- } catch (e) {
3343
- console.error(`[feishu_plugin_state_normalize] rmrf ${target} failed: ${e.message}`);
3344
- }
3345
- }
3346
- }
3347
- //#endregion
3348
- //#region src/version-compat.ts
3349
- const VERSION_COMPAT_MAP = Object.freeze([
3350
- {
3351
- openclawLarkVersion: "2026.5.20",
3352
- minOpenclawVersion: "2026.5.7"
3353
- },
3354
- {
3355
- openclawLarkVersion: "2026.5.13",
3356
- minOpenclawVersion: "2026.5.7"
3357
- },
3358
- {
3359
- openclawLarkVersion: "2026.5.12",
3360
- minOpenclawVersion: "2026.5.7"
3361
- },
3362
- {
3363
- openclawLarkVersion: "2026.5.7",
3364
- minOpenclawVersion: "2026.5.6"
3365
- },
3366
- {
3367
- openclawLarkVersion: "2026.4.10",
3368
- minOpenclawVersion: "2026.4.27"
3369
- },
3370
- {
3371
- openclawLarkVersion: "2026.4.9",
3372
- minOpenclawVersion: "2026.3.28"
3373
- },
3374
- {
3375
- openclawLarkVersion: "2026.4.8",
3376
- minOpenclawVersion: "2026.3.28"
3377
- },
3378
- {
3379
- openclawLarkVersion: "2026.4.7",
3380
- minOpenclawVersion: "2026.3.28"
3381
- },
3382
- {
3383
- openclawLarkVersion: "2026.4.1",
3384
- minOpenclawVersion: "2026.3.28"
3385
- },
3386
- {
3387
- openclawLarkVersion: "2026.3.31",
3388
- minOpenclawVersion: "2026.3.28"
3389
- },
3390
- {
3391
- openclawLarkVersion: "2026.3.30",
3392
- minOpenclawVersion: "2026.3.28"
3393
- },
3394
- {
3395
- openclawLarkVersion: "2026.3.29",
3396
- minOpenclawVersion: "2026.3.28"
3397
- },
3398
- {
3399
- openclawLarkVersion: "2026.3.26",
3400
- minOpenclawVersion: "2026.3.22"
3401
- },
3402
- {
3403
- openclawLarkVersion: "2026.3.25",
3404
- minOpenclawVersion: "2026.3.22"
3405
- },
3406
- {
3407
- openclawLarkVersion: "2026.3.24",
3408
- minOpenclawVersion: "2026.3.22"
3409
- },
3410
- {
3411
- openclawLarkVersion: "2026.3.18",
3412
- minOpenclawVersion: "2026.2.26",
3413
- maxOpenclawVersion: "2026.3.13"
3414
- },
3415
- {
3416
- openclawLarkVersion: "2026.3.17",
3417
- minOpenclawVersion: "2026.2.26",
3418
- maxOpenclawVersion: "2026.3.13"
3419
- },
3420
- {
3421
- openclawLarkVersion: "2026.3.15",
3422
- minOpenclawVersion: "2026.2.26",
3423
- maxOpenclawVersion: "2026.3.13"
3424
- },
3425
- {
3426
- openclawLarkVersion: "2026.3.12",
3427
- minOpenclawVersion: "2026.2.26",
3428
- maxOpenclawVersion: "2026.3.13"
3429
- },
3430
- {
3431
- openclawLarkVersion: "2026.3.10",
3432
- minOpenclawVersion: "2026.2.26",
3433
- maxOpenclawVersion: "2026.3.13"
3434
- },
3435
- {
3436
- openclawLarkVersion: "2026.3.9",
3437
- minOpenclawVersion: "2026.2.26",
3438
- maxOpenclawVersion: "2026.3.13"
3439
- }
3440
- ]);
3441
- /**
3442
- * Coerce a CalVer string ("YYYY.M.D[suffix]") to a clean semver string.
3443
- * semver.coerce strips non-numeric suffixes (e.g. "-rc.1") and fills in
3444
- * missing components with 0. Returns "0.0.0" for unparseable input.
3445
- */
3446
- function coerceCalVer(v) {
3447
- return semver.default.coerce(v)?.version ?? "0.0.0";
3613
+ return list.some((t) => FEISHU_TOOLS.includes(t));
3448
3614
  }
3449
3615
  /**
3450
- * Compare two CalVer strings ("YYYY.M.D") numerically.
3451
- * Delegates to semver for correct numeric component ordering —
3452
- * plain string comparison would give wrong results (e.g. '2026.3.18' < '2026.3.9').
3453
- * Suffixes like "-rc.1" are stripped via coercion before comparison.
3616
+ * 决定 feishu_* 应补充到哪个授权键:
3617
+ * - 已有非空 tools.allow(限制式白名单)→ 'allow'(合并进 allow,避免与 alsoAllow 冲突)
3618
+ * - 否则 'alsoAllow'(追加式,叠加在 profile 基线上)
3619
+ * openclaw schema 禁止同一 scope 同时设 allow + alsoAllow,故有 allow 时必须并入 allow,
3620
+ * 否则会触发 tools_allow_also_allow_conflict 规则反复来回改写。
3454
3621
  */
3455
- function compareCalVer(a, b) {
3456
- return semver.default.compare(coerceCalVer(a), coerceCalVer(b));
3622
+ function feishuGrantTarget(config) {
3623
+ return readTopLevelToolsList(config, "allow").length > 0 ? "allow" : "alsoAllow";
3457
3624
  }
3458
3625
  /**
3459
- * 该条目的有效上界——开区间(EXCLUSIVE):openclaw 此值即不兼容。
3460
- * undefined = 最高档,无上界。
3461
- *
3462
- * 上界一律为开区间,两种来源:
3463
- * - 有显式 maxOpenclawVersion → 直接用它(它本身就是「首个不兼容的 openclaw 版本」)。
3464
- * - 无显式 max → 向更高插件版本(表降序,index 更小)扫描,取首个「严格更高 min」的
3465
- * minOpenclawVersion,即下一不兼容区间的起点。
3626
+ * 仅看顶层 tools——agent 级授权是用户对单 agent 的精细化配置,doctor 不动。
3627
+ * 返回缺失的目标键名('allow' / 'alsoAllow'),不缺时返回 null。
3466
3628
  *
3467
- * 只接受严格更高的 min(跳过相同 min 的同档;跳过更低 min 防止非单调表产生空区间
3468
- * [min, max) 把本条目永久判为不兼容)。
3629
+ * 不关心 lark-cli:tools.alsoAllow(授权层)与 channels.feishu.tools.deny(插件注册层)
3630
+ * 是两个正交的 config key,可并存——授权一个被 deny 的工具是无害空操作。因此本规则始终
3631
+ * 补全 FEISHU_TOOLS(让 openclaw-lark 实际注册的工具都可用),重叠工具的"不可用"由独立的
3632
+ * deny 规则在注册层处理,互不干扰。
3469
3633
  */
3470
- function inferEffectiveMax(index) {
3471
- const cur = VERSION_COMPAT_MAP[index];
3472
- if (cur.maxOpenclawVersion != null) return cur.maxOpenclawVersion;
3473
- for (let i = index - 1; i >= 0; i--) if (compareCalVer(VERSION_COMPAT_MAP[i].minOpenclawVersion, cur.minOpenclawVersion) > 0) return VERSION_COMPAT_MAP[i].minOpenclawVersion;
3634
+ function missingFeishuToolsGrant(config) {
3635
+ const target = feishuGrantTarget(config);
3636
+ return hasFeishuTool(readTopLevelToolsList(config, target)) ? null : target;
3474
3637
  }
3475
- /**
3476
- * 单一真相源:floor 匹配 + 有效上界,得到某插件版本的半开兼容区间 [min, max)(含命中条目)。
3477
- * evaluateVersionRange / findClosestEntry / effectiveCompatRange 全部基于它,
3478
- * 避免 floor 匹配与 inferEffectiveMax 在多处重复。max 一律为开区间上界(EXCLUSIVE);
3479
- * 最高档 max 为 undefined(无上界)。undefined = 版本比全表都旧(无 floor 条目)。
3480
- */
3481
- function resolveRange(pluginVersion) {
3482
- const index = VERSION_COMPAT_MAP.findIndex((e) => compareCalVer(e.openclawLarkVersion, pluginVersion) <= 0);
3483
- if (index === -1) return void 0;
3484
- const entry = VERSION_COMPAT_MAP[index];
3485
- return {
3486
- entry,
3487
- min: entry.minOpenclawVersion,
3488
- max: inferEffectiveMax(index)
3489
- };
3638
+ function findLegacyResiduals(ctx) {
3639
+ const found = [];
3640
+ const plugins = asRecord(ctx.config.plugins);
3641
+ if (asRecord(plugins?.entries)?.["feishu-openclaw-plugin"] != null) found.push("entries[legacy]");
3642
+ const allow = plugins?.allow;
3643
+ if (Array.isArray(allow) && allow.includes("feishu-openclaw-plugin")) found.push("allow[legacy]");
3644
+ if (asRecord(getPluginInstallsMap(ctx.configPath, ctx.config)["feishu-openclaw-plugin"]) != null) found.push("installs[legacy]");
3645
+ const extDir = getExtensionsDir(ctx.configPath);
3646
+ for (const name of LEGACY_DIRS_TO_REMOVE) if (node_fs.default.existsSync(node_path.default.join(extDir, name))) found.push(`fs/${name}`);
3647
+ return found;
3490
3648
  }
3491
- /**
3492
- * 对一个**已对标到 VERSION_COMPAT_MAP 键**的版本评估其与 openclaw 的兼容性,
3493
- * 并在不兼容时给出方向。fork pin、legacy 分类等在 `checkPluginCompat` 处理,
3494
- * 本函数只负责「版本 × openclaw 区间」这一段唯一逻辑。
3495
- *
3496
- * - index === -1(插件版本比全表都旧,无 floor 条目)→ 无区间可判,视为插件过旧 → 'lark'
3497
- * - oc < entry.minOpenclawVersion 'openclaw'
3498
- * - oc 有效上界(开区间)→ 'lark'
3499
- * - 否则兼容
3500
- *
3501
- * 区间统一为半开 [min, max):min 闭、max 开(显式与推断上界一律 EXCLUSIVE)。
3502
- */
3503
- function evaluateVersionRange(pluginVersion, openclawVersion) {
3504
- const r = resolveRange(pluginVersion);
3505
- if (!r) return {
3506
- compatible: false,
3507
- direction: "lark",
3508
- entry: void 0
3509
- };
3510
- const oc = coerceCalVer(openclawVersion);
3511
- if (semver.default.lt(oc, coerceCalVer(r.min))) return {
3512
- compatible: false,
3513
- direction: "openclaw",
3514
- entry: r.entry
3515
- };
3516
- if (r.max !== void 0 && semver.default.gte(oc, coerceCalVer(r.max))) return {
3517
- compatible: false,
3518
- direction: "lark",
3519
- entry: r.entry
3520
- };
3521
- return {
3522
- compatible: true,
3523
- direction: null,
3524
- entry: r.entry
3525
- };
3649
+ function findExternalLarkResiduals(ctx) {
3650
+ const found = [];
3651
+ const plugins = asRecord(ctx.config.plugins);
3652
+ const entries = asRecord(plugins?.entries);
3653
+ const installs = getPluginInstallsMap(ctx.configPath, ctx.config);
3654
+ const allow = plugins?.allow;
3655
+ for (const name of [LARK_PLUGIN_NAME, LEGACY_LARK_PLUGIN_NAME]) {
3656
+ if (entries?.[name] != null) found.push(`entries[${name}]`);
3657
+ if (Array.isArray(allow) && allow.includes(name)) found.push(`allow[${name}]`);
3658
+ if (asRecord(installs[name]) != null) found.push(`installs[${name}]`);
3659
+ if (node_fs.default.existsSync(node_path.default.join(getExtensionsDir(ctx.configPath), name))) found.push(`fs/${name}`);
3660
+ }
3661
+ return found;
3526
3662
  }
3527
- /**
3528
- * Fork plugin handling.
3529
- *
3530
- * `@lark-apaas/openclaw-lark` is an internal fork whose own version numbers
3531
- * (e.g. 2026.4.3, 2026.4.4) are NOT keys in VERSION_COMPAT_MAP. Floor-matching
3532
- * a fork version against the official-keyed map only works by coincidence of
3533
- * numbering and breaks once the fork version drifts past an official entry.
3534
- * Both the install-time compat check and the diagnose rule must therefore pin
3535
- * the fork to its official-equivalent version before consulting the map.
3536
- */
3537
- /** fork 对标的官方 openclaw-lark 版本(与 VERSION_COMPAT_MAP 强耦合,故留在此处)。 */
3538
- const FORK_LARK_PLUGIN_PINNED_VERSION = "2026.4.1";
3539
- /**
3540
- * 返回某插件版本(floor 匹配后)的有效兼容区间 [min, max),含 evaluateVersionRange 所用的
3541
- * (显式或推断的)开区间上界——区别于仅暴露显式 maxOpenclawVersion 的 findClosestEntry。
3542
- * undefined = 版本比全表都旧(无 floor 条目)。
3543
- */
3544
- function effectiveCompatRange(pluginVersion) {
3545
- const r = resolveRange(pluginVersion);
3546
- return r ? {
3547
- min: r.min,
3548
- max: r.max
3549
- } : void 0;
3663
+ function setEntryEnabled(config, key, enabled) {
3664
+ const entries = ensureRecord(ensureRecord(config, "plugins"), "entries");
3665
+ entries[key] = {
3666
+ ...asRecord(entries[key]) ?? {},
3667
+ enabled
3668
+ };
3550
3669
  }
3551
- /** "@scope/name" → "name";无 scope 原样返回。 */
3552
- function extractBaseName(name) {
3553
- if (!name) return void 0;
3554
- return name.startsWith("@") ? name.split("/")[1] : name;
3670
+ function ensureFeishuTools(config) {
3671
+ const target = feishuGrantTarget(config);
3672
+ const current = readTopLevelToolsList(config, target);
3673
+ if (hasFeishuTool(current)) return;
3674
+ ensureRecord(config, "tools")[target] = [...new Set([...current, ...FEISHU_TOOLS])];
3555
3675
  }
3556
- function reasonFromRange(r) {
3557
- if (r.compatible) return "compatible";
3558
- if (r.direction === "openclaw") return "oc-below-min";
3559
- return r.entry ? "oc-above-max" : "version-not-in-map";
3676
+ function cleanupLegacyResiduals(ctx) {
3677
+ const plugins = asRecord(ctx.config.plugins);
3678
+ if (plugins) {
3679
+ const entries = asRecord(plugins.entries);
3680
+ if (entries && "feishu-openclaw-plugin" in entries) delete entries[LEGACY_LARK_PLUGIN_NAME];
3681
+ const allow = plugins.allow;
3682
+ if (Array.isArray(allow)) {
3683
+ for (let i = allow.length - 1; i >= 0; i--) if (allow[i] === "feishu-openclaw-plugin") allow.splice(i, 1);
3684
+ if (!allow.includes("openclaw-lark")) allow.push(LARK_PLUGIN_NAME);
3685
+ }
3686
+ }
3687
+ const installs = { ...getPluginInstallsMap(ctx.configPath, ctx.config) };
3688
+ if ("feishu-openclaw-plugin" in installs) {
3689
+ delete installs[LEGACY_LARK_PLUGIN_NAME];
3690
+ setPluginInstallsMap(ctx.configPath, ctx.config, installs);
3691
+ }
3692
+ const extDir = getExtensionsDir(ctx.configPath);
3693
+ for (const name of LEGACY_DIRS_TO_REMOVE) {
3694
+ const target = node_path.default.join(extDir, name);
3695
+ const rel = node_path.default.relative(extDir, target);
3696
+ if (!rel || rel.startsWith("..") || node_path.default.isAbsolute(rel)) continue;
3697
+ try {
3698
+ rmrfTolerant(target);
3699
+ } catch (e) {
3700
+ console.error(`[feishu_plugin_state_normalize] rmrf ${target} failed: ${e.message}`);
3701
+ }
3702
+ }
3560
3703
  }
3561
- /**
3562
- * 统一的飞书插件兼容判定入口——唯一的判定真相源。
3563
- *
3564
- * 入参只有三个:插件包名、插件版本、当前 openclaw 版本(**不涉及 recommendedOpenclawTag**,
3565
- * 推荐版本的处理交由各业务点)。判定算法:
3566
- *
3567
- * 1. fork `@lark-apaas/openclaw-lark`:先 pin 到对标官方版本(FORK_LARK_PLUGIN_PINNED_VERSION)
3568
- * 再按区间判定上下界。
3569
- * 2. 其他 `@lark-apaas/*` fork:无条件豁免(兼容)。
3570
- * 3. legacy `feishu-openclaw-plugin`(含带 scope 形式):一律不兼容,方向 = 换飞书插件。
3571
- * 4. 官方版及其余包:用自身版本按 VERSION_COMPAT_MAP 区间判定;读不到版本号 → 不兼容(换插件)。
3572
- *
3573
- * 所有消费方(install-extension 预检、feishu_plugin_version_compat 两条规则、needsLarkUpgrade)
3574
- * 都应消费本方法,不再各自实现兼容/方向逻辑,避免口径漂移。
3575
- */
3576
- function checkPluginCompat(packageName, pluginVersion, openclawVersion) {
3577
- if (packageName === "@lark-apaas/openclaw-lark") {
3578
- const resolvedVersion = FORK_LARK_PLUGIN_PINNED_VERSION;
3579
- const r = evaluateVersionRange(resolvedVersion, openclawVersion);
3580
- return {
3581
- ...r,
3582
- reason: reasonFromRange(r),
3583
- resolvedVersion
3584
- };
3704
+ function cleanupExternalLarkResiduals(ctx) {
3705
+ const externalNames = new Set([LARK_PLUGIN_NAME, LEGACY_LARK_PLUGIN_NAME]);
3706
+ const plugins = asRecord(ctx.config.plugins);
3707
+ if (plugins) {
3708
+ const entries = asRecord(plugins.entries);
3709
+ if (entries) for (const name of externalNames) delete entries[name];
3710
+ const allow = plugins.allow;
3711
+ if (Array.isArray(allow)) {
3712
+ for (let i = allow.length - 1; i >= 0; i--) if (externalNames.has(String(allow[i]))) allow.splice(i, 1);
3713
+ }
3714
+ }
3715
+ const installs = { ...getPluginInstallsMap(ctx.configPath, ctx.config) };
3716
+ for (const name of externalNames) delete installs[name];
3717
+ setPluginInstallsMap(ctx.configPath, ctx.config, installs);
3718
+ const extDir = getExtensionsDir(ctx.configPath);
3719
+ for (const name of externalNames) {
3720
+ const target = node_path.default.join(extDir, name);
3721
+ const rel = node_path.default.relative(extDir, target);
3722
+ if (!rel || rel.startsWith("..") || node_path.default.isAbsolute(rel)) continue;
3723
+ try {
3724
+ rmrfTolerant(target);
3725
+ } catch (e) {
3726
+ console.error(`[feishu_plugin_state_normalize] rmrf ${target} failed: ${e.message}`);
3727
+ }
3585
3728
  }
3586
- if ((packageName && packageName.startsWith("@") ? packageName.split("/")[0] : void 0) === "@lark-apaas") return {
3587
- compatible: true,
3588
- direction: null,
3589
- reason: "fork-exempt",
3590
- resolvedVersion: pluginVersion,
3591
- entry: void 0
3592
- };
3593
- if (extractBaseName(packageName) === "feishu-openclaw-plugin") return {
3594
- compatible: false,
3595
- direction: "lark",
3596
- reason: "legacy",
3597
- resolvedVersion: pluginVersion,
3598
- entry: void 0
3599
- };
3600
- if (!pluginVersion) return {
3601
- compatible: false,
3602
- direction: "lark",
3603
- reason: "version-unknown",
3604
- resolvedVersion: void 0,
3605
- entry: void 0
3606
- };
3607
- const r = evaluateVersionRange(pluginVersion, openclawVersion);
3608
- return {
3609
- ...r,
3610
- reason: reasonFromRange(r),
3611
- resolvedVersion: pluginVersion
3612
- };
3613
3729
  }
3614
3730
  //#endregion
3615
3731
  //#region src/rules/feishu-plugin-version-compat.ts
@@ -3752,13 +3868,13 @@ function detectInstalledLarkPlugin(ctx) {
3752
3868
  const allowRaw = asRecord(ctx.config.plugins)?.allow;
3753
3869
  const allow = Array.isArray(allowRaw) ? allowRaw.filter((e) => typeof e === "string") : [];
3754
3870
  const extDir = getExtensionsDir(ctx.configPath);
3755
- const installs = getNestedMap(ctx.config, "plugins", "installs");
3871
+ const installs = getPluginInstallsMap(ctx.configPath, ctx.config);
3756
3872
  for (const name of [LARK_PLUGIN_NAME, ...LEGACY_SHORT_NAMES]) {
3757
3873
  if (!allow.includes(name)) continue;
3758
3874
  const pkgPath = node_path.default.join(extDir, name, "package.json");
3759
3875
  if (!node_fs.default.existsSync(pkgPath)) continue;
3760
3876
  const pkg = readPluginPackageJson(pkgPath) ?? {};
3761
- const installEntry = installs && asRecord(installs[name]);
3877
+ const installEntry = asRecord(installs[name]);
3762
3878
  return {
3763
3879
  allowName: name,
3764
3880
  fullName: pkg.name ?? extractScopedNameFromSpec$1(installEntry?.spec),
@@ -4324,8 +4440,7 @@ function readInstalledLarkPlugin(ctx) {
4324
4440
  } catch {
4325
4441
  pkg = {};
4326
4442
  }
4327
- const installs = getNestedMap(ctx.config, "plugins", "installs");
4328
- const installEntry = installs ? asRecord(installs[LARK_PLUGIN_NAME]) : void 0;
4443
+ const installEntry = asRecord(getPluginInstallsMap(ctx.configPath, ctx.config)[LARK_PLUGIN_NAME]);
4329
4444
  return {
4330
4445
  name: pkg.name ?? extractScopedNameFromSpec(installEntry?.spec),
4331
4446
  version: pkg.version ?? (typeof installEntry?.version === "string" ? installEntry.version : void 0)
@@ -5559,6 +5674,21 @@ async function installExtension(tag, ossFileMap, opts = {}) {
5559
5674
  }
5560
5675
  console.error(`[install-extension] tag=${tag} targets=${targets.length}`);
5561
5676
  const t0 = Date.now();
5677
+ const targetOpenclawVersion = resolveInstallTargetOpenclawVersion(tag);
5678
+ if (supportsBuiltinFeishu(targetOpenclawVersion) && targets.some((p) => p.name === "openclaw-lark")) {
5679
+ const configPath = opts.configPath ?? node_path.default.join(homeBase, DEFAULT_CONFIG_REL);
5680
+ console.error(`[install-extension] openclaw@${targetOpenclawVersion} uses built-in feishu — skipping external ${LARK_PLUGIN_NAME}`);
5681
+ cleanupExternalLarkForBuiltinFeishu({
5682
+ homeBase,
5683
+ configPath,
5684
+ updateConfig: !opts.skipConfigUpdate
5685
+ });
5686
+ targets = targets.filter((p) => p.name !== LARK_PLUGIN_NAME);
5687
+ if (targets.length === 0) {
5688
+ console.error(`[install-extension] done in ${Date.now() - t0}ms`);
5689
+ return;
5690
+ }
5691
+ }
5562
5692
  const larkTarget = opts.skipConfigUpdate ? void 0 : targets.find((p) => p.name === LARK_PLUGIN_NAME);
5563
5693
  if (larkTarget) {
5564
5694
  if (await installLarkPluginWithCompatCheck({
@@ -5599,12 +5729,13 @@ async function installExtension(tag, ossFileMap, opts = {}) {
5599
5729
  }
5600
5730
  const PLUGINS_TO_AUTO_ENABLE = [LARK_PLUGIN_NAME, MIAODA_PLUGIN_NAME];
5601
5731
  /**
5602
- * Merge each installed extension's installMetadata into openclaw.json's
5603
- * plugins.installs[<pkg.name>]. Atomic write via tmp + rename.
5732
+ * Merge each installed extension's installMetadata into the plugin installs
5733
+ * store. Old sandboxes only have openclaw.json.plugins.installs; newer
5734
+ * templates also carry plugins/installs.json and keep openclaw.json as a mirror.
5604
5735
  *
5605
5736
  * - No openclaw.json → log + return (not an error; some install contexts don't have it yet)
5606
5737
  * - Extension without installMetadata in manifest → skip that entry (log)
5607
- * - Existing plugins.installs entries for other extensions left untouched
5738
+ * - Existing install entries for other extensions left untouched
5608
5739
  *
5609
5740
  * Special-case for mem0: when openclaw-mem0-plugin is among the installed
5610
5741
  * targets, also append it to plugins.allow (idempotent) and seed
@@ -5613,7 +5744,7 @@ const PLUGINS_TO_AUTO_ENABLE = [LARK_PLUGIN_NAME, MIAODA_PLUGIN_NAME];
5613
5744
  */
5614
5745
  function updatePluginInstalls(configPath, installedPkgs) {
5615
5746
  if (!node_fs.default.existsSync(configPath)) {
5616
- console.error(`[install-extension] no config at ${configPath} — skip plugins.installs update`);
5747
+ console.error(`[install-extension] no config at ${configPath} — skip plugin installs update`);
5617
5748
  return;
5618
5749
  }
5619
5750
  const JSON5 = loadJSON5();
@@ -5621,8 +5752,7 @@ function updatePluginInstalls(configPath, installedPkgs) {
5621
5752
  const config = JSON5.parse(raw);
5622
5753
  if (!config.plugins || typeof config.plugins !== "object") config.plugins = {};
5623
5754
  const plugins = config.plugins;
5624
- if (!plugins.installs || typeof plugins.installs !== "object") plugins.installs = {};
5625
- const installs = plugins.installs;
5755
+ const installs = { ...getPluginInstallsMap(configPath, config) };
5626
5756
  let updated = 0;
5627
5757
  let skipped = 0;
5628
5758
  for (const pkg of installedPkgs) if (pkg.installMetadata) {
@@ -5650,10 +5780,11 @@ function updatePluginInstalls(configPath, installedPkgs) {
5650
5780
  enabled: true
5651
5781
  };
5652
5782
  }
5783
+ setPluginInstallsMap(configPath, config, installs);
5653
5784
  const tmpPath = configPath + ".installs-tmp";
5654
5785
  node_fs.default.writeFileSync(tmpPath, JSON.stringify(config, null, 2), "utf-8");
5655
5786
  moveSafe(tmpPath, configPath);
5656
- console.error(`[install-extension] plugins.installs updated: ${updated} entry(ies) in ${configPath}` + (skipped > 0 ? ` (${skipped} package(s) without installMetadata skipped)` : ""));
5787
+ console.error(`[install-extension] plugin installs updated: ${updated} entry(ies) for ${configPath}` + (skipped > 0 ? ` (${skipped} package(s) without installMetadata skipped)` : ""));
5657
5788
  }
5658
5789
  function installOne$1(pkg, tarball, homeBase) {
5659
5790
  const destDir = node_path.default.join(homeBase, pkg.installPath);
@@ -5685,6 +5816,41 @@ function installOne$1(pkg, tarball, homeBase) {
5685
5816
  force: true
5686
5817
  });
5687
5818
  }
5819
+ function resolveInstallTargetOpenclawVersion(tag) {
5820
+ return tag.match(/\d+\.\d+\.\d+/)?.[0] ?? readOpenclawRuntimeVersion();
5821
+ }
5822
+ function cleanupExternalLarkForBuiltinFeishu(opts) {
5823
+ const externalNames = new Set([LARK_PLUGIN_NAME, LEGACY_LARK_PLUGIN_NAME]);
5824
+ const extDir = node_path.default.join(opts.homeBase, "workspace", "agent", "extensions");
5825
+ for (const name of externalNames) {
5826
+ const target = node_path.default.join(extDir, name);
5827
+ try {
5828
+ node_fs.default.rmSync(target, {
5829
+ recursive: true,
5830
+ force: true
5831
+ });
5832
+ } catch (e) {
5833
+ console.error(`[install-extension] WARN: failed to remove ${target}: ${e.message}`);
5834
+ }
5835
+ }
5836
+ if (!opts.updateConfig || !node_fs.default.existsSync(opts.configPath)) return;
5837
+ const config = loadJSON5().parse(node_fs.default.readFileSync(opts.configPath, "utf-8"));
5838
+ const plugins = asRecord(config.plugins);
5839
+ if (plugins) {
5840
+ const allow = plugins.allow;
5841
+ if (Array.isArray(allow)) {
5842
+ for (let i = allow.length - 1; i >= 0; i--) if (externalNames.has(String(allow[i]))) allow.splice(i, 1);
5843
+ }
5844
+ const entries = asRecord(plugins.entries);
5845
+ if (entries) for (const name of externalNames) delete entries[name];
5846
+ }
5847
+ const installs = { ...getPluginInstallsMap(opts.configPath, config) };
5848
+ for (const name of externalNames) delete installs[name];
5849
+ setPluginInstallsMap(opts.configPath, config, installs);
5850
+ const tmpPath = opts.configPath + ".builtin-feishu-tmp";
5851
+ node_fs.default.writeFileSync(tmpPath, JSON.stringify(config, null, 2), "utf-8");
5852
+ moveSafe(tmpPath, opts.configPath);
5853
+ }
5688
5854
  /**
5689
5855
  * Install openclaw-lark with a pre-flight compatibility check.
5690
5856
  *
@@ -11253,7 +11419,7 @@ async function reportCliRun(opts) {
11253
11419
  //#region src/help.ts
11254
11420
  const BIN = "mclaw-diagnose";
11255
11421
  function versionBanner() {
11256
- return `v0.1.20`;
11422
+ return `v0.1.21-alpha.1`;
11257
11423
  }
11258
11424
  const COMMANDS = [
11259
11425
  {
@@ -11447,7 +11613,7 @@ OPTIONS
11447
11613
  DESCRIPTION
11448
11614
  Downloads + installs one or more openclaw extension tarballs
11449
11615
  (feishu, miaoda, etc.) into <home_base>/workspace/agent/extensions/,
11450
- then splices installMetadata into openclaw.json's plugins.installs
11616
+ then splices installMetadata into the plugin installs store
11451
11617
  unless --skip-config-update is passed.
11452
11618
 
11453
11619
  ARGUMENTS
@@ -11459,7 +11625,7 @@ OPTIONS
11459
11625
  --extension=<name> Install a specific extension (repeatable).
11460
11626
  --home_base=<dir> Override the /home/gem base (tests).
11461
11627
  --config_path=<p> Override the openclaw.json path (tests).
11462
- --skip-config-update Leave plugins.installs in openclaw.json untouched.
11628
+ --skip-config-update Leave plugin install records untouched.
11463
11629
  --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi.
11464
11630
  `
11465
11631
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/openclaw-scripts-diagnose-cli",
3
- "version": "0.1.20",
3
+ "version": "0.1.21-alpha.1",
4
4
  "description": "CLI for OpenClaw config diagnose and repair with JSON5 support",
5
5
  "main": "dist/index.cjs",
6
6
  "bin": {