@lark-apaas/openclaw-scripts-diagnose-cli 0.1.15-alpha.4 → 0.1.15-alpha.5

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 +1136 -321
  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.4";
55
+ return "0.1.15-alpha.5";
56
56
  }
57
57
  //#endregion
58
58
  //#region src/rule-engine/base.ts
@@ -2520,6 +2520,345 @@ 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
+ /**
2547
+ * upgrade-lark 每次运行的日志文件路径,含时间戳便于按时间排序定位。
2548
+ * checkOnly=true 时文件名含 "-check" 后缀,便于与正式安装日志区分。
2549
+ */
2550
+ function upgradeLarkLogFile(runId, checkOnly = false) {
2551
+ const ts = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/:/g, "-");
2552
+ return `${DIAGNOSE_DIR}/upgrade-lark${checkOnly ? "-check" : ""}-${ts}-${runId.slice(0, 8)}.log`;
2553
+ }
2554
+ //#endregion
2555
+ //#region src/lark-cli-init.ts
2556
+ const LARK_PLUGIN_NAMES$1 = ["openclaw-lark", "feishu-openclaw-plugin"];
2557
+ const PE_XML_TAG = "lark-cli-pe";
2558
+ const PE_PLACEHOLDER = `
2559
+ <${PE_XML_TAG}>
2560
+ **【强制要求 - 无例外】飞书所有操作必须先读 feishu-lark-cli skill。未读skill不得调用任何飞书工具,违者视为操作失误。**
2561
+ </${PE_XML_TAG}>
2562
+ `;
2563
+ function isLarkPluginInstalled(configPath) {
2564
+ const extDir = getExtensionsDir(configPath);
2565
+ return LARK_PLUGIN_NAMES$1.some((name) => {
2566
+ try {
2567
+ return node_fs.default.existsSync(node_path.default.join(extDir, name, "package.json"));
2568
+ } catch {
2569
+ return false;
2570
+ }
2571
+ });
2572
+ }
2573
+ function isLarkCliAvailable$2() {
2574
+ try {
2575
+ return (0, node_child_process.spawnSync)("lark-cli", ["--version"], {
2576
+ encoding: "utf-8",
2577
+ timeout: 5e3,
2578
+ stdio: [
2579
+ "ignore",
2580
+ "pipe",
2581
+ "ignore"
2582
+ ]
2583
+ }).status === 0;
2584
+ } catch {
2585
+ return false;
2586
+ }
2587
+ }
2588
+ function readConfig(configPath) {
2589
+ try {
2590
+ const raw = node_fs.default.readFileSync(configPath, "utf-8");
2591
+ const parsed = loadJSON5().parse(raw);
2592
+ return parsed != null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
2593
+ } catch {
2594
+ return null;
2595
+ }
2596
+ }
2597
+ /**
2598
+ * Resolve the feishu app secret for the given appId.
2599
+ *
2600
+ * Lookup order:
2601
+ * 1. channels.feishu.appSecret (single-agent: feishu.appId === appId)
2602
+ * 2. channels.feishu.accounts[key].appSecret (multi-agent: account.appId === appId)
2603
+ *
2604
+ * Value interpretation:
2605
+ * - string → use directly
2606
+ * - object → secret is managed by a provider; use `feishuAppSecret` param instead
2607
+ *
2608
+ * Returns null when the secret cannot be determined.
2609
+ */
2610
+ function resolveAppSecret(appId, config, feishuAppSecret) {
2611
+ const feishu = getNestedMap(config, "channels", "feishu");
2612
+ if (!feishu) return null;
2613
+ let rawSecret;
2614
+ if (typeof feishu.appId === "string" && feishu.appId === appId) rawSecret = feishu.appSecret;
2615
+ else {
2616
+ const accounts = asRecord(feishu.accounts);
2617
+ if (accounts) for (const [, val] of Object.entries(accounts)) {
2618
+ const account = asRecord(val);
2619
+ if (account?.appId === appId) {
2620
+ rawSecret = account.appSecret ?? feishu.appSecret;
2621
+ break;
2622
+ }
2623
+ }
2624
+ }
2625
+ if (typeof rawSecret === "string" && rawSecret) return rawSecret;
2626
+ if (rawSecret != null && typeof rawSecret === "object") return feishuAppSecret ?? null;
2627
+ return null;
2628
+ }
2629
+ /**
2630
+ * Resolve the agents.md path for the given appId from the openclaw config.
2631
+ *
2632
+ * Case 1: appId matches channels.feishu.appId (single-agent path)
2633
+ * → WORKSPACE_DIR/AGENTS.md
2634
+ *
2635
+ * Case 2: appId found in channels.feishu.accounts (multi-agent path)
2636
+ * → find account key where account.appId === appId
2637
+ * → find binding where match.channel=feishu && match.accountId=that key
2638
+ * → if agentId === 'main' → WORKSPACE_DIR/agents.md
2639
+ * → else find agent in agents.list by id → agent.workspace/agents.md
2640
+ *
2641
+ * Returns null when the path cannot be determined.
2642
+ */
2643
+ function resolveAgentsMdPath(appId, config) {
2644
+ const feishu = getNestedMap(config, "channels", "feishu");
2645
+ if (!feishu) {
2646
+ console.error("resolveAgentsMdPath: channels.feishu not found");
2647
+ return null;
2648
+ }
2649
+ if (typeof feishu.appId === "string" && feishu.appId === appId) {
2650
+ console.error(`resolveAgentsMdPath: case=single-agent feishu.appId=${feishu.appId}`);
2651
+ return node_path.default.join(WORKSPACE_DIR, "workspace", "AGENTS.md");
2652
+ }
2653
+ const accounts = asRecord(feishu.accounts);
2654
+ if (!accounts) {
2655
+ console.error("resolveAgentsMdPath: feishu.accounts not found");
2656
+ return null;
2657
+ }
2658
+ let accountId;
2659
+ for (const [key, val] of Object.entries(accounts)) if (asRecord(val)?.appId === appId) {
2660
+ accountId = key;
2661
+ break;
2662
+ }
2663
+ if (!accountId) {
2664
+ console.error(`resolveAgentsMdPath: no account found with appId=${appId} in feishu.accounts keys=[${Object.keys(accounts).join(",")}]`);
2665
+ return null;
2666
+ }
2667
+ console.error(`resolveAgentsMdPath: found accountId=${accountId}`);
2668
+ const bindings = Array.isArray(config.bindings) ? config.bindings : [];
2669
+ let agentId;
2670
+ for (const b of bindings) {
2671
+ const binding = asRecord(b);
2672
+ if (!binding) continue;
2673
+ const match = asRecord(binding.match);
2674
+ if (match?.channel === "feishu" && match?.accountId === accountId) {
2675
+ if (typeof binding.agentId === "string") {
2676
+ agentId = binding.agentId;
2677
+ break;
2678
+ }
2679
+ }
2680
+ }
2681
+ if (!agentId) {
2682
+ console.error(`resolveAgentsMdPath: no binding found for accountId=${accountId} in bindings(count=${bindings.length})`);
2683
+ return null;
2684
+ }
2685
+ console.error(`resolveAgentsMdPath: found agentId=${agentId}`);
2686
+ if (agentId === "main") {
2687
+ console.error("resolveAgentsMdPath: case=multi-agent-main");
2688
+ return node_path.default.join(WORKSPACE_DIR, "workspace", "AGENTS.md");
2689
+ }
2690
+ const agentsObj = asRecord(config.agents);
2691
+ const list = Array.isArray(agentsObj?.list) ? agentsObj.list : [];
2692
+ for (const a of list) {
2693
+ const agent = asRecord(a);
2694
+ if (agent?.id === agentId) {
2695
+ const ws = typeof agent.workspace === "string" ? agent.workspace : node_path.default.join(WORKSPACE_DIR, "workspace");
2696
+ console.error(`resolveAgentsMdPath: case=multi-agent-custom agentId=${agentId} workspace=${ws}`);
2697
+ return node_path.default.join(ws, "AGENTS.md");
2698
+ }
2699
+ }
2700
+ console.error(`resolveAgentsMdPath: agentId=${agentId} not found in agents.list(count=${list.length})`);
2701
+ return null;
2702
+ }
2703
+ function appendPeToAgentsMd(agentsMdPath) {
2704
+ const dir = node_path.default.dirname(agentsMdPath);
2705
+ if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
2706
+ const existing = node_fs.default.existsSync(agentsMdPath) ? node_fs.default.readFileSync(agentsMdPath, "utf-8") : "";
2707
+ if (existing.includes(`<lark-cli-pe>`)) {
2708
+ console.error(`lark-cli-init: <${PE_XML_TAG}> already present in ${agentsMdPath}, skipping`);
2709
+ return;
2710
+ }
2711
+ const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
2712
+ node_fs.default.appendFileSync(agentsMdPath, `${sep}${PE_PLACEHOLDER}`, "utf-8");
2713
+ console.error(`lark-cli-init: appended PE placeholder to ${agentsMdPath}`);
2714
+ }
2715
+ /**
2716
+ * Collect every Feishu bot appId declared in the openclaw config.
2717
+ * Covers both single-agent (channels.feishu.appId) and multi-agent
2718
+ * (channels.feishu.accounts[*].appId) layouts. Returns a deduplicated list.
2719
+ */
2720
+ function collectFeishuAppIds(configPath) {
2721
+ const config = readConfig(configPath ?? CONFIG_PATH);
2722
+ if (!config) return [];
2723
+ const feishu = getNestedMap(config, "channels", "feishu");
2724
+ if (!feishu) return [];
2725
+ const appIds = /* @__PURE__ */ new Set();
2726
+ const topAppId = feishu.appId;
2727
+ if (typeof topAppId === "string" && topAppId.trim()) appIds.add(topAppId.trim());
2728
+ const accounts = asRecord(feishu.accounts);
2729
+ if (accounts) for (const val of Object.values(accounts)) {
2730
+ const appId = asRecord(val)?.appId;
2731
+ if (typeof appId === "string" && appId.trim()) appIds.add(appId.trim());
2732
+ }
2733
+ return [...appIds];
2734
+ }
2735
+ function runLarkCliInit(opts) {
2736
+ const configPath = opts.configPath ?? CONFIG_PATH;
2737
+ if (!isLarkPluginInstalled(configPath)) {
2738
+ console.error("lark-cli-init: skipping — openclaw-lark plugin not installed");
2739
+ return {
2740
+ ok: true,
2741
+ skipped: true,
2742
+ skipReason: "openclaw-lark plugin not installed"
2743
+ };
2744
+ }
2745
+ if (!isLarkCliAvailable$2()) {
2746
+ console.error("lark-cli-init: skipping — lark-cli command not found");
2747
+ return {
2748
+ ok: true,
2749
+ skipped: true,
2750
+ skipReason: "lark-cli command not found"
2751
+ };
2752
+ }
2753
+ const config = readConfig(configPath);
2754
+ if (!config) return {
2755
+ ok: false,
2756
+ error: `could not read config at ${configPath}`
2757
+ };
2758
+ const agentsMdPath = resolveAgentsMdPath(opts.appId, config);
2759
+ console.error(`lark-cli-init: resolved agents.md path=${agentsMdPath ?? "(null)"}`);
2760
+ if (!agentsMdPath) return {
2761
+ ok: false,
2762
+ error: `could not resolve agents.md path for appId=${opts.appId}`
2763
+ };
2764
+ const appSecret = resolveAppSecret(opts.appId, config, opts.feishuAppSecret);
2765
+ if (!appSecret) return {
2766
+ ok: false,
2767
+ error: `could not resolve appSecret for appId=${opts.appId}`
2768
+ };
2769
+ console.error(`lark-cli-init: running lark-cli config init --name ${opts.appId} --app-id ${opts.appId} --brand feishu --app-secret-stdin --force-init`);
2770
+ const initRes = (0, node_child_process.spawnSync)("lark-cli", [
2771
+ "config",
2772
+ "init",
2773
+ "--name",
2774
+ opts.appId,
2775
+ "--app-id",
2776
+ opts.appId,
2777
+ "--brand",
2778
+ "feishu",
2779
+ "--app-secret-stdin",
2780
+ "--force-init"
2781
+ ], {
2782
+ stdio: [
2783
+ "pipe",
2784
+ "pipe",
2785
+ "pipe"
2786
+ ],
2787
+ encoding: "utf-8",
2788
+ input: appSecret
2789
+ });
2790
+ const configInitStdout = initRes.stdout?.trim() || void 0;
2791
+ const configInitStderr = initRes.stderr?.trim() || void 0;
2792
+ if (configInitStdout) console.error(`lark-cli config init stdout: ${configInitStdout}`);
2793
+ if (configInitStderr) console.error(`lark-cli config init stderr: ${configInitStderr}`);
2794
+ if (initRes.error) return {
2795
+ ok: false,
2796
+ configInitStdout,
2797
+ configInitStderr,
2798
+ error: `lark-cli config init spawn error: ${initRes.error.message}`
2799
+ };
2800
+ if (initRes.status !== 0) return {
2801
+ ok: false,
2802
+ configInitExitCode: initRes.status ?? void 0,
2803
+ configInitStdout,
2804
+ configInitStderr,
2805
+ error: `lark-cli config init exited with code ${initRes.status}`
2806
+ };
2807
+ appendPeToAgentsMd(agentsMdPath);
2808
+ return {
2809
+ ok: true,
2810
+ configInitExitCode: 0,
2811
+ agentsMdPath
2812
+ };
2813
+ }
2814
+ //#endregion
2815
+ //#region src/rules/agents-md-lark-cli-pe.ts
2816
+ function isLarkCliAvailable$1() {
2817
+ try {
2818
+ return (0, node_child_process.spawnSync)("lark-cli", ["--version"], {
2819
+ encoding: "utf-8",
2820
+ timeout: 5e3,
2821
+ stdio: [
2822
+ "ignore",
2823
+ "pipe",
2824
+ "ignore"
2825
+ ]
2826
+ }).status === 0;
2827
+ } catch {
2828
+ return false;
2829
+ }
2830
+ }
2831
+ let AgentsMdLarkCliPeRule = class AgentsMdLarkCliPeRule extends DiagnoseRule {
2832
+ validate(ctx) {
2833
+ if (!isLarkCliAvailable$1()) return { pass: true };
2834
+ const missingPath = collectExistingAgentsMdPaths(ctx).find((filePath) => {
2835
+ return !node_fs.default.readFileSync(filePath, "utf-8").includes(`<${PE_XML_TAG}>`);
2836
+ });
2837
+ if (!missingPath) return { pass: true };
2838
+ return {
2839
+ pass: false,
2840
+ message: `${missingPath} 中缺少 lark-cli-pe PE 内容,需要追加`
2841
+ };
2842
+ }
2843
+ repair(ctx) {
2844
+ if (!isLarkCliAvailable$1()) return;
2845
+ for (const filePath of collectExistingAgentsMdPaths(ctx)) {
2846
+ const content = node_fs.default.readFileSync(filePath, "utf-8");
2847
+ if (content.includes(`<lark-cli-pe>`)) continue;
2848
+ const sep = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
2849
+ node_fs.default.appendFileSync(filePath, `${sep}${PE_PLACEHOLDER}`, "utf-8");
2850
+ console.error(`agents-md-lark-cli-pe: appended PE to ${filePath}`);
2851
+ }
2852
+ }
2853
+ };
2854
+ AgentsMdLarkCliPeRule = __decorate([Rule({
2855
+ key: "agents_md_lark_cli_pe",
2856
+ description: "检测各智能体 AGENTS.md 中是否缺失 lark-cli-pe PE 内容,lark-cli 存在时自动追加",
2857
+ dependsOn: ["config_syntax_check"],
2858
+ repairMode: "standard",
2859
+ level: "silent"
2860
+ })], AgentsMdLarkCliPeRule);
2861
+ //#endregion
2523
2862
  //#region src/rules/miaoda-official-plugins-install-spec-unlock.ts
