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