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