@lark-apaas/openclaw-scripts-diagnose-cli 0.1.15-alpha.0 → 0.1.15-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +830 -635
- 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.1";
|
|
56
56
|
}
|
|
57
57
|
//#endregion
|
|
58
58
|
//#region src/rule-engine/base.ts
|
|
@@ -2520,6 +2520,341 @@ function upsertResourceConstrainedToolsBlock(content) {
|
|
|
2520
2520
|
return `${content}${content.length > 0 && !content.endsWith("\n") ? "\n\n" : "\n"}${RESOURCE_CONSTRAINED_TOOLS_BLOCK}\n`;
|
|
2521
2521
|
}
|
|
2522
2522
|
//#endregion
|
|
2523
|
+
//#region src/paths.ts
|
|
2524
|
+
/**
|
|
2525
|
+
* Central directory for all ephemeral diagnose/reset artifacts: task status
|
|
2526
|
+
* files (`reset-<taskId>.json`) and human-readable step logs
|
|
2527
|
+
* (`reset-<taskId>.log`). Having everything under one dir makes debugging a
|
|
2528
|
+
* stuck reset much easier — `ls /tmp/openclaw-diagnose/` shows every recent
|
|
2529
|
+
* run, and each run's log is right next to its state.
|
|
2530
|
+
*/
|
|
2531
|
+
const DIAGNOSE_DIR = "/tmp/openclaw-diagnose";
|
|
2532
|
+
function resetResultFile(taskId) {
|
|
2533
|
+
return `${DIAGNOSE_DIR}/reset-${taskId}.json`;
|
|
2534
|
+
}
|
|
2535
|
+
function resetLogFile(taskId) {
|
|
2536
|
+
return `${DIAGNOSE_DIR}/reset-${taskId}.log`;
|
|
2537
|
+
}
|
|
2538
|
+
/** Sandbox workspace root where openclaw config + agent state lives. */
|
|
2539
|
+
const WORKSPACE_DIR = "/home/gem/workspace/agent";
|
|
2540
|
+
/** File containing the provider key used by the openclaw miaoda provider. */
|
|
2541
|
+
const PROVIDER_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-provider-key";
|
|
2542
|
+
/** File containing the miaoda openclaw secrets JSON. */
|
|
2543
|
+
const SECRETS_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-openclaw-secrets.json";
|
|
2544
|
+
/** Absolute path to the openclaw config JSON. */
|
|
2545
|
+
const CONFIG_PATH = `${WORKSPACE_DIR}/openclaw.json`;
|
|
2546
|
+
/** upgrade-lark 每次运行的日志文件路径,含时间戳便于按时间排序定位。 */
|
|
2547
|
+
function upgradeLarkLogFile(runId) {
|
|
2548
|
+
return `${DIAGNOSE_DIR}/upgrade-lark-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/:/g, "-")}-${runId.slice(0, 8)}.log`;
|
|
2549
|
+
}
|
|
2550
|
+
//#endregion
|
|
2551
|
+
//#region src/lark-cli-init.ts
|
|
2552
|
+
const LARK_PLUGIN_NAMES$1 = ["openclaw-lark", "feishu-openclaw-plugin"];
|
|
2553
|
+
const PE_XML_TAG = "lark-cli-pe";
|
|
2554
|
+
const PE_PLACEHOLDER = `
|
|
2555
|
+
<${PE_XML_TAG}>
|
|
2556
|
+
**【强制要求 - 无例外】飞书所有操作必须先读 feishu-lark-cli skill。未读skill不得调用任何飞书工具,违者视为操作失误。**
|
|
2557
|
+
</${PE_XML_TAG}>
|
|
2558
|
+
`;
|
|
2559
|
+
function isLarkPluginInstalled(configPath) {
|
|
2560
|
+
const extDir = getExtensionsDir(configPath);
|
|
2561
|
+
return LARK_PLUGIN_NAMES$1.some((name) => {
|
|
2562
|
+
try {
|
|
2563
|
+
return node_fs.default.existsSync(node_path.default.join(extDir, name, "package.json"));
|
|
2564
|
+
} catch {
|
|
2565
|
+
return false;
|
|
2566
|
+
}
|
|
2567
|
+
});
|
|
2568
|
+
}
|
|
2569
|
+
function isLarkCliAvailable$2() {
|
|
2570
|
+
try {
|
|
2571
|
+
return (0, node_child_process.spawnSync)("lark-cli", ["--version"], {
|
|
2572
|
+
encoding: "utf-8",
|
|
2573
|
+
timeout: 5e3,
|
|
2574
|
+
stdio: [
|
|
2575
|
+
"ignore",
|
|
2576
|
+
"pipe",
|
|
2577
|
+
"ignore"
|
|
2578
|
+
]
|
|
2579
|
+
}).status === 0;
|
|
2580
|
+
} catch {
|
|
2581
|
+
return false;
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
function readConfig(configPath) {
|
|
2585
|
+
try {
|
|
2586
|
+
const raw = node_fs.default.readFileSync(configPath, "utf-8");
|
|
2587
|
+
const parsed = loadJSON5().parse(raw);
|
|
2588
|
+
return parsed != null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
2589
|
+
} catch {
|
|
2590
|
+
return null;
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
/**
|
|
2594
|
+
* Resolve the feishu app secret for the given appId.
|
|
2595
|
+
*
|
|
2596
|
+
* Lookup order:
|
|
2597
|
+
* 1. channels.feishu.appSecret (single-agent: feishu.appId === appId)
|
|
2598
|
+
* 2. channels.feishu.accounts[key].appSecret (multi-agent: account.appId === appId)
|
|
2599
|
+
*
|
|
2600
|
+
* Value interpretation:
|
|
2601
|
+
* - string → use directly
|
|
2602
|
+
* - object → secret is managed by a provider; use `feishuAppSecret` param instead
|
|
2603
|
+
*
|
|
2604
|
+
* Returns null when the secret cannot be determined.
|
|
2605
|
+
*/
|
|
2606
|
+
function resolveAppSecret(appId, config, feishuAppSecret) {
|
|
2607
|
+
const feishu = getNestedMap(config, "channels", "feishu");
|
|
2608
|
+
if (!feishu) return null;
|
|
2609
|
+
let rawSecret;
|
|
2610
|
+
if (typeof feishu.appId === "string" && feishu.appId === appId) rawSecret = feishu.appSecret;
|
|
2611
|
+
else {
|
|
2612
|
+
const accounts = asRecord(feishu.accounts);
|
|
2613
|
+
if (accounts) for (const [, val] of Object.entries(accounts)) {
|
|
2614
|
+
const account = asRecord(val);
|
|
2615
|
+
if (account?.appId === appId) {
|
|
2616
|
+
rawSecret = account.appSecret ?? feishu.appSecret;
|
|
2617
|
+
break;
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
if (typeof rawSecret === "string" && rawSecret) return rawSecret;
|
|
2622
|
+
if (rawSecret != null && typeof rawSecret === "object") return feishuAppSecret ?? null;
|
|
2623
|
+
return null;
|
|
2624
|
+
}
|
|
2625
|
+
/**
|
|
2626
|
+
* Resolve the agents.md path for the given appId from the openclaw config.
|
|
2627
|
+
*
|
|
2628
|
+
* Case 1: appId matches channels.feishu.appId (single-agent path)
|
|
2629
|
+
* → WORKSPACE_DIR/AGENTS.md
|
|
2630
|
+
*
|
|
2631
|
+
* Case 2: appId found in channels.feishu.accounts (multi-agent path)
|
|
2632
|
+
* → find account key where account.appId === appId
|
|
2633
|
+
* → find binding where match.channel=feishu && match.accountId=that key
|
|
2634
|
+
* → if agentId === 'main' → WORKSPACE_DIR/agents.md
|
|
2635
|
+
* → else find agent in agents.list by id → agent.workspace/agents.md
|
|
2636
|
+
*
|
|
2637
|
+
* Returns null when the path cannot be determined.
|
|
2638
|
+
*/
|
|
2639
|
+
function resolveAgentsMdPath(appId, config) {
|
|
2640
|
+
const feishu = getNestedMap(config, "channels", "feishu");
|
|
2641
|
+
if (!feishu) {
|
|
2642
|
+
console.error("resolveAgentsMdPath: channels.feishu not found");
|
|
2643
|
+
return null;
|
|
2644
|
+
}
|
|
2645
|
+
if (typeof feishu.appId === "string" && feishu.appId === appId) {
|
|
2646
|
+
console.error(`resolveAgentsMdPath: case=single-agent feishu.appId=${feishu.appId}`);
|
|
2647
|
+
return node_path.default.join(WORKSPACE_DIR, "workspace", "AGENTS.md");
|
|
2648
|
+
}
|
|
2649
|
+
const accounts = asRecord(feishu.accounts);
|
|
2650
|
+
if (!accounts) {
|
|
2651
|
+
console.error("resolveAgentsMdPath: feishu.accounts not found");
|
|
2652
|
+
return null;
|
|
2653
|
+
}
|
|
2654
|
+
let accountId;
|
|
2655
|
+
for (const [key, val] of Object.entries(accounts)) if (asRecord(val)?.appId === appId) {
|
|
2656
|
+
accountId = key;
|
|
2657
|
+
break;
|
|
2658
|
+
}
|
|
2659
|
+
if (!accountId) {
|
|
2660
|
+
console.error(`resolveAgentsMdPath: no account found with appId=${appId} in feishu.accounts keys=[${Object.keys(accounts).join(",")}]`);
|
|
2661
|
+
return null;
|
|
2662
|
+
}
|
|
2663
|
+
console.error(`resolveAgentsMdPath: found accountId=${accountId}`);
|
|
2664
|
+
const bindings = Array.isArray(config.bindings) ? config.bindings : [];
|
|
2665
|
+
let agentId;
|
|
2666
|
+
for (const b of bindings) {
|
|
2667
|
+
const binding = asRecord(b);
|
|
2668
|
+
if (!binding) continue;
|
|
2669
|
+
const match = asRecord(binding.match);
|
|
2670
|
+
if (match?.channel === "feishu" && match?.accountId === accountId) {
|
|
2671
|
+
if (typeof binding.agentId === "string") {
|
|
2672
|
+
agentId = binding.agentId;
|
|
2673
|
+
break;
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
if (!agentId) {
|
|
2678
|
+
console.error(`resolveAgentsMdPath: no binding found for accountId=${accountId} in bindings(count=${bindings.length})`);
|
|
2679
|
+
return null;
|
|
2680
|
+
}
|
|
2681
|
+
console.error(`resolveAgentsMdPath: found agentId=${agentId}`);
|
|
2682
|
+
if (agentId === "main") {
|
|
2683
|
+
console.error("resolveAgentsMdPath: case=multi-agent-main");
|
|
2684
|
+
return node_path.default.join(WORKSPACE_DIR, "workspace", "AGENTS.md");
|
|
2685
|
+
}
|
|
2686
|
+
const agentsObj = asRecord(config.agents);
|
|
2687
|
+
const list = Array.isArray(agentsObj?.list) ? agentsObj.list : [];
|
|
2688
|
+
for (const a of list) {
|
|
2689
|
+
const agent = asRecord(a);
|
|
2690
|
+
if (agent?.id === agentId) {
|
|
2691
|
+
const ws = typeof agent.workspace === "string" ? agent.workspace : node_path.default.join(WORKSPACE_DIR, "workspace");
|
|
2692
|
+
console.error(`resolveAgentsMdPath: case=multi-agent-custom agentId=${agentId} workspace=${ws}`);
|
|
2693
|
+
return node_path.default.join(ws, "AGENTS.md");
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
console.error(`resolveAgentsMdPath: agentId=${agentId} not found in agents.list(count=${list.length})`);
|
|
2697
|
+
return null;
|
|
2698
|
+
}
|
|
2699
|
+
function appendPeToAgentsMd(agentsMdPath) {
|
|
2700
|
+
const dir = node_path.default.dirname(agentsMdPath);
|
|
2701
|
+
if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
|
|
2702
|
+
const existing = node_fs.default.existsSync(agentsMdPath) ? node_fs.default.readFileSync(agentsMdPath, "utf-8") : "";
|
|
2703
|
+
if (existing.includes(`<lark-cli-pe>`)) {
|
|
2704
|
+
console.error(`lark-cli-init: <${PE_XML_TAG}> already present in ${agentsMdPath}, skipping`);
|
|
2705
|
+
return;
|
|
2706
|
+
}
|
|
2707
|
+
const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
2708
|
+
node_fs.default.appendFileSync(agentsMdPath, `${sep}${PE_PLACEHOLDER}`, "utf-8");
|
|
2709
|
+
console.error(`lark-cli-init: appended PE placeholder to ${agentsMdPath}`);
|
|
2710
|
+
}
|
|
2711
|
+
/**
|
|
2712
|
+
* Collect every Feishu bot appId declared in the openclaw config.
|
|
2713
|
+
* Covers both single-agent (channels.feishu.appId) and multi-agent
|
|
2714
|
+
* (channels.feishu.accounts[*].appId) layouts. Returns a deduplicated list.
|
|
2715
|
+
*/
|
|
2716
|
+
function collectFeishuAppIds(configPath) {
|
|
2717
|
+
const config = readConfig(configPath ?? CONFIG_PATH);
|
|
2718
|
+
if (!config) return [];
|
|
2719
|
+
const feishu = getNestedMap(config, "channels", "feishu");
|
|
2720
|
+
if (!feishu) return [];
|
|
2721
|
+
const appIds = /* @__PURE__ */ new Set();
|
|
2722
|
+
const topAppId = feishu.appId;
|
|
2723
|
+
if (typeof topAppId === "string" && topAppId.trim()) appIds.add(topAppId.trim());
|
|
2724
|
+
const accounts = asRecord(feishu.accounts);
|
|
2725
|
+
if (accounts) for (const val of Object.values(accounts)) {
|
|
2726
|
+
const appId = asRecord(val)?.appId;
|
|
2727
|
+
if (typeof appId === "string" && appId.trim()) appIds.add(appId.trim());
|
|
2728
|
+
}
|
|
2729
|
+
return [...appIds];
|
|
2730
|
+
}
|
|
2731
|
+
function runLarkCliInit(opts) {
|
|
2732
|
+
const configPath = opts.configPath ?? CONFIG_PATH;
|
|
2733
|
+
if (!isLarkPluginInstalled(configPath)) {
|
|
2734
|
+
console.error("lark-cli-init: skipping — openclaw-lark plugin not installed");
|
|
2735
|
+
return {
|
|
2736
|
+
ok: true,
|
|
2737
|
+
skipped: true,
|
|
2738
|
+
skipReason: "openclaw-lark plugin not installed"
|
|
2739
|
+
};
|
|
2740
|
+
}
|
|
2741
|
+
if (!isLarkCliAvailable$2()) {
|
|
2742
|
+
console.error("lark-cli-init: skipping — lark-cli command not found");
|
|
2743
|
+
return {
|
|
2744
|
+
ok: true,
|
|
2745
|
+
skipped: true,
|
|
2746
|
+
skipReason: "lark-cli command not found"
|
|
2747
|
+
};
|
|
2748
|
+
}
|
|
2749
|
+
const config = readConfig(configPath);
|
|
2750
|
+
if (!config) return {
|
|
2751
|
+
ok: false,
|
|
2752
|
+
error: `could not read config at ${configPath}`
|
|
2753
|
+
};
|
|
2754
|
+
const agentsMdPath = resolveAgentsMdPath(opts.appId, config);
|
|
2755
|
+
console.error(`lark-cli-init: resolved agents.md path=${agentsMdPath ?? "(null)"}`);
|
|
2756
|
+
if (!agentsMdPath) return {
|
|
2757
|
+
ok: false,
|
|
2758
|
+
error: `could not resolve agents.md path for appId=${opts.appId}`
|
|
2759
|
+
};
|
|
2760
|
+
const appSecret = resolveAppSecret(opts.appId, config, opts.feishuAppSecret);
|
|
2761
|
+
if (!appSecret) return {
|
|
2762
|
+
ok: false,
|
|
2763
|
+
error: `could not resolve appSecret for appId=${opts.appId}`
|
|
2764
|
+
};
|
|
2765
|
+
console.error(`lark-cli-init: running lark-cli config init --name ${opts.appId} --app-id ${opts.appId} --brand feishu --app-secret-stdin --force-init`);
|
|
2766
|
+
const initRes = (0, node_child_process.spawnSync)("lark-cli", [
|
|
2767
|
+
"config",
|
|
2768
|
+
"init",
|
|
2769
|
+
"--name",
|
|
2770
|
+
opts.appId,
|
|
2771
|
+
"--app-id",
|
|
2772
|
+
opts.appId,
|
|
2773
|
+
"--brand",
|
|
2774
|
+
"feishu",
|
|
2775
|
+
"--app-secret-stdin",
|
|
2776
|
+
"--force-init"
|
|
2777
|
+
], {
|
|
2778
|
+
stdio: [
|
|
2779
|
+
"pipe",
|
|
2780
|
+
"pipe",
|
|
2781
|
+
"pipe"
|
|
2782
|
+
],
|
|
2783
|
+
encoding: "utf-8",
|
|
2784
|
+
input: appSecret
|
|
2785
|
+
});
|
|
2786
|
+
const configInitStdout = initRes.stdout?.trim() || void 0;
|
|
2787
|
+
const configInitStderr = initRes.stderr?.trim() || void 0;
|
|
2788
|
+
if (configInitStdout) console.error(`lark-cli config init stdout: ${configInitStdout}`);
|
|
2789
|
+
if (configInitStderr) console.error(`lark-cli config init stderr: ${configInitStderr}`);
|
|
2790
|
+
if (initRes.error) return {
|
|
2791
|
+
ok: false,
|
|
2792
|
+
configInitStdout,
|
|
2793
|
+
configInitStderr,
|
|
2794
|
+
error: `lark-cli config init spawn error: ${initRes.error.message}`
|
|
2795
|
+
};
|
|
2796
|
+
if (initRes.status !== 0) return {
|
|
2797
|
+
ok: false,
|
|
2798
|
+
configInitExitCode: initRes.status ?? void 0,
|
|
2799
|
+
configInitStdout,
|
|
2800
|
+
configInitStderr,
|
|
2801
|
+
error: `lark-cli config init exited with code ${initRes.status}`
|
|
2802
|
+
};
|
|
2803
|
+
appendPeToAgentsMd(agentsMdPath);
|
|
2804
|
+
return {
|
|
2805
|
+
ok: true,
|
|
2806
|
+
configInitExitCode: 0,
|
|
2807
|
+
agentsMdPath
|
|
2808
|
+
};
|
|
2809
|
+
}
|
|
2810
|
+
//#endregion
|
|
2811
|
+
//#region src/rules/agents-md-lark-cli-pe.ts
|
|
2812
|
+
function isLarkCliAvailable$1() {
|
|
2813
|
+
try {
|
|
2814
|
+
return (0, node_child_process.spawnSync)("lark-cli", ["--version"], {
|
|
2815
|
+
encoding: "utf-8",
|
|
2816
|
+
timeout: 5e3,
|
|
2817
|
+
stdio: [
|
|
2818
|
+
"ignore",
|
|
2819
|
+
"pipe",
|
|
2820
|
+
"ignore"
|
|
2821
|
+
]
|
|
2822
|
+
}).status === 0;
|
|
2823
|
+
} catch {
|
|
2824
|
+
return false;
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
let AgentsMdLarkCliPeRule = class AgentsMdLarkCliPeRule extends DiagnoseRule {
|
|
2828
|
+
validate(ctx) {
|
|
2829
|
+
if (!isLarkCliAvailable$1()) return { pass: true };
|
|
2830
|
+
const missingPath = collectExistingAgentsMdPaths(ctx).find((filePath) => {
|
|
2831
|
+
return !node_fs.default.readFileSync(filePath, "utf-8").includes(`<${PE_XML_TAG}>`);
|
|
2832
|
+
});
|
|
2833
|
+
if (!missingPath) return { pass: true };
|
|
2834
|
+
return {
|
|
2835
|
+
pass: false,
|
|
2836
|
+
message: `${missingPath} 中缺少 lark-cli-pe PE 内容,需要追加`
|
|
2837
|
+
};
|
|
2838
|
+
}
|
|
2839
|
+
repair(ctx) {
|
|
2840
|
+
if (!isLarkCliAvailable$1()) return;
|
|
2841
|
+
for (const filePath of collectExistingAgentsMdPaths(ctx)) {
|
|
2842
|
+
const content = node_fs.default.readFileSync(filePath, "utf-8");
|
|
2843
|
+
if (content.includes(`<lark-cli-pe>`)) continue;
|
|
2844
|
+
const sep = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
2845
|
+
node_fs.default.appendFileSync(filePath, `${sep}${PE_PLACEHOLDER}`, "utf-8");
|
|
2846
|
+
console.error(`agents-md-lark-cli-pe: appended PE to ${filePath}`);
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
};
|
|
2850
|
+
AgentsMdLarkCliPeRule = __decorate([Rule({
|
|
2851
|
+
key: "agents_md_lark_cli_pe",
|
|
2852
|
+
description: "检测各智能体 AGENTS.md 中是否缺失 lark-cli-pe PE 内容,lark-cli 存在时自动追加",
|
|
2853
|
+
dependsOn: ["config_syntax_check"],
|
|
2854
|
+
repairMode: "standard",
|
|
2855
|
+
level: "silent"
|
|
2856
|
+
})], AgentsMdLarkCliPeRule);
|
|
2857
|
+
//#endregion
|
|
2523
2858
|
//#region src/rules/miaoda-official-plugins-install-spec-unlock.ts
|
|
2524
2859
|
/**
|
|
2525
2860
|
* Official miaoda-side plugins that must track manifest — version-locked specs
|
|
@@ -2623,11 +2958,11 @@ function getAllow$1(config) {
|
|
|
2623
2958
|
//#region src/rules/lark-plugin-allow.ts
|
|
2624
2959
|
const LARK_PLUGIN = "openclaw-lark";
|
|
2625
2960
|
const LEGACY_LARK_PLUGIN = "feishu-openclaw-plugin";
|
|
2626
|
-
const LARK_PLUGIN_NAMES
|
|
2961
|
+
const LARK_PLUGIN_NAMES = [LARK_PLUGIN, LEGACY_LARK_PLUGIN];
|
|
2627
2962
|
let LarkPluginAllowRule = class LarkPluginAllowRule extends DiagnoseRule {
|
|
2628
2963
|
validate(ctx) {
|
|
2629
2964
|
const allow = getAllow(ctx.config);
|
|
2630
|
-
if (LARK_PLUGIN_NAMES
|
|
2965
|
+
if (LARK_PLUGIN_NAMES.some((name) => allow.includes(name))) return { pass: true };
|
|
2631
2966
|
const installed = detectInstalledLarkPlugin(getExtensionsDir(ctx.configPath));
|
|
2632
2967
|
if (installed == null) return { pass: true };
|
|
2633
2968
|
return {
|
|
@@ -2646,7 +2981,7 @@ let LarkPluginAllowRule = class LarkPluginAllowRule extends DiagnoseRule {
|
|
|
2646
2981
|
const rawAllow = pluginsMap.allow;
|
|
2647
2982
|
const original = Array.isArray(rawAllow) ? rawAllow : [];
|
|
2648
2983
|
const stringAllow = original.filter((e) => typeof e === "string");
|
|
2649
|
-
if (LARK_PLUGIN_NAMES
|
|
2984
|
+
if (LARK_PLUGIN_NAMES.some((name) => stringAllow.includes(name))) return;
|
|
2650
2985
|
original.push(installed);
|
|
2651
2986
|
pluginsMap.allow = original;
|
|
2652
2987
|
}
|
|
@@ -3420,11 +3755,14 @@ FeishuPluginLarkUpgradeRule = __decorate([Rule({
|
|
|
3420
3755
|
usesVars: ["recommendedOpenclawTag"]
|
|
3421
3756
|
})], FeishuPluginLarkUpgradeRule);
|
|
3422
3757
|
/**
|
|
3423
|
-
*
|
|
3424
|
-
*
|
|
3758
|
+
* 核心判断:非 fork 插件是否需要升级 lark,基于当前 openclaw 版本的兼容性。
|
|
3759
|
+
*
|
|
3760
|
+
* 被 FeishuPluginLarkUpgradeRule.validate 和 needsLarkUpgrade 共用。
|
|
3761
|
+
* 调用方需在调用前自行处理 fork 插件的情况(fork 插件不走本函数)。
|
|
3425
3762
|
*
|
|
3426
|
-
*
|
|
3427
|
-
*
|
|
3763
|
+
* - 有 recommendedOc:走 resolveUpgradeDirection 判断方向是否为 'lark'
|
|
3764
|
+
* - 无 recommendedOc(doctor 无推荐版本):legacy 插件直接需要升级;
|
|
3765
|
+
* 非 legacy 则检查当前版本是否在兼容表内
|
|
3428
3766
|
*/
|
|
3429
3767
|
function isLarkUpgradeNeededFromCC(cc) {
|
|
3430
3768
|
const { ocCur, recommendedOc, installed, isLegacy } = cc;
|
|
@@ -3526,9 +3864,8 @@ function extractScopedNameFromSpec$1(spec) {
|
|
|
3526
3864
|
return at === -1 ? spec : spec.slice(0, at);
|
|
3527
3865
|
}
|
|
3528
3866
|
/**
|
|
3529
|
-
*
|
|
3530
|
-
*
|
|
3531
|
-
* Used by the upgrade_lark_needed rule and the upgrade-lark pre-check gate.
|
|
3867
|
+
* 判断已安装的飞书插件是否与当前 openclaw 版本不兼容(或为需要替换的 legacy 插件)。
|
|
3868
|
+
* 被 upgrade-lark 前置检测门控(--check-only 和正式安装模式)调用。
|
|
3532
3869
|
*/
|
|
3533
3870
|
function needsLarkUpgrade(ctx) {
|
|
3534
3871
|
const cc = resolveCompatContext({
|
|
@@ -3547,177 +3884,6 @@ function needsLarkUpgrade(ctx) {
|
|
|
3547
3884
|
return isLarkUpgradeNeededFromCC(cc);
|
|
3548
3885
|
}
|
|
3549
3886
|
//#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
3887
|
//#region src/rules/cleanup-install-backup-dirs.ts
|
|
3722
3888
|
const DIR_PREFIX = ".openclaw-install-";
|
|
3723
3889
|
function resolveExtensionsDir(configPath) {
|
|
@@ -3800,7 +3966,7 @@ function extractScopedNameFromSpec(spec) {
|
|
|
3800
3966
|
const at = spec.indexOf("@", 1);
|
|
3801
3967
|
return at === -1 ? spec : spec.slice(0, at);
|
|
3802
3968
|
}
|
|
3803
|
-
function isLarkCliAvailable
|
|
3969
|
+
function isLarkCliAvailable() {
|
|
3804
3970
|
try {
|
|
3805
3971
|
return (0, node_child_process.spawnSync)(LARK_CLI_NAME$1, ["--version"], {
|
|
3806
3972
|
encoding: "utf-8",
|
|
@@ -3841,7 +4007,7 @@ function installLarkCliOnce(tag) {
|
|
|
3841
4007
|
let LarkCliMissingForInstalledLarkPluginRule = class LarkCliMissingForInstalledLarkPluginRule extends DiagnoseRule {
|
|
3842
4008
|
validate(ctx) {
|
|
3843
4009
|
if (!isTargetForkPlugin(readInstalledLarkPlugin(ctx))) return { pass: true };
|
|
3844
|
-
if (isLarkCliAvailable
|
|
4010
|
+
if (isLarkCliAvailable()) return { pass: true };
|
|
3845
4011
|
return {
|
|
3846
4012
|
pass: false,
|
|
3847
4013
|
message: `${FORK_PACKAGE_NAME}@${TARGET_VERSION} 已安装,但 lark-cli 不可用;将执行一次 lark-cli 安装`
|
|
@@ -3849,7 +4015,7 @@ let LarkCliMissingForInstalledLarkPluginRule = class LarkCliMissingForInstalledL
|
|
|
3849
4015
|
}
|
|
3850
4016
|
repair(ctx) {
|
|
3851
4017
|
if (!isTargetForkPlugin(readInstalledLarkPlugin(ctx))) return;
|
|
3852
|
-
if (isLarkCliAvailable
|
|
4018
|
+
if (isLarkCliAvailable()) return;
|
|
3853
4019
|
installLarkCliOnce(ctx.vars.recommendedOpenclawTag ?? TARGET_VERSION);
|
|
3854
4020
|
}
|
|
3855
4021
|
};
|
|
@@ -3862,6 +4028,117 @@ LarkCliMissingForInstalledLarkPluginRule = __decorate([Rule({
|
|
|
3862
4028
|
usesVars: ["recommendedOpenclawTag"]
|
|
3863
4029
|
})], LarkCliMissingForInstalledLarkPluginRule);
|
|
3864
4030
|
//#endregion
|
|
4031
|
+
//#region src/rules/feishu-bot-channel-config.ts
|
|
4032
|
+
/**
|
|
4033
|
+
* Ensures each bot account's channel config is correct:
|
|
4034
|
+
* 1. `allowFrom` contains its own `creatorOpenID` from larkApps
|
|
4035
|
+
* 2. `appSecret` is either the canonical provider-ref or matches larkApps plaintext
|
|
4036
|
+
*
|
|
4037
|
+
* Covers both multi-account (channels.feishu.accounts) and single-account
|
|
4038
|
+
* (channels.feishu.appId + allowFrom at top level) layouts.
|
|
4039
|
+
*/
|
|
4040
|
+
let FeishuBotChannelConfigRule = class FeishuBotChannelConfigRule extends DiagnoseRule {
|
|
4041
|
+
validate(ctx) {
|
|
4042
|
+
const larkApps = ctx.vars.larkApps;
|
|
4043
|
+
if (!larkApps || larkApps.length === 0) return { pass: true };
|
|
4044
|
+
const feishu = asRecord(getNestedMap(ctx.config, "channels", "feishu"));
|
|
4045
|
+
if (!feishu) return { pass: true };
|
|
4046
|
+
const issues = [];
|
|
4047
|
+
const accounts = asRecord(feishu.accounts);
|
|
4048
|
+
if (accounts) for (const [accountId, account] of Object.entries(accounts)) {
|
|
4049
|
+
const bot = asRecord(account);
|
|
4050
|
+
if (!bot) continue;
|
|
4051
|
+
const appId = bot.appId;
|
|
4052
|
+
if (typeof appId !== "string" || !appId.startsWith("cli_")) continue;
|
|
4053
|
+
const larkApp = larkApps.find((e) => e.larkAppID === appId);
|
|
4054
|
+
if (!larkApp) continue;
|
|
4055
|
+
this.checkBot(accountId, bot, larkApp, issues);
|
|
4056
|
+
}
|
|
4057
|
+
const singleAppId = feishu.appId;
|
|
4058
|
+
if (typeof singleAppId === "string" && singleAppId.startsWith("cli_") && !accounts) {
|
|
4059
|
+
const larkApp = larkApps.find((e) => e.larkAppID === singleAppId);
|
|
4060
|
+
if (larkApp) this.checkBot("feishu", feishu, larkApp, issues);
|
|
4061
|
+
}
|
|
4062
|
+
if (issues.length === 0) return { pass: true };
|
|
4063
|
+
return {
|
|
4064
|
+
pass: false,
|
|
4065
|
+
message: issues.join("; ")
|
|
4066
|
+
};
|
|
4067
|
+
}
|
|
4068
|
+
/** Check a single bot entry (either an account object or the feishu channel itself).
|
|
4069
|
+
* appSecret is validated based on its current type:
|
|
4070
|
+
* - object → must match canonical provider-ref
|
|
4071
|
+
* - string → must match larkApps plaintext
|
|
4072
|
+
*/
|
|
4073
|
+
checkBot(label, bot, larkApp, issues) {
|
|
4074
|
+
const creatorOpenID = larkApp.creatorOpenID;
|
|
4075
|
+
const allowFrom = Array.isArray(bot.allowFrom) ? bot.allowFrom : [];
|
|
4076
|
+
if (typeof creatorOpenID === "string" && creatorOpenID !== "") {
|
|
4077
|
+
if (!allowFrom.includes(creatorOpenID)) issues.push(`${label} allowFrom missing creatorOpenID ${creatorOpenID.length > 8 ? creatorOpenID.slice(0, 4) + "***" + creatorOpenID.slice(-4) : "***"}`);
|
|
4078
|
+
} else if (allowFrom.length === 0) issues.push(`${label} allowFrom is empty (creatorOpenID unavailable, cannot auto-fix)`);
|
|
4079
|
+
const secret = bot.appSecret;
|
|
4080
|
+
if (typeof secret === "object" && secret !== null && !Array.isArray(secret)) {
|
|
4081
|
+
if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) issues.push(`${label} appSecret is a provider-ref but not the canonical one`);
|
|
4082
|
+
} else if (typeof secret === "string") {
|
|
4083
|
+
if (secret !== larkApp.appSecret) issues.push(`${label} appSecret plaintext mismatch`);
|
|
4084
|
+
} else issues.push(`${label} appSecret has unexpected type ${typeof secret}`);
|
|
4085
|
+
}
|
|
4086
|
+
repair(ctx) {
|
|
4087
|
+
const larkApps = ctx.vars.larkApps;
|
|
4088
|
+
if (!larkApps || larkApps.length === 0) return;
|
|
4089
|
+
const feishu = asRecord(getNestedMap(ctx.config, "channels", "feishu"));
|
|
4090
|
+
if (!feishu) return;
|
|
4091
|
+
const accounts = asRecord(feishu.accounts);
|
|
4092
|
+
if (accounts) for (const [, account] of Object.entries(accounts)) {
|
|
4093
|
+
const bot = asRecord(account);
|
|
4094
|
+
if (!bot) continue;
|
|
4095
|
+
const appId = bot.appId;
|
|
4096
|
+
if (typeof appId !== "string" || !appId.startsWith("cli_")) continue;
|
|
4097
|
+
const larkApp = larkApps.find((e) => e.larkAppID === appId);
|
|
4098
|
+
if (!larkApp) continue;
|
|
4099
|
+
this.fixBot(bot, larkApp);
|
|
4100
|
+
}
|
|
4101
|
+
const singleAppId = feishu.appId;
|
|
4102
|
+
if (typeof singleAppId === "string" && singleAppId.startsWith("cli_") && !accounts) {
|
|
4103
|
+
const larkApp = larkApps.find((e) => e.larkAppID === singleAppId);
|
|
4104
|
+
if (larkApp) this.fixBot(feishu, larkApp);
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
/** Fix a single bot entry in-place.
|
|
4108
|
+
* appSecret is repaired based on its current type:
|
|
4109
|
+
* - object → fix to canonical provider-ref
|
|
4110
|
+
* - string → fix to larkApps plaintext
|
|
4111
|
+
*/
|
|
4112
|
+
fixBot(bot, larkApp) {
|
|
4113
|
+
const creatorOpenID = larkApp.creatorOpenID;
|
|
4114
|
+
if (typeof creatorOpenID === "string" && creatorOpenID !== "") {
|
|
4115
|
+
const allowFrom = Array.isArray(bot.allowFrom) ? [...bot.allowFrom] : [];
|
|
4116
|
+
if (!allowFrom.includes(creatorOpenID)) {
|
|
4117
|
+
allowFrom.push(creatorOpenID);
|
|
4118
|
+
bot.allowFrom = allowFrom;
|
|
4119
|
+
}
|
|
4120
|
+
}
|
|
4121
|
+
const secret = bot.appSecret;
|
|
4122
|
+
if (typeof secret === "object" && secret !== null && !Array.isArray(secret)) {
|
|
4123
|
+
if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) bot.appSecret = { ...DEFAULT_FEISHU_APP_SECRET };
|
|
4124
|
+
} else if (typeof secret === "string") {
|
|
4125
|
+
if (secret !== larkApp.appSecret) bot.appSecret = larkApp.appSecret;
|
|
4126
|
+
}
|
|
4127
|
+
}
|
|
4128
|
+
};
|
|
4129
|
+
FeishuBotChannelConfigRule = __decorate([Rule({
|
|
4130
|
+
key: "feishu_bot_channel_config",
|
|
4131
|
+
description: "确保飞书配置中 bot 账号的 allowFrom 包含其创建者 openID 且 appSecret 值正确",
|
|
4132
|
+
dependsOn: [
|
|
4133
|
+
"config_syntax_check",
|
|
4134
|
+
"feishu_default_account",
|
|
4135
|
+
"feishu_bot_id"
|
|
4136
|
+
],
|
|
4137
|
+
repairMode: "standard",
|
|
4138
|
+
usesVars: ["larkApps"],
|
|
4139
|
+
level: "critical"
|
|
4140
|
+
})], FeishuBotChannelConfigRule);
|
|
4141
|
+
//#endregion
|
|
3865
4142
|
//#region src/check.ts
|
|
3866
4143
|
/** Telemetry-aware entry: returns both the legacy CheckResult (for stdout)
|
|
3867
4144
|
* AND a DoctorReport-shape payload (for `openclaw.report_cli_run`). The
|
|
@@ -4274,59 +4551,32 @@ function finalize$1(results, aborted) {
|
|
|
4274
4551
|
};
|
|
4275
4552
|
for (const r of results) switch (r.status) {
|
|
4276
4553
|
case "pass":
|
|
4277
|
-
summary.pass++;
|
|
4278
|
-
break;
|
|
4279
|
-
case "failed":
|
|
4280
|
-
summary.failed++;
|
|
4281
|
-
break;
|
|
4282
|
-
case "fixed":
|
|
4283
|
-
summary.fixed++;
|
|
4284
|
-
break;
|
|
4285
|
-
case "still-broken":
|
|
4286
|
-
summary.stillBroken++;
|
|
4287
|
-
break;
|
|
4288
|
-
case "skipped":
|
|
4289
|
-
summary.skipped++;
|
|
4290
|
-
break;
|
|
4291
|
-
case "error":
|
|
4292
|
-
summary.error++;
|
|
4293
|
-
break;
|
|
4294
|
-
case "unknown":
|
|
4295
|
-
summary.unknown++;
|
|
4296
|
-
break;
|
|
4297
|
-
}
|
|
4298
|
-
return {
|
|
4299
|
-
results,
|
|
4300
|
-
summary,
|
|
4301
|
-
aborted
|
|
4302
|
-
};
|
|
4303
|
-
}
|
|
4304
|
-
//#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`;
|
|
4554
|
+
summary.pass++;
|
|
4555
|
+
break;
|
|
4556
|
+
case "failed":
|
|
4557
|
+
summary.failed++;
|
|
4558
|
+
break;
|
|
4559
|
+
case "fixed":
|
|
4560
|
+
summary.fixed++;
|
|
4561
|
+
break;
|
|
4562
|
+
case "still-broken":
|
|
4563
|
+
summary.stillBroken++;
|
|
4564
|
+
break;
|
|
4565
|
+
case "skipped":
|
|
4566
|
+
summary.skipped++;
|
|
4567
|
+
break;
|
|
4568
|
+
case "error":
|
|
4569
|
+
summary.error++;
|
|
4570
|
+
break;
|
|
4571
|
+
case "unknown":
|
|
4572
|
+
summary.unknown++;
|
|
4573
|
+
break;
|
|
4574
|
+
}
|
|
4575
|
+
return {
|
|
4576
|
+
results,
|
|
4577
|
+
summary,
|
|
4578
|
+
aborted
|
|
4579
|
+
};
|
|
4330
4580
|
}
|
|
4331
4581
|
//#endregion
|
|
4332
4582
|
//#region src/run-log.ts
|
|
@@ -4464,9 +4714,10 @@ function makeLogger(logFile) {
|
|
|
4464
4714
|
/**
|
|
4465
4715
|
* Start an async reset task: spawn a detached child process and return the taskId.
|
|
4466
4716
|
*
|
|
4467
|
-
* The child process runs: node cli.js reset --worker --task-id=xxx
|
|
4717
|
+
* The child process runs: node cli.js reset --worker --task-id=xxx
|
|
4718
|
+
* The worker fetches ctx from innerApi itself — no --ctx passthrough.
|
|
4468
4719
|
*/
|
|
4469
|
-
function startAsyncReset(
|
|
4720
|
+
function startAsyncReset() {
|
|
4470
4721
|
const taskId = (0, node_crypto.randomUUID)();
|
|
4471
4722
|
const resultFile = resetResultFile(taskId);
|
|
4472
4723
|
const log = makeLogger(resetLogFile(taskId));
|
|
@@ -4490,8 +4741,7 @@ function startAsyncReset(ctxBase64) {
|
|
|
4490
4741
|
process.argv[1],
|
|
4491
4742
|
"reset",
|
|
4492
4743
|
"--worker",
|
|
4493
|
-
`--task-id=${taskId}
|
|
4494
|
-
`--ctx=${ctxBase64}`
|
|
4744
|
+
`--task-id=${taskId}`
|
|
4495
4745
|
], {
|
|
4496
4746
|
detached: true,
|
|
4497
4747
|
stdio: "ignore",
|
|
@@ -4857,309 +5107,49 @@ function updatePluginInstalls(configPath, installedPkgs) {
|
|
|
4857
5107
|
for (const pkg of installedPkgs) {
|
|
4858
5108
|
if (!PLUGINS_TO_AUTO_ENABLE.includes(pkg.name)) continue;
|
|
4859
5109
|
if (!Array.isArray(plugins.allow)) plugins.allow = [];
|
|
4860
|
-
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"
|
|
5110
|
+
const allow = plugins.allow;
|
|
5111
|
+
if (!allow.includes(pkg.name)) allow.push(pkg.name);
|
|
5112
|
+
if (!plugins.entries || typeof plugins.entries !== "object" || Array.isArray(plugins.entries)) plugins.entries = {};
|
|
5113
|
+
const entries = plugins.entries;
|
|
5114
|
+
entries[pkg.name] = {
|
|
5115
|
+
...asRecord(entries[pkg.name]) ?? {},
|
|
5116
|
+
enabled: true
|
|
5093
5117
|
};
|
|
5094
5118
|
}
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5119
|
+
const tmpPath = configPath + ".installs-tmp";
|
|
5120
|
+
node_fs.default.writeFileSync(tmpPath, JSON.stringify(config, null, 2), "utf-8");
|
|
5121
|
+
moveSafe(tmpPath, configPath);
|
|
5122
|
+
console.error(`[install-extension] plugins.installs updated: ${updated} entry(ies) in ${configPath}` + (skipped > 0 ? ` (${skipped} package(s) without installMetadata skipped)` : ""));
|
|
5123
|
+
}
|
|
5124
|
+
function installOne$1(pkg, tarball, homeBase) {
|
|
5125
|
+
const destDir = node_path.default.join(homeBase, pkg.installPath);
|
|
5126
|
+
const stagingDir = destDir + ".new";
|
|
5127
|
+
const oldDir = destDir + ".old";
|
|
5128
|
+
node_fs.default.mkdirSync(node_path.default.dirname(destDir), { recursive: true });
|
|
5129
|
+
if (node_fs.default.existsSync(stagingDir)) node_fs.default.rmSync(stagingDir, {
|
|
5130
|
+
recursive: true,
|
|
5131
|
+
force: true
|
|
5132
|
+
});
|
|
5133
|
+
node_fs.default.mkdirSync(stagingDir);
|
|
5134
|
+
try {
|
|
5135
|
+
extractTarballTolerant(tarball, stagingDir, { stripComponents: 1 });
|
|
5136
|
+
if (!node_fs.default.existsSync(node_path.default.join(stagingDir, "package.json"))) throw new Error(`extension tarball missing package.json: ${pkg.name}`);
|
|
5137
|
+
} catch (e) {
|
|
5138
|
+
try {
|
|
5139
|
+
node_fs.default.rmSync(stagingDir, {
|
|
5140
|
+
recursive: true,
|
|
5141
|
+
force: true
|
|
5142
|
+
});
|
|
5143
|
+
} catch {}
|
|
5144
|
+
throw e;
|
|
5102
5145
|
}
|
|
5103
|
-
const
|
|
5104
|
-
if (
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
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
|
|
5146
|
+
const hadOld = node_fs.default.existsSync(destDir);
|
|
5147
|
+
if (hadOld) moveSafe(destDir, oldDir);
|
|
5148
|
+
moveSafe(stagingDir, destDir);
|
|
5149
|
+
if (hadOld && node_fs.default.existsSync(oldDir)) node_fs.default.rmSync(oldDir, {
|
|
5150
|
+
recursive: true,
|
|
5151
|
+
force: true
|
|
5139
5152
|
});
|
|
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
5153
|
}
|
|
5164
5154
|
//#endregion
|
|
5165
5155
|
//#region ../../openclaw-slardar/lib/client.js
|
|
@@ -7005,6 +6995,60 @@ function mergeCoreBackupAndOrigins(configPath, vars, resetData, log) {
|
|
|
7005
6995
|
log(`allowedOrigins: added ${added.length} (${JSON.stringify(added)}), total now ${mergedOrigins.length}`);
|
|
7006
6996
|
}
|
|
7007
6997
|
/**
|
|
6998
|
+
* Fix bot account allowFrom and appSecret using larkApps from innerApi.
|
|
6999
|
+
*
|
|
7000
|
+
* For each bot account (key starts with `bot-cli_`):
|
|
7001
|
+
* - allowFrom must contain the bot's own creatorOpenID from larkApps
|
|
7002
|
+
* - appSecret must be either the canonical provider-ref or match larkApps plaintext
|
|
7003
|
+
*
|
|
7004
|
+
* Runs after mergeCoreBackupAndOrigins so it operates on the final config state.
|
|
7005
|
+
*/
|
|
7006
|
+
function fixBotChannelConfig(configPath, larkApps, log) {
|
|
7007
|
+
if (!larkApps || larkApps.length === 0) {
|
|
7008
|
+
log("no larkApps data, skip bot channel config fix");
|
|
7009
|
+
return;
|
|
7010
|
+
}
|
|
7011
|
+
const config = loadJSON5().parse(node_fs.default.readFileSync(configPath, "utf-8"));
|
|
7012
|
+
const accounts = asRecord(getNestedMap(config, "channels", "feishu")?.accounts);
|
|
7013
|
+
if (!accounts) {
|
|
7014
|
+
log("no feishu accounts in config, skip bot channel config fix");
|
|
7015
|
+
return;
|
|
7016
|
+
}
|
|
7017
|
+
let fixCount = 0;
|
|
7018
|
+
for (const [, account] of Object.entries(accounts)) {
|
|
7019
|
+
const bot = asRecord(account);
|
|
7020
|
+
if (!bot) continue;
|
|
7021
|
+
const appId = bot.appId;
|
|
7022
|
+
if (typeof appId !== "string" || !appId.startsWith("cli_")) continue;
|
|
7023
|
+
const larkApp = larkApps.find((e) => e.larkAppID === appId);
|
|
7024
|
+
if (!larkApp) continue;
|
|
7025
|
+
const creatorOpenID = larkApp.creatorOpenID;
|
|
7026
|
+
if (typeof creatorOpenID === "string" && creatorOpenID !== "") {
|
|
7027
|
+
const allowFrom = Array.isArray(bot.allowFrom) ? [...bot.allowFrom] : [];
|
|
7028
|
+
if (!allowFrom.includes(creatorOpenID)) {
|
|
7029
|
+
allowFrom.push(creatorOpenID);
|
|
7030
|
+
bot.allowFrom = allowFrom;
|
|
7031
|
+
fixCount++;
|
|
7032
|
+
}
|
|
7033
|
+
}
|
|
7034
|
+
const secret = bot.appSecret;
|
|
7035
|
+
let needsFix = false;
|
|
7036
|
+
if (typeof secret === "object" && secret !== null && !Array.isArray(secret)) {
|
|
7037
|
+
if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) needsFix = true;
|
|
7038
|
+
} else if (typeof secret === "string") {
|
|
7039
|
+
if (secret !== larkApp.appSecret) needsFix = true;
|
|
7040
|
+
} else needsFix = true;
|
|
7041
|
+
if (needsFix) {
|
|
7042
|
+
bot.appSecret = { ...DEFAULT_FEISHU_APP_SECRET };
|
|
7043
|
+
fixCount++;
|
|
7044
|
+
}
|
|
7045
|
+
}
|
|
7046
|
+
if (fixCount > 0) {
|
|
7047
|
+
node_fs.default.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
7048
|
+
log(`fixed ${fixCount} bot channel config issue(s) (allowFrom/appSecret)`);
|
|
7049
|
+
} else log("bot channel config ok, no fixes needed");
|
|
7050
|
+
}
|
|
7051
|
+
/**
|
|
7008
7052
|
* Step 7: Verify startup scripts landed in configDir/scripts/.
|
|
7009
7053
|
*
|
|
7010
7054
|
* Scripts are extracted directly to configDir/scripts/ during stageTemplate —
|
|
@@ -7149,6 +7193,7 @@ async function runReset(input, taskId, resultFile) {
|
|
|
7149
7193
|
await step5InstallOpenclaw(openclawTag, ossFileMap, log);
|
|
7150
7194
|
step(6);
|
|
7151
7195
|
mergeCoreBackupAndOrigins(configPath, vars, resetData, log);
|
|
7196
|
+
fixBotChannelConfig(configPath, vars.larkApps, log);
|
|
7152
7197
|
step(7);
|
|
7153
7198
|
verifyStartupScripts(configDir, log);
|
|
7154
7199
|
step(8);
|
|
@@ -7947,7 +7992,8 @@ function normalizeCtx(raw) {
|
|
|
7947
7992
|
reset: {
|
|
7948
7993
|
templateVars: r.reset.templateVars ?? {},
|
|
7949
7994
|
coreBackup: r.reset.coreBackup
|
|
7950
|
-
}
|
|
7995
|
+
},
|
|
7996
|
+
larkApps: Array.isArray(r.larkApps) ? r.larkApps : []
|
|
7951
7997
|
};
|
|
7952
7998
|
}
|
|
7953
7999
|
const vars = r.vars ?? {};
|
|
@@ -7972,7 +8018,8 @@ function normalizeCtx(raw) {
|
|
|
7972
8018
|
reset: {
|
|
7973
8019
|
templateVars: resetData.templateVars ?? {},
|
|
7974
8020
|
coreBackup: resetData.coreBackup
|
|
7975
|
-
}
|
|
8021
|
+
},
|
|
8022
|
+
larkApps: Array.isArray(r.larkApps) ? r.larkApps : []
|
|
7976
8023
|
};
|
|
7977
8024
|
}
|
|
7978
8025
|
function fillApp(src) {
|
|
@@ -8037,7 +8084,8 @@ function buildCheckInput(raw, configPathOverride) {
|
|
|
8037
8084
|
providerFilePath: PROVIDER_FILE_PATH,
|
|
8038
8085
|
secretsFilePath: SECRETS_FILE_PATH,
|
|
8039
8086
|
templateVars: ctx.app.templateVars,
|
|
8040
|
-
recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
|
|
8087
|
+
recommendedOpenclawTag: ctx.app.recommendedOpenclawTag,
|
|
8088
|
+
larkApps: ctx.larkApps
|
|
8041
8089
|
},
|
|
8042
8090
|
templateVars: ctx.app.templateVars
|
|
8043
8091
|
};
|
|
@@ -8069,7 +8117,8 @@ function buildRepairInput(raw, configPathOverride) {
|
|
|
8069
8117
|
providerFilePath: PROVIDER_FILE_PATH,
|
|
8070
8118
|
secretsFilePath: SECRETS_FILE_PATH,
|
|
8071
8119
|
templateVars: ctx.app.templateVars,
|
|
8072
|
-
recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
|
|
8120
|
+
recommendedOpenclawTag: ctx.app.recommendedOpenclawTag,
|
|
8121
|
+
larkApps: ctx.larkApps
|
|
8073
8122
|
},
|
|
8074
8123
|
repairData: {
|
|
8075
8124
|
secretsContent: ctx.secrets.secretsContent,
|
|
@@ -8105,7 +8154,8 @@ function buildResetInput(raw, configPathOverride) {
|
|
|
8105
8154
|
providerFilePath: PROVIDER_FILE_PATH,
|
|
8106
8155
|
secretsFilePath: SECRETS_FILE_PATH,
|
|
8107
8156
|
templateVars: ctx.app.templateVars,
|
|
8108
|
-
recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
|
|
8157
|
+
recommendedOpenclawTag: ctx.app.recommendedOpenclawTag,
|
|
8158
|
+
larkApps: ctx.larkApps
|
|
8109
8159
|
},
|
|
8110
8160
|
resetData: {
|
|
8111
8161
|
templateVars: ctx.reset.templateVars,
|
|
@@ -10336,6 +10386,122 @@ function finalize(results, aborted) {
|
|
|
10336
10386
|
};
|
|
10337
10387
|
}
|
|
10338
10388
|
//#endregion
|
|
10389
|
+
//#region src/channels-probe.ts
|
|
10390
|
+
const FEISHU_INVALID_CONFIG_MSG = "channels.feishu: invalid config: must NOT have additional properties";
|
|
10391
|
+
const CHANNEL_LINE_RE = /^-\s+Feishu\s+([^:]+):\s+(.+)$/;
|
|
10392
|
+
/**
|
|
10393
|
+
* 判断单个飞书账号是否处于"可用"状态。
|
|
10394
|
+
* 移植自 feishu-channel-success-rate skill 的 Python `_account_is_working`。
|
|
10395
|
+
*
|
|
10396
|
+
* 会先剥离 "key:value" 形式的 bit(dm:、bot:、in:、out:、token:、allow:、
|
|
10397
|
+
* intents:、groups:、health: 等),再按以下公式判断:
|
|
10398
|
+
*
|
|
10399
|
+
* @param ignoreProbeFailed 为 true 时忽略 "probe failed" bit,不视为失败。
|
|
10400
|
+
* 升级前置检测中传 true,因为 probe 失败通常反映网络状况而非配置问题。
|
|
10401
|
+
* @param gatewayReachable 为 false 时(gateway 尚未启动),只要 enabled+configured
|
|
10402
|
+
* 即视为可用;为 true 时还需要 running/works 且无 error: bit。
|
|
10403
|
+
*/
|
|
10404
|
+
function accountIsWorking(bits, ignoreProbeFailed = true, gatewayReachable = true) {
|
|
10405
|
+
const bitTokens = /* @__PURE__ */ new Set();
|
|
10406
|
+
let hasError = false;
|
|
10407
|
+
let hasProbeFailed = false;
|
|
10408
|
+
for (const raw of bits) {
|
|
10409
|
+
const b = raw.trim();
|
|
10410
|
+
if (!b) continue;
|
|
10411
|
+
if (b.startsWith("error:")) {
|
|
10412
|
+
hasError = true;
|
|
10413
|
+
continue;
|
|
10414
|
+
}
|
|
10415
|
+
if (b === "probe failed") {
|
|
10416
|
+
hasProbeFailed = true;
|
|
10417
|
+
continue;
|
|
10418
|
+
}
|
|
10419
|
+
bitTokens.add(b.split(":")[0]);
|
|
10420
|
+
}
|
|
10421
|
+
if (!bitTokens.has("enabled") || !bitTokens.has("configured")) return false;
|
|
10422
|
+
if (!gatewayReachable) return true;
|
|
10423
|
+
if (bitTokens.has("works")) return true;
|
|
10424
|
+
if (bitTokens.has("running") && !hasError && (ignoreProbeFailed || !hasProbeFailed)) return true;
|
|
10425
|
+
return false;
|
|
10426
|
+
}
|
|
10427
|
+
/**
|
|
10428
|
+
* 解析 `openclaw channels status --probe` 的原始 stdout。
|
|
10429
|
+
* 移植自 feishu-channel-success-rate skill 的 Python `extract_channels_probe`。
|
|
10430
|
+
*/
|
|
10431
|
+
function parseChannelsProbeOutput(text, { ignoreProbeFailed = true } = {}) {
|
|
10432
|
+
const gatewayReachable = text.includes("Gateway reachable");
|
|
10433
|
+
const feishuConfigInvalid = text.includes(FEISHU_INVALID_CONFIG_MSG);
|
|
10434
|
+
const accounts = [];
|
|
10435
|
+
let anyAccountWorking = false;
|
|
10436
|
+
for (const line of text.split("\n")) {
|
|
10437
|
+
const m = CHANNEL_LINE_RE.exec(line.trim());
|
|
10438
|
+
if (!m) continue;
|
|
10439
|
+
const [, acct, rest] = m;
|
|
10440
|
+
const bits = rest.split(",").map((b) => b.trim());
|
|
10441
|
+
const isWorking = accountIsWorking(bits, ignoreProbeFailed, gatewayReachable);
|
|
10442
|
+
if (isWorking) anyAccountWorking = true;
|
|
10443
|
+
accounts.push({
|
|
10444
|
+
id: acct.trim(),
|
|
10445
|
+
bits,
|
|
10446
|
+
isWorking,
|
|
10447
|
+
raw: line.trim()
|
|
10448
|
+
});
|
|
10449
|
+
}
|
|
10450
|
+
return {
|
|
10451
|
+
gatewayReachable,
|
|
10452
|
+
feishuConfigInvalid,
|
|
10453
|
+
accounts,
|
|
10454
|
+
anyAccountWorking
|
|
10455
|
+
};
|
|
10456
|
+
}
|
|
10457
|
+
/**
|
|
10458
|
+
* 执行 `openclaw channels status --probe`,返回结构化结果。
|
|
10459
|
+
*
|
|
10460
|
+
* 部分 bot 账号 probe 失败时命令会以非零退出码退出,但 stdout 仍有可用内容。
|
|
10461
|
+
* 因此即使退出码非零,也尝试解析 stdout;只有真正没有任何输出时才返回 unavailable。
|
|
10462
|
+
*
|
|
10463
|
+
* @param timeoutMs 最长等待时长,默认 60 s。v2026.4.x 缺少单请求 HTTP 超时,
|
|
10464
|
+
* 可能无限阻塞,此超时是唯一保护。
|
|
10465
|
+
* @param ignoreProbeFailed 为 true 时,"probe failed" 账号仍计入"可用"。
|
|
10466
|
+
* 升级前置检测中应传 true,避免网络抖动导致误判为不可用。
|
|
10467
|
+
*/
|
|
10468
|
+
function runChannelsProbe(timeoutMs = 6e4, { ignoreProbeFailed = true } = {}) {
|
|
10469
|
+
let stdout = "";
|
|
10470
|
+
let stderrText = "";
|
|
10471
|
+
let execError;
|
|
10472
|
+
try {
|
|
10473
|
+
stdout = (0, node_child_process.execSync)("openclaw channels status --probe", {
|
|
10474
|
+
encoding: "utf-8",
|
|
10475
|
+
timeout: timeoutMs,
|
|
10476
|
+
stdio: [
|
|
10477
|
+
"ignore",
|
|
10478
|
+
"pipe",
|
|
10479
|
+
"pipe"
|
|
10480
|
+
]
|
|
10481
|
+
});
|
|
10482
|
+
} catch (e) {
|
|
10483
|
+
const err = e;
|
|
10484
|
+
const stdoutRaw = err.stdout;
|
|
10485
|
+
stdout = typeof stdoutRaw === "string" ? stdoutRaw : stdoutRaw?.toString("utf-8") ?? "";
|
|
10486
|
+
execError = err.message;
|
|
10487
|
+
const stderrRaw = err.stderr;
|
|
10488
|
+
stderrText = (typeof stderrRaw === "string" ? stderrRaw : stderrRaw?.toString("utf-8") ?? "").trim();
|
|
10489
|
+
if (stderrText) console.error(`channels-probe: stderr from CLI: ${stderrText}`);
|
|
10490
|
+
}
|
|
10491
|
+
if (stdout.trim()) return {
|
|
10492
|
+
available: true,
|
|
10493
|
+
...parseChannelsProbeOutput(stdout, { ignoreProbeFailed })
|
|
10494
|
+
};
|
|
10495
|
+
return {
|
|
10496
|
+
available: false,
|
|
10497
|
+
gatewayReachable: false,
|
|
10498
|
+
feishuConfigInvalid: stderrText.includes(FEISHU_INVALID_CONFIG_MSG),
|
|
10499
|
+
accounts: [],
|
|
10500
|
+
anyAccountWorking: false,
|
|
10501
|
+
error: execError ?? "no output from openclaw channels status --probe"
|
|
10502
|
+
};
|
|
10503
|
+
}
|
|
10504
|
+
//#endregion
|
|
10339
10505
|
//#region src/innerapi/reportCliRun.ts
|
|
10340
10506
|
/**
|
|
10341
10507
|
* CLI-side client for studio_server's `openclaw.report_cli_run` inner
|
|
@@ -10415,7 +10581,7 @@ async function reportCliRun(opts) {
|
|
|
10415
10581
|
//#region src/help.ts
|
|
10416
10582
|
const BIN = "mclaw-diagnose";
|
|
10417
10583
|
function versionBanner() {
|
|
10418
|
-
return `v0.1.15-alpha.
|
|
10584
|
+
return `v0.1.15-alpha.1`;
|
|
10419
10585
|
}
|
|
10420
10586
|
const COMMANDS = [
|
|
10421
10587
|
{
|
|
@@ -10519,16 +10685,12 @@ EXIT CODES
|
|
|
10519
10685
|
hidden: true,
|
|
10520
10686
|
summary: "Run rule-engine check only",
|
|
10521
10687
|
help: `USAGE
|
|
10522
|
-
${BIN} check
|
|
10688
|
+
${BIN} check
|
|
10523
10689
|
|
|
10524
10690
|
DESCRIPTION
|
|
10525
10691
|
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).
|
|
10692
|
+
returns { failedRules }. Ctx is fetched from innerapi automatically.
|
|
10693
|
+
End-users should prefer \`doctor\`.
|
|
10532
10694
|
`
|
|
10533
10695
|
},
|
|
10534
10696
|
{
|
|
@@ -10536,16 +10698,11 @@ OPTIONS
|
|
|
10536
10698
|
hidden: true,
|
|
10537
10699
|
summary: "Apply standard-mode repairs",
|
|
10538
10700
|
help: `USAGE
|
|
10539
|
-
${BIN} repair
|
|
10701
|
+
${BIN} repair
|
|
10540
10702
|
|
|
10541
10703
|
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.
|
|
10704
|
+
Runs repair for the failing rules. Ctx is fetched from innerapi
|
|
10705
|
+
automatically. End-users should use \`doctor --fix\` instead.
|
|
10549
10706
|
`
|
|
10550
10707
|
},
|
|
10551
10708
|
{
|
|
@@ -10553,14 +10710,15 @@ OPTIONS
|
|
|
10553
10710
|
hidden: true,
|
|
10554
10711
|
summary: "Re-initialize sandbox via the 9-step reset pipeline",
|
|
10555
10712
|
help: `USAGE
|
|
10556
|
-
${BIN} reset --async
|
|
10557
|
-
${BIN} reset --worker --task-id=<id>
|
|
10713
|
+
${BIN} reset --async
|
|
10714
|
+
${BIN} reset --worker --task-id=<id>
|
|
10558
10715
|
|
|
10559
10716
|
DESCRIPTION
|
|
10560
10717
|
Two-phase pipeline driven asynchronously: the --async invocation spawns
|
|
10561
10718
|
a detached worker and returns { taskId } immediately; the --worker
|
|
10562
10719
|
invocation (spawned by --async) runs the actual 9 steps and writes
|
|
10563
10720
|
progress to /tmp/openclaw-diagnose/reset-<taskId>.json.
|
|
10721
|
+
Ctx is fetched from innerapi automatically.
|
|
10564
10722
|
|
|
10565
10723
|
Poll progress with \`${BIN} get_reset_task --task-id=<id>\`.
|
|
10566
10724
|
|
|
@@ -10568,7 +10726,6 @@ OPTIONS
|
|
|
10568
10726
|
--async Start a detached worker and return taskId on stdout.
|
|
10569
10727
|
--worker Internal — run the 9-step pipeline (launched by --async).
|
|
10570
10728
|
--task-id=<id> Required with --worker; identifies the progress file.
|
|
10571
|
-
--ctx=<base64> Opaque ctx JSON; fetched from innerapi when absent.
|
|
10572
10729
|
`
|
|
10573
10730
|
},
|
|
10574
10731
|
{
|
|
@@ -10591,7 +10748,7 @@ OPTIONS
|
|
|
10591
10748
|
hidden: true,
|
|
10592
10749
|
summary: "Download + install the openclaw tarball",
|
|
10593
10750
|
help: `USAGE
|
|
10594
|
-
${BIN} install-openclaw <tag> [--
|
|
10751
|
+
${BIN} install-openclaw <tag> [--oss_file_map=<base64>]
|
|
10595
10752
|
|
|
10596
10753
|
DESCRIPTION
|
|
10597
10754
|
Downloads the openclaw@<tag> tgz via the signed OSS URL found in the
|
|
@@ -10603,9 +10760,9 @@ ARGUMENTS
|
|
|
10603
10760
|
<tag> Openclaw version tag, e.g. 2026.4.11.
|
|
10604
10761
|
|
|
10605
10762
|
OPTIONS
|
|
10606
|
-
--ctx=<base64> Opaque ctx; ossFileMap is extracted from it.
|
|
10607
10763
|
--oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi
|
|
10608
|
-
entirely.
|
|
10764
|
+
entirely. When absent, ossFileMap is fetched from
|
|
10765
|
+
innerapi automatically.
|
|
10609
10766
|
`
|
|
10610
10767
|
},
|
|
10611
10768
|
{
|
|
@@ -10631,8 +10788,7 @@ OPTIONS
|
|
|
10631
10788
|
--home_base=<dir> Override the /home/gem base (tests).
|
|
10632
10789
|
--config_path=<p> Override the openclaw.json path (tests).
|
|
10633
10790
|
--skip-config-update Leave plugins.installs in openclaw.json untouched.
|
|
10634
|
-
--
|
|
10635
|
-
--oss_file_map=... Pre-built OSS URL map (base64 JSON).
|
|
10791
|
+
--oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi.
|
|
10636
10792
|
`
|
|
10637
10793
|
},
|
|
10638
10794
|
{
|
|
@@ -10659,7 +10815,6 @@ OPTIONS
|
|
|
10659
10815
|
--cli=<name> CLI package to install by short name or scoped
|
|
10660
10816
|
packageName (repeatable, at least one required).
|
|
10661
10817
|
--home_base=<dir> Override the /home/gem base (tests).
|
|
10662
|
-
--ctx=<base64> Opaque ctx; ossFileMap is extracted from it.
|
|
10663
10818
|
--oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi.
|
|
10664
10819
|
|
|
10665
10820
|
EXAMPLES
|
|
@@ -10720,12 +10875,17 @@ EXIT CODES
|
|
|
10720
10875
|
hidden: false,
|
|
10721
10876
|
summary: "Upgrade the Feishu/Lark plugin via @larksuite/openclaw-lark-tools",
|
|
10722
10877
|
help: `USAGE
|
|
10723
|
-
${BIN} upgrade-lark [--scene=<scene>] [--caller=<n>] [--trace-id=<id>]
|
|
10878
|
+
${BIN} upgrade-lark [--check-only] [--scene=<scene>] [--caller=<n>] [--trace-id=<id>]
|
|
10724
10879
|
|
|
10725
10880
|
DESCRIPTION
|
|
10726
10881
|
Upgrades the Feishu/Lark plugin by running:
|
|
10727
10882
|
npx -y @larksuite/openclaw-lark-tools update --use-existing
|
|
10728
10883
|
|
|
10884
|
+
Before the upgrade, a pre-check gate runs to verify the upgrade is needed:
|
|
10885
|
+
- version incompatible OR feishu channel config invalid, AND
|
|
10886
|
+
- no feishu account is currently working
|
|
10887
|
+
If the gate is not triggered, the command skips with exit code 0.
|
|
10888
|
+
|
|
10729
10889
|
Before the upgrade, the following files are backed up:
|
|
10730
10890
|
- openclaw.json
|
|
10731
10891
|
- extensions/openclaw-lark/ (if present)
|
|
@@ -10743,16 +10903,23 @@ DESCRIPTION
|
|
|
10743
10903
|
{ "ok": false, "error": "...", "stderr": "...", "exitCode": 1,
|
|
10744
10904
|
"rollbackOk": true, "validationError": "...", "logFile": "..." }
|
|
10745
10905
|
|
|
10906
|
+
With --check-only:
|
|
10907
|
+
{ "ok": true, "skipped": true, "upgradeNeeded": false, "logFile": "..." }
|
|
10908
|
+
{ "ok": true, "skipped": true, "upgradeNeeded": true, "logFile": "..." } ← exit 1
|
|
10909
|
+
|
|
10746
10910
|
OPTIONS
|
|
10911
|
+
--check-only Diagnose only: run the pre-check gate and report whether
|
|
10912
|
+
upgrade is needed without installing. Exit 1 if needed.
|
|
10747
10913
|
--scene=<scene> Telemetry label forwarded to Slardar only.
|
|
10748
10914
|
Known values: PageUpgradeLark, etc. Custom strings accepted.
|
|
10749
10915
|
--caller=<name> Optional metadata passed to innerapi.
|
|
10750
10916
|
--trace-id=<id> Optional log-correlation id.
|
|
10751
10917
|
|
|
10752
10918
|
EXIT CODES
|
|
10753
|
-
0 Success: upgrade ran and all validations passed.
|
|
10919
|
+
0 Success: upgrade ran and all validations passed; or gate skipped upgrade.
|
|
10754
10920
|
1 Failure: npx error, validation failed, or git commit failed.
|
|
10755
10921
|
File rollback was attempted (see rollbackOk in the JSON output).
|
|
10922
|
+
With --check-only: exit 1 means upgrade IS needed.
|
|
10756
10923
|
`
|
|
10757
10924
|
},
|
|
10758
10925
|
{
|
|
@@ -10841,8 +11008,7 @@ OPTIONS
|
|
|
10841
11008
|
--role=<role> Package role (e.g. template, config).
|
|
10842
11009
|
--name=<name> Package name within the role.
|
|
10843
11010
|
--dir=<dir> Target dir (defaults to dirname(pkg.installPath)).
|
|
10844
|
-
--
|
|
10845
|
-
--oss_file_map=... Pre-built OSS URL map (base64 JSON).
|
|
11011
|
+
--oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi.
|
|
10846
11012
|
`
|
|
10847
11013
|
}
|
|
10848
11014
|
];
|
|
@@ -10918,31 +11084,31 @@ function planVarsFields(opts = {}) {
|
|
|
10918
11084
|
*
|
|
10919
11085
|
* Per-command group needs:
|
|
10920
11086
|
*
|
|
10921
|
-
* doctor / check app
|
|
10922
|
-
* repair app + secrets
|
|
10923
|
-
* reset app + secrets + install + reset
|
|
11087
|
+
* doctor / check app + larkApps
|
|
11088
|
+
* repair app + secrets + larkApps
|
|
11089
|
+
* reset app + secrets + install + reset + larkApps
|
|
10924
11090
|
* install-* install only
|
|
10925
11091
|
*
|
|
10926
11092
|
* Empty result (`{}`) means "no group needed" — the CLI can skip the
|
|
10927
11093
|
* `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
11094
|
*/
|
|
10931
11095
|
function planCtxPopulate(opts) {
|
|
10932
11096
|
if (opts.command === "install") return { install: true };
|
|
10933
11097
|
const populate = {};
|
|
10934
|
-
|
|
11098
|
+
if (planVarsFields({
|
|
10935
11099
|
disabled: opts.disabled,
|
|
10936
11100
|
onlyRules: opts.onlyRules,
|
|
10937
11101
|
profile: opts.profile
|
|
10938
|
-
});
|
|
10939
|
-
if (
|
|
10940
|
-
|
|
10941
|
-
|
|
11102
|
+
}).length > 0) populate.app = true;
|
|
11103
|
+
if (opts.command === "repair") {
|
|
11104
|
+
populate.secrets = true;
|
|
11105
|
+
populate.larkApps = true;
|
|
11106
|
+
} else if (opts.command === "reset") {
|
|
10942
11107
|
populate.secrets = true;
|
|
10943
11108
|
populate.install = true;
|
|
10944
11109
|
populate.reset = true;
|
|
10945
|
-
|
|
11110
|
+
populate.larkApps = true;
|
|
11111
|
+
} else if (opts.command === "doctor" || opts.command === "check") populate.larkApps = true;
|
|
10946
11112
|
return populate;
|
|
10947
11113
|
}
|
|
10948
11114
|
//#endregion
|
|
@@ -10996,6 +11162,7 @@ function reportDoctorRunToSlardar(opts) {
|
|
|
10996
11162
|
}
|
|
10997
11163
|
});
|
|
10998
11164
|
}
|
|
11165
|
+
/** 读取日志文件全文;文件不存在或读取失败时返回空字符串。 */
|
|
10999
11166
|
function readLogFile(filePath) {
|
|
11000
11167
|
try {
|
|
11001
11168
|
return node_fs.default.readFileSync(filePath, "utf-8");
|
|
@@ -11003,8 +11170,18 @@ function readLogFile(filePath) {
|
|
|
11003
11170
|
return "";
|
|
11004
11171
|
}
|
|
11005
11172
|
}
|
|
11173
|
+
/**
|
|
11174
|
+
* 向 Slardar 上报 upgrade-lark 运行结果(upgrade_lark_run 事件)。
|
|
11175
|
+
*
|
|
11176
|
+
* extraCategories 记录字符串维度:scene、exit_code、rollback_ok、
|
|
11177
|
+
* validation_error、error_msg、log_content(日志文件全文)。
|
|
11178
|
+
*
|
|
11179
|
+
* extraMetrics 记录各阶段耗时(毫秒);未执行的阶段上报 -1 作为哨兵值,
|
|
11180
|
+
* 便于在 Slardar 查询时区分"未运行"和"运行了 0ms"。
|
|
11181
|
+
*/
|
|
11006
11182
|
function reportUpgradeLarkToSlardar(opts) {
|
|
11007
11183
|
console.error(`[slardar] upgrade_lark_run scene=${opts.scene ?? ""} success=${opts.success} exitCode=${opts.exitCode ?? ""} rollbackOk=${opts.rollbackOk ?? ""}`);
|
|
11184
|
+
const t = opts.timing ?? {};
|
|
11008
11185
|
const logContent = readLogFile(opts.logFile);
|
|
11009
11186
|
reportTask({
|
|
11010
11187
|
eventName: "upgrade_lark_run",
|
|
@@ -11017,12 +11194,20 @@ function reportUpgradeLarkToSlardar(opts) {
|
|
|
11017
11194
|
validation_error: opts.validationError ?? "",
|
|
11018
11195
|
error_msg: opts.error ?? "",
|
|
11019
11196
|
log_content: logContent
|
|
11197
|
+
},
|
|
11198
|
+
extraMetrics: {
|
|
11199
|
+
pre_probe_ms: t.preProbeMs ?? -1,
|
|
11200
|
+
version_check_ms: t.versionCheckMs ?? -1,
|
|
11201
|
+
backup_ms: t.backupMs ?? -1,
|
|
11202
|
+
npx_install_ms: t.npxInstallMs ?? -1,
|
|
11203
|
+
post_probe_ms: t.postProbeMs ?? -1,
|
|
11204
|
+
doctor_fix_ms: t.doctorFixMs ?? -1
|
|
11020
11205
|
}
|
|
11021
11206
|
});
|
|
11022
11207
|
}
|
|
11023
11208
|
//#endregion
|
|
11024
11209
|
//#region src/upgrade-lark.ts
|
|
11025
|
-
/**
|
|
11210
|
+
/** 升级前需备份的 extensions/ 下的插件目录 */
|
|
11026
11211
|
const FEISHU_PLUGIN_DIRS = ["openclaw-lark", "feishu-openclaw-plugin"];
|
|
11027
11212
|
function backupFiles(opts) {
|
|
11028
11213
|
const { workspaceDir, configPath, backupDir, log } = opts;
|
|
@@ -11125,7 +11310,7 @@ function countFeishuBots(configPath) {
|
|
|
11125
11310
|
return 0;
|
|
11126
11311
|
}
|
|
11127
11312
|
}
|
|
11128
|
-
/**
|
|
11313
|
+
/** 执行 channels probe 并将结果写入日志,从不抛出异常(异常时返回全零结果)。 */
|
|
11129
11314
|
function probeChannels(label, log, timeoutMs) {
|
|
11130
11315
|
try {
|
|
11131
11316
|
const r = runChannelsProbe(timeoutMs);
|
|
@@ -11163,12 +11348,16 @@ function runUpgradeLark(opts) {
|
|
|
11163
11348
|
log(` cwd : ${cwd}`);
|
|
11164
11349
|
log(` configPath : ${configPath}`);
|
|
11165
11350
|
log(`${"=".repeat(60)}`);
|
|
11351
|
+
const timing = {};
|
|
11166
11352
|
log("");
|
|
11167
11353
|
log("── [Pre-check A] channels probe(升级前)────────────────");
|
|
11354
|
+
const t_preProbeStart = Date.now();
|
|
11168
11355
|
const beforeChannels = probeChannels("before", log, 6e4);
|
|
11356
|
+
timing.preProbeMs = Date.now() - t_preProbeStart;
|
|
11169
11357
|
log("");
|
|
11170
11358
|
log("── [Pre-check B] 版本兼容预检 ───────────────────────────");
|
|
11171
11359
|
let versionIncompatible = false;
|
|
11360
|
+
const t_versionCheckStart = Date.now();
|
|
11172
11361
|
try {
|
|
11173
11362
|
const rawConfig = node_fs.default.readFileSync(configPath, "utf-8");
|
|
11174
11363
|
versionIncompatible = needsLarkUpgrade({
|
|
@@ -11184,6 +11373,7 @@ function runUpgradeLark(opts) {
|
|
|
11184
11373
|
} catch (e) {
|
|
11185
11374
|
log(` version-compat pre-check error: ${e.message} — version signal unavailable`);
|
|
11186
11375
|
}
|
|
11376
|
+
timing.versionCheckMs = Date.now() - t_versionCheckStart;
|
|
11187
11377
|
const feishuConfigInvalid = beforeChannels.feishuConfigInvalid;
|
|
11188
11378
|
log(` feishu config invalid : ${feishuConfigInvalid}`);
|
|
11189
11379
|
log("");
|
|
@@ -11201,6 +11391,8 @@ function runUpgradeLark(opts) {
|
|
|
11201
11391
|
ok: true,
|
|
11202
11392
|
skipped: true,
|
|
11203
11393
|
skipReason: reason,
|
|
11394
|
+
upgradeNeeded: false,
|
|
11395
|
+
timing,
|
|
11204
11396
|
logFile
|
|
11205
11397
|
};
|
|
11206
11398
|
}
|
|
@@ -11214,19 +11406,38 @@ function runUpgradeLark(opts) {
|
|
|
11214
11406
|
ok: true,
|
|
11215
11407
|
skipped: true,
|
|
11216
11408
|
skipReason: reason,
|
|
11409
|
+
upgradeNeeded: false,
|
|
11410
|
+
timing,
|
|
11217
11411
|
logFile
|
|
11218
11412
|
};
|
|
11219
11413
|
}
|
|
11220
11414
|
log(` PROCEED: requiresLarkUpgrade=true (version=${versionIncompatible}, feishuConfig=${feishuConfigInvalid}) AND channels not working → running upgrade`);
|
|
11415
|
+
if (opts.checkOnly) {
|
|
11416
|
+
log(` check-only: upgrade IS needed — returning without installing`);
|
|
11417
|
+
log(`${"=".repeat(60)}`);
|
|
11418
|
+
log("upgrade-lark check-only complete");
|
|
11419
|
+
log(`${"=".repeat(60)}`);
|
|
11420
|
+
return {
|
|
11421
|
+
ok: true,
|
|
11422
|
+
skipped: true,
|
|
11423
|
+
skipReason: "check-only",
|
|
11424
|
+
upgradeNeeded: true,
|
|
11425
|
+
timing,
|
|
11426
|
+
logFile
|
|
11427
|
+
};
|
|
11428
|
+
}
|
|
11221
11429
|
log("");
|
|
11222
11430
|
log("── [1/6] 文件备份 ────────────────────────────────────────");
|
|
11223
11431
|
log(`before-state: botCount=${countFeishuBots(configPath)}`);
|
|
11432
|
+
const t_backupStart = Date.now();
|
|
11224
11433
|
const backup = backupFiles(fsOpts);
|
|
11434
|
+
timing.backupMs = Date.now() - t_backupStart;
|
|
11225
11435
|
if (!backup.ok) {
|
|
11226
11436
|
log(`ERROR: ${backup.error}`);
|
|
11227
11437
|
return {
|
|
11228
11438
|
ok: false,
|
|
11229
11439
|
error: backup.error,
|
|
11440
|
+
timing,
|
|
11230
11441
|
logFile
|
|
11231
11442
|
};
|
|
11232
11443
|
}
|
|
@@ -11244,6 +11455,7 @@ function runUpgradeLark(opts) {
|
|
|
11244
11455
|
else log(` skipped: ${localOpenclawBin} (not found)`);
|
|
11245
11456
|
log("");
|
|
11246
11457
|
log("── [3/6] npx install (@larksuite/openclaw-lark-tools update) ──");
|
|
11458
|
+
const t_npxStart = Date.now();
|
|
11247
11459
|
const npxResult = (0, node_child_process.spawnSync)("npx", [
|
|
11248
11460
|
"-y",
|
|
11249
11461
|
"@larksuite/openclaw-lark-tools",
|
|
@@ -11258,6 +11470,7 @@ function runUpgradeLark(opts) {
|
|
|
11258
11470
|
],
|
|
11259
11471
|
timeout: 12e4
|
|
11260
11472
|
});
|
|
11473
|
+
timing.npxInstallMs = Date.now() - t_npxStart;
|
|
11261
11474
|
const npxStdout = npxResult.stdout?.trim() ?? "";
|
|
11262
11475
|
const npxStderr = npxResult.stderr?.trim() ?? "";
|
|
11263
11476
|
const npxExitCode = npxResult.status ?? 1;
|
|
@@ -11282,6 +11495,7 @@ function runUpgradeLark(opts) {
|
|
|
11282
11495
|
stderr: npxStderr,
|
|
11283
11496
|
exitCode: npxExitCode,
|
|
11284
11497
|
rollbackOk,
|
|
11498
|
+
timing,
|
|
11285
11499
|
logFile
|
|
11286
11500
|
};
|
|
11287
11501
|
};
|
|
@@ -11304,7 +11518,9 @@ function runUpgradeLark(opts) {
|
|
|
11304
11518
|
} catch (e) {
|
|
11305
11519
|
log(` version-compat post-check error: ${e.message} — version signal unavailable`);
|
|
11306
11520
|
}
|
|
11521
|
+
const t_postProbeStart = Date.now();
|
|
11307
11522
|
const afterChannels = probeChannels("after", log, 6e4);
|
|
11523
|
+
timing.postProbeMs = Date.now() - t_postProbeStart;
|
|
11308
11524
|
log(` feishu config invalid after: ${afterChannels.feishuConfigInvalid}`);
|
|
11309
11525
|
const stillNeedsUpgrade = (afterVersionIncompatible || afterChannels.feishuConfigInvalid) && !afterChannels.anyAccountWorking;
|
|
11310
11526
|
log(` post-check: stillNeedsUpgrade=${stillNeedsUpgrade} (version=${afterVersionIncompatible}, feishuConfig=${afterChannels.feishuConfigInvalid}, channelsWorking=${afterChannels.anyAccountWorking})`);
|
|
@@ -11314,6 +11530,7 @@ function runUpgradeLark(opts) {
|
|
|
11314
11530
|
log("── [6/6] doctor --fix ────────────────────────────────────");
|
|
11315
11531
|
const fixArgs = ["doctor", "--fix"];
|
|
11316
11532
|
if (opts.scene) fixArgs.push(`--scene=${opts.scene}`);
|
|
11533
|
+
const t_doctorFixStart = Date.now();
|
|
11317
11534
|
const fixResult = (0, node_child_process.spawnSync)(process.execPath, [cliScript, ...fixArgs], {
|
|
11318
11535
|
cwd,
|
|
11319
11536
|
encoding: "utf-8",
|
|
@@ -11325,6 +11542,7 @@ function runUpgradeLark(opts) {
|
|
|
11325
11542
|
timeout: 6e4,
|
|
11326
11543
|
env: process.env
|
|
11327
11544
|
});
|
|
11545
|
+
timing.doctorFixMs = Date.now() - t_doctorFixStart;
|
|
11328
11546
|
if (fixResult.stdout?.trim()) log(`doctor(fix) stdout:\n${fixResult.stdout.trim()}`);
|
|
11329
11547
|
if (fixResult.stderr?.trim()) log(`doctor(fix) stderr:\n${fixResult.stderr.trim()}`);
|
|
11330
11548
|
log(`doctor(fix) exit: ${fixResult.status ?? "null"}${fixResult.error ? ` error: ${fixResult.error.message}` : ""}`);
|
|
@@ -11337,6 +11555,7 @@ function runUpgradeLark(opts) {
|
|
|
11337
11555
|
stdout: npxStdout,
|
|
11338
11556
|
stderr: npxStderr,
|
|
11339
11557
|
exitCode: npxExitCode,
|
|
11558
|
+
timing,
|
|
11340
11559
|
logFile
|
|
11341
11560
|
};
|
|
11342
11561
|
}
|
|
@@ -11345,21 +11564,6 @@ function runUpgradeLark(opts) {
|
|
|
11345
11564
|
const args = node_process.default.argv.slice(2);
|
|
11346
11565
|
const mode = args.find((a) => !a.startsWith("-"));
|
|
11347
11566
|
/**
|
|
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
11567
|
* Pull the first non-flag positional after the mode name.
|
|
11364
11568
|
* (The mode itself is args[0] in the filtered set, so we skip index 0.)
|
|
11365
11569
|
*/
|
|
@@ -11387,8 +11591,8 @@ function getMultiFlag(args, name) {
|
|
|
11387
11591
|
* case but is no longer consulted.
|
|
11388
11592
|
*/
|
|
11389
11593
|
async function reportRun(command, rc, _raw, invocation, durationMs, outcome, slardar = {
|
|
11390
|
-
scene,
|
|
11391
|
-
profile,
|
|
11594
|
+
scene: void 0,
|
|
11595
|
+
profile: "standard",
|
|
11392
11596
|
fix: false
|
|
11393
11597
|
}) {
|
|
11394
11598
|
console.error(`${command}: telemetry calling report_cli_run`);
|
|
@@ -11452,7 +11656,7 @@ async function main() {
|
|
|
11452
11656
|
console.error(`${mode}: begin argv=[${args.join(" ")}] version=${getVersion()} traceId=${traceId ?? "-"} caller=${caller ?? "-"} runIdGenerated=${rc.generated}`);
|
|
11453
11657
|
switch (mode) {
|
|
11454
11658
|
case "check": {
|
|
11455
|
-
const raw =
|
|
11659
|
+
const raw = await fetchCtxViaInnerApi({
|
|
11456
11660
|
populate: planCtxPopulate({
|
|
11457
11661
|
command: "check",
|
|
11458
11662
|
profile
|
|
@@ -11477,7 +11681,7 @@ async function main() {
|
|
|
11477
11681
|
break;
|
|
11478
11682
|
}
|
|
11479
11683
|
case "repair": {
|
|
11480
|
-
const raw =
|
|
11684
|
+
const raw = await fetchCtxViaInnerApi({
|
|
11481
11685
|
populate: planCtxPopulate({
|
|
11482
11686
|
command: "repair",
|
|
11483
11687
|
profile
|
|
@@ -11548,27 +11752,15 @@ async function main() {
|
|
|
11548
11752
|
break;
|
|
11549
11753
|
}
|
|
11550
11754
|
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")) {
|
|
11755
|
+
if (args.includes("--async")) console.log(JSON.stringify(startAsyncReset()));
|
|
11756
|
+
else if (args.includes("--worker")) {
|
|
11565
11757
|
const taskId = args.find((a) => a.startsWith("--task-id="))?.slice(10);
|
|
11566
11758
|
if (!taskId) {
|
|
11567
11759
|
console.error("Error: --task-id=<id> is required for worker");
|
|
11568
11760
|
node_process.default.exit(1);
|
|
11569
11761
|
}
|
|
11570
11762
|
const resultFile = resetResultFile(taskId);
|
|
11571
|
-
const raw =
|
|
11763
|
+
const raw = await fetchCtxViaInnerApi({
|
|
11572
11764
|
populate: planCtxPopulate({ command: "reset" }),
|
|
11573
11765
|
caller,
|
|
11574
11766
|
traceId
|
|
@@ -11592,7 +11784,7 @@ async function main() {
|
|
|
11592
11784
|
return;
|
|
11593
11785
|
}
|
|
11594
11786
|
} else {
|
|
11595
|
-
console.error("Usage: reset --async
|
|
11787
|
+
console.error("Usage: reset --async | reset --worker --task-id=<id>");
|
|
11596
11788
|
node_process.default.exit(1);
|
|
11597
11789
|
}
|
|
11598
11790
|
break;
|
|
@@ -11608,14 +11800,14 @@ async function main() {
|
|
|
11608
11800
|
case "install-openclaw": {
|
|
11609
11801
|
const tag = getPositionalTag(args, "install-openclaw");
|
|
11610
11802
|
if (!tag) {
|
|
11611
|
-
console.error("Usage: install-openclaw <tag> [--
|
|
11803
|
+
console.error("Usage: install-openclaw <tag> [--oss_file_map=<base64>]");
|
|
11612
11804
|
node_process.default.exit(1);
|
|
11613
11805
|
}
|
|
11614
11806
|
const ossFileMapFlag = getFlag(args, "oss_file_map");
|
|
11615
11807
|
let installOssFileMap;
|
|
11616
11808
|
let rawForTelemetry;
|
|
11617
11809
|
if (!ossFileMapFlag) {
|
|
11618
|
-
rawForTelemetry =
|
|
11810
|
+
rawForTelemetry = await fetchCtxViaInnerApi({
|
|
11619
11811
|
populate: planCtxPopulate({ command: "install" }),
|
|
11620
11812
|
caller,
|
|
11621
11813
|
traceId
|
|
@@ -11650,7 +11842,7 @@ async function main() {
|
|
|
11650
11842
|
case "install-extension": {
|
|
11651
11843
|
const tag = getPositionalTag(args, "install-extension");
|
|
11652
11844
|
if (!tag) {
|
|
11653
|
-
console.error("Usage: install-extension <tag> (--all | --extension=<name>...) [--home_base=<dir>] [--config_path=<path>] [--skip-config-update] [--
|
|
11845
|
+
console.error("Usage: install-extension <tag> (--all | --extension=<name>...) [--home_base=<dir>] [--config_path=<path>] [--skip-config-update] [--oss_file_map=<base64>]");
|
|
11654
11846
|
node_process.default.exit(1);
|
|
11655
11847
|
}
|
|
11656
11848
|
const all = args.includes("--all");
|
|
@@ -11662,7 +11854,7 @@ async function main() {
|
|
|
11662
11854
|
let installOssFileMap;
|
|
11663
11855
|
let rawForTelemetry;
|
|
11664
11856
|
if (!ossFileMapFlag) {
|
|
11665
|
-
rawForTelemetry =
|
|
11857
|
+
rawForTelemetry = await fetchCtxViaInnerApi({
|
|
11666
11858
|
populate: planCtxPopulate({ command: "install" }),
|
|
11667
11859
|
caller,
|
|
11668
11860
|
traceId
|
|
@@ -11708,12 +11900,12 @@ async function main() {
|
|
|
11708
11900
|
case "install-cli": {
|
|
11709
11901
|
const tag = getPositionalTag(args, "install-cli");
|
|
11710
11902
|
if (!tag) {
|
|
11711
|
-
console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--
|
|
11903
|
+
console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--oss_file_map=<base64>]");
|
|
11712
11904
|
node_process.default.exit(1);
|
|
11713
11905
|
}
|
|
11714
11906
|
const names = getMultiFlag(args, "cli");
|
|
11715
11907
|
if (names.length === 0) {
|
|
11716
|
-
console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--
|
|
11908
|
+
console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--oss_file_map=<base64>]");
|
|
11717
11909
|
node_process.default.exit(1);
|
|
11718
11910
|
}
|
|
11719
11911
|
const homeBase = getFlag(args, "home_base");
|
|
@@ -11721,7 +11913,7 @@ async function main() {
|
|
|
11721
11913
|
let installOssFileMap;
|
|
11722
11914
|
let rawForTelemetry;
|
|
11723
11915
|
if (!ossFileMapFlag) {
|
|
11724
|
-
rawForTelemetry =
|
|
11916
|
+
rawForTelemetry = await fetchCtxViaInnerApi({
|
|
11725
11917
|
populate: planCtxPopulate({ command: "install" }),
|
|
11726
11918
|
caller,
|
|
11727
11919
|
traceId
|
|
@@ -11769,7 +11961,7 @@ async function main() {
|
|
|
11769
11961
|
case "download-resource": {
|
|
11770
11962
|
const tag = getPositionalTag(args, "download-resource");
|
|
11771
11963
|
if (!tag) {
|
|
11772
|
-
console.error("Usage: download-resource <tag> --role=<role> --name=<name> [--dir=<dir>] [--
|
|
11964
|
+
console.error("Usage: download-resource <tag> --role=<role> --name=<name> [--dir=<dir>] [--oss_file_map=<base64>]");
|
|
11773
11965
|
node_process.default.exit(1);
|
|
11774
11966
|
}
|
|
11775
11967
|
const role = getFlag(args, "role");
|
|
@@ -11783,7 +11975,7 @@ async function main() {
|
|
|
11783
11975
|
let installOssFileMap;
|
|
11784
11976
|
let rawForTelemetry;
|
|
11785
11977
|
if (!ossFileMapFlag) {
|
|
11786
|
-
rawForTelemetry =
|
|
11978
|
+
rawForTelemetry = await fetchCtxViaInnerApi({
|
|
11787
11979
|
populate: planCtxPopulate({ command: "install" }),
|
|
11788
11980
|
caller,
|
|
11789
11981
|
traceId
|
|
@@ -11858,9 +12050,11 @@ async function main() {
|
|
|
11858
12050
|
break;
|
|
11859
12051
|
}
|
|
11860
12052
|
case "upgrade-lark": {
|
|
12053
|
+
const checkOnly = args.includes("--check-only");
|
|
11861
12054
|
const result = runUpgradeLark({
|
|
11862
12055
|
runId: rc.runId,
|
|
11863
|
-
scene
|
|
12056
|
+
scene,
|
|
12057
|
+
checkOnly
|
|
11864
12058
|
});
|
|
11865
12059
|
const upgradeDurationMs = Date.now() - t0;
|
|
11866
12060
|
console.log(JSON.stringify(result));
|
|
@@ -11872,7 +12066,8 @@ async function main() {
|
|
|
11872
12066
|
exitCode: result.exitCode,
|
|
11873
12067
|
rollbackOk: result.rollbackOk,
|
|
11874
12068
|
validationError: result.validationError,
|
|
11875
|
-
error: result.error
|
|
12069
|
+
error: result.error,
|
|
12070
|
+
timing: result.timing
|
|
11876
12071
|
});
|
|
11877
12072
|
try {
|
|
11878
12073
|
await reportCliRun({
|
|
@@ -11890,7 +12085,7 @@ async function main() {
|
|
|
11890
12085
|
} catch (e) {
|
|
11891
12086
|
console.error(`[telemetry] reportCliRun failed: ${e.message}`);
|
|
11892
12087
|
}
|
|
11893
|
-
if (!result.ok) {
|
|
12088
|
+
if (!result.ok || checkOnly && result.upgradeNeeded) {
|
|
11894
12089
|
node_process.default.exitCode = 1;
|
|
11895
12090
|
return;
|
|
11896
12091
|
}
|