2524
2863
  /**
2525
2864
  * Official miaoda-side plugins that must track manifest — version-locked specs
@@ -2623,11 +2962,11 @@ function getAllow$1(config) {
2623
2962
  //#region src/rules/lark-plugin-allow.ts
2624
2963
  const LARK_PLUGIN = "openclaw-lark";
2625
2964
  const LEGACY_LARK_PLUGIN = "feishu-openclaw-plugin";
2626
- const LARK_PLUGIN_NAMES$1 = [LARK_PLUGIN, LEGACY_LARK_PLUGIN];
2965
+ const LARK_PLUGIN_NAMES = [LARK_PLUGIN, LEGACY_LARK_PLUGIN];
2627
2966
  let LarkPluginAllowRule = class LarkPluginAllowRule extends DiagnoseRule {
2628
2967
  validate(ctx) {
2629
2968
  const allow = getAllow(ctx.config);
2630
- if (LARK_PLUGIN_NAMES$1.some((name) => allow.includes(name))) return { pass: true };
2969
+ if (LARK_PLUGIN_NAMES.some((name) => allow.includes(name))) return { pass: true };
2631
2970
  const installed = detectInstalledLarkPlugin(getExtensionsDir(ctx.configPath));
2632
2971
  if (installed == null) return { pass: true };
2633
2972
  return {
@@ -2646,7 +2985,7 @@ let LarkPluginAllowRule = class LarkPluginAllowRule extends DiagnoseRule {
2646
2985
  const rawAllow = pluginsMap.allow;
2647
2986
  const original = Array.isArray(rawAllow) ? rawAllow : [];
2648
2987
  const stringAllow = original.filter((e) => typeof e === "string");
2649
- if (LARK_PLUGIN_NAMES$1.some((name) => stringAllow.includes(name))) return;
2988
+ if (LARK_PLUGIN_NAMES.some((name) => stringAllow.includes(name))) return;
2650
2989
  original.push(installed);
2651
2990
  pluginsMap.allow = original;
2652
2991
  }
@@ -3347,7 +3686,6 @@ function resolveUpgradeDirection(installed, ocCur, recommendedOc, isLegacy) {
3347
3686
  /** 提取公共前置上下文;任何前置条件不满足时返回 null(规则 pass)。 */
3348
3687
  function resolveCompatContext(ctx) {
3349
3688
  const recommendedOc = ctx.vars.recommendedOpenclawTag;
3350
- if (!recommendedOc) return null;
3351
3689
  const ocCur = getOcVersion();
3352
3690
  if (!ocCur) return null;
3353
3691
  const installed = getInstalledPlugin(ctx);
@@ -3370,6 +3708,7 @@ let FeishuPluginOpenclawUpgradeRule = class FeishuPluginOpenclawUpgradeRule exte
3370
3708
  if (!cc) return { pass: true };
3371
3709
  const { ocCur, recommendedOc, installed, isLegacy } = cc;
3372
3710
  if (isForkPlugin(installed)) return validateForkPlugin(installed, ocCur, recommendedOc);
3711
+ if (!recommendedOc) return { pass: true };
3373
3712
  if (resolveUpgradeDirection(installed, ocCur, recommendedOc, isLegacy) !== "openclaw") return { pass: true };
3374
3713
  return {
3375
3714
  pass: false,
@@ -3397,11 +3736,17 @@ let FeishuPluginLarkUpgradeRule = class FeishuPluginLarkUpgradeRule extends Diag
3397
3736
  if (!cc) return { pass: true };
3398
3737
  const { ocCur, recommendedOc, installed, isLegacy } = cc;
3399
3738
  if (isForkPlugin(installed)) return { pass: true };
3400
- if (resolveUpgradeDirection(installed, ocCur, recommendedOc, isLegacy) !== "lark") return { pass: true };
3739
+ if (!isLarkUpgradeNeededFromCC(cc)) return { pass: true };
3740
+ const prefix = buildCompatPrefix(installed, ocCur, isLegacy);
3741
+ if (!recommendedOc) return {
3742
+ pass: false,
3743
+ action: "upgrade_lark",
3744
+ message: `${prefix};建议升级飞书插件至兼容版本`
3745
+ };
3401
3746
  return {
3402
3747
  pass: false,
3403
3748
  action: "upgrade_lark",
3404
- message: `${buildCompatPrefix(installed, ocCur, isLegacy)};当前 openclaw@${ocCur} 已达推荐版本,可直接升级飞书插件`
3749
+ message: `${prefix};当前 openclaw@${ocCur} 已达推荐版本,可直接升级飞书插件`
3405
3750
  };
3406
3751
  }
3407
3752
  };
@@ -3413,6 +3758,21 @@ FeishuPluginLarkUpgradeRule = __decorate([Rule({
3413
3758
  level: "critical",
3414
3759
  usesVars: ["recommendedOpenclawTag"]
3415
3760
  })], FeishuPluginLarkUpgradeRule);
