@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.
Files changed (2) hide show
  1. package/dist/index.cjs +830 -635
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -52,7 +52,7 @@ node_assert = __toESM(node_assert);
52
52
  * it terse and parseable.
53
53
  */
54
54
  function getVersion() {
55
- return "0.1.15-alpha.0";
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$1 = [LARK_PLUGIN, LEGACY_LARK_PLUGIN];
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$1.some((name) => allow.includes(name))) return { pass: true };
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$1.some((name) => stringAllow.includes(name))) return;
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
- * Core predicate: returns true when the lark plugin needs upgrading for a
3424
- * non-fork plugin, based on version compatibility with the current openclaw.
3758
+ * 核心判断:非 fork 插件是否需要升级 lark,基于当前 openclaw 版本的兼容性。
3759
+ *
3760
+ * 被 FeishuPluginLarkUpgradeRule.validate 和 needsLarkUpgrade 共用。
3761
+ * 调用方需在调用前自行处理 fork 插件的情况(fork 插件不走本函数)。
3425
3762
  *
3426
- * Shared by FeishuPluginLarkUpgradeRule.validate and needsLarkUpgrade.
3427
- * Callers must handle fork plugin cases before invoking this function.
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
- * Returns true if the installed feishu plugin is version-incompatible with
3530
- * the current openclaw (or is a legacy plugin that must be replaced).
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$1() {
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$1()) return { pass: true };
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$1()) return;
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 --ctx=base64
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(ctxBase64) {
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
- if (!isLarkCliAvailable()) {
5096
- console.error("lark-cli-init: skipping lark-cli command not found");
5097
- return {
5098
- ok: true,
5099
- skipped: true,
5100
- skipReason: "lark-cli command not found"
5101
- };
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 config = readConfig(configPath);
5104
- if (!config) return {
5105
- ok: false,
5106
- error: `could not read config at ${configPath}`
5107
- };
5108
- const agentsMdPath = resolveAgentsMdPath(opts.appId, config);
5109
- console.error(`lark-cli-init: resolved agents.md path=${agentsMdPath ?? "(null)"}`);
5110
- if (!agentsMdPath) return {
5111
- ok: false,
5112
- error: `could not resolve agents.md path for appId=${opts.appId}`
5113
- };
5114
- const appSecret = resolveAppSecret(opts.appId, config, opts.feishuAppSecret);
5115
- if (!appSecret) return {
5116
- ok: false,
5117
- error: `could not resolve appSecret for appId=${opts.appId}`
5118
- };
5119
- console.error(`lark-cli-init: running lark-cli config init --name ${opts.appId} --app-id ${opts.appId} --brand feishu --app-secret-stdin --force-init`);
5120
- const initRes = (0, node_child_process.spawnSync)("lark-cli", [
5121
- "config",
5122
- "init",
5123
- "--name",
5124
- opts.appId,
5125
- "--app-id",
5126
- opts.appId,
5127
- "--brand",
5128
- "feishu",
5129
- "--app-secret-stdin",
5130
- "--force-init"
5131
- ], {
5132
- stdio: [
5133
- "pipe",
5134
- "pipe",
5135
- "pipe"
5136
- ],
5137
- encoding: "utf-8",
5138
- input: appSecret
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.0`;
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 [--ctx=<base64>]
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 }. Used by sandbox_console's push-style callers
10527
- that already own the ctx — end-users should prefer \`doctor\`.
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 [--ctx=<base64>]
10701
+ ${BIN} repair
10540
10702
 
10541
10703
  DESCRIPTION
10542
- Runs repair for the failing rules listed inside the ctx's repairData.
10543
- Intended for sandbox_console's push path — end-users should use
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 [--ctx=<base64>]
10557
- ${BIN} reset --worker --task-id=<id> [--ctx=<base64>]
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> [--ctx=<base64> | --oss_file_map=<base64>]
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. Wins over --ctx when both provided.
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
- --ctx=<base64> Opaque ctx; see install-openclaw for semantics.
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
- --ctx=<base64> Opaque ctx; ossFileMap is extracted from it.
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 (rule-driven)
10922
- * repair app + secrets (writes secretsContent / providerKeyContent)
10923
- * reset app + secrets + install + reset (the works)
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
- const appFields = planVarsFields({
11098
+ if (planVarsFields({
10935
11099
  disabled: opts.disabled,
10936
11100
  onlyRules: opts.onlyRules,
10937
11101
  profile: opts.profile
10938
- });
10939
- if (appFields.length > 0) populate.app = appFields;
10940
- if (opts.command === "repair") populate.secrets = true;
10941
- else if (opts.command === "reset") {
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
- /** Plugin directories under extensions/ that are backed up before upgrade */
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
- /** Run channels probe, log results, and return the result. Never throws. */
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 = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
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 = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
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
- const ctxArg = args.find((a) => a.startsWith("--ctx="));
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 = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
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 [--ctx=<base64>] | reset --worker --task-id=<id> [--ctx=<base64>]");
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> [--ctx=<base64> | --oss_file_map=<base64>]");
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 = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
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] [--ctx=<base64> | --oss_file_map=<base64>]");
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 = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
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>] [--ctx=<base64> | --oss_file_map=<base64>]");
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>] [--ctx=<base64> | --oss_file_map=<base64>]");
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 = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
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>] [--ctx=<base64> | --oss_file_map=<base64>]");
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 = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
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
  }