@lark-apaas/openclaw-scripts-diagnose-cli 0.1.7-alpha.1 → 0.1.7-beta.0

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 +427 -550
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -50,7 +50,7 @@ node_assert = __toESM(node_assert);
50
50
  * it terse and parseable.
51
51
  */
52
52
  function getVersion() {
53
- return "0.1.7-alpha.1";
53
+ return "0.1.7-beta.0";
54
54
  }
55
55
  //#endregion
56
56
  //#region src/rule-engine/base.ts
@@ -76,6 +76,19 @@ function Rule(meta) {
76
76
  function getAllRules() {
77
77
  return topoSort(ruleRegistry.map((Ctor) => new Ctor()));
78
78
  }
79
+ /** Return serialisable metadata for all registered rules (topo-sorted). */
80
+ function getRuleMetas(keys) {
81
+ const rules = getAllRules();
82
+ return (keys && keys.length > 0 ? rules.filter((r) => keys.includes(r.meta.key)) : rules).map((r) => ({
83
+ key: r.meta.key,
84
+ description: r.meta.description ?? "(no description)",
85
+ repairMode: r.meta.repairMode,
86
+ profile: r.meta.profile ?? "standard",
87
+ dependsOn: r.meta.dependsOn ?? [],
88
+ hasSkipWhen: r.meta.skipWhen !== void 0,
89
+ hasRepair: r.repair !== DiagnoseRule.prototype.repair
90
+ }));
91
+ }
79
92
  /**
80
93
  * 判断规则在当前 profile 下是否应执行。
81
94
  * experimental profile 运行所有规则;standard 只运行 standard(或未标注)规则。
@@ -1284,6 +1297,7 @@ let OpenclawRuntimeMissingRule = class OpenclawRuntimeMissingRule extends Diagno
1284
1297
  };
1285
1298
  OpenclawRuntimeMissingRule = __decorate([Rule({
1286
1299
  key: "openclaw_runtime_missing",
1300
+ description: "检查 openclaw 二进制是否在 PATH 中可用;缺失时提示重新安装",
1287
1301
  repairMode: "user-confirm"
1288
1302
  })], OpenclawRuntimeMissingRule);
1289
1303
  //#endregion
@@ -1307,6 +1321,7 @@ let ProcessStatusRule = class ProcessStatusRule extends DiagnoseRule {
1307
1321
  };
1308
1322
  ProcessStatusRule = __decorate([Rule({
1309
1323
  key: "multi_process_detect",
1324
+ description: "通过 pgrep 检测是否存在多个并发的 openclaw-gateway 进程;多进程通常表明上次会话未正常退出",
1310
1325
  repairMode: "standard"
1311
1326
  })], ProcessStatusRule);
1312
1327
  //#endregion
@@ -1372,6 +1387,7 @@ let ConfigFileBackupRule = class ConfigFileBackupRule extends DiagnoseRule {
1372
1387
  };
1373
1388
  ConfigFileBackupRule = __decorate([Rule({
1374
1389
  key: "config_file_recover",
1390
+ description: "扫描 .bak* 配置备份,当主配置 openclaw.json 缺失时恢复编号最高的备份",
1375
1391
  repairMode: "standard"
1376
1392
  })], ConfigFileBackupRule);
1377
1393
  //#endregion
@@ -1405,6 +1421,7 @@ let ConfigFileMissingRule = class ConfigFileMissingRule extends DiagnoseRule {
1405
1421
  };
1406
1422
  ConfigFileMissingRule = __decorate([Rule({
1407
1423
  key: "config_file_missing",
1424
+ description: "在执行恢复后检查 openclaw.json 是否存在;若仍缺失则触发全量重置",
1408
1425
  dependsOn: ["config_file_recover"],
1409
1426
  repairMode: "reset"
1410
1427
  })], ConfigFileMissingRule);
@@ -1427,6 +1444,7 @@ let ConfigSyntaxRule = class ConfigSyntaxRule extends DiagnoseRule {
1427
1444
  };
1428
1445
  ConfigSyntaxRule = __decorate([Rule({
1429
1446
  key: "config_syntax_check",
1447
+ description: "验证 openclaw.json 的 JSON5 语法;schema 校验失败或解析错误时需要 AI 辅助修复",
1430
1448
  dependsOn: ["config_file_recover", "config_file_missing"],
1431
1449
  repairMode: "ai"
1432
1450
  })], ConfigSyntaxRule);
@@ -1456,6 +1474,7 @@ let TemplateVarsUnreplacedRule = class TemplateVarsUnreplacedRule extends Diagno
1456
1474
  };
1457
1475
  TemplateVarsUnreplacedRule = __decorate([Rule({
1458
1476
  key: "template_vars_unreplaced",
1477
+ description: "检测 openclaw.json 中未替换的 $__XXX__ 占位符,并从注入的环境变量中填充",
1459
1478
  dependsOn: ["config_syntax_check"],
1460
1479
  repairMode: "standard",
1461
1480
  usesVars: ["templateVars"]
@@ -1604,6 +1623,7 @@ let ModelProviderRule = class ModelProviderRule extends DiagnoseRule {
1604
1623
  };
1605
1624
  ModelProviderRule = __decorate([Rule({
1606
1625
  key: "model_provider",
1626
+ description: "检查妙搭 model provider 配置项(baseUrl、apiKey、必要 headers)是否存在且格式正确;非妙搭沙箱时跳过",
1607
1627
  dependsOn: ["config_syntax_check"],
1608
1628
  repairMode: "standard",
1609
1629
  usesVars: ["innerAPIKey", "baseURL"],
@@ -1671,6 +1691,7 @@ let SecretProviderRule = class SecretProviderRule extends DiagnoseRule {
1671
1691
  };
1672
1692
  SecretProviderRule = _SecretProviderRule = __decorate([Rule({
1673
1693
  key: "secret_provider",
1694
+ description: "验证 miaoda-provider / miaoda-secret-provider 配置块是否存在且完整;未检测到妙搭 provider 时跳过",
1674
1695
  dependsOn: ["config_syntax_check"],
1675
1696
  repairMode: "standard",
1676
1697
  skipWhen: ({ hasMiaoda, deps }) => !hasMiaoda || !deps.usesMiaodaProvider && !deps.usesMiaodaSecretProvider
@@ -1735,6 +1756,7 @@ let FeishuChannelRule = class FeishuChannelRule extends DiagnoseRule {
1735
1756
  };
1736
1757
  FeishuChannelRule = __decorate([Rule({
1737
1758
  key: "feishu_channel",
1759
+ description: "确保 channels.feishu 已启用,且包含单 agent 所需的 appId 和 appSecret 字段",
1738
1760
  dependsOn: ["config_syntax_check", "feishu_default_account"],
1739
1761
  repairMode: "standard",
1740
1762
  usesVars: ["feishuAppID", "feishuAppSecret"]
@@ -1851,6 +1873,7 @@ let FeishuDefaultAccountRule = class FeishuDefaultAccountRule extends DiagnoseRu
1851
1873
  };
1852
1874
  FeishuDefaultAccountRule = __decorate([Rule({
1853
1875
  key: "feishu_default_account",
1876
+ description: "将旧版 v1/v2 飞书 channel 配置迁移为规范的 v3 bot-<appId> 账号结构",
1854
1877
  dependsOn: ["config_syntax_check"],
1855
1878
  repairMode: "standard",
1856
1879
  usesVars: ["feishuAppID", "teamChatID"]
@@ -1979,6 +2002,7 @@ let GatewayRule = class GatewayRule extends DiagnoseRule {
1979
2002
  };
1980
2003
  GatewayRule = __decorate([Rule({
1981
2004
  key: "gateway",
2005
+ description: "验证 gateway 必填字段(port、mode、bind、auth、trustedProxies)是否存在且有效;修复缺失或非法值",
1982
2006
  dependsOn: ["config_syntax_check"],
1983
2007
  repairMode: "standard",
1984
2008
  usesVars: ["gatewayToken"]
@@ -2024,6 +2048,7 @@ let AllowedOriginsRule = class AllowedOriginsRule extends DiagnoseRule {
2024
2048
  };
2025
2049
  AllowedOriginsRule = __decorate([Rule({
2026
2050
  key: "allowed_origins",
2051
+ description: "确保所有 expectedOrigins 条目都存在于 gateway.auth.allowedOrigins 中;自动追加缺失的条目",
2027
2052
  dependsOn: ["config_syntax_check"],
2028
2053
  repairMode: "standard",
2029
2054
  usesVars: ["expectedOrigins"]
@@ -2103,6 +2128,7 @@ let JwtTokenRule = class JwtTokenRule extends DiagnoseRule {
2103
2128
  };
2104
2129
  JwtTokenRule = __decorate([Rule({
2105
2130
  key: "jwt_token",
2131
+ description: "验证 miaoda provider 引用的 JWT token 文件存在且未过期;非妙搭沙箱时跳过",
2106
2132
  dependsOn: ["config_syntax_check"],
2107
2133
  repairMode: "standard",
2108
2134
  usesVars: ["providerFilePath"],
@@ -2159,6 +2185,7 @@ let SecretsFileRule = class SecretsFileRule extends DiagnoseRule {
2159
2185
  };
2160
2186
  SecretsFileRule = __decorate([Rule({
2161
2187
  key: "secrets_file",
2188
+ description: "验证 miaoda secret-provider 引用的 secrets.json 文件存在且可解析;非妙搭或非 secret-provider 沙箱时跳过",
2162
2189
  dependsOn: ["config_syntax_check"],
2163
2190
  repairMode: "standard",
2164
2191
  usesVars: [
@@ -2214,6 +2241,7 @@ let CleanupInstallBackupDirsRule = class CleanupInstallBackupDirsRule extends Di
2214
2241
  };
2215
2242
  CleanupInstallBackupDirsRule = __decorate([Rule({
2216
2243
  key: "cleanup_install_backup_dirs",
2244
+ description: "清理 extensions/ 目录中升级中断遗留的 .openclaw-install-stage-* 和 .openclaw-install-backups/ 脏目录",
2217
2245
  repairMode: "standard"
2218
2246
  })], CleanupInstallBackupDirsRule);
2219
2247
  //#endregion
@@ -2269,6 +2297,7 @@ let MiaodaOfficialPluginsInstallSpecUnlockRule = class MiaodaOfficialPluginsInst
2269
2297
  };
2270
2298
  MiaodaOfficialPluginsInstallSpecUnlockRule = __decorate([Rule({
2271
2299
  key: "miaoda_official_plugins_install_spec_unlock",
2300
+ description: "移除官方妙搭插件安装条目中的锁版本 npm spec,使其跟随最新 manifest 版本",
2272
2301
  dependsOn: ["config_syntax_check"],
2273
2302
  repairMode: "standard"
2274
2303
  })], MiaodaOfficialPluginsInstallSpecUnlockRule);
@@ -2335,6 +2364,7 @@ let OldMiaodaPluginsCleanupRule = class OldMiaodaPluginsCleanupRule extends Diag
2335
2364
  };
2336
2365
  OldMiaodaPluginsCleanupRule = __decorate([Rule({
2337
2366
  key: "old_miaoda_plugins_cleanup",
2367
+ description: "当新版 openclaw-extension-miaoda 已存在时,清理过时插件引用(openclaw-feishu-greeting、openclaw-miaoda-keepalive 等)",
2338
2368
  dependsOn: ["config_syntax_check"],
2339
2369
  repairMode: "standard"
2340
2370
  })], OldMiaodaPluginsCleanupRule);
@@ -2372,6 +2402,7 @@ let LarkPluginAllowRule = class LarkPluginAllowRule extends DiagnoseRule {
2372
2402
  };
2373
2403
  LarkPluginAllowRule = __decorate([Rule({
2374
2404
  key: "lark_plugin_allow",
2405
+ description: "当飞书插件(openclaw-lark 或旧版名)已在磁盘安装但未加入 plugins.allow 时,自动添加",
2375
2406
  dependsOn: ["config_syntax_check"],
2376
2407
  repairMode: "standard"
2377
2408
  })], LarkPluginAllowRule);
@@ -2427,6 +2458,7 @@ let MiaodaPluginAllowRule = class MiaodaPluginAllowRule extends DiagnoseRule {
2427
2458
  };
2428
2459
  MiaodaPluginAllowRule = __decorate([Rule({
2429
2460
  key: "miaoda_plugin_allow",
2461
+ description: "当 openclaw-extension-miaoda 已在磁盘安装但未在 allow 列表中时,将其添加到 plugins.allow(实验性)",
2430
2462
  dependsOn: ["config_syntax_check"],
2431
2463
  repairMode: "standard",
2432
2464
  profile: "experimental"
@@ -2468,6 +2500,7 @@ let BuiltinPluginMissingRule = class BuiltinPluginMissingRule extends DiagnoseRu
2468
2500
  };
2469
2501
  BuiltinPluginMissingRule = __decorate([Rule({
2470
2502
  key: "builtin_plugin_missing",
2503
+ description: "检查所有内置扩展插件(openclaw-lark、openclaw-extension-miaoda 等)是否已在磁盘安装;缺失时提示重新安装(实验性)",
2471
2504
  dependsOn: ["config_syntax_check"],
2472
2505
  repairMode: "user-confirm",
2473
2506
  profile: "experimental"
@@ -2510,6 +2543,7 @@ let BuiltinPluginInstallsCleanupRule = class BuiltinPluginInstallsCleanupRule ex
2510
2543
  };
2511
2544
  BuiltinPluginInstallsCleanupRule = __decorate([Rule({
2512
2545
  key: "builtin_plugin_installs_cleanup",
2546
+ description: "在 builtin_plugin_missing 通过后,清除 plugins.installs 中磁盘目录已不存在的条目(实验性)",
2513
2547
  dependsOn: ["builtin_plugin_missing"],
2514
2548
  repairMode: "standard",
2515
2549
  profile: "experimental"
@@ -2792,6 +2826,7 @@ let FeishuPluginStateNormalizeRule = class FeishuPluginStateNormalizeRule extend
2792
2826
  };
2793
2827
  FeishuPluginStateNormalizeRule = __decorate([Rule({
2794
2828
  key: "feishu_plugin_state_normalize",
2829
+ description: "规范化磁盘上的飞书插件状态:删除旧版插件目录、清理内置 feishu 目录,并将 openclaw.json 条目对齐到 openclaw-lark 插件",
2795
2830
  dependsOn: ["config_syntax_check"],
2796
2831
  repairMode: "standard"
2797
2832
  })], FeishuPluginStateNormalizeRule);
@@ -3025,6 +3060,7 @@ let FeishuPluginVersionCompatRule = class FeishuPluginVersionCompatRule extends
3025
3060
  };
3026
3061
  FeishuPluginVersionCompatRule = __decorate([Rule({
3027
3062
  key: "feishu_plugin_version_compat",
3063
+ description: "检查飞书插件与 openclaw 版本兼容性;不兼容时推荐 upgrade_openclaw 或 upgrade_lark 操作",
3028
3064
  dependsOn: ["config_syntax_check"],
3029
3065
  repairMode: "user-confirm",
3030
3066
  usesVars: ["recommendedOpenclawTag"]
@@ -3185,6 +3221,7 @@ let FeishuAccountsConsistencyRule = class FeishuAccountsConsistencyRule extends
3185
3221
  };
3186
3222
  FeishuAccountsConsistencyRule = __decorate([Rule({
3187
3223
  key: "feishu_accounts_consistency",
3224
+ description: "检测多 agent 配置中 channels.feishu.accounts、agents.list 与 feishu bindings 之间的数量/ID 不一致(实验性)",
3188
3225
  dependsOn: ["config_syntax_check"],
3189
3226
  repairMode: "check-only",
3190
3227
  usesVars: ["feishuAppID"],
@@ -4134,7 +4171,7 @@ async function installExtension(tag, ossFileMap, opts = {}) {
4134
4171
  };
4135
4172
  }));
4136
4173
  for (const { pkg, tarball } of tarballs) {
4137
- installOne$1(pkg, tarball, homeBase);
4174
+ installOne(pkg, tarball, homeBase);
4138
4175
  console.error(`[install-extension] ${pkg.name}: installed`);
4139
4176
  }
4140
4177
  if (!opts.skipConfigUpdate) updatePluginInstalls(opts.configPath ?? node_path.default.join(homeBase, "workspace/agent/openclaw.json"), targets);
@@ -4187,7 +4224,7 @@ function updatePluginInstalls(configPath, installedPkgs) {
4187
4224
  moveSafe(tmpPath, configPath);
4188
4225
  console.error(`[install-extension] plugins.installs updated: ${updated} entry(ies) in ${configPath}` + (skipped > 0 ? ` (${skipped} package(s) without installMetadata skipped)` : ""));
4189
4226
  }
4190
- function installOne$1(pkg, tarball, homeBase) {
4227
+ function installOne(pkg, tarball, homeBase) {
4191
4228
  const destDir = node_path.default.join(homeBase, pkg.installPath);
4192
4229
  const stagingDir = destDir + ".new";
4193
4230
  const oldDir = destDir + ".old";
@@ -4729,464 +4766,114 @@ function sleepSync(ms) {
4729
4766
  Atomics.wait(arr, 0, 0, ms);
4730
4767
  }
4731
4768
  //#endregion
4732
- //#region src/lark-cli-init.ts
4733
- const LARK_PLUGIN_NAMES = ["openclaw-lark", "feishu-openclaw-plugin"];
4734
- const PE_XML_TAG = "lark-cli-pe";
4735
- const PE_PLACEHOLDER = `
4736
- <${PE_XML_TAG}>
4737
- **【强制要求 - 无例外】飞书所有操作必须先读 feishu-lark-cli skill。未读skill不得调用任何飞书工具,违者视为操作失误。**
4738
- </${PE_XML_TAG}>
4739
- `;
4740
- function isLarkPluginInstalled(configPath) {
4741
- const extDir = getExtensionsDir(configPath);
4742
- return LARK_PLUGIN_NAMES.some((name) => {
4743
- try {
4744
- return node_fs.default.existsSync(node_path.default.join(extDir, name, "package.json"));
4745
- } catch {
4746
- return false;
4747
- }
4748
- });
4749
- }
4750
- function isLarkCliAvailable() {
4751
- try {
4752
- return (0, node_child_process.spawnSync)("lark-cli", ["--version"], {
4753
- encoding: "utf-8",
4754
- timeout: 5e3,
4755
- stdio: [
4756
- "ignore",
4757
- "pipe",
4758
- "ignore"
4759
- ]
4760
- }).status === 0;
4761
- } catch {
4762
- return false;
4763
- }
4764
- }
4765
- function readConfig(configPath) {
4766
- try {
4767
- const raw = node_fs.default.readFileSync(configPath, "utf-8");
4768
- const parsed = loadJSON5().parse(raw);
4769
- return parsed != null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
4770
- } catch {
4771
- return null;
4772
- }
4773
- }
4769
+ //#region src/oss/resolveOssFileMap.ts
4774
4770
  /**
4775
- * Resolve the feishu app secret for the given appId.
4776
- *
4777
- * Lookup order:
4778
- * 1. channels.feishu.appSecret (single-agent: feishu.appId === appId)
4779
- * 2. channels.feishu.accounts[key].appSecret (multi-agent: account.appId === appId)
4780
- *
4781
- * Value interpretation:
4782
- * - string → use directly
4783
- * - object → secret is managed by a provider; use `feishuAppSecret` param instead
4771
+ * Pick an OssFileMap in the order of decreasing specificity:
4772
+ * 1. `--oss_file_map=` flag — operator override (manual invocations, tests)
4773
+ * 2. `ctx.install.ossFileMap` — new shape (innerapi-driven DoctorCtx)
4774
+ * 3. `ctx.resetData.ossFileMap` — legacy shape (sandbox_console push path)
4784
4775
  *
4785
- * Returns null when the secret cannot be determined.
4776
+ * Throws when none of the three yields a non-empty map. Empty maps are
4777
+ * treated as missing — an empty map is useless downstream and almost always
4778
+ * indicates a ctx wiring bug.
4786
4779
  */
4787
- function resolveAppSecret(appId, config, feishuAppSecret) {
4788
- const feishu = getNestedMap(config, "channels", "feishu");
4789
- if (!feishu) return null;
4790
- let rawSecret;
4791
- if (typeof feishu.appId === "string" && feishu.appId === appId) rawSecret = feishu.appSecret;
4792
- else {
4793
- const accounts = asRecord(feishu.accounts);
4794
- if (accounts) for (const [, val] of Object.entries(accounts)) {
4795
- const account = asRecord(val);
4796
- if (account?.appId === appId) {
4797
- rawSecret = account.appSecret ?? feishu.appSecret;
4798
- break;
4799
- }
4800
- }
4801
- }
4802
- if (typeof rawSecret === "string" && rawSecret) return rawSecret;
4803
- if (rawSecret != null && typeof rawSecret === "object") return feishuAppSecret ?? null;
4804
- return null;
4780
+ function resolveOssFileMap(args) {
4781
+ if (args.ossFileMapFlag) return JSON.parse(Buffer.from(args.ossFileMapFlag, "base64").toString("utf-8"));
4782
+ if (args.installOssFileMap && Object.keys(args.installOssFileMap).length > 0) return args.installOssFileMap;
4783
+ if (args.resetDataOssFileMap && Object.keys(args.resetDataOssFileMap).length > 0) return args.resetDataOssFileMap;
4784
+ throw new Error("ossFileMap missing: provide --oss_file_map flag, ctx.install.ossFileMap, or resetData.ossFileMap");
4805
4785
  }
4806
- /**
4807
- * Resolve the agents.md path for the given appId from the openclaw config.
4808
- *
4809
- * Case 1: appId matches channels.feishu.appId (single-agent path)
4810
- * → WORKSPACE_DIR/AGENTS.md
4811
- *
4812
- * Case 2: appId found in channels.feishu.accounts (multi-agent path)
4813
- * → find account key where account.appId === appId
4814
- * → find binding where match.channel=feishu && match.accountId=that key
4815
- * → if agentId === 'main' → WORKSPACE_DIR/agents.md
4816
- * → else find agent in agents.list by id → agent.workspace/agents.md
4817
- *
4818
- * Returns null when the path cannot be determined.
4819
- */
4820
- function resolveAgentsMdPath(appId, config) {
4821
- const feishu = getNestedMap(config, "channels", "feishu");
4822
- if (!feishu) {
4823
- console.error("resolveAgentsMdPath: channels.feishu not found");
4824
- return null;
4825
- }
4826
- if (typeof feishu.appId === "string" && feishu.appId === appId) {
4827
- console.error(`resolveAgentsMdPath: case=single-agent feishu.appId=${feishu.appId}`);
4828
- return node_path.default.join(WORKSPACE_DIR, "workspace", "AGENTS.md");
4829
- }
4830
- const accounts = asRecord(feishu.accounts);
4831
- if (!accounts) {
4832
- console.error("resolveAgentsMdPath: feishu.accounts not found");
4833
- return null;
4834
- }
4835
- let accountId;
4836
- for (const [key, val] of Object.entries(accounts)) if (asRecord(val)?.appId === appId) {
4837
- accountId = key;
4838
- break;
4839
- }
4840
- if (!accountId) {
4841
- console.error(`resolveAgentsMdPath: no account found with appId=${appId} in feishu.accounts keys=[${Object.keys(accounts).join(",")}]`);
4842
- return null;
4786
+ //#endregion
4787
+ //#region src/innerapi/client.ts
4788
+ var import_dist = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
4789
+ var __defProp = Object.defineProperty;
4790
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4791
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4792
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
4793
+ var __export = (target, all) => {
4794
+ for (var name in all) __defProp(target, name, {
4795
+ get: all[name],
4796
+ enumerable: true
4797
+ });
4798
+ };
4799
+ var __copyProps = (to, from, except, desc) => {
4800
+ if (from && typeof from === "object" || typeof from === "function") {
4801
+ for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
4802
+ get: () => from[key],
4803
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
4804
+ });
4805
+ }
4806
+ return to;
4807
+ };
4808
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
4809
+ var index_exports = {};
4810
+ __export(index_exports, {
4811
+ DEFAULT_CLOCK_TOLERANCE_SEC: () => DEFAULT_CLOCK_TOLERANCE_SEC,
4812
+ DEFAULT_JWT_EXPIRE_TIME_MS: () => DEFAULT_JWT_EXPIRE_TIME_MS,
4813
+ HttpClient: () => HttpClient,
4814
+ HttpError: () => HttpError,
4815
+ generateJWTToken: () => generateJWTToken,
4816
+ parseJWTTokenWithVerify: () => parseJWTTokenWithVerify,
4817
+ registerPlatformPlugin: () => registerPlatformPlugin,
4818
+ resolvePlatformBaseURL: () => resolvePlatformBaseURL
4819
+ });
4820
+ module.exports = __toCommonJS(index_exports);
4821
+ var import_crypto = require("crypto");
4822
+ var DEFAULT_JWT_EXPIRE_TIME_MS = 1800 * 1e3;
4823
+ var DEFAULT_CLOCK_TOLERANCE_SEC = 60;
4824
+ var JWT_HEADER = {
4825
+ alg: "HS256",
4826
+ typ: "JWT"
4827
+ };
4828
+ function generateJWTToken(customClaims, config) {
4829
+ const nowSeconds = Math.floor(Date.now() / 1e3);
4830
+ const payload = {
4831
+ ...customClaims,
4832
+ iss: customClaims.access_key,
4833
+ iat: nowSeconds,
4834
+ nbf: nowSeconds,
4835
+ exp: nowSeconds + Math.floor(config.expireTimeMs / 1e3),
4836
+ jti: (0, import_crypto.randomUUID)()
4837
+ };
4838
+ const encodedHeader = base64UrlEncode(JSON.stringify(JWT_HEADER));
4839
+ const encodedPayload = base64UrlEncode(JSON.stringify(payload));
4840
+ return `${encodedHeader}.${encodedPayload}.${sign(`${encodedHeader}.${encodedPayload}`, config.secretKey)}`;
4843
4841
  }
4844
- console.error(`resolveAgentsMdPath: found accountId=${accountId}`);
4845
- const bindings = Array.isArray(config.bindings) ? config.bindings : [];
4846
- let agentId;
4847
- for (const b of bindings) {
4848
- const binding = asRecord(b);
4849
- if (!binding) continue;
4850
- const match = asRecord(binding.match);
4851
- if (match?.channel === "feishu" && match?.accountId === accountId) {
4852
- if (typeof binding.agentId === "string") {
4853
- agentId = binding.agentId;
4854
- break;
4842
+ function parseJWTTokenWithVerify(tokenString, config, options) {
4843
+ const segments = tokenString.split(".");
4844
+ if (segments.length !== 3) throw new Error("invalid JWT token format");
4845
+ const [encodedHeader, encodedPayload, signature] = segments;
4846
+ if (JSON.parse(base64UrlDecode(encodedHeader)).alg !== "HS256") throw new Error("unsupported JWT alg");
4847
+ const expectedSignature = sign(`${encodedHeader}.${encodedPayload}`, config.secretKey);
4848
+ const expectedBuffer = Buffer.from(expectedSignature);
4849
+ const signatureBuffer = Buffer.from(signature);
4850
+ if (expectedBuffer.length !== signatureBuffer.length || !(0, import_crypto.timingSafeEqual)(expectedBuffer, signatureBuffer)) throw new Error("JWT signature verification failed");
4851
+ const payload = JSON.parse(base64UrlDecode(encodedPayload));
4852
+ if (!options?.skipExpiration) {
4853
+ const clockTolerance = options?.clockTolerance ?? DEFAULT_CLOCK_TOLERANCE_SEC;
4854
+ const now = Math.floor(Date.now() / 1e3);
4855
+ if (payload.exp !== void 0) {
4856
+ if (payload.exp + clockTolerance < now) throw new Error(`JWT token expired at ${(/* @__PURE__ */ new Date(payload.exp * 1e3)).toISOString()}`);
4857
+ }
4858
+ if (payload.nbf !== void 0) {
4859
+ if (payload.nbf - clockTolerance > now) throw new Error(`JWT token not yet valid, will be valid at ${(/* @__PURE__ */ new Date(payload.nbf * 1e3)).toISOString()}`);
4860
+ }
4861
+ if (payload.iat !== void 0) {
4862
+ if (payload.iat - clockTolerance > now) throw new Error(`JWT token issued in the future at ${(/* @__PURE__ */ new Date(payload.iat * 1e3)).toISOString()}`);
4855
4863
  }
4856
4864
  }
4865
+ return payload;
4857
4866
  }
4858
- if (!agentId) {
4859
- console.error(`resolveAgentsMdPath: no binding found for accountId=${accountId} in bindings(count=${bindings.length})`);
4860
- return null;
4861
- }
4862
- console.error(`resolveAgentsMdPath: found agentId=${agentId}`);
4863
- if (agentId === "main") {
4864
- console.error("resolveAgentsMdPath: case=multi-agent-main");
4865
- return node_path.default.join(WORKSPACE_DIR, "workspace", "AGENTS.md");
4866
- }
4867
- const agentsObj = asRecord(config.agents);
4868
- const list = Array.isArray(agentsObj?.list) ? agentsObj.list : [];
4869
- for (const a of list) {
4870
- const agent = asRecord(a);
4871
- if (agent?.id === agentId) {
4872
- const ws = typeof agent.workspace === "string" ? agent.workspace : node_path.default.join(WORKSPACE_DIR, "workspace");
4873
- console.error(`resolveAgentsMdPath: case=multi-agent-custom agentId=${agentId} workspace=${ws}`);
4874
- return node_path.default.join(ws, "AGENTS.md");
4875
- }
4867
+ function sign(input, secret) {
4868
+ return (0, import_crypto.createHmac)("sha256", secret).update(input).digest("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
4876
4869
  }
4877
- console.error(`resolveAgentsMdPath: agentId=${agentId} not found in agents.list(count=${list.length})`);
4878
- return null;
4879
- }
4880
- function appendPeToAgentsMd(agentsMdPath) {
4881
- const dir = node_path.default.dirname(agentsMdPath);
4882
- if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
4883
- const existing = node_fs.default.existsSync(agentsMdPath) ? node_fs.default.readFileSync(agentsMdPath, "utf-8") : "";
4884
- if (existing.includes(`<${PE_XML_TAG}>`)) {
4885
- console.error(`lark-cli-init: <${PE_XML_TAG}> already present in ${agentsMdPath}, skipping`);
4886
- return;
4870
+ function base64UrlEncode(input) {
4871
+ return Buffer.from(input).toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
4887
4872
  }
4888
- const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
4889
- node_fs.default.appendFileSync(agentsMdPath, `${sep}${PE_PLACEHOLDER}`, "utf-8");
4890
- console.error(`lark-cli-init: appended PE placeholder to ${agentsMdPath}`);
4891
- }
4892
- /**
4893
- * Collect every Feishu bot appId declared in the openclaw config.
4894
- * Covers both single-agent (channels.feishu.appId) and multi-agent
4895
- * (channels.feishu.accounts[*].appId) layouts. Returns a deduplicated list.
4896
- */
4897
- function collectFeishuAppIds(configPath) {
4898
- const config = readConfig(configPath ?? CONFIG_PATH);
4899
- if (!config) return [];
4900
- const feishu = getNestedMap(config, "channels", "feishu");
4901
- if (!feishu) return [];
4902
- const appIds = /* @__PURE__ */ new Set();
4903
- if (typeof feishu.appId === "string" && feishu.appId) appIds.add(feishu.appId);
4904
- const accounts = asRecord(feishu.accounts);
4905
- if (accounts) for (const val of Object.values(accounts)) {
4906
- const account = asRecord(val);
4907
- if (typeof account?.appId === "string" && account.appId) appIds.add(account.appId);
4908
- }
4909
- return [...appIds];
4910
- }
4911
- function runLarkCliInit(opts) {
4912
- const configPath = opts.configPath ?? CONFIG_PATH;
4913
- if (!isLarkPluginInstalled(configPath)) {
4914
- console.error("lark-cli-init: skipping — openclaw-lark plugin not installed");
4915
- return {
4916
- ok: true,
4917
- skipped: true,
4918
- skipReason: "openclaw-lark plugin not installed"
4919
- };
4920
- }
4921
- if (!isLarkCliAvailable()) {
4922
- console.error("lark-cli-init: skipping — lark-cli command not found");
4923
- return {
4924
- ok: true,
4925
- skipped: true,
4926
- skipReason: "lark-cli command not found"
4927
- };
4928
- }
4929
- const config = readConfig(configPath);
4930
- if (!config) return {
4931
- ok: false,
4932
- error: `could not read config at ${configPath}`
4933
- };
4934
- const agentsMdPath = resolveAgentsMdPath(opts.appId, config);
4935
- console.error(`lark-cli-init: resolved agents.md path=${agentsMdPath ?? "(null)"}`);
4936
- if (!agentsMdPath) return {
4937
- ok: false,
4938
- error: `could not resolve agents.md path for appId=${opts.appId}`
4939
- };
4940
- const appSecret = resolveAppSecret(opts.appId, config, opts.feishuAppSecret);
4941
- if (!appSecret) return {
4942
- ok: false,
4943
- error: `could not resolve appSecret for appId=${opts.appId}`
4944
- };
4945
- console.error(`lark-cli-init: running lark-cli config init --name ${opts.appId} --app-id ${opts.appId} --brand feishu --app-secret-stdin --force-init`);
4946
- const initRes = (0, node_child_process.spawnSync)("lark-cli", [
4947
- "config",
4948
- "init",
4949
- "--name",
4950
- opts.appId,
4951
- "--app-id",
4952
- opts.appId,
4953
- "--brand",
4954
- "feishu",
4955
- "--app-secret-stdin",
4956
- "--force-init"
4957
- ], {
4958
- stdio: [
4959
- "pipe",
4960
- "pipe",
4961
- "pipe"
4962
- ],
4963
- encoding: "utf-8",
4964
- input: appSecret
4965
- });
4966
- const configInitStdout = initRes.stdout?.trim() || void 0;
4967
- const configInitStderr = initRes.stderr?.trim() || void 0;
4968
- if (configInitStdout) console.error(`lark-cli config init stdout: ${configInitStdout}`);
4969
- if (configInitStderr) console.error(`lark-cli config init stderr: ${configInitStderr}`);
4970
- if (initRes.error) return {
4971
- ok: false,
4972
- configInitStdout,
4973
- configInitStderr,
4974
- error: `lark-cli config init spawn error: ${initRes.error.message}`
4975
- };
4976
- if (initRes.status !== 0) return {
4977
- ok: false,
4978
- configInitExitCode: initRes.status ?? void 0,
4979
- configInitStdout,
4980
- configInitStderr,
4981
- error: `lark-cli config init exited with code ${initRes.status}`
4982
- };
4983
- appendPeToAgentsMd(agentsMdPath);
4984
- return {
4985
- ok: true,
4986
- configInitExitCode: 0,
4987
- agentsMdPath
4988
- };
4989
- }
4990
- const LARK_CLI_NAME = "lark-cli";
4991
- async function installClis(tag, ossFileMap, opts) {
4992
- const homeBase = opts.homeBase ?? "/home/gem";
4993
- if (opts.names.length === 0) throw new Error("install-clis: must provide at least one --cli=<name>");
4994
- const manifest = await fetchManifest(ossFileMap, tag);
4995
- console.error(`[install-clis] manifest=${JSON.stringify(manifest)}`);
4996
- const allClis = manifest.packages.filter((p) => p.role === "cli" && p.name !== "openclaw");
4997
- const wanted = new Set(opts.names);
4998
- const targets = allClis.filter((p) => wanted.has(p.name) || p.packageName != null && wanted.has(p.packageName));
4999
- const foundKeys = /* @__PURE__ */ new Set();
5000
- for (const t of targets) {
5001
- foundKeys.add(t.name);
5002
- if (t.packageName) foundKeys.add(t.packageName);
5003
- }
5004
- const missing = opts.names.filter((n) => !foundKeys.has(n));
5005
- if (missing.length > 0) throw new Error(`install-clis: not found in manifest: ${missing.join(", ")}`);
5006
- console.error(`[install-clis] tag=${tag} targets=${targets.length}`);
5007
- const t0 = Date.now();
5008
- const tarballs = await Promise.all(targets.map(async (p) => {
5009
- const tb = await downloadWithCache(p, ossFileMap, opts);
5010
- console.error(`[install-clis] ${p.name}: downloaded`);
5011
- return {
5012
- pkg: p,
5013
- tarball: tb
5014
- };
5015
- }));
5016
- for (const { pkg, tarball } of tarballs) {
5017
- installOne(pkg, tarball, homeBase, opts.tmpRoot);
5018
- console.error(`[install-clis] ${pkg.name}: installed`);
5019
- }
5020
- if (targets.some((p) => p.name === LARK_CLI_NAME)) {
5021
- const appIds = collectFeishuAppIds();
5022
- console.error(`[install-clis] lark-cli installed — running lark-cli-init for ${appIds.length} appId(s): [${appIds.join(", ")}]`);
5023
- for (const appId of appIds) {
5024
- console.error(`[install-clis] lark-cli-init: appId=${appId}`);
5025
- const result = runLarkCliInit({
5026
- appId,
5027
- feishuAppSecret: opts.feishuAppSecret
5028
- });
5029
- console.error(`[install-clis] lark-cli-init: appId=${appId} ok=${result.ok}` + (result.skipped ? ` skipped=true reason=${result.skipReason}` : "") + (result.error ? ` error=${result.error}` : ""));
5030
- }
5031
- }
5032
- console.error(`[install-clis] done ${targets.length}/${targets.length} in ${Date.now() - t0}ms`);
5033
- }
5034
- function installOne(pkg, tarball, homeBase, tmpRoot) {
5035
- const targetDir = node_path.default.join(homeBase, pkg.installPath);
5036
- const bakDir = targetDir + ".bak";
5037
- const newDir = targetDir + ".new";
5038
- node_fs.default.mkdirSync(node_path.default.dirname(targetDir), { recursive: true });
5039
- if (node_fs.default.existsSync(newDir)) node_fs.default.rmSync(newDir, {
5040
- recursive: true,
5041
- force: true
5042
- });
5043
- if (node_fs.default.existsSync(bakDir)) node_fs.default.rmSync(bakDir, {
5044
- recursive: true,
5045
- force: true
5046
- });
5047
- const tmpStage = node_fs.default.mkdtempSync(node_path.default.join(tmpRoot ?? node_os.default.tmpdir(), "cli-install-"));
5048
- try {
5049
- extractTarballTolerant(tarball, tmpStage, { stripComponents: 1 });
5050
- if (!node_fs.default.existsSync(node_path.default.join(tmpStage, "package.json"))) throw new Error(`cli tarball missing package.json: ${pkg.name}`);
5051
- moveSafe(tmpStage, newDir);
5052
- const hadExisting = node_fs.default.existsSync(targetDir);
5053
- try {
5054
- if (hadExisting) moveSafe(targetDir, bakDir);
5055
- moveSafe(newDir, targetDir);
5056
- } catch (e) {
5057
- if (hadExisting && !node_fs.default.existsSync(targetDir) && node_fs.default.existsSync(bakDir)) try {
5058
- moveSafe(bakDir, targetDir);
5059
- } catch {}
5060
- try {
5061
- node_fs.default.rmSync(newDir, {
5062
- recursive: true,
5063
- force: true
5064
- });
5065
- } catch {}
5066
- throw e;
5067
- }
5068
- if (hadExisting && node_fs.default.existsSync(bakDir)) node_fs.default.rmSync(bakDir, {
5069
- recursive: true,
5070
- force: true
5071
- });
5072
- } finally {
5073
- if (node_fs.default.existsSync(tmpStage)) try {
5074
- node_fs.default.rmSync(tmpStage, {
5075
- recursive: true,
5076
- force: true
5077
- });
5078
- } catch {}
5079
- }
5080
- }
5081
- //#endregion
5082
- //#region src/oss/resolveOssFileMap.ts
5083
- /**
5084
- * Pick an OssFileMap in the order of decreasing specificity:
5085
- * 1. `--oss_file_map=` flag — operator override (manual invocations, tests)
5086
- * 2. `ctx.install.ossFileMap` — new shape (innerapi-driven DoctorCtx)
5087
- * 3. `ctx.resetData.ossFileMap` — legacy shape (sandbox_console push path)
5088
- *
5089
- * Throws when none of the three yields a non-empty map. Empty maps are
5090
- * treated as missing — an empty map is useless downstream and almost always
5091
- * indicates a ctx wiring bug.
5092
- */
5093
- function resolveOssFileMap(args) {
5094
- if (args.ossFileMapFlag) return JSON.parse(Buffer.from(args.ossFileMapFlag, "base64").toString("utf-8"));
5095
- if (args.installOssFileMap && Object.keys(args.installOssFileMap).length > 0) return args.installOssFileMap;
5096
- if (args.resetDataOssFileMap && Object.keys(args.resetDataOssFileMap).length > 0) return args.resetDataOssFileMap;
5097
- throw new Error("ossFileMap missing: provide --oss_file_map flag, ctx.install.ossFileMap, or resetData.ossFileMap");
5098
- }
5099
- //#endregion
5100
- //#region src/innerapi/client.ts
5101
- var import_dist = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
5102
- var __defProp = Object.defineProperty;
5103
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5104
- var __getOwnPropNames = Object.getOwnPropertyNames;
5105
- var __hasOwnProp = Object.prototype.hasOwnProperty;
5106
- var __export = (target, all) => {
5107
- for (var name in all) __defProp(target, name, {
5108
- get: all[name],
5109
- enumerable: true
5110
- });
5111
- };
5112
- var __copyProps = (to, from, except, desc) => {
5113
- if (from && typeof from === "object" || typeof from === "function") {
5114
- for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
5115
- get: () => from[key],
5116
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
5117
- });
5118
- }
5119
- return to;
5120
- };
5121
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
5122
- var index_exports = {};
5123
- __export(index_exports, {
5124
- DEFAULT_CLOCK_TOLERANCE_SEC: () => DEFAULT_CLOCK_TOLERANCE_SEC,
5125
- DEFAULT_JWT_EXPIRE_TIME_MS: () => DEFAULT_JWT_EXPIRE_TIME_MS,
5126
- HttpClient: () => HttpClient,
5127
- HttpError: () => HttpError,
5128
- generateJWTToken: () => generateJWTToken,
5129
- parseJWTTokenWithVerify: () => parseJWTTokenWithVerify,
5130
- registerPlatformPlugin: () => registerPlatformPlugin,
5131
- resolvePlatformBaseURL: () => resolvePlatformBaseURL
5132
- });
5133
- module.exports = __toCommonJS(index_exports);
5134
- var import_crypto = require("crypto");
5135
- var DEFAULT_JWT_EXPIRE_TIME_MS = 1800 * 1e3;
5136
- var DEFAULT_CLOCK_TOLERANCE_SEC = 60;
5137
- var JWT_HEADER = {
5138
- alg: "HS256",
5139
- typ: "JWT"
5140
- };
5141
- function generateJWTToken(customClaims, config) {
5142
- const nowSeconds = Math.floor(Date.now() / 1e3);
5143
- const payload = {
5144
- ...customClaims,
5145
- iss: customClaims.access_key,
5146
- iat: nowSeconds,
5147
- nbf: nowSeconds,
5148
- exp: nowSeconds + Math.floor(config.expireTimeMs / 1e3),
5149
- jti: (0, import_crypto.randomUUID)()
5150
- };
5151
- const encodedHeader = base64UrlEncode(JSON.stringify(JWT_HEADER));
5152
- const encodedPayload = base64UrlEncode(JSON.stringify(payload));
5153
- return `${encodedHeader}.${encodedPayload}.${sign(`${encodedHeader}.${encodedPayload}`, config.secretKey)}`;
5154
- }
5155
- function parseJWTTokenWithVerify(tokenString, config, options) {
5156
- const segments = tokenString.split(".");
5157
- if (segments.length !== 3) throw new Error("invalid JWT token format");
5158
- const [encodedHeader, encodedPayload, signature] = segments;
5159
- if (JSON.parse(base64UrlDecode(encodedHeader)).alg !== "HS256") throw new Error("unsupported JWT alg");
5160
- const expectedSignature = sign(`${encodedHeader}.${encodedPayload}`, config.secretKey);
5161
- const expectedBuffer = Buffer.from(expectedSignature);
5162
- const signatureBuffer = Buffer.from(signature);
5163
- if (expectedBuffer.length !== signatureBuffer.length || !(0, import_crypto.timingSafeEqual)(expectedBuffer, signatureBuffer)) throw new Error("JWT signature verification failed");
5164
- const payload = JSON.parse(base64UrlDecode(encodedPayload));
5165
- if (!options?.skipExpiration) {
5166
- const clockTolerance = options?.clockTolerance ?? DEFAULT_CLOCK_TOLERANCE_SEC;
5167
- const now = Math.floor(Date.now() / 1e3);
5168
- if (payload.exp !== void 0) {
5169
- if (payload.exp + clockTolerance < now) throw new Error(`JWT token expired at ${(/* @__PURE__ */ new Date(payload.exp * 1e3)).toISOString()}`);
5170
- }
5171
- if (payload.nbf !== void 0) {
5172
- if (payload.nbf - clockTolerance > now) throw new Error(`JWT token not yet valid, will be valid at ${(/* @__PURE__ */ new Date(payload.nbf * 1e3)).toISOString()}`);
5173
- }
5174
- if (payload.iat !== void 0) {
5175
- if (payload.iat - clockTolerance > now) throw new Error(`JWT token issued in the future at ${(/* @__PURE__ */ new Date(payload.iat * 1e3)).toISOString()}`);
5176
- }
5177
- }
5178
- return payload;
5179
- }
5180
- function sign(input, secret) {
5181
- return (0, import_crypto.createHmac)("sha256", secret).update(input).digest("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
5182
- }
5183
- function base64UrlEncode(input) {
5184
- return Buffer.from(input).toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
5185
- }
5186
- function base64UrlDecode(input) {
5187
- const padLength = (4 - (input.length % 4 || 4)) % 4;
5188
- const padded = `${input}${"=".repeat(padLength)}`.replace(/-/g, "+").replace(/_/g, "/");
5189
- return Buffer.from(padded, "base64").toString("utf8");
4873
+ function base64UrlDecode(input) {
4874
+ const padLength = (4 - (input.length % 4 || 4)) % 4;
4875
+ const padded = `${input}${"=".repeat(padLength)}`.replace(/-/g, "+").replace(/_/g, "/");
4876
+ return Buffer.from(padded, "base64").toString("utf8");
5190
4877
  }
5191
4878
  var JWTTokenManager = class {
5192
4879
  cache = /* @__PURE__ */ new Map();
@@ -8180,14 +7867,254 @@ function finalize(results, aborted) {
8180
7867
  };
8181
7868
  }
8182
7869
  //#endregion
8183
- //#region src/innerapi/reportCliRun.ts
8184
- /**
8185
- * CLI-side client for studio_server's `openclaw.report_cli_run` inner
8186
- * API the unified telemetry sink for every CLI command.
8187
- *
8188
- * Best-effort: gated by `DoctorCtx.telemetry.reportCliRun` returned by
8189
- * `get_doctor_ctx`. When the server hasn't rolled out the API yet the
8190
- * flag is absent and the CLI never invokes this. When enabled, the CLI
7870
+ //#region src/lark-cli-init.ts
7871
+ const LARK_PLUGIN_NAMES = ["openclaw-lark", "feishu-openclaw-plugin"];
7872
+ const PE_XML_TAG = "lark-cli-pe";
7873
+ const PE_PLACEHOLDER = `
7874
+ <${PE_XML_TAG}>
7875
+ **【强制要求 - 无例外】飞书所有操作必须先读 feishu-lark-cli skill。未读skill不得调用任何飞书工具,违者视为操作失误。**
7876
+ </${PE_XML_TAG}>
7877
+ `;
7878
+ function isLarkPluginInstalled(configPath) {
7879
+ const extDir = getExtensionsDir(configPath);
7880
+ return LARK_PLUGIN_NAMES.some((name) => {
7881
+ try {
7882
+ return node_fs.default.existsSync(node_path.default.join(extDir, name, "package.json"));
7883
+ } catch {
7884
+ return false;
7885
+ }
7886
+ });
7887
+ }
7888
+ function isLarkCliAvailable() {
7889
+ try {
7890
+ return (0, node_child_process.spawnSync)("lark-cli", ["--version"], {
7891
+ encoding: "utf-8",
7892
+ timeout: 5e3,
7893
+ stdio: [
7894
+ "ignore",
7895
+ "pipe",
7896
+ "ignore"
7897
+ ]
7898
+ }).status === 0;
7899
+ } catch {
7900
+ return false;
7901
+ }
7902
+ }
7903
+ function readConfig(configPath) {
7904
+ try {
7905
+ const raw = node_fs.default.readFileSync(configPath, "utf-8");
7906
+ const parsed = loadJSON5().parse(raw);
7907
+ return parsed != null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
7908
+ } catch {
7909
+ return null;
7910
+ }
7911
+ }
7912
+ /**
7913
+ * Resolve the feishu app secret for the given appId.
7914
+ *
7915
+ * Lookup order:
7916
+ * 1. channels.feishu.appSecret (single-agent: feishu.appId === appId)
7917
+ * 2. channels.feishu.accounts[key].appSecret (multi-agent: account.appId === appId)
7918
+ *
7919
+ * Value interpretation:
7920
+ * - string → use directly
7921
+ * - object → secret is managed by a provider; use `feishuAppSecret` param instead
7922
+ *
7923
+ * Returns null when the secret cannot be determined.
7924
+ */
7925
+ function resolveAppSecret(appId, config, feishuAppSecret) {
7926
+ const feishu = getNestedMap(config, "channels", "feishu");
7927
+ if (!feishu) return null;
7928
+ let rawSecret;
7929
+ if (typeof feishu.appId === "string" && feishu.appId === appId) rawSecret = feishu.appSecret;
7930
+ else {
7931
+ const accounts = asRecord(feishu.accounts);
7932
+ if (accounts) for (const [, val] of Object.entries(accounts)) {
7933
+ const account = asRecord(val);
7934
+ if (account?.appId === appId) {
7935
+ rawSecret = account.appSecret ?? feishu.appSecret;
7936
+ break;
7937
+ }
7938
+ }
7939
+ }
7940
+ if (typeof rawSecret === "string" && rawSecret) return rawSecret;
7941
+ if (rawSecret != null && typeof rawSecret === "object") return feishuAppSecret ?? null;
7942
+ return null;
7943
+ }
7944
+ /**
7945
+ * Resolve the agents.md path for the given appId from the openclaw config.
7946
+ *
7947
+ * Case 1: appId matches channels.feishu.appId (single-agent path)
7948
+ * → WORKSPACE_DIR/AGENTS.md
7949
+ *
7950
+ * Case 2: appId found in channels.feishu.accounts (multi-agent path)
7951
+ * → find account key where account.appId === appId
7952
+ * → find binding where match.channel=feishu && match.accountId=that key
7953
+ * → if agentId === 'main' → WORKSPACE_DIR/agents.md
7954
+ * → else find agent in agents.list by id → agent.workspace/agents.md
7955
+ *
7956
+ * Returns null when the path cannot be determined.
7957
+ */
7958
+ function resolveAgentsMdPath(appId, config) {
7959
+ const feishu = getNestedMap(config, "channels", "feishu");
7960
+ if (!feishu) {
7961
+ console.error("resolveAgentsMdPath: channels.feishu not found");
7962
+ return null;
7963
+ }
7964
+ if (typeof feishu.appId === "string" && feishu.appId === appId) {
7965
+ console.error(`resolveAgentsMdPath: case=single-agent feishu.appId=${feishu.appId}`);
7966
+ return node_path.default.join(WORKSPACE_DIR, "workspace", "AGENTS.md");
7967
+ }
7968
+ const accounts = asRecord(feishu.accounts);
7969
+ if (!accounts) {
7970
+ console.error("resolveAgentsMdPath: feishu.accounts not found");
7971
+ return null;
7972
+ }
7973
+ let accountId;
7974
+ for (const [key, val] of Object.entries(accounts)) if (asRecord(val)?.appId === appId) {
7975
+ accountId = key;
7976
+ break;
7977
+ }
7978
+ if (!accountId) {
7979
+ console.error(`resolveAgentsMdPath: no account found with appId=${appId} in feishu.accounts keys=[${Object.keys(accounts).join(",")}]`);
7980
+ return null;
7981
+ }
7982
+ console.error(`resolveAgentsMdPath: found accountId=${accountId}`);
7983
+ const bindings = Array.isArray(config.bindings) ? config.bindings : [];
7984
+ let agentId;
7985
+ for (const b of bindings) {
7986
+ const binding = asRecord(b);
7987
+ if (!binding) continue;
7988
+ const match = asRecord(binding.match);
7989
+ if (match?.channel === "feishu" && match?.accountId === accountId) {
7990
+ if (typeof binding.agentId === "string") {
7991
+ agentId = binding.agentId;
7992
+ break;
7993
+ }
7994
+ }
7995
+ }
7996
+ if (!agentId) {
7997
+ console.error(`resolveAgentsMdPath: no binding found for accountId=${accountId} in bindings(count=${bindings.length})`);
7998
+ return null;
7999
+ }
8000
+ console.error(`resolveAgentsMdPath: found agentId=${agentId}`);
8001
+ if (agentId === "main") {
8002
+ console.error("resolveAgentsMdPath: case=multi-agent-main");
8003
+ return node_path.default.join(WORKSPACE_DIR, "workspace", "AGENTS.md");
8004
+ }
8005
+ const agentsObj = asRecord(config.agents);
8006
+ const list = Array.isArray(agentsObj?.list) ? agentsObj.list : [];
8007
+ for (const a of list) {
8008
+ const agent = asRecord(a);
8009
+ if (agent?.id === agentId) {
8010
+ const ws = typeof agent.workspace === "string" ? agent.workspace : node_path.default.join(WORKSPACE_DIR, "workspace");
8011
+ console.error(`resolveAgentsMdPath: case=multi-agent-custom agentId=${agentId} workspace=${ws}`);
8012
+ return node_path.default.join(ws, "AGENTS.md");
8013
+ }
8014
+ }
8015
+ console.error(`resolveAgentsMdPath: agentId=${agentId} not found in agents.list(count=${list.length})`);
8016
+ return null;
8017
+ }
8018
+ function appendPeToAgentsMd(agentsMdPath) {
8019
+ const dir = node_path.default.dirname(agentsMdPath);
8020
+ if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
8021
+ const existing = node_fs.default.existsSync(agentsMdPath) ? node_fs.default.readFileSync(agentsMdPath, "utf-8") : "";
8022
+ if (existing.includes(`<${PE_XML_TAG}>`)) {
8023
+ console.error(`lark-cli-init: <${PE_XML_TAG}> already present in ${agentsMdPath}, skipping`);
8024
+ return;
8025
+ }
8026
+ const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
8027
+ node_fs.default.appendFileSync(agentsMdPath, `${sep}${PE_PLACEHOLDER}`, "utf-8");
8028
+ console.error(`lark-cli-init: appended PE placeholder to ${agentsMdPath}`);
8029
+ }
8030
+ function runLarkCliInit(opts) {
8031
+ const configPath = opts.configPath ?? CONFIG_PATH;
8032
+ if (!isLarkPluginInstalled(configPath)) {
8033
+ console.error("lark-cli-init: skipping — openclaw-lark plugin not installed");
8034
+ return {
8035
+ ok: true,
8036
+ skipped: true,
8037
+ skipReason: "openclaw-lark plugin not installed"
8038
+ };
8039
+ }
8040
+ if (!isLarkCliAvailable()) {
8041
+ console.error("lark-cli-init: skipping — lark-cli command not found");
8042
+ return {
8043
+ ok: true,
8044
+ skipped: true,
8045
+ skipReason: "lark-cli command not found"
8046
+ };
8047
+ }
8048
+ const config = readConfig(configPath);
8049
+ if (!config) return {
8050
+ ok: false,
8051
+ error: `could not read config at ${configPath}`
8052
+ };
8053
+ const agentsMdPath = resolveAgentsMdPath(opts.appId, config);
8054
+ console.error(`lark-cli-init: resolved agents.md path=${agentsMdPath ?? "(null)"}`);
8055
+ if (!agentsMdPath) return {
8056
+ ok: false,
8057
+ error: `could not resolve agents.md path for appId=${opts.appId}`
8058
+ };
8059
+ const appSecret = resolveAppSecret(opts.appId, config, opts.feishuAppSecret);
8060
+ if (!appSecret) return {
8061
+ ok: false,
8062
+ error: `could not resolve appSecret for appId=${opts.appId}`
8063
+ };
8064
+ console.error(`lark-cli-init: running lark-cli config init --name ${opts.appId} --app-id ${opts.appId} --brand feishu --app-secret-stdin --force-init`);
8065
+ const initRes = (0, node_child_process.spawnSync)("lark-cli", [
8066
+ "config",
8067
+ "init",
8068
+ "--name",
8069
+ opts.appId,
8070
+ "--app-id",
8071
+ opts.appId,
8072
+ "--brand",
8073
+ "feishu",
8074
+ "--app-secret-stdin",
8075
+ "--force-init"
8076
+ ], {
8077
+ stdio: [
8078
+ "pipe",
8079
+ "pipe",
8080
+ "pipe"
8081
+ ],
8082
+ encoding: "utf-8",
8083
+ input: appSecret
8084
+ });
8085
+ const configInitStdout = initRes.stdout?.trim() || void 0;
8086
+ const configInitStderr = initRes.stderr?.trim() || void 0;
8087
+ if (configInitStdout) console.error(`lark-cli config init stdout: ${configInitStdout}`);
8088
+ if (configInitStderr) console.error(`lark-cli config init stderr: ${configInitStderr}`);
8089
+ if (initRes.error) return {
8090
+ ok: false,
8091
+ configInitStdout,
8092
+ configInitStderr,
8093
+ error: `lark-cli config init spawn error: ${initRes.error.message}`
8094
+ };
8095
+ if (initRes.status !== 0) return {
8096
+ ok: false,
8097
+ configInitExitCode: initRes.status ?? void 0,
8098
+ configInitStdout,
8099
+ configInitStderr,
8100
+ error: `lark-cli config init exited with code ${initRes.status}`
8101
+ };
8102
+ appendPeToAgentsMd(agentsMdPath);
8103
+ return {
8104
+ ok: true,
8105
+ configInitExitCode: 0,
8106
+ agentsMdPath
8107
+ };
8108
+ }
8109
+ //#endregion
8110
+ //#region src/innerapi/reportCliRun.ts
8111
+ /**
8112
+ * CLI-side client for studio_server's `openclaw.report_cli_run` inner
8113
+ * API — the unified telemetry sink for every CLI command.
8114
+ *
8115
+ * Best-effort: gated by `DoctorCtx.telemetry.reportCliRun` returned by
8116
+ * `get_doctor_ctx`. When the server hasn't rolled out the API yet the
8117
+ * flag is absent and the CLI never invokes this. When enabled, the CLI
8191
8118
  * fires-and-forgets one report per command after work is done.
8192
8119
  *
8193
8120
  * Failures here MUST NOT change exit code or otherwise affect the
@@ -8259,7 +8186,7 @@ async function reportCliRun(opts) {
8259
8186
  //#region src/help.ts
8260
8187
  const BIN = "mclaw-diagnose";
8261
8188
  function versionBanner() {
8262
- return `v0.1.7-alpha.1`;
8189
+ return `v0.1.7-beta.0`;
8263
8190
  }
8264
8191
  const COMMANDS = [
8265
8192
  {
@@ -8477,37 +8404,6 @@ OPTIONS
8477
8404
  --skip-config-update Leave plugins.installs in openclaw.json untouched.
8478
8405
  --ctx=<base64> Opaque ctx; see install-openclaw for semantics.
8479
8406
  --oss_file_map=... Pre-built OSS URL map (base64 JSON).
8480
- `
8481
- },
8482
- {
8483
- name: "install-clis",
8484
- hidden: true,
8485
- summary: "Install CLI package(s) (role=cli) from the manifest",
8486
- help: `USAGE
8487
- ${BIN} install-clis <tag> --cli=<name>... [options]
8488
-
8489
- DESCRIPTION
8490
- Downloads + installs one or more CLI tarballs (role=cli in the manifest)
8491
- into <home_base>/<pkg.installPath>. Currently the only CLI in this
8492
- category is lark-cli (@larksuite/cli); openclaw itself is handled by the
8493
- dedicated install-openclaw command.
8494
-
8495
- Each package is first extracted into a temporary directory (os.tmpdir(),
8496
- typically tmpfs) to avoid overlayfs race conditions, then ferried to a
8497
- sibling .new directory and atomically swapped into the final target path.
8498
-
8499
- ARGUMENTS
8500
- <tag> Openclaw version tag, e.g. 2026.4.11.
8501
-
8502
- OPTIONS
8503
- --cli=<name> CLI package to install by short name or scoped
8504
- packageName (repeatable, at least one required).
8505
- --home_base=<dir> Override the /home/gem base (tests).
8506
- --ctx=<base64> Opaque ctx; ossFileMap is extracted from it.
8507
- --oss_file_map=... Pre-built OSS URL map (base64 JSON); skips innerapi.
8508
-
8509
- EXAMPLES
8510
- ${BIN} install-clis 2026.4.11 --cli=lark-cli
8511
8407
  `
8512
8408
  },
8513
8409
  {
@@ -8557,6 +8453,39 @@ OPTIONS
8557
8453
  EXIT CODES
8558
8454
  0 Success or skipped (prerequisites not met).
8559
8455
  1 Secret/path unresolvable, lark-cli failed, or config unreadable.
8456
+ `
8457
+ },
8458
+ {
8459
+ name: "rules",
8460
+ hidden: true,
8461
+ summary: "List all registered rule metadata as JSON",
8462
+ help: `USAGE
8463
+ ${BIN} rules [--rule=<key>]...
8464
+
8465
+ DESCRIPTION
8466
+ Prints a JSON array of serialisable metadata for every registered rule,
8467
+ topologically sorted by dependsOn. Useful for tooling, dashboards, and
8468
+ analysis scripts that need a stable machine-readable view of the rule
8469
+ registry.
8470
+
8471
+ Each element has the shape:
8472
+ {
8473
+ "key": "gateway",
8474
+ "description": "...",
8475
+ "repairMode": "standard" | "user-confirm" | "ai" | "reset" | "check-only",
8476
+ "profile": "standard" | "experimental",
8477
+ "dependsOn": ["config_syntax_check"],
8478
+ "hasSkipWhen": false
8479
+ }
8480
+
8481
+ OPTIONS
8482
+ --rule=<key> Filter output to specific rule keys (repeatable). Returns
8483
+ all rules when omitted.
8484
+
8485
+ EXAMPLES
8486
+ ${BIN} rules # all rules
8487
+ ${BIN} rules --rule=gateway # single rule
8488
+ ${BIN} rules --rule=gateway --rule=feishu_channel # multiple rules
8560
8489
  `
8561
8490
  },
8562
8491
  {
@@ -10003,7 +9932,7 @@ function parseCtxFlag(args) {
10003
9932
  * (The mode itself is args[0] in the filtered set, so we skip index 0.)
10004
9933
  */
10005
9934
  function getPositionalTag(args, modeName) {
10006
- return args.find((a, i) => i > 0 && !a.startsWith("-") && a !== modeName);
9935
+ return args.find((a, i) => i > 0 && !a.startsWith("--") && a !== modeName);
10007
9936
  }
10008
9937
  function getFlag(args, name) {
10009
9938
  const prefix = `--${name}=`;
@@ -10344,64 +10273,6 @@ async function main() {
10344
10273
  if (error) throw error;
10345
10274
  break;
10346
10275
  }
10347
- case "install-clis": {
10348
- const tag = getPositionalTag(args, "install-clis");
10349
- if (!tag) {
10350
- console.error("Usage: install-clis <tag> --cli=<name>... [--home_base=<dir>] [--ctx=<base64> | --oss_file_map=<base64>]");
10351
- node_process.default.exit(1);
10352
- }
10353
- const names = getMultiFlag(args, "cli");
10354
- const homeBase = getFlag(args, "home_base");
10355
- const ossFileMapFlag = getFlag(args, "oss_file_map");
10356
- let installOssFileMap;
10357
- let rawForTelemetry;
10358
- if (!ossFileMapFlag) {
10359
- rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
10360
- populate: planCtxPopulate({ command: "install" }),
10361
- caller,
10362
- traceId
10363
- });
10364
- installOssFileMap = normalizeCtx(rawForTelemetry).install.ossFileMap;
10365
- }
10366
- const ossFileMap = resolveOssFileMap({
10367
- ossFileMapFlag,
10368
- installOssFileMap
10369
- });
10370
- console.error(`[install-clis] ossFileMap=${JSON.stringify(ossFileMap)}`);
10371
- let feishuAppSecret;
10372
- if (names.includes("lark-cli")) feishuAppSecret = normalizeCtx(await fetchCtxViaInnerApi({
10373
- populate: { app: ["feishuAppSecret"] },
10374
- caller,
10375
- traceId
10376
- })).app.feishuAppSecret || void 0;
10377
- let success = true;
10378
- let error;
10379
- try {
10380
- await installClis(tag, ossFileMap, {
10381
- names,
10382
- homeBase,
10383
- feishuAppSecret
10384
- });
10385
- } catch (e) {
10386
- success = false;
10387
- error = e;
10388
- }
10389
- if (success) console.log(JSON.stringify({ ok: true }));
10390
- await reportRun("install-clis", rc, rawForTelemetry, args.join(" "), Date.now() - t0, {
10391
- success,
10392
- result: {
10393
- tag,
10394
- names
10395
- },
10396
- error
10397
- }, {
10398
- scene,
10399
- profile,
10400
- fix: false
10401
- });
10402
- if (error) throw error;
10403
- break;
10404
- }
10405
10276
  case "download-resource": {
10406
10277
  const tag = getPositionalTag(args, "download-resource");
10407
10278
  if (!tag) {
@@ -10460,6 +10331,12 @@ async function main() {
10460
10331
  if (error) throw error;
10461
10332
  break;
10462
10333
  }
10334
+ case "rules": {
10335
+ const keys = getMultiFlag(args, "rule");
10336
+ const metas = getRuleMetas(keys.length > 0 ? keys : void 0);
10337
+ console.log(JSON.stringify(metas));
10338
+ break;
10339
+ }
10463
10340
  case "lark-cli-init": {
10464
10341
  const appId = getFlag(args, "app-id");
10465
10342
  if (!appId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/openclaw-scripts-diagnose-cli",
3
- "version": "0.1.7-alpha.1",
3
+ "version": "0.1.7-beta.0",
4
4
  "description": "CLI for OpenClaw config diagnose and repair with JSON5 support",
5
5
  "main": "dist/index.cjs",
6
6
  "bin": {