3761
+ /**
3762
+ * 核心判断:非 fork 插件是否需要升级 lark,基于当前 openclaw 版本的兼容性。
3763
+ *
3764
+ * 被 FeishuPluginLarkUpgradeRule.validate 和 needsLarkUpgrade 共用。
3765
+ * 调用方需在调用前自行处理 fork 插件的情况(fork 插件不走本函数)。
3766
+ *
3767
+ * - 有 recommendedOc:走 resolveUpgradeDirection 判断方向是否为 'lark'
3768
+ * - 无 recommendedOc(doctor 无推荐版本):legacy 插件直接需要升级;
3769
+ * 非 legacy 则检查当前版本是否在兼容表内
3770
+ */
3771
+ function isLarkUpgradeNeededFromCC(cc) {
3772
+ const { ocCur, recommendedOc, installed, isLegacy } = cc;
3773
+ if (!recommendedOc) return isLegacy || !isVersionCompatible(installed, ocCur);
3774
+ return resolveUpgradeDirection(installed, ocCur, recommendedOc, isLegacy) === "lark";
3775
+ }
3416
3776
  function isForkPlugin(p) {
3417
3777
  return p.scope != null && FORK_SCOPES.includes(p.scope);
3418
3778
  }
@@ -3451,14 +3811,16 @@ function describeCompatConstraint(entry, pluginVersion) {
3451
3811
  /**
3452
3812
  * @lark-apaas/openclaw-lark 豁免 VERSION_COMPAT_MAP,但仍要求 openclaw ≥ FORK_LARK_PLUGIN_MIN_OC_VERSION。
3453
3813
  * 其他 @lark-apaas scope 的 fork 插件继续无条件 pass。
3814
+ * recommendedOc 可为 undefined(doctor 模式),此时只检测最低版本要求,不指定目标升级版本。
3454
3815
  */
3455
3816
  function validateForkPlugin(installed, ocCur, recommendedOc) {
3456
3817
  if (installed.fullName !== FORK_LARK_PLUGIN_FULL_NAME) return { pass: true };
3457
3818
  if (compareCalVer(ocCur, FORK_LARK_PLUGIN_MIN_OC_VERSION) >= 0) return { pass: true };
3819
+ const recommendation = recommendedOc ? `;将 openclaw 升级到 ${recommendedOc} 即可满足` : `;请升级 openclaw 至 ${FORK_LARK_PLUGIN_MIN_OC_VERSION} 或更高版本`;
3458
3820
  return {
3459
3821
  pass: false,
3460
3822
  action: "upgrade_openclaw",
3461
- message: `飞书插件 ${describePlugin(installed)}(fork 版)要求 openclaw ≥ ${FORK_LARK_PLUGIN_MIN_OC_VERSION},当前 openclaw@${ocCur} 低于此要求;将 openclaw 升级到 ${recommendedOc} 即可满足`
3823
+ message: `飞书插件 ${describePlugin(installed)}(fork 版)要求 openclaw ≥ ${FORK_LARK_PLUGIN_MIN_OC_VERSION},当前 openclaw@${ocCur} 低于此要求${recommendation}`
3462
3824
  };
3463
3825
  }
3464
3826
  function describePlugin(p) {
@@ -3505,6 +3867,26 @@ function extractScopedNameFromSpec$1(spec) {
3505
3867
  const at = spec.indexOf("@", 1);
3506
3868
  return at === -1 ? spec : spec.slice(0, at);
3507
3869
  }
3870
+ /**
3871
+ * 判断已安装的飞书插件是否与当前 openclaw 版本不兼容(或为需要替换的 legacy 插件)。
3872
+ * 被 upgrade-lark 前置检测门控(--check-only 和正式安装模式)调用。
3873
+ */
3874
+ function needsLarkUpgrade(ctx) {
3875
+ const cc = resolveCompatContext({
3876
+ ...ctx,
3877
+ vars: {
3878
+ ...ctx.vars,
3879
+ recommendedOpenclawTag: void 0
3880
+ }
3881
+ });
3882
+ if (!cc) return false;
3883
+ const { ocCur, installed } = cc;
3884
+ if (isForkPlugin(installed)) {
3885
+ if (installed.fullName === FORK_LARK_PLUGIN_FULL_NAME) return compareCalVer(ocCur, FORK_LARK_PLUGIN_MIN_OC_VERSION) < 0;
3886
+ return false;
3887
+ }
3888
+ return isLarkUpgradeNeededFromCC(cc);
3889
+ }
3508
3890
  //#endregion
3509
3891
  //#region src/rules/cleanup-install-backup-dirs.ts
3510
3892
  const DIR_PREFIX = ".openclaw-install-";
@@ -3588,7 +3970,7 @@ function extractScopedNameFromSpec(spec) {
3588
3970
  const at = spec.indexOf("@", 1);
3589
3971
  return at === -1 ? spec : spec.slice(0, at);
3590
3972
  }
3591
- function isLarkCliAvailable$1() {
3973
+ function isLarkCliAvailable() {
3592
3974
  try {
3593
3975
  return (0, node_child_process.spawnSync)(LARK_CLI_NAME$1, ["--version"], {
3594
3976
  encoding: "utf-8",
@@ -3629,7 +4011,7 @@ function installLarkCliOnce(tag) {
3629
4011
  let LarkCliMissingForInstalledLarkPluginRule = class LarkCliMissingForInstalledLarkPluginRule extends DiagnoseRule {
3630
4012
  validate(ctx) {
3631
4013
  if (!isTargetForkPlugin(readInstalledLarkPlugin(ctx))) return { pass: true };
3632
- if (isLarkCliAvailable$1()) return { pass: true };
4014
+ if (isLarkCliAvailable()) return { pass: true };
3633
4015
  return {
3634
4016
  pass: false,
3635
4017
  message: `${FORK_PACKAGE_NAME}@${TARGET_VERSION} 已安装,但 lark-cli 不可用;将执行一次 lark-cli 安装`
@@ -3637,7 +4019,7 @@ let LarkCliMissingForInstalledLarkPluginRule = class LarkCliMissingForInstalledL
3637
4019
  }
3638
4020
  repair(ctx) {
3639
4021
  if (!isTargetForkPlugin(readInstalledLarkPlugin(ctx))) return;
3640
- if (isLarkCliAvailable$1()) return;
4022
+ if (isLarkCliAvailable()) return;
3641
4023
  installLarkCliOnce(ctx.vars.recommendedOpenclawTag ?? TARGET_VERSION);
3642
4024
  }
3643
4025
  };
@@ -3703,7 +4085,7 @@ let FeishuBotChannelConfigRule = class FeishuBotChannelConfigRule extends Diagno
3703
4085
  if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) issues.push(`${label} appSecret is a provider-ref but not the canonical one`);
3704
4086
  } else if (typeof secret === "string") {
3705
4087
  if (secret !== larkApp.appSecret) issues.push(`${label} appSecret plaintext mismatch`);
3706
- } else issues.push(`${label} appSecret is missing or has unexpected type`);
4088
+ } else issues.push(`${label} appSecret has unexpected type ${typeof secret}`);
3707
4089
  }
3708
4090
  repair(ctx) {
3709
4091
  const larkApps = ctx.vars.larkApps;
@@ -3745,7 +4127,7 @@ let FeishuBotChannelConfigRule = class FeishuBotChannelConfigRule extends Diagno
3745
4127
  if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) bot.appSecret = { ...DEFAULT_FEISHU_APP_SECRET };
3746
4128
  } else if (typeof secret === "string") {
3747
4129
  if (secret !== larkApp.appSecret) bot.appSecret = larkApp.appSecret;
3748
- } else bot.appSecret = larkApp.appSecret;
4130
+ }
3749
4131
  }
3750
4132
  };
3751
4133
  FeishuBotChannelConfigRule = __decorate([Rule({
@@ -4196,34 +4578,10 @@ function finalize$1(results, aborted) {
4196
4578
  }
4197
4579
  return {
4198
4580
  results,
4199
- summary,
4200
- aborted
4201
- };
4202
- }
4203
- //#endregion
4204
- //#region src/paths.ts
4205
- /**
4206
- * Central directory for all ephemeral diagnose/reset artifacts: task status
4207
- * files (`reset-<taskId>.json`) and human-readable step logs
4208
- * (`reset-<taskId>.log`). Having everything under one dir makes debugging a
4209
- * stuck reset much easier — `ls /tmp/openclaw-diagnose/` shows every recent
4210
- * run, and each run's log is right next to its state.
4211
- */
4212
- const DIAGNOSE_DIR = "/tmp/openclaw-diagnose";
4213
- function resetResultFile(taskId) {
4214
- return `${DIAGNOSE_DIR}/reset-${taskId}.json`;
4215
- }
4216
- function resetLogFile(taskId) {
4217
- return `${DIAGNOSE_DIR}/reset-${taskId}.log`;
4218
- }
4219
- /** Sandbox workspace root where openclaw config + agent state lives. */
4220
- const WORKSPACE_DIR = "/home/gem/workspace/agent";
4221
- /** File containing the provider key used by the openclaw miaoda provider. */
4222
- const PROVIDER_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-provider-key";
4223
- /** File containing the miaoda openclaw secrets JSON. */
4224
- const SECRETS_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-openclaw-secrets.json";
4225
- /** Absolute path to the openclaw config JSON. */
4226
- const CONFIG_PATH = `${WORKSPACE_DIR}/openclaw.json`;
4581
+ summary,
4582
+ aborted
4583
+ };
4584
+ }
4227
4585
  //#endregion
4228
4586
  //#region src/run-log.ts
4229
4587
  let currentRunContext;
@@ -4776,286 +5134,26 @@ function installOne$1(pkg, tarball, homeBase) {
4776
5134
  recursive: true,
4777
5135
  force: true
4778
5136
  });
4779
- node_fs.default.mkdirSync(stagingDir);
4780
- try {
4781
- extractTarballTolerant(tarball, stagingDir, { stripComponents: 1 });
4782
- if (!node_fs.default.existsSync(node_path.default.join(stagingDir, "package.json"))) throw new Error(`extension tarball missing package.json: ${pkg.name}`);
4783
- } catch (e) {
4784
- try {
4785
- node_fs.default.rmSync(stagingDir, {
4786
- recursive: true,
4787
- force: true
4788
- });
4789
- } catch {}
4790
- throw e;
4791
- }
4792
- const hadOld = node_fs.default.existsSync(destDir);
4793
- if (hadOld) moveSafe(destDir, oldDir);
4794
- moveSafe(stagingDir, destDir);
4795
- if (hadOld && node_fs.default.existsSync(oldDir)) node_fs.default.rmSync(oldDir, {
4796
- recursive: true,
4797
- force: true
4798
- });
4799
- }
4800
- //#endregion
4801
- //#region src/lark-cli-init.ts
4802
- const LARK_PLUGIN_NAMES = ["openclaw-lark", "feishu-openclaw-plugin"];
4803
- const PE_XML_TAG = "lark-cli-pe";
4804
- const PE_PLACEHOLDER = `
4805
- <${PE_XML_TAG}>
4806
- **【强制要求 - 无例外】飞书所有操作必须先读 feishu-lark-cli skill。未读skill不得调用任何飞书工具,违者视为操作失误。**
4807
- </${PE_XML_TAG}>
4808
- `;
4809
- function isLarkPluginInstalled(configPath) {
4810
- const extDir = getExtensionsDir(configPath);
4811
- return LARK_PLUGIN_NAMES.some((name) => {
4812
- try {
4813
- return node_fs.default.existsSync(node_path.default.join(extDir, name, "package.json"));
4814
- } catch {
4815
- return false;
4816
- }
4817
- });
4818
- }
4819
- function isLarkCliAvailable() {
4820
- try {
4821
- return (0, node_child_process.spawnSync)("lark-cli", ["--version"], {
4822
- encoding: "utf-8",
4823
- timeout: 5e3,
4824
- stdio: [
4825
- "ignore",
4826
- "pipe",
4827
- "ignore"
4828
- ]
4829
- }).status === 0;
4830
- } catch {
4831
- return false;
4832
- }
4833
- }
4834
- function readConfig(configPath) {
4835
- try {
4836
- const raw = node_fs.default.readFileSync(configPath, "utf-8");
4837
- const parsed = loadJSON5().parse(raw);
4838
- return parsed != null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
4839
- } catch {
4840
- return null;
4841
- }
4842
- }
4843
- /**
4844
- * Resolve the feishu app secret for the given appId.
4845
- *
4846
- * Lookup order:
4847
- * 1. channels.feishu.appSecret (single-agent: feishu.appId === appId)
4848
- * 2. channels.feishu.accounts[key].appSecret (multi-agent: account.appId === appId)
4849
- *
4850
- * Value interpretation:
4851
- * - string → use directly
4852
- * - object → secret is managed by a provider; use `feishuAppSecret` param instead
4853
- *
4854
- * Returns null when the secret cannot be determined.
4855
- */
4856
- function resolveAppSecret(appId, config, feishuAppSecret) {
4857
- const feishu = getNestedMap(config, "channels", "feishu");
4858
- if (!feishu) return null;
4859
- let rawSecret;
4860
- if (typeof feishu.appId === "string" && feishu.appId === appId) rawSecret = feishu.appSecret;
4861
- else {
4862
- const accounts = asRecord(feishu.accounts);
4863
- if (accounts) for (const [, val] of Object.entries(accounts)) {
4864
- const account = asRecord(val);
4865
- if (account?.appId === appId) {
4866
- rawSecret = account.appSecret ?? feishu.appSecret;
4867
- break;
4868
- }
4869
- }
4870
- }
4871
- if (typeof rawSecret === "string" && rawSecret) return rawSecret;
4872
- if (rawSecret != null && typeof rawSecret === "object") return feishuAppSecret ?? null;
4873
- return null;
4874
- }
4875
- /**
4876
- * Resolve the agents.md path for the given appId from the openclaw config.
4877
- *
4878
- * Case 1: appId matches channels.feishu.appId (single-agent path)
4879
- * → WORKSPACE_DIR/AGENTS.md
4880
- *
4881
- * Case 2: appId found in channels.feishu.accounts (multi-agent path)
4882
- * → find account key where account.appId === appId
4883
- * → find binding where match.channel=feishu && match.accountId=that key
4884
- * → if agentId === 'main' → WORKSPACE_DIR/agents.md
4885
- * → else find agent in agents.list by id → agent.workspace/agents.md
4886
- *
4887
- * Returns null when the path cannot be determined.
4888
- */
4889
- function resolveAgentsMdPath(appId, config) {
4890
- const feishu = getNestedMap(config, "channels", "feishu");
4891
- if (!feishu) {
4892
- console.error("resolveAgentsMdPath: channels.feishu not found");
4893
- return null;
4894
- }
4895
- if (typeof feishu.appId === "string" && feishu.appId === appId) {
4896
- console.error(`resolveAgentsMdPath: case=single-agent feishu.appId=${feishu.appId}`);
4897
- return node_path.default.join(WORKSPACE_DIR, "workspace", "AGENTS.md");
4898
- }
4899
- const accounts = asRecord(feishu.accounts);
4900
- if (!accounts) {
4901
- console.error("resolveAgentsMdPath: feishu.accounts not found");
4902
- return null;
4903
- }
4904
- let accountId;
4905
- for (const [key, val] of Object.entries(accounts)) if (asRecord(val)?.appId === appId) {
4906
- accountId = key;
4907
- break;
4908
- }
4909
- if (!accountId) {
4910
- console.error(`resolveAgentsMdPath: no account found with appId=${appId} in feishu.accounts keys=[${Object.keys(accounts).join(",")}]`);
4911
- return null;
4912
- }
4913
- console.error(`resolveAgentsMdPath: found accountId=${accountId}`);
4914
- const bindings = Array.isArray(config.bindings) ? config.bindings : [];
4915
- let agentId;
4916
- for (const b of bindings) {
4917
- const binding = asRecord(b);
4918
- if (!binding) continue;
4919
- const match = asRecord(binding.match);
4920
- if (match?.channel === "feishu" && match?.accountId === accountId) {
4921
- if (typeof binding.agentId === "string") {
4922
- agentId = binding.agentId;
4923
- break;
4924
- }
4925
- }
4926
- }
4927
- if (!agentId) {
4928
- console.error(`resolveAgentsMdPath: no binding found for accountId=${accountId} in bindings(count=${bindings.length})`);
4929
- return null;
4930
- }
4931
- console.error(`resolveAgentsMdPath: found agentId=${agentId}`);
4932
- if (agentId === "main") {
4933
- console.error("resolveAgentsMdPath: case=multi-agent-main");
4934
- return node_path.default.join(WORKSPACE_DIR, "workspace", "AGENTS.md");
4935
- }
4936
- const agentsObj = asRecord(config.agents);
4937
- const list = Array.isArray(agentsObj?.list) ? agentsObj.list : [];
4938
- for (const a of list) {
4939
- const agent = asRecord(a);
4940
- if (agent?.id === agentId) {
4941
- const ws = typeof agent.workspace === "string" ? agent.workspace : node_path.default.join(WORKSPACE_DIR, "workspace");
4942
- console.error(`resolveAgentsMdPath: case=multi-agent-custom agentId=${agentId} workspace=${ws}`);
4943
- return node_path.default.join(ws, "AGENTS.md");
4944
- }
4945
- }
4946
- console.error(`resolveAgentsMdPath: agentId=${agentId} not found in agents.list(count=${list.length})`);
4947
- return null;
4948
- }
4949
- function appendPeToAgentsMd(agentsMdPath) {
4950
- const dir = node_path.default.dirname(agentsMdPath);
4951
- if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
4952
- const existing = node_fs.default.existsSync(agentsMdPath) ? node_fs.default.readFileSync(agentsMdPath, "utf-8") : "";
4953
- if (existing.includes(`<${PE_XML_TAG}>`)) {
4954
- console.error(`lark-cli-init: <${PE_XML_TAG}> already present in ${agentsMdPath}, skipping`);
4955
- return;
4956
- }
4957
- const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
4958
- node_fs.default.appendFileSync(agentsMdPath, `${sep}${PE_PLACEHOLDER}`, "utf-8");
4959
- console.error(`lark-cli-init: appended PE placeholder to ${agentsMdPath}`);
4960
- }
4961
- /**
4962
- * Collect every Feishu bot appId declared in the openclaw config.
4963
- * Covers both single-agent (channels.feishu.appId) and multi-agent
4964
- * (channels.feishu.accounts[*].appId) layouts. Returns a deduplicated list.
4965
- */
4966
- function collectFeishuAppIds(configPath) {
4967
- const config = readConfig(configPath ?? CONFIG_PATH);
4968
- if (!config) return [];
4969
- const feishu = getNestedMap(config, "channels", "feishu");
4970
- if (!feishu) return [];
4971
- const appIds = /* @__PURE__ */ new Set();
4972
- const topAppId = feishu.appId;
4973
- if (typeof topAppId === "string" && topAppId.trim()) appIds.add(topAppId.trim());
4974
- const accounts = asRecord(feishu.accounts);
4975
- if (accounts) for (const val of Object.values(accounts)) {
4976
- const appId = asRecord(val)?.appId;
4977
- if (typeof appId === "string" && appId.trim()) appIds.add(appId.trim());
4978
- }
4979
- return [...appIds];
4980
- }
4981
- function runLarkCliInit(opts) {
4982
- const configPath = opts.configPath ?? CONFIG_PATH;
4983
- if (!isLarkPluginInstalled(configPath)) {
4984
- console.error("lark-cli-init: skipping — openclaw-lark plugin not installed");
4985
- return {
4986
- ok: true,
4987
- skipped: true,
4988
- skipReason: "openclaw-lark plugin not installed"
4989
- };
4990
- }
4991
- if (!isLarkCliAvailable()) {
4992
- console.error("lark-cli-init: skipping — lark-cli command not found");
4993
- return {
4994
- ok: true,
4995
- skipped: true,
4996
- skipReason: "lark-cli command not found"
4997
- };
4998
- }
4999
- const config = readConfig(configPath);
5000
- if (!config) return {
5001
- ok: false,
5002
- error: `could not read config at ${configPath}`
5003
- };
5004
- const agentsMdPath = resolveAgentsMdPath(opts.appId, config);
5005
- console.error(`lark-cli-init: resolved agents.md path=${agentsMdPath ?? "(null)"}`);
5006
- if (!agentsMdPath) return {
5007
- ok: false,
5008
- error: `could not resolve agents.md path for appId=${opts.appId}`
5009
- };
5010
- const appSecret = resolveAppSecret(opts.appId, config, opts.feishuAppSecret);
5011
- if (!appSecret) return {
5012
- ok: false,
5013
- error: `could not resolve appSecret for appId=${opts.appId}`
5014
- };
5015
- console.error(`lark-cli-init: running lark-cli config init --name ${opts.appId} --app-id ${opts.appId} --brand feishu --app-secret-stdin --force-init`);
5016
- const initRes = (0, node_child_process.spawnSync)("lark-cli", [
5017
- "config",
5018
- "init",
5019
- "--name",
5020
- opts.appId,
5021
- "--app-id",
5022
- opts.appId,
5023
- "--brand",
5024
- "feishu",
5025
- "--app-secret-stdin",
5026
- "--force-init"
5027
- ], {
5028
- stdio: [
5029
- "pipe",
5030
- "pipe",
5031
- "pipe"
5032
- ],
5033
- encoding: "utf-8",
5034
- input: appSecret
5137
+ node_fs.default.mkdirSync(stagingDir);
5138
+ try {
5139
+ extractTarballTolerant(tarball, stagingDir, { stripComponents: 1 });
5140
+ if (!node_fs.default.existsSync(node_path.default.join(stagingDir, "package.json"))) throw new Error(`extension tarball missing package.json: ${pkg.name}`);
5141
+ } catch (e) {
5142
+ try {
5143
+ node_fs.default.rmSync(stagingDir, {
5144
+ recursive: true,
5145
+ force: true
5146
+ });
5147
+ } catch {}
5148
+ throw e;
5149
+ }
5150
+ const hadOld = node_fs.default.existsSync(destDir);
5151
+ if (hadOld) moveSafe(destDir, oldDir);
5152
+ moveSafe(stagingDir, destDir);
5153
+ if (hadOld && node_fs.default.existsSync(oldDir)) node_fs.default.rmSync(oldDir, {
5154
+ recursive: true,
5155
+ force: true
5035
5156
  });
5036
- const configInitStdout = initRes.stdout?.trim() || void 0;
5037
- const configInitStderr = initRes.stderr?.trim() || void 0;
5038
- if (configInitStdout) console.error(`lark-cli config init stdout: ${configInitStdout}`);
5039
- if (configInitStderr) console.error(`lark-cli config init stderr: ${configInitStderr}`);
5040
- if (initRes.error) return {
5041
- ok: false,
5042
- configInitStdout,
5043
- configInitStderr,
5044
- error: `lark-cli config init spawn error: ${initRes.error.message}`
5045
- };
5046
- if (initRes.status !== 0) return {
5047
- ok: false,
5048
- configInitExitCode: initRes.status ?? void 0,
5049
- configInitStdout,
5050
- configInitStderr,
5051
- error: `lark-cli config init exited with code ${initRes.status}`
5052
- };
5053
- appendPeToAgentsMd(agentsMdPath);
5054
- return {
5055
- ok: true,
5056
- configInitExitCode: 0,
5057
- agentsMdPath
5058
- };
5059
5157
  }
5060
5158
  //#endregion
5061
5159
  //#region ../../openclaw-slardar/lib/client.js
@@ -10292,6 +10390,139 @@ function finalize(results, aborted) {
10292
10390
  };
10293
10391
  }
10294
10392
  //#endregion
10393
+ //#region src/channels-probe.ts
10394
+ const FEISHU_INVALID_CONFIG_MSG = "channels.feishu: invalid config: must NOT have additional properties";
10395
+ const CHANNEL_LINE_RE = /^-\s+(?:Feishu|openclaw-lark|@larksuite\/openclaw-lark)\s+([^:]+):\s+(.+)$/i;
10396
+ /**
10397
+ * 判断单个飞书账号是否处于"可用"状态。
10398
+ * 移植自 feishu-channel-success-rate skill 的 Python `_account_is_working`。
10399
+ *
10400
+ * 会先剥离 "key:value" 形式的 bit(dm:、bot:、in:、out:、token:、allow:、
10401
+ * intents:、groups:、health: 等),再按以下公式判断:
10402
+ *
10403
+ * @param ignoreProbeFailed 为 true 时忽略 "probe failed" bit,不视为失败。
10404
+ * 升级前置检测中传 true,因为 probe 失败通常反映网络状况而非配置问题。
10405
+ * @param gatewayReachable 为 false 时(gateway 尚未启动),只要 enabled+configured
10406
+ * 即视为可用;为 true 时还需要 running/works 且无 error: bit。
10407
+ */
10408
+ function accountIsWorking(bits, ignoreProbeFailed = true, gatewayReachable = true) {
10409
+ const bitTokens = /* @__PURE__ */ new Set();
10410
+ let hasError = false;
10411
+ let hasProbeFailed = false;
10412
+ for (const raw of bits) {
10413
+ const b = raw.trim();
10414
+ if (!b) continue;
10415
+ if (b.startsWith("error:")) {
10416
+ hasError = true;
10417
+ continue;
10418
+ }
10419
+ if (b === "probe failed") {
10420
+ hasProbeFailed = true;
10421
+ continue;
10422
+ }
10423
+ bitTokens.add(b.split(":")[0]);
10424
+ }
10425
+ if (!bitTokens.has("enabled") || !bitTokens.has("configured")) return false;
10426
+ if (!gatewayReachable) return true;
10427
+ if (bitTokens.has("works")) return true;
10428
+ if (bitTokens.has("running") && !hasError && (ignoreProbeFailed || !hasProbeFailed)) return true;
10429
+ return false;
10430
+ }
10431
+ /**
10432
+ * 解析 `openclaw channels status --probe` 的原始 stdout。
10433
+ * 移植自 feishu-channel-success-rate skill 的 Python `extract_channels_probe`。
10434
+ */
10435
+ function parseChannelsProbeOutput(text, { ignoreProbeFailed = true } = {}) {
10436
+ const gatewayReachable = text.includes("Gateway reachable");
10437
+ const feishuConfigInvalid = text.includes(FEISHU_INVALID_CONFIG_MSG);
10438
+ const accounts = [];
10439
+ let anyAccountWorking = false;
10440
+ for (const line of text.split("\n")) {
10441
+ const m = CHANNEL_LINE_RE.exec(line.trim());
10442
+ if (!m) continue;
10443
+ const [, acct, rest] = m;
10444
+ const bits = rest.split(",").map((b) => b.trim());
10445
+ const isWorking = accountIsWorking(bits, ignoreProbeFailed, gatewayReachable);
10446
+ if (isWorking) anyAccountWorking = true;
10447
+ accounts.push({
10448
+ id: acct.trim(),
10449
+ bits,
10450
+ isWorking,
10451
+ raw: line.trim()
10452
+ });
10453
+ }
10454
+ return {
10455
+ gatewayReachable,
10456
+ feishuConfigInvalid,
10457
+ accounts,
10458
+ anyAccountWorking
10459
+ };
10460
+ }
10461
+ /**
10462
+ * 执行 `openclaw channels status --probe`,返回结构化结果。
10463
+ *
10464
+ * 部分 bot 账号 probe 失败时命令会以非零退出码退出,但 stdout 仍有可用内容。
10465
+ * 因此即使退出码非零,也尝试解析 stdout;只有真正没有任何输出时才返回 unavailable。
10466
+ *
10467
+ * @param timeoutMs 最长等待时长,默认 60 s。v2026.4.x 缺少单请求 HTTP 超时,
10468
+ * 可能无限阻塞,此超时是唯一保护。
10469
+ * @param ignoreProbeFailed 为 true 时,"probe failed" 账号仍计入"可用"。
10470
+ * 升级前置检测中应传 true,避免网络抖动导致误判为不可用。
10471
+ */
10472
+ function runChannelsProbe(timeoutMs = 6e4, { ignoreProbeFailed = true } = {}) {
10473
+ let stdout = "";
10474
+ let stderrText = "";
10475
+ let execError;
10476
+ try {
10477
+ stdout = (0, node_child_process.execSync)("openclaw channels status --probe", {
10478
+ encoding: "utf-8",
10479
+ timeout: timeoutMs,
10480
+ stdio: [
10481
+ "ignore",
10482
+ "pipe",
10483
+ "pipe"
10484
+ ]
10485
+ });
10486
+ } catch (e) {
10487
+ const err = e;
10488
+ const stdoutRaw = err.stdout;
10489
+ stdout = typeof stdoutRaw === "string" ? stdoutRaw : stdoutRaw?.toString("utf-8") ?? "";
10490
+ execError = err.message;
10491
+ const stderrRaw = err.stderr;
10492
+ stderrText = (typeof stderrRaw === "string" ? stderrRaw : stderrRaw?.toString("utf-8") ?? "").trim();
10493
+ if (stderrText) console.error(`channels-probe: stderr from CLI: ${stderrText}`);
10494
+ }
10495
+ if (stdout.trim()) return {
10496
+ available: true,
10497
+ rawOutput: stdout.trim(),
10498
+ ...parseChannelsProbeOutput(stdout, { ignoreProbeFailed })
10499
+ };
10500
+ return {
10501
+ available: false,
10502
+ gatewayReachable: false,
10503
+ feishuConfigInvalid: stderrText.includes(FEISHU_INVALID_CONFIG_MSG),
10504
+ accounts: [],
10505
+ anyAccountWorking: false,
10506
+ error: execError ?? "no output from openclaw channels status --probe"
10507
+ };
10508
+ }
10509
+ /**
10510
+ * 判断 channels probe 结果是否处于"仅一个 Feishu 默认账号、enabled 但未配置"的状态。
10511
+ *
10512
+ * 这是插件全新安装后的初始状态:npx 工具创建了一个默认账号占位,但用户尚未
10513
+ * 填写 AppID / Secret,因此账号显示为 enabled 但 not configured。
10514
+ * 此时 anyAccountWorking=false(configured 缺失),但安装本身是成功的。
10515
+ *
10516
+ * 用于安装后校验的补充分支:当安装前 channels 不可用,且安装后恰好处于此状态时,
10517
+ * 视为安装成功,不触发回滚。
10518
+ */
10519
+ function isDefaultOnlyState(result) {
10520
+ if (result.accounts.length !== 1) return false;
10521
+ const acct = result.accounts[0];
10522
+ const bitTokens = new Set(acct.bits.map((b) => b.trim().split(":")[0]));
10523
+ return bitTokens.has("enabled") && !bitTokens.has("configured");
10524
+ }
10525
+ //#endregion
10295
10526
  //#region src/innerapi/reportCliRun.ts
10296
10527
  /**
10297
10528
  * CLI-side client for studio_server's `openclaw.report_cli_run` inner
@@ -10371,7 +10602,7 @@ async function reportCliRun(opts) {
10371
10602
  //#region src/help.ts
10372
10603
  const BIN = "mclaw-diagnose";
10373
10604
  function versionBanner() {
10374
- return `v0.1.15-alpha.4`;
10605
+ return `v0.1.15-alpha.5`;
10375
10606
  }
10376
10607
  const COMMANDS = [
10377
10608
  {
@@ -10658,6 +10889,63 @@ OPTIONS
10658
10889
  EXIT CODES
10659
10890
  0 Success or skipped (prerequisites not met).
10660
10891
  1 Secret/path unresolvable, lark-cli failed, or config unreadable.
10892
+ `
10893
+ },
10894
+ {
10895
+ name: "upgrade-lark",
10896
+ hidden: false,
10897
+ summary: "Upgrade the Feishu/Lark plugin via @larksuite/openclaw-lark-tools",
10898
+ help: `USAGE
10899
+ ${BIN} upgrade-lark [--check] [--skip-restart] [--scene=<scene>] [--caller=<n>] [--trace-id=<id>]
10900
+
10901
+ DESCRIPTION
10902
+ Upgrades the Feishu/Lark plugin by running:
10903
+ npx -y @larksuite/openclaw-lark-tools update --use-existing
10904
+
10905
+ Before the upgrade, a pre-check gate runs to verify the upgrade is needed:
10906
+ - version incompatible OR feishu channel config invalid, AND
10907
+ - no feishu account is currently working
10908
+ If the gate is not triggered, the command skips with exit code 0.
10909
+
10910
+ Before the upgrade, the following files are backed up:
10911
+ - openclaw.json
10912
+ - extensions/openclaw-lark/ (if present)
10913
+ - extensions/feishu-openclaw-plugin/ (if present)
10914
+ After the upgrade, the result is validated:
10915
+ - feishu.accounts bot count must not decrease
10916
+ - gateway config structure must remain valid (port/mode/bind/auth/trustedProxies)
10917
+ If the upgrade command fails, or validation fails, the backed-up files are
10918
+ restored to roll back the changes.
10919
+
10920
+ After a successful upgrade, the openclaw service is restarted via
10921
+ /opt/force/bin/openclaw_scripts/restart.sh
10922
+ Pass --skip-restart to skip this step (e.g. when restart is handled externally).
10923
+
10924
+ Execution is logged to /tmp/openclaw-diagnose/upgrade-lark-<runId>.log.
10925
+
10926
+ Output is a single JSON object on stdout:
10927
+ { "ok": true, "stdout": "...", "stderr": "...", "logFile": "..." }
10928
+ { "ok": false, "error": "...", "stderr": "...", "exitCode": 1,
10929
+ "rollbackOk": true, "validationError": "...", "logFile": "..." }
10930
+
10931
+ With --check:
10932
+ { "ok": true, "skipped": true, "upgradeNeeded": false, "logFile": "..." }
10933
+ { "ok": true, "skipped": true, "upgradeNeeded": true, "logFile": "..." } ← exit 1
10934
+
10935
+ OPTIONS
10936
+ --check Diagnose only: run the pre-check gate and report whether
10937
+ upgrade is needed without installing. Exit 1 if needed.
10938
+ --skip-restart Skip the post-install service restart (default: restart).
10939
+ --scene=<scene> Telemetry label forwarded to Slardar only.
10940
+ Known values: PageUpgradeLark, etc. Custom strings accepted.
10941
+ --caller=<name> Optional metadata passed to innerapi.
10942
+ --trace-id=<id> Optional log-correlation id.
10943
+
10944
+ EXIT CODES
10945
+ 0 Success: upgrade ran and all validations passed; or gate skipped upgrade.
10946
+ 1 Failure: npx error, validation failed, or git commit failed.
10947
+ File rollback was attempted (see rollbackOk in the JSON output).
10948
+ With --check: exit 1 means upgrade IS needed.
10661
10949
  `
10662
10950
  },
10663
10951
  {
@@ -10691,6 +10979,41 @@ EXAMPLES
10691
10979
  ${BIN} rules # all rules
10692
10980
  ${BIN} rules --rule=gateway # single rule
10693
10981
  ${BIN} rules --rule=gateway --rule=feishu_channel # multiple rules
10982
+ `
10983
+ },
10984
+ {
10985
+ name: "channels-probe",
10986
+ hidden: true,
10987
+ summary: "Check feishu channel health via openclaw channels status --probe",
10988
+ help: `USAGE
10989
+ ${BIN} channels-probe [--timeout=<ms>]
10990
+
10991
+ DESCRIPTION
10992
+ Runs \`openclaw channels status --probe\` and returns a structured JSON
10993
+ summary of whether the current environment's feishu channels are
10994
+ configured and working correctly.
10995
+
10996
+ Output:
10997
+ {
10998
+ "available": true,
10999
+ "gatewayReachable": true,
11000
+ "accounts": [
11001
+ { "id": "default", "bits": ["enabled","configured","running","works"],
11002
+ "isWorking": true, "raw": "- Feishu default: ..." }
11003
+ ],
11004
+ "anyAccountWorking": true
11005
+ }
11006
+
11007
+ An account is considered working when:
11008
+ enabled ∧ configured ∧ ( works ∨ ( running ∧ no error: ∧ no probe failed ) )
11009
+
11010
+ "available": false means the CLI invocation itself failed (openclaw not
11011
+ found, gateway unreachable, or no parseable output returned).
11012
+
11013
+ OPTIONS
11014
+ --timeout=<ms> Max wait in milliseconds (default: 60000). The probe
11015
+ can hang indefinitely on openclaw v2026.4.x due to a
11016
+ missing per-request HTTP timeout — set this accordingly.
10694
11017
  `
10695
11018
  },
10696
11019
  {
@@ -10865,6 +11188,448 @@ function reportDoctorRunToSlardar(opts) {
10865
11188
  }
10866
11189
  });
10867
11190
  }
11191
+ const LOG_PREFIX_RE = /^\[\d{4}-\d{2}-\d{2}T[\d:.]+Z\](?:\s+\[run=[^\]]+\])?\s*/;
11192
+ /**
11193
+ * 读取日志文件末尾最多 maxBytes 字节,过滤噪音后返回纯内容。
11194
+ * - 剥除每行的时间戳 + run tag 前缀
11195
+ * - 过滤纯分隔符行(全为 '=')
11196
+ * 日志头部为固定 banner,有价值的内容集中在末尾,从尾部截取。
11197
+ */
11198
+ function readLogFileTail(filePath, maxBytes = 4e3) {
11199
+ try {
11200
+ const buf = node_fs.default.readFileSync(filePath);
11201
+ let start = buf.length > maxBytes ? buf.length - maxBytes : 0;
11202
+ while (start < buf.length && buf[start] !== 10) start++;
11203
+ return buf.subarray(start > 0 ? start + 1 : 0).toString("utf-8").split("\n").map((line) => line.replace(LOG_PREFIX_RE, "")).filter((line) => !/^=+$/.test(line.trim())).join("\n").trim();
11204
+ } catch {
11205
+ return "";
11206
+ }
11207
+ }
11208
+ /**
11209
+ * 向 Slardar 上报 upgrade-lark 运行结果(upgrade_lark_run 事件)。
11210
+ *
11211
+ * extraCategories 记录字符串维度:scene、exit_code、rollback_ok、
11212
+ * validation_error、error_msg、log_content(日志文件末尾 4000 字节,含关键结果)。
11213
+ *
11214
+ * extraMetrics 记录各阶段耗时(毫秒);未执行的阶段上报 -1 作为哨兵值,
11215
+ * 便于在 Slardar 查询时区分"未运行"和"运行了 0ms"。
11216
+ */
11217
+ function reportUpgradeLarkToSlardar(opts) {
11218
+ console.error(`[slardar] upgrade_lark_run scene=${opts.scene ?? ""} checkOnly=${opts.checkOnly} success=${opts.success} exitCode=${opts.exitCode ?? ""} rollbackOk=${opts.rollbackOk ?? ""}`);
11219
+ const t = opts.timing ?? {};
11220
+ const logContent = readLogFileTail(opts.logFile);
11221
+ reportTask({
11222
+ eventName: "upgrade_lark_run",
11223
+ durationMs: opts.durationMs,
11224
+ status: opts.success ? "success" : "failed",
11225
+ extraCategories: {
11226
+ scene: opts.scene ?? "",
11227
+ check_only: String(opts.checkOnly),
11228
+ exit_code: String(opts.exitCode ?? ""),
11229
+ rollback_ok: opts.rollbackOk != null ? String(opts.rollbackOk) : "",
11230
+ validation_error: opts.validationError ?? "",
11231
+ error_msg: opts.error ?? "",
11232
+ log_content: logContent
11233
+ },
11234
+ extraMetrics: {
11235
+ pre_probe_ms: t.preProbeMs ?? -1,
11236
+ version_check_ms: t.versionCheckMs ?? -1,
11237
+ backup_ms: t.backupMs ?? -1,
11238
+ npx_install_ms: t.npxInstallMs ?? -1,
11239
+ post_probe_ms: t.postProbeMs ?? -1,
11240
+ doctor_fix_ms: t.doctorFixMs ?? -1
11241
+ }
11242
+ });
11243
+ }
11244
+ //#endregion
11245
+ //#region src/upgrade-lark.ts
11246
+ /** 升级前需备份的 extensions/ 下的插件目录 */
11247
+ const FEISHU_PLUGIN_DIRS = ["openclaw-lark", "feishu-openclaw-plugin"];
11248
+ function backupFiles(opts) {
11249
+ const { workspaceDir, configPath, backupDir, log } = opts;
11250
+ try {
11251
+ node_fs.default.mkdirSync(backupDir, { recursive: true });
11252
+ log(`backup dir: ${backupDir}`);
11253
+ if (node_fs.default.existsSync(configPath)) {
11254
+ const stat = node_fs.default.statSync(configPath);
11255
+ node_fs.default.copyFileSync(configPath, node_path.default.join(backupDir, "openclaw.json"));
11256
+ log(` backed up: openclaw.json (${stat.size} bytes)`);
11257
+ } else log(` skipped: openclaw.json (not found)`);
11258
+ node_fs.default.mkdirSync(node_path.default.join(backupDir, "extensions"), { recursive: true });
11259
+ const extSrc = node_path.default.join(workspaceDir, "extensions");
11260
+ for (const pluginDir of FEISHU_PLUGIN_DIRS) {
11261
+ const src = node_path.default.join(extSrc, pluginDir);
11262
+ if (node_fs.default.existsSync(src)) {
11263
+ const dst = node_path.default.join(backupDir, "extensions", pluginDir);
11264
+ node_fs.default.cpSync(src, dst, { recursive: true });
11265
+ const version = readPkgVersion(node_path.default.join(src, "package.json"));
11266
+ log(` backed up: extensions/${pluginDir}${version ? ` (version: ${version})` : ""}`);
11267
+ } else log(` skipped: extensions/${pluginDir} (not found)`);
11268
+ }
11269
+ return { ok: true };
11270
+ } catch (e) {
11271
+ const msg = `backup failed: ${e.message}`;
11272
+ log(`ERROR: ${msg}`);
11273
+ return {
11274
+ ok: false,
11275
+ error: msg
11276
+ };
11277
+ }
11278
+ }
11279
+ function restoreFiles(opts) {
11280
+ const { workspaceDir, configPath, backupDir, log } = opts;
11281
+ try {
11282
+ if (node_fs.default.existsSync(configPath)) {
11283
+ node_fs.default.rmSync(configPath, { force: true });
11284
+ log(` deleted: openclaw.json`);
11285
+ }
11286
+ const extDst = node_path.default.join(workspaceDir, "extensions");
11287
+ for (const pluginDir of FEISHU_PLUGIN_DIRS) {
11288
+ const dst = node_path.default.join(extDst, pluginDir);
11289
+ if (node_fs.default.existsSync(dst)) {
11290
+ node_fs.default.rmSync(dst, {
11291
+ recursive: true,
11292
+ force: true
11293
+ });
11294
+ log(` deleted: extensions/${pluginDir}`);
11295
+ }
11296
+ }
11297
+ const configBackup = node_path.default.join(backupDir, "openclaw.json");
11298
+ if (node_fs.default.existsSync(configBackup)) {
11299
+ node_fs.default.copyFileSync(configBackup, configPath);
11300
+ log(` restored: openclaw.json`);
11301
+ } else log(` skipped restore: openclaw.json (not in backup — was not present before upgrade)`);
11302
+ for (const pluginDir of FEISHU_PLUGIN_DIRS) {
11303
+ const backupSrc = node_path.default.join(backupDir, "extensions", pluginDir);
11304
+ if (node_fs.default.existsSync(backupSrc)) {
11305
+ node_fs.default.cpSync(backupSrc, node_path.default.join(extDst, pluginDir), { recursive: true });
11306
+ log(` restored: extensions/${pluginDir}`);
11307
+ } else log(` skipped restore: extensions/${pluginDir} (not in backup — was not present before upgrade)`);
11308
+ }
11309
+ return true;
11310
+ } catch (e) {
11311
+ log(` restore error: ${e.message}`);
11312
+ return false;
11313
+ }
11314
+ }
11315
+ function readPkgVersion(pkgPath) {
11316
+ try {
11317
+ const pkg = JSON.parse(node_fs.default.readFileSync(pkgPath, "utf-8"));
11318
+ return typeof pkg.version === "string" ? pkg.version : null;
11319
+ } catch {
11320
+ return null;
11321
+ }
11322
+ }
11323
+ function snapshotVersions(cwd, log) {
11324
+ const ocResult = (0, node_child_process.spawnSync)("openclaw", ["--version"], {
11325
+ cwd,
11326
+ encoding: "utf-8",
11327
+ stdio: [
11328
+ "ignore",
11329
+ "pipe",
11330
+ "pipe"
11331
+ ],
11332
+ timeout: 5e3
11333
+ });
11334
+ const ocRaw = (ocResult.stdout ?? "").trim() || (ocResult.stderr ?? "").trim();
11335
+ const extDir = node_path.default.join(cwd, "extensions");
11336
+ const larkPkg = node_path.default.join(extDir, "openclaw-lark", "package.json");
11337
+ const feishuPkg = node_path.default.join(extDir, "feishu-openclaw-plugin", "package.json");
11338
+ log(` version-check paths: ${larkPkg} [${node_fs.default.existsSync(larkPkg) ? "exists" : "missing"}]`);
11339
+ log(` version-check paths: ${feishuPkg} [${node_fs.default.existsSync(feishuPkg) ? "exists" : "missing"}]`);
11340
+ return {
11341
+ openclaw: ocRaw || null,
11342
+ openclawLark: readPkgVersion(larkPkg),
11343
+ feishuOpenclawPlugin: readPkgVersion(feishuPkg)
11344
+ };
11345
+ }
11346
+ function logVersionSnapshot(label, v, log) {
11347
+ log(`${label}: openclaw=${v.openclaw ?? "n/a"} openclaw-lark=${v.openclawLark ?? "n/a"} feishu-openclaw-plugin=${v.feishuOpenclawPlugin ?? "n/a"}`);
11348
+ }
11349
+ function countFeishuBots(configPath) {
11350
+ try {
11351
+ const raw = node_fs.default.readFileSync(configPath, "utf-8");
11352
+ const config = loadJSON5().parse(raw);
11353
+ const accounts = getNestedMap(config, "channels", "feishu", "accounts");
11354
+ if (accounts) return Object.keys(accounts).length;
11355
+ const feishu = getNestedMap(config, "channels", "feishu");
11356
+ return typeof feishu?.appId === "string" && feishu.appId ? 1 : 0;
11357
+ } catch {
11358
+ return 0;
11359
+ }
11360
+ }
11361
+ /** 执行 channels probe 并将结果写入日志,从不抛出异常(异常时返回全零结果)。 */
11362
+ function probeChannels(label, log, timeoutMs) {
11363
+ try {
11364
+ const r = runChannelsProbe(timeoutMs);
11365
+ log(` ${label} available=${r.available} anyAccountWorking=${r.anyAccountWorking}`);
11366
+ if (r.error) log(` ${label} error: ${r.error}`);
11367
+ if (r.gatewayReachable != null) log(` ${label} gatewayReachable: ${r.gatewayReachable}`);
11368
+ for (const acct of r.accounts ?? []) log(` ${label} account ${acct.id}: isWorking=${acct.isWorking} bits=[${acct.bits.join(",")}]`);
11369
+ if (r.rawOutput) log(` ${label} raw output:\n${r.rawOutput}`);
11370
+ return r;
11371
+ } catch (e) {
11372
+ log(` ${label} channels probe threw: ${e.message}`);
11373
+ return {
11374
+ available: false,
11375
+ gatewayReachable: false,
11376
+ feishuConfigInvalid: false,
11377
+ accounts: [],
11378
+ anyAccountWorking: false
11379
+ };
11380
+ }
11381
+ }
11382
+ function runUpgradeLark(opts) {
11383
+ const cwd = opts.cwd ?? "/home/gem/workspace/agent";
11384
+ const configPath = opts.configPath ?? CONFIG_PATH;
11385
+ const logFile = upgradeLarkLogFile(opts.runId, opts.checkOnly);
11386
+ const log = makeLogger(logFile);
11387
+ const fsOpts = {
11388
+ workspaceDir: cwd,
11389
+ configPath,
11390
+ backupDir: node_path.default.join(opts.backupBaseDir ?? "/tmp/openclaw-diagnose", `upgrade-lark-backup-${opts.runId}`),
11391
+ log
11392
+ };
11393
+ const cliScript = opts.cliScript ?? process.argv[1];
11394
+ const statusCheckDelayMs = opts.statusCheckDelayMs ?? 5e3;
11395
+ log(`${"=".repeat(60)}`);
11396
+ log(`upgrade-lark started runId=${opts.runId}`);
11397
+ log(` cwd : ${cwd}`);
11398
+ log(` configPath : ${configPath}`);
11399
+ log(`${"=".repeat(60)}`);
11400
+ const timing = {};
11401
+ log("");
11402
+ log("── [Pre-check A] channels probe(升级前)────────────────");
11403
+ const t_preProbeStart = Date.now();
11404
+ const beforeChannels = probeChannels("before", log, 3e5);
11405
+ timing.preProbeMs = Date.now() - t_preProbeStart;
11406
+ log("");
11407
+ log("── [Pre-check B] 版本兼容预检 ───────────────────────────");
11408
+ let versionIncompatible = false;
11409
+ const t_versionCheckStart = Date.now();
11410
+ try {
11411
+ const rawConfig = node_fs.default.readFileSync(configPath, "utf-8");
11412
+ versionIncompatible = needsLarkUpgrade({
11413
+ config: loadJSON5().parse(rawConfig),
11414
+ configPath,
11415
+ vars: {},
11416
+ providerDeps: {
11417
+ usesMiaodaProvider: false,
11418
+ usesMiaodaSecretProvider: false
11419
+ }
11420
+ });
11421
+ log(` version-compat pre-check: ${versionIncompatible ? "NEEDS_UPGRADE" : "ok"}`);
11422
+ } catch (e) {
11423
+ log(` version-compat pre-check error: ${e.message} — version signal unavailable`);
11424
+ }
11425
+ timing.versionCheckMs = Date.now() - t_versionCheckStart;
11426
+ const feishuConfigInvalid = beforeChannels.feishuConfigInvalid;
11427
+ log(` feishu config invalid : ${feishuConfigInvalid}`);
11428
+ log("");
11429
+ log("── [Gate] 升级前置条件检查 ───────────────────────────────");
11430
+ log(` versionIncompatible : ${versionIncompatible}`);
11431
+ log(` feishuConfigInvalid : ${feishuConfigInvalid}`);
11432
+ log(` channels working before: ${beforeChannels.anyAccountWorking}`);
11433
+ if (!(versionIncompatible || feishuConfigInvalid)) {
11434
+ const reason = "version compatible and feishu channel config valid — upgrade not needed";
11435
+ log(` SKIP: ${reason}`);
11436
+ log(`${"=".repeat(60)}`);
11437
+ log("upgrade-lark skipped (pre-check gate)");
11438
+ log(`${"=".repeat(60)}`);
11439
+ return {
11440
+ ok: true,
11441
+ skipped: true,
11442
+ skipReason: reason,
11443
+ upgradeNeeded: false,
11444
+ timing,
11445
+ logFile
11446
+ };
11447
+ }
11448
+ if (beforeChannels.anyAccountWorking) {
11449
+ const reason = "channels are working — upgrade not needed (issue detected but system is functional)";
11450
+ log(` SKIP: ${reason}`);
11451
+ log(`${"=".repeat(60)}`);
11452
+ log("upgrade-lark skipped (pre-check gate)");
11453
+ log(`${"=".repeat(60)}`);
11454
+ return {
11455
+ ok: true,
11456
+ skipped: true,
11457
+ skipReason: reason,
11458
+ upgradeNeeded: false,
11459
+ timing,
11460
+ logFile
11461
+ };
11462
+ }
11463
+ log(` PROCEED: requiresLarkUpgrade=true (version=${versionIncompatible}, feishuConfig=${feishuConfigInvalid}) AND channels not working → running upgrade`);
11464
+ if (opts.checkOnly) {
11465
+ log(` --check 模式:需要升级 — 不执行安装,直接返回`);
11466
+ log(`${"=".repeat(60)}`);
11467
+ log("upgrade-lark check complete");
11468
+ log(`${"=".repeat(60)}`);
11469
+ return {
11470
+ ok: true,
11471
+ skipped: true,
11472
+ skipReason: "check",
11473
+ upgradeNeeded: true,
11474
+ timing,
11475
+ logFile
11476
+ };
11477
+ }
11478
+ log("");
11479
+ log("── [1/6] 文件备份 ────────────────────────────────────────");
11480
+ log(`before-state: botCount=${countFeishuBots(configPath)}`);
11481
+ const t_backupStart = Date.now();
11482
+ const backup = backupFiles(fsOpts);
11483
+ timing.backupMs = Date.now() - t_backupStart;
11484
+ if (!backup.ok) {
11485
+ log(`ERROR: ${backup.error}`);
11486
+ return {
11487
+ ok: false,
11488
+ error: backup.error,
11489
+ timing,
11490
+ logFile
11491
+ };
11492
+ }
11493
+ log("backup: ok");
11494
+ logVersionSnapshot("before-versions", snapshotVersions(cwd, log), log);
11495
+ log("");
11496
+ log("── [2/6] 清理本地 openclaw shim ─────────────────────────");
11497
+ const localOpenclawBin = node_path.default.join(cwd, "node_modules", ".bin", "openclaw");
11498
+ if (node_fs.default.existsSync(localOpenclawBin)) try {
11499
+ node_fs.default.rmSync(localOpenclawBin);
11500
+ log(` removed: ${localOpenclawBin}`);
11501
+ } catch (e) {
11502
+ log(` WARN: failed to remove ${localOpenclawBin}: ${e.message}`);
11503
+ }
11504
+ else log(` skipped: ${localOpenclawBin} (not found)`);
11505
+ log("");
11506
+ log("── [3/6] npx install (@larksuite/openclaw-lark-tools update) ──");
11507
+ const t_npxStart = Date.now();
11508
+ const npxResult = (0, node_child_process.spawnSync)("npx", [
11509
+ "-y",
11510
+ "@larksuite/openclaw-lark-tools",
11511
+ "update"
11512
+ ], {
11513
+ cwd,
11514
+ encoding: "utf-8",
11515
+ stdio: [
11516
+ "ignore",
11517
+ "pipe",
11518
+ "pipe"
11519
+ ],
11520
+ timeout: 6e5
11521
+ });
11522
+ timing.npxInstallMs = Date.now() - t_npxStart;
11523
+ const npxStdout = npxResult.stdout?.trim() ?? "";
11524
+ const npxStderr = npxResult.stderr?.trim() ?? "";
11525
+ const npxExitCode = npxResult.status ?? 1;
11526
+ if (npxStdout) log(`npx stdout:\n${npxStdout}`);
11527
+ if (npxStderr) log(`npx stderr:\n${npxStderr}`);
11528
+ log(`npx exit: ${npxExitCode}${npxResult.error ? ` error: ${npxResult.error.message}` : ""}`);
11529
+ if (statusCheckDelayMs > 0) {
11530
+ log("");
11531
+ log(`── 等待 ${statusCheckDelayMs / 1e3}s(让 openclaw 服务完成重启) ─────────────`);
11532
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, statusCheckDelayMs);
11533
+ log("wait done");
11534
+ }
11535
+ const doRollback = (reason) => {
11536
+ log(`ERROR: ${reason}`);
11537
+ const rollbackOk = restoreFiles(fsOpts);
11538
+ log(`rollback: ${rollbackOk ? "ok" : "FAILED"}`);
11539
+ return {
11540
+ ok: false,
11541
+ error: reason,
11542
+ validationError: reason,
11543
+ stdout: npxStdout,
11544
+ stderr: npxStderr,
11545
+ exitCode: npxExitCode,
11546
+ rollbackOk,
11547
+ timing,
11548
+ logFile
11549
+ };
11550
+ };
11551
+ log("");
11552
+ log("── [4/5] 安装后诊断校验 ─────────────────────────────────");
11553
+ logVersionSnapshot("after-versions", snapshotVersions(cwd, log), log);
11554
+ let afterVersionIncompatible = false;
11555
+ try {
11556
+ const rawConfig = node_fs.default.readFileSync(configPath, "utf-8");
11557
+ afterVersionIncompatible = needsLarkUpgrade({
11558
+ config: loadJSON5().parse(rawConfig),
11559
+ configPath,
11560
+ vars: {},
11561
+ providerDeps: {
11562
+ usesMiaodaProvider: false,
11563
+ usesMiaodaSecretProvider: false
11564
+ }
11565
+ });
11566
+ log(` version-compat post-check: ${afterVersionIncompatible ? "STILL_INCOMPATIBLE" : "ok"}`);
11567
+ } catch (e) {
11568
+ log(` version-compat post-check error: ${e.message} — version signal unavailable`);
11569
+ }
11570
+ const t_postProbeStart = Date.now();
11571
+ const afterChannels = probeChannels("after", log, 3e5);
11572
+ timing.postProbeMs = Date.now() - t_postProbeStart;
11573
+ log(` feishu config invalid after: ${afterChannels.feishuConfigInvalid}`);
11574
+ const stillNeedsUpgrade = (afterVersionIncompatible || afterChannels.feishuConfigInvalid) && !afterChannels.anyAccountWorking;
11575
+ const isNewDefaultOnly = !beforeChannels.anyAccountWorking && isDefaultOnlyState(afterChannels);
11576
+ log(` post-check: stillNeedsUpgrade=${stillNeedsUpgrade} (version=${afterVersionIncompatible}, feishuConfig=${afterChannels.feishuConfigInvalid}, channelsWorking=${afterChannels.anyAccountWorking}) isNewDefaultOnly=${isNewDefaultOnly}`);
11577
+ if (stillNeedsUpgrade && !isNewDefaultOnly) return doRollback(`post-install diagnosis still shows anomaly: versionIncompatible=${afterVersionIncompatible}, feishuConfigInvalid=${afterChannels.feishuConfigInvalid}, anyAccountWorking=${afterChannels.anyAccountWorking}`);
11578
+ if (isNewDefaultOnly) log(" post-install diagnosis: ok (new default account — plugin installed, awaiting configuration)");
11579
+ else log(" post-install diagnosis: ok (upgrade conditions resolved)");
11580
+ log("");
11581
+ log("── [6/6] doctor --fix ────────────────────────────────────");
11582
+ const fixArgs = ["doctor", "--fix"];
11583
+ if (opts.scene) fixArgs.push(`--scene=${opts.scene}`);
11584
+ const t_doctorFixStart = Date.now();
11585
+ const fixResult = (0, node_child_process.spawnSync)(process.execPath, [cliScript, ...fixArgs], {
11586
+ cwd,
11587
+ encoding: "utf-8",
11588
+ stdio: [
11589
+ "ignore",
11590
+ "pipe",
11591
+ "pipe"
11592
+ ],
11593
+ timeout: 6e4,
11594
+ env: process.env
11595
+ });
11596
+ timing.doctorFixMs = Date.now() - t_doctorFixStart;
11597
+ if (fixResult.stdout?.trim()) log(`doctor(fix) stdout:\n${fixResult.stdout.trim()}`);
11598
+ if (fixResult.stderr?.trim()) log(`doctor(fix) stderr:\n${fixResult.stderr.trim()}`);
11599
+ log(`doctor(fix) exit: ${fixResult.status ?? "null"}${fixResult.error ? ` error: ${fixResult.error.message}` : ""}`);
11600
+ log("");
11601
+ log("── [7/7] 重启 openclaw 服务 ──────────────────────────────");
11602
+ const restartScript = "/opt/force/bin/openclaw_scripts/restart.sh";
11603
+ if (opts.skipRestart) log(" skipped: --skip-restart");
11604
+ else if (node_fs.default.existsSync(restartScript)) {
11605
+ const t_restart = Date.now();
11606
+ const restartResult = (0, node_child_process.spawnSync)("bash", [restartScript], {
11607
+ encoding: "utf-8",
11608
+ stdio: [
11609
+ "ignore",
11610
+ "pipe",
11611
+ "pipe"
11612
+ ],
11613
+ timeout: 3e4
11614
+ });
11615
+ const restartMs = Date.now() - t_restart;
11616
+ if (restartResult.stdout?.trim()) log(` restart stdout:\n${restartResult.stdout.trim()}`);
11617
+ if (restartResult.stderr?.trim()) log(` restart stderr:\n${restartResult.stderr.trim()}`);
11618
+ log(` restart.sh exit: ${restartResult.status ?? "null"} (${restartMs}ms)${restartResult.error ? ` error: ${restartResult.error.message}` : ""}`);
11619
+ } else log(` skipped: ${restartScript} not found`);
11620
+ log("");
11621
+ log(`${"=".repeat(60)}`);
11622
+ log("upgrade-lark completed successfully");
11623
+ log(`${"=".repeat(60)}`);
11624
+ return {
11625
+ ok: true,
11626
+ stdout: npxStdout,
11627
+ stderr: npxStderr,
11628
+ exitCode: npxExitCode,
11629
+ timing,
11630
+ logFile
11631
+ };
11632
+ }
10868
11633
  //#endregion
10869
11634
  //#region src/index.ts
10870
11635
  const args = node_process.default.argv.slice(2);
@@ -11355,6 +12120,56 @@ async function main() {
11355
12120
  if (!result.ok) node_process.default.exit(1);
11356
12121
  break;
11357
12122
  }
12123
+ case "upgrade-lark": {
12124
+ const checkOnly = args.includes("--check");
12125
+ const skipRestart = args.includes("--skip-restart");
12126
+ const result = runUpgradeLark({
12127
+ runId: rc.runId,
12128
+ scene,
12129
+ checkOnly,
12130
+ skipRestart
12131
+ });
12132
+ const upgradeDurationMs = Date.now() - t0;
12133
+ console.log(JSON.stringify(result));
12134
+ reportUpgradeLarkToSlardar({
12135
+ scene,
12136
+ checkOnly,
12137
+ durationMs: upgradeDurationMs,
12138
+ success: result.ok,
12139
+ logFile: result.logFile,
12140
+ exitCode: result.exitCode,
12141
+ rollbackOk: result.rollbackOk,
12142
+ validationError: result.validationError,
12143
+ error: result.error,
12144
+ timing: result.timing
12145
+ });
12146
+ try {
12147
+ await reportCliRun({
12148
+ command: "upgrade-lark",
12149
+ runId: rc.runId,
12150
+ version: getVersion(),
12151
+ invocation: args.join(" "),
12152
+ durationMs: upgradeDurationMs,
12153
+ caller: rc.caller,
12154
+ traceId: rc.traceId,
12155
+ success: result.ok,
12156
+ result,
12157
+ error: result.ok ? void 0 : { message: result.error ?? "upgrade-lark failed" }
12158
+ });
12159
+ } catch (e) {
12160
+ console.error(`[telemetry] reportCliRun failed: ${e.message}`);
12161
+ }
12162
+ if (!result.ok || checkOnly && result.upgradeNeeded) {
12163
+ node_process.default.exitCode = 1;
12164
+ return;
12165
+ }
12166
+ break;
12167
+ }
12168
+ case "channels-probe": {
12169
+ const result = runChannelsProbe(getFlag(args, "timeout") ? Number(getFlag(args, "timeout")) : void 0);
12170
+ console.log(JSON.stringify(result));
12171
+ break;
12172
+ }
11358
12173
  default:
11359
12174
  node_process.default.stderr.write(`Unknown command: ${mode}\n\n`);
11360
12175
  node_process.default.stderr.write(formatTopLevelHelp(helpFlags.expert));