@lark-apaas/openclaw-scripts-diagnose-cli 0.1.17 → 0.1.18-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.cjs +404 -115
  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.17";
55
+ return "0.1.18-alpha.1";
56
56
  }
57
57
  //#endregion
58
58
  //#region src/rule-engine/base.ts
@@ -3743,12 +3743,53 @@ function coerceCalVer(v) {
3743
3743
  function compareCalVer(a, b) {
3744
3744
  return semver.default.compare(coerceCalVer(a), coerceCalVer(b));
3745
3745
  }
3746
- /** Whether the given openclaw version falls inside this plugin entry's compat range. */
3747
- function compatible(entry, openclawVersion) {
3746
+ /** Look up an entry by exact plugin version; undefined if not in the table. */
3747
+ function findEntry(pluginVersion) {
3748
+ return VERSION_COMPAT_MAP.find((e) => e.openclawLarkVersion === pluginVersion);
3749
+ }
3750
+ /**
3751
+ * Infer the effective upper bound for a compat entry that has no explicit maxOpenclawVersion.
3752
+ *
3753
+ * Scans toward higher plugin versions (lower indices in the descending table) and returns
3754
+ * the first entry whose minOpenclawVersion differs from the current entry's.
3755
+ * That minOpenclawVersion becomes an EXCLUSIVE upper bound — the openclaw version that
3756
+ * starts a new incompatible range.
3757
+ *
3758
+ * Returns undefined for the topmost entry (no upper bound).
3759
+ */
3760
+ function inferEffectiveMax(index) {
3761
+ const cur = VERSION_COMPAT_MAP[index];
3762
+ if (cur.maxOpenclawVersion != null) return {
3763
+ max: cur.maxOpenclawVersion,
3764
+ exclusive: false
3765
+ };
3766
+ for (let i = index - 1; i >= 0; i--) if (compareCalVer(VERSION_COMPAT_MAP[i].minOpenclawVersion, cur.minOpenclawVersion) > 0) return {
3767
+ max: VERSION_COMPAT_MAP[i].minOpenclawVersion,
3768
+ exclusive: true
3769
+ };
3770
+ }
3771
+ /**
3772
+ * Full version compatibility check that infers the effective maxOpenclawVersion for
3773
+ * entries that only define minOpenclawVersion.
3774
+ *
3775
+ * For entries with an explicit maxOpenclawVersion the upper bound is INCLUSIVE (existing
3776
+ * semantics). For entries without one the upper bound is EXCLUSIVE — it is derived from
3777
+ * the minOpenclawVersion of the next entry group that requires a higher openclaw version.
3778
+ *
3779
+ * Example: openclaw-lark@2026.4.10 (min=2026.4.27, no explicit max) gets an inferred
3780
+ * exclusive max of '2026.5.6', so openclaw@2026.5.7 is correctly detected as incompatible
3781
+ * whereas the old compatible() call would have returned true.
3782
+ */
3783
+ function effectiveCompatible(pluginVersion, openclawVersion) {
3784
+ const index = VERSION_COMPAT_MAP.findIndex((e) => compareCalVer(e.openclawLarkVersion, pluginVersion) <= 0);
3785
+ if (index === -1) return false;
3786
+ const entry = VERSION_COMPAT_MAP[index];
3748
3787
  const oc = coerceCalVer(openclawVersion);
3749
3788
  if (semver.default.lt(oc, coerceCalVer(entry.minOpenclawVersion))) return false;
3750
- if (entry.maxOpenclawVersion != null && semver.default.gt(oc, coerceCalVer(entry.maxOpenclawVersion))) return false;
3751
- return true;
3789
+ const maxInfo = inferEffectiveMax(index);
3790
+ if (!maxInfo) return true;
3791
+ const max = coerceCalVer(maxInfo.max);
3792
+ return maxInfo.exclusive ? semver.default.lt(oc, max) : !semver.default.gt(oc, max);
3752
3793
  }
3753
3794
  /**
3754
3795
  * Floor match: find the entry with the largest openclawLarkVersion that is
@@ -3766,10 +3807,19 @@ function findClosestEntry(pluginVersion) {
3766
3807
  const PLUGIN_NAME$1 = "openclaw-lark";
3767
3808
  const LEGACY_SHORT_NAMES = ["feishu-openclaw-plugin"];
3768
3809
  const FORK_SCOPES = ["@lark-apaas"];
3769
- /** 特化 fork 版全名:虽免于 VERSION_COMPAT_MAP 检查,仍需 openclaw ≥ 此版本 */
3810
+ /** 特化 fork 版全名:复用官方 effectiveCompatible,按对标版本取完整兼容区间 */
3770
3811
  const FORK_LARK_PLUGIN_FULL_NAME = "@lark-apaas/openclaw-lark";
3771
- /** 来自 VERSION_COMPAT_MAP openclawLarkVersion=2026.4.1 对应的 minOpenclawVersion */
3772
- const FORK_LARK_PLUGIN_MIN_OC_VERSION = "2026.3.28";
3812
+ /**
3813
+ * fork 版对标的官方 openclaw-lark 版本。fork 自身版本不在 VERSION_COMPAT_MAP 内,
3814
+ * 兼容性按此对标版本经 effectiveCompatible 取完整区间(含推断上界)判定。
3815
+ */
3816
+ const FORK_LARK_PLUGIN_PINNED_VERSION = "2026.4.1";
3817
+ /**
3818
+ * fork 版兼容区间的下界,取自 VERSION_COMPAT_MAP 中 openclawLarkVersion=2026.4.1
3819
+ * 的 minOpenclawVersion,避免与映射表脱钩写死。仅用于区分升级方向
3820
+ * (oc 低于下界 → 升 openclaw;高于上界 → 升 lark);该条目被移除时回退到已知值。
3821
+ */
3822
+ const FORK_LARK_PLUGIN_MIN_OC_VERSION = findEntry(FORK_LARK_PLUGIN_PINNED_VERSION)?.minOpenclawVersion ?? "2026.3.28";
3773
3823
  let _ocVersion = void 0;
3774
3824
  function getOcVersion() {
3775
3825
  if (_ocVersion === void 0) _ocVersion = readOpenclawRuntimeVersion();
@@ -3845,7 +3895,15 @@ let FeishuPluginLarkUpgradeRule = class FeishuPluginLarkUpgradeRule extends Diag
3845
3895
  const cc = resolveCompatContext(ctx);
3846
3896
  if (!cc) return { pass: true };
3847
3897
  const { ocCur, recommendedOc, installed, isLegacy } = cc;
3848
- if (isForkPlugin(installed)) return { pass: true };
3898
+ if (isForkPlugin(installed)) {
3899
+ if (installed.fullName !== FORK_LARK_PLUGIN_FULL_NAME) return { pass: true };
3900
+ if (resolveForkUpgradeDirection(ocCur) !== "lark") return { pass: true };
3901
+ return {
3902
+ pass: false,
3903
+ action: "upgrade_lark",
3904
+ message: `飞书插件 ${describePlugin(installed)}(fork 版,对标 openclaw-lark@${FORK_LARK_PLUGIN_PINNED_VERSION})与当前 openclaw@${ocCur} 不兼容;建议升级飞书插件至兼容版本`
3905
+ };
3906
+ }
3849
3907
  if (!isLarkUpgradeNeededFromCC(cc)) return { pass: true };
3850
3908
  const prefix = buildCompatPrefix(installed, ocCur, isLegacy);
3851
3909
  if (!recommendedOc) return {
@@ -3890,13 +3948,12 @@ function isLegacyPlugin(p) {
3890
3948
  return LEGACY_SHORT_NAMES.includes(p.allowName);
3891
3949
  }
3892
3950
  /**
3893
- * floor 匹配找到最近的兼容表条目,检查 ocCur 是否在其 [min, max] 范围内。
3894
- * 版本读不到、表中无 当前插件版本的条目、或不在范围内,均视为不兼容。
3951
+ * 检查已装插件版本与当前 openclaw 版本是否兼容。
3952
+ * 使用 effectiveCompatible() 以正确推断无显式 maxOpenclawVersion 条目的上界。
3895
3953
  */
3896
3954
  function isVersionCompatible(p, ocCur) {
3897
3955
  if (!p.version) return false;
3898
- const entry = findClosestEntry(p.version);
3899
- return entry != null && compatible(entry, ocCur);
3956
+ return effectiveCompatible(p.version, ocCur);
3900
3957
  }
3901
3958
  /**
3902
3959
  * 豁免判断:若推荐的 openclaw 版本本身低于最近条目要求的最低版本,
@@ -3919,13 +3976,25 @@ function describeCompatConstraint(entry, pluginVersion) {
3919
3976
  return `该插件版本要求 openclaw ≥ ${entry.minOpenclawVersion}`;
3920
3977
  }
3921
3978
  /**
3922
- * @lark-apaas/openclaw-lark 豁免 VERSION_COMPAT_MAP,但仍要求 openclaw ≥ FORK_LARK_PLUGIN_MIN_OC_VERSION。
3979
+ * fork @lark-apaas/openclaw-lark 的升级方向:复用官方 effectiveCompatible,按对标版本
3980
+ * FORK_LARK_PLUGIN_PINNED_VERSION (2026.4.1) 取完整兼容区间。
3981
+ * null → 兼容
3982
+ * 'openclaw' → oc 低于区间下界,需升级 openclaw
3983
+ * 'lark' → oc 高于区间上界(插件相对当前 openclaw 过旧),需升级飞书插件
3984
+ */
3985
+ function resolveForkUpgradeDirection(ocCur) {
3986
+ if (effectiveCompatible(FORK_LARK_PLUGIN_PINNED_VERSION, ocCur)) return null;
3987
+ return compareCalVer(ocCur, FORK_LARK_PLUGIN_MIN_OC_VERSION) < 0 ? "openclaw" : "lark";
3988
+ }
3989
+ /**
3990
+ * @lark-apaas/openclaw-lark 复用官方 effectiveCompatible(按对标版本 2026.4.1 完整区间)。
3991
+ * 本函数(Rule 1)只负责 openclaw 方向(oc 低于下界);oc 高于上界由 Rule 2 报 upgrade_lark。
3923
3992
  * 其他 @lark-apaas scope 的 fork 插件继续无条件 pass。
3924
- * recommendedOc 可为 undefined(doctor 模式),此时只检测最低版本要求,不指定目标升级版本。
3993
+ * recommendedOc 可为 undefined(doctor 模式),此时不指定目标升级版本。
3925
3994
  */
3926
3995
  function validateForkPlugin(installed, ocCur, recommendedOc) {
3927
3996
  if (installed.fullName !== FORK_LARK_PLUGIN_FULL_NAME) return { pass: true };
3928
- if (compareCalVer(ocCur, FORK_LARK_PLUGIN_MIN_OC_VERSION) >= 0) return { pass: true };
3997
+ if (resolveForkUpgradeDirection(ocCur) !== "openclaw") return { pass: true };
3929
3998
  const recommendation = recommendedOc ? `;将 openclaw 升级到 ${recommendedOc} 即可满足` : `;请升级 openclaw 至 ${FORK_LARK_PLUGIN_MIN_OC_VERSION} 或更高版本`;
3930
3999
  return {
3931
4000
  pass: false,
@@ -3992,7 +4061,7 @@ function needsLarkUpgrade(ctx) {
3992
4061
  if (!cc) return false;
3993
4062
  const { ocCur, installed } = cc;
3994
4063
  if (isForkPlugin(installed)) {
3995
- if (installed.fullName === FORK_LARK_PLUGIN_FULL_NAME) return compareCalVer(ocCur, FORK_LARK_PLUGIN_MIN_OC_VERSION) < 0;
4064
+ if (installed.fullName === FORK_LARK_PLUGIN_FULL_NAME) return !effectiveCompatible(FORK_LARK_PLUGIN_PINNED_VERSION, ocCur);
3996
4065
  return false;
3997
4066
  }
3998
4067
  return isLarkUpgradeNeededFromCC(cc);
@@ -5137,6 +5206,139 @@ async function installOpenclaw(openclawTag, ossFileMap, opts = {}) {
5137
5206
  }
5138
5207
  console.error(`[install-openclaw] done in ${Date.now() - t0}ms`);
5139
5208
  }
5209
+ //#endregion
5210
+ //#region src/lark-tools-update.ts
5211
+ /**
5212
+ * 共享的飞书插件备份 / 恢复 / `openclaw-lark-tools update` 调用。
5213
+ *
5214
+ * upgrade-lark(场景修复流)与 install-extension(升级触发的兼容性预判路径)都需要
5215
+ * 「备份 → npx 适配升级 → 失败回滚」的同一套处理,此处统一封装,避免两份实现漂移。
5216
+ *
5217
+ * 日志通过可选 `log` 注入:upgrade-lark 传写日志文件的 Logger,install-extension 传
5218
+ * `console.error`,独立调用方不传则静默。
5219
+ */
5220
+ const NOOP_LOG = () => {};
5221
+ /** 升级前需备份的 extensions/ 下的插件目录(含 legacy) */
5222
+ const FEISHU_PLUGIN_DIRS = ["openclaw-lark", "feishu-openclaw-plugin"];
5223
+ /** 读取 package.json 的 version 字段,失败返回 null(仅用于日志) */
5224
+ function readPkgVersion(pkgPath) {
5225
+ try {
5226
+ const pkg = JSON.parse(node_fs.default.readFileSync(pkgPath, "utf-8"));
5227
+ return typeof pkg.version === "string" ? pkg.version : null;
5228
+ } catch {
5229
+ return null;
5230
+ }
5231
+ }
5232
+ /**
5233
+ * 备份 openclaw.json + FEISHU_PLUGIN_DIRS 下存在的插件目录到 backupDir。
5234
+ * 只备份当前存在的文件;不存在的记日志后跳过(恢复时据此判断「升级前是否存在」)。
5235
+ */
5236
+ function backupFeishuPlugins(opts) {
5237
+ const { workspaceDir, configPath, backupDir } = opts;
5238
+ const log = opts.log ?? NOOP_LOG;
5239
+ try {
5240
+ node_fs.default.mkdirSync(backupDir, { recursive: true });
5241
+ log(`backup dir: ${backupDir}`);
5242
+ if (node_fs.default.existsSync(configPath)) {
5243
+ const stat = node_fs.default.statSync(configPath);
5244
+ node_fs.default.copyFileSync(configPath, node_path.default.join(backupDir, "openclaw.json"));
5245
+ log(` backed up: openclaw.json (${stat.size} bytes)`);
5246
+ } else log(` skipped: openclaw.json (not found)`);
5247
+ node_fs.default.mkdirSync(node_path.default.join(backupDir, "extensions"), { recursive: true });
5248
+ const extSrc = node_path.default.join(workspaceDir, "extensions");
5249
+ for (const pluginDir of FEISHU_PLUGIN_DIRS) {
5250
+ const src = node_path.default.join(extSrc, pluginDir);
5251
+ if (node_fs.default.existsSync(src)) {
5252
+ const dst = node_path.default.join(backupDir, "extensions", pluginDir);
5253
+ node_fs.default.cpSync(src, dst, { recursive: true });
5254
+ const version = readPkgVersion(node_path.default.join(src, "package.json"));
5255
+ log(` backed up: extensions/${pluginDir}${version ? ` (version: ${version})` : ""}`);
5256
+ } else log(` skipped: extensions/${pluginDir} (not found)`);
5257
+ }
5258
+ return { ok: true };
5259
+ } catch (e) {
5260
+ const msg = `backup failed: ${e.message}`;
5261
+ log(`ERROR: ${msg}`);
5262
+ return {
5263
+ ok: false,
5264
+ error: msg
5265
+ };
5266
+ }
5267
+ }
5268
+ /**
5269
+ * 回滚到备份快照:先删除当前 openclaw.json + 插件目录,再从备份恢复(仅恢复备份中存在的,
5270
+ * 即升级前确实存在的文件)。这样升级过程中新建的文件会被清理,真正回到 pre-state。
5271
+ */
5272
+ function restoreFeishuPlugins(opts) {
5273
+ const { workspaceDir, configPath, backupDir } = opts;
5274
+ const log = opts.log ?? NOOP_LOG;
5275
+ try {
5276
+ if (node_fs.default.existsSync(configPath)) {
5277
+ node_fs.default.rmSync(configPath, { force: true });
5278
+ log(` deleted: openclaw.json`);
5279
+ }
5280
+ const extDst = node_path.default.join(workspaceDir, "extensions");
5281
+ for (const pluginDir of FEISHU_PLUGIN_DIRS) {
5282
+ const dst = node_path.default.join(extDst, pluginDir);
5283
+ if (node_fs.default.existsSync(dst)) {
5284
+ node_fs.default.rmSync(dst, {
5285
+ recursive: true,
5286
+ force: true
5287
+ });
5288
+ log(` deleted: extensions/${pluginDir}`);
5289
+ }
5290
+ }
5291
+ const configBackup = node_path.default.join(backupDir, "openclaw.json");
5292
+ if (node_fs.default.existsSync(configBackup)) {
5293
+ node_fs.default.copyFileSync(configBackup, configPath);
5294
+ log(` restored: openclaw.json`);
5295
+ } else log(` skipped restore: openclaw.json (not in backup — was not present before upgrade)`);
5296
+ for (const pluginDir of FEISHU_PLUGIN_DIRS) {
5297
+ const backupSrc = node_path.default.join(backupDir, "extensions", pluginDir);
5298
+ if (node_fs.default.existsSync(backupSrc)) {
5299
+ node_fs.default.cpSync(backupSrc, node_path.default.join(extDst, pluginDir), { recursive: true });
5300
+ log(` restored: extensions/${pluginDir}`);
5301
+ } else log(` skipped restore: extensions/${pluginDir} (not in backup — was not present before upgrade)`);
5302
+ }
5303
+ return true;
5304
+ } catch (e) {
5305
+ log(` restore error: ${e.message}`);
5306
+ return false;
5307
+ }
5308
+ }
5309
+ /**
5310
+ * 执行 `npx -y @larksuite/openclaw-lark-tools update`,自动按当前 openclaw 内核选择
5311
+ * 适配的 openclaw-lark 版本。stdout/stderr 已 trim;exitCode 缺失时归一为 1。
5312
+ */
5313
+ function runLarkToolsUpdate(opts) {
5314
+ const log = opts.log ?? NOOP_LOG;
5315
+ const r = (0, node_child_process.spawnSync)("npx", [
5316
+ "-y",
5317
+ "@larksuite/openclaw-lark-tools",
5318
+ "update"
5319
+ ], {
5320
+ cwd: opts.cwd,
5321
+ encoding: "utf-8",
5322
+ stdio: [
5323
+ "ignore",
5324
+ "pipe",
5325
+ "pipe"
5326
+ ],
5327
+ timeout: opts.timeoutMs ?? 6e5
5328
+ });
5329
+ const stdout = r.stdout?.trim() ?? "";
5330
+ const stderr = r.stderr?.trim() ?? "";
5331
+ const exitCode = r.status ?? 1;
5332
+ if (stdout) log(`npx stdout:\n${stdout}`);
5333
+ if (stderr) log(`npx stderr:\n${stderr}`);
5334
+ log(`npx exit: ${exitCode}${r.error ? ` error: ${r.error.message}` : ""}`);
5335
+ return {
5336
+ exitCode,
5337
+ stdout,
5338
+ stderr,
5339
+ spawnError: r.error?.message
5340
+ };
5341
+ }
5140
5342
  async function installExtension(tag, ossFileMap, opts = {}) {
5141
5343
  const homeBase = resolveHomeBase(opts.homeBase);
5142
5344
  const hasAll = !!opts.all;
@@ -5159,6 +5361,24 @@ async function installExtension(tag, ossFileMap, opts = {}) {
5159
5361
  }
5160
5362
  console.error(`[install-extension] tag=${tag} targets=${targets.length}`);
5161
5363
  const t0 = Date.now();
5364
+ const larkTarget = opts.skipConfigUpdate ? void 0 : targets.find((p) => p.name === LARK_PLUGIN_NAME$1);
5365
+ if (larkTarget) {
5366
+ if (await installLarkPluginWithCompatCheck({
5367
+ tag,
5368
+ pkg: larkTarget,
5369
+ homeBase,
5370
+ configPath: opts.configPath ?? node_path.default.join(homeBase, "workspace/agent/openclaw.json"),
5371
+ workspaceDir: node_path.default.join(homeBase, "workspace", "agent"),
5372
+ ossFileMap,
5373
+ downloadOpts: opts
5374
+ })) {
5375
+ targets = targets.filter((p) => p.name !== LARK_PLUGIN_NAME$1);
5376
+ if (targets.length === 0) {
5377
+ console.error(`[install-extension] done in ${Date.now() - t0}ms`);
5378
+ return;
5379
+ }
5380
+ }
5381
+ }
5162
5382
  const tarballs = await Promise.all(targets.map(async (p) => {
5163
5383
  const tb = await downloadWithCache(p, ossFileMap, opts);
5164
5384
  console.error(`[install-extension] ${p.name}: downloaded`);
@@ -5175,6 +5395,7 @@ async function installExtension(tag, ossFileMap, opts = {}) {
5175
5395
  else console.error(`[install-extension] skipConfigUpdate=true — not touching openclaw.json`);
5176
5396
  console.error(`[install-extension] done ${targets.length}/${targets.length} in ${Date.now() - t0}ms`);
5177
5397
  }
5398
+ const LARK_PLUGIN_NAME$1 = "openclaw-lark";
5178
5399
  const MEM0_PLUGIN_NAME = "openclaw-mem0-plugin";
5179
5400
  const PLUGINS_TO_AUTO_ENABLE = ["openclaw-lark", "openclaw-extension-miaoda"];
5180
5401
  /**
@@ -5264,6 +5485,105 @@ function installOne$1(pkg, tarball, homeBase) {
5264
5485
  force: true
5265
5486
  });
5266
5487
  }
5488
+ /**
5489
+ * Install openclaw-lark with a pre-flight compatibility check.
5490
+ *
5491
+ * When the recommended version in the manifest is incompatible with the currently
5492
+ * running openclaw, falls back to `npx @larksuite/openclaw-lark-tools update` which
5493
+ * selects the correct plugin version automatically.
5494
+ *
5495
+ * Returns true when the lark plugin was handled by this function (either path),
5496
+ * false when it should fall through to the standard tarball install (e.g. when the
5497
+ * current openclaw version cannot be read).
5498
+ */
5499
+ async function installLarkPluginWithCompatCheck(opts) {
5500
+ const { tag, pkg, homeBase, configPath, workspaceDir } = opts;
5501
+ const ocCur = readOpenclawRuntimeVersion();
5502
+ if (!ocCur) {
5503
+ console.error("[install-extension] WARN: cannot read openclaw version — falling back to direct tarball install for openclaw-lark");
5504
+ return false;
5505
+ }
5506
+ const compatible = pkg.version ? effectiveCompatible(pkg.version, ocCur) : true;
5507
+ console.error(`[install-extension] openclaw-lark@${pkg.version ?? "unknown"} compat with openclaw@${ocCur}: ${compatible}`);
5508
+ if (compatible) return false;
5509
+ console.error(`[install-extension] openclaw-lark@${pkg.version} incompatible with openclaw@${ocCur} — using openclaw-lark-tools update`);
5510
+ const log = (msg) => console.error(`[install-extension] ${msg}`);
5511
+ const backupDir = node_path.default.join(homeBase, `.openclaw-lark-backup-${tag}`);
5512
+ const backupOk = backupFeishuPlugins({
5513
+ workspaceDir,
5514
+ configPath,
5515
+ backupDir,
5516
+ log
5517
+ }).ok;
5518
+ if (!backupOk) console.error("[install-extension] WARN: backup failed — proceeding without backup for openclaw-lark-tools path");
5519
+ try {
5520
+ const { exitCode } = runLarkToolsUpdate({
5521
+ cwd: workspaceDir,
5522
+ log
5523
+ });
5524
+ if (exitCode !== 0) {
5525
+ console.error("[install-extension] openclaw-lark-tools failed — attempting rollback");
5526
+ if (backupOk) restoreFeishuPlugins({
5527
+ workspaceDir,
5528
+ configPath,
5529
+ backupDir,
5530
+ log
5531
+ });
5532
+ throw new Error(`openclaw-lark-tools update exited with code ${exitCode}`);
5533
+ }
5534
+ const validation = validateLarkPluginInstall({
5535
+ homeBase,
5536
+ configPath
5537
+ });
5538
+ if (!validation.ok) {
5539
+ console.error(`[install-extension] post-install validation failed: ${validation.error} — rolling back`);
5540
+ if (backupOk) restoreFeishuPlugins({
5541
+ workspaceDir,
5542
+ configPath,
5543
+ backupDir,
5544
+ log
5545
+ });
5546
+ throw new Error(`openclaw-lark post-install validation failed: ${validation.error}`);
5547
+ }
5548
+ console.error("[install-extension] openclaw-lark-tools update succeeded");
5549
+ return true;
5550
+ } finally {
5551
+ if (backupOk && node_fs.default.existsSync(backupDir)) try {
5552
+ node_fs.default.rmSync(backupDir, {
5553
+ recursive: true,
5554
+ force: true
5555
+ });
5556
+ } catch {}
5557
+ }
5558
+ }
5559
+ /**
5560
+ * Post-install validation for the tools-based upgrade path.
5561
+ * Checks: plugin directory exists + plugin is enabled in openclaw.json.
5562
+ */
5563
+ function validateLarkPluginInstall(opts) {
5564
+ const { homeBase, configPath } = opts;
5565
+ const pkgJson = node_path.default.join(homeBase, "workspace", "agent", "extensions", LARK_PLUGIN_NAME$1, "package.json");
5566
+ if (!node_fs.default.existsSync(pkgJson)) return {
5567
+ ok: false,
5568
+ error: `plugin directory missing: ${pkgJson}`
5569
+ };
5570
+ if (!node_fs.default.existsSync(configPath)) return {
5571
+ ok: false,
5572
+ error: `openclaw.json not found at ${configPath}`
5573
+ };
5574
+ try {
5575
+ if (asRecord(getNestedMap(loadJSON5().parse(node_fs.default.readFileSync(configPath, "utf-8")), "plugins", "entries")?.[LARK_PLUGIN_NAME$1])?.enabled !== true) return {
5576
+ ok: false,
5577
+ error: `plugins.entries["${LARK_PLUGIN_NAME$1}"].enabled is not true`
5578
+ };
5579
+ } catch (e) {
5580
+ return {
5581
+ ok: false,
5582
+ error: `config parse error: ${e.message}`
5583
+ };
5584
+ }
5585
+ return { ok: true };
5586
+ }
5267
5587
  //#endregion
5268
5588
  //#region ../../openclaw-slardar/lib/client.js
5269
5589
  var import_index_cjs = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports) => {
@@ -6547,6 +6867,31 @@ function reportError(params) {
6547
6867
  const LARK_CLI_NAME = "lark-cli";
6548
6868
  const AGENT_SKILLS_NAME = "agent-skills";
6549
6869
  const WORKSPACE_AGENT_REL = "workspace/agent";
6870
+ const LARK_PLUGIN_NAME = "openclaw-lark";
6871
+ /**
6872
+ * openclaw-lark tools that overlap with lark-cli functionality.
6873
+ * Written to channels.feishu.tools.deny after lark-cli is installed so that
6874
+ * openclaw-lark's shouldRegisterTool() skips them, avoiding duplicate tools.
6875
+ * Supports trailing-* wildcards (handled by openclaw-lark's matchesAnyPattern).
6876
+ *
6877
+ * Kept: feishu_chat*, feishu_get_user, feishu_search_user, feishu_im_*,
6878
+ * feishu_oauth*, feishu_auth, feishu_diagnose, feishu_doctor
6879
+ */
6880
+ const LARK_CLI_OVERLAP_TOOL_DENY = Object.freeze([
6881
+ "feishu_create_doc",
6882
+ "feishu_fetch_doc",
6883
+ "feishu_update_doc",
6884
+ "feishu_doc_comments",
6885
+ "feishu_doc_media",
6886
+ "feishu_drive_file",
6887
+ "feishu_wiki_space",
6888
+ "feishu_wiki_space_node",
6889
+ "feishu_search_doc_wiki",
6890
+ "feishu_bitable_*",
6891
+ "feishu_calendar_*",
6892
+ "feishu_task_*",
6893
+ "feishu_sheet"
6894
+ ]);
6550
6895
  async function installClis(tag, ossFileMap, opts) {
6551
6896
  const homeBase = resolveHomeBase(opts.homeBase);
6552
6897
  if (opts.names.length === 0) throw new Error("install-cli: must provide at least one --cli=<name>");
@@ -6611,6 +6956,7 @@ async function installClis(tag, ossFileMap, opts) {
6611
6956
  }
6612
6957
  });
6613
6958
  }
6959
+ disableLarkCliOverlapTools(node_path.default.join(homeBase, WORKSPACE_AGENT_REL, "openclaw.json"), homeBase);
6614
6960
  }
6615
6961
  console.error(`[install-cli] done ${targets.length}/${targets.length} in ${Date.now() - t0}ms`);
6616
6962
  }
@@ -6785,6 +7131,42 @@ function installOne(pkg, tarball, homeBase, tmpRoot) {
6785
7131
  } catch {}
6786
7132
  }
6787
7133
  }
7134
+ /**
7135
+ * Write overlapping tool names to channels.feishu.tools.deny in openclaw.json so that
7136
+ * openclaw-lark's shouldRegisterTool() skips them when lark-cli is present.
7137
+ *
7138
+ * Only runs when openclaw-lark is installed (extensions/openclaw-lark/ exists).
7139
+ * Idempotent: merges with any existing deny entries.
7140
+ * Failures are non-fatal (logged as warnings).
7141
+ */
7142
+ function disableLarkCliOverlapTools(configPath, homeBase) {
7143
+ const larkPluginDir = node_path.default.join(homeBase, WORKSPACE_AGENT_REL, "extensions", LARK_PLUGIN_NAME);
7144
+ if (!node_fs.default.existsSync(larkPluginDir)) {
7145
+ console.error(`[install-cli] disableLarkCliOverlapTools: ${LARK_PLUGIN_NAME} not installed — skipping`);
7146
+ return;
7147
+ }
7148
+ if (!node_fs.default.existsSync(configPath)) {
7149
+ console.error(`[install-cli] disableLarkCliOverlapTools: config not found at ${configPath} — skipping`);
7150
+ return;
7151
+ }
7152
+ try {
7153
+ const config = loadJSON5().parse(node_fs.default.readFileSync(configPath, "utf-8"));
7154
+ if (!config.channels || typeof config.channels !== "object") config.channels = {};
7155
+ const channels = config.channels;
7156
+ if (!channels.feishu || typeof channels.feishu !== "object") channels.feishu = {};
7157
+ const feishu = channels.feishu;
7158
+ if (!feishu.tools || typeof feishu.tools !== "object") feishu.tools = {};
7159
+ const tools = feishu.tools;
7160
+ const existing = Array.isArray(tools.deny) ? tools.deny : [];
7161
+ tools.deny = [...new Set([...existing, ...LARK_CLI_OVERLAP_TOOL_DENY])];
7162
+ const tmp = configPath + ".lark-cli-deny-tmp";
7163
+ node_fs.default.writeFileSync(tmp, JSON.stringify(config, null, 2), "utf-8");
7164
+ moveSafe(tmp, configPath);
7165
+ console.error(`[install-cli] disableLarkCliOverlapTools: channels.feishu.tools.deny updated (${LARK_CLI_OVERLAP_TOOL_DENY.length} patterns)`);
7166
+ } catch (e) {
7167
+ console.error(`[install-cli] WARN: disableLarkCliOverlapTools failed: ${e.message}`);
7168
+ }
7169
+ }
6788
7170
  //#endregion
6789
7171
  //#region src/download-resource.ts
6790
7172
  /**
@@ -10720,7 +11102,7 @@ async function reportCliRun(opts) {
10720
11102
  //#region src/help.ts
10721
11103
  const BIN = "mclaw-diagnose";
10722
11104
  function versionBanner() {
10723
- return `v0.1.17`;
11105
+ return `v0.1.18-alpha.1`;
10724
11106
  }
10725
11107
  const COMMANDS = [
10726
11108
  {
@@ -11406,83 +11788,6 @@ function buildUpgradeLarkResultSummary(result, checkOnly) {
11406
11788
  }
11407
11789
  //#endregion
11408
11790
  //#region src/upgrade-lark.ts
11409
- /** 升级前需备份的 extensions/ 下的插件目录 */
11410
- const FEISHU_PLUGIN_DIRS = ["openclaw-lark", "feishu-openclaw-plugin"];
11411
- function backupFiles(opts) {
11412
- const { workspaceDir, configPath, backupDir, log } = opts;
11413
- try {
11414
- node_fs.default.mkdirSync(backupDir, { recursive: true });
11415
- log(`backup dir: ${backupDir}`);
11416
- if (node_fs.default.existsSync(configPath)) {
11417
- const stat = node_fs.default.statSync(configPath);
11418
- node_fs.default.copyFileSync(configPath, node_path.default.join(backupDir, "openclaw.json"));
11419
- log(` backed up: openclaw.json (${stat.size} bytes)`);
11420
- } else log(` skipped: openclaw.json (not found)`);
11421
- node_fs.default.mkdirSync(node_path.default.join(backupDir, "extensions"), { recursive: true });
11422
- const extSrc = node_path.default.join(workspaceDir, "extensions");
11423
- for (const pluginDir of FEISHU_PLUGIN_DIRS) {
11424
- const src = node_path.default.join(extSrc, pluginDir);
11425
- if (node_fs.default.existsSync(src)) {
11426
- const dst = node_path.default.join(backupDir, "extensions", pluginDir);
11427
- node_fs.default.cpSync(src, dst, { recursive: true });
11428
- const version = readPkgVersion(node_path.default.join(src, "package.json"));
11429
- log(` backed up: extensions/${pluginDir}${version ? ` (version: ${version})` : ""}`);
11430
- } else log(` skipped: extensions/${pluginDir} (not found)`);
11431
- }
11432
- return { ok: true };
11433
- } catch (e) {
11434
- const msg = `backup failed: ${e.message}`;
11435
- log(`ERROR: ${msg}`);
11436
- return {
11437
- ok: false,
11438
- error: msg
11439
- };
11440
- }
11441
- }
11442
- function restoreFiles(opts) {
11443
- const { workspaceDir, configPath, backupDir, log } = opts;
11444
- try {
11445
- if (node_fs.default.existsSync(configPath)) {
11446
- node_fs.default.rmSync(configPath, { force: true });
11447
- log(` deleted: openclaw.json`);
11448
- }
11449
- const extDst = node_path.default.join(workspaceDir, "extensions");
11450
- for (const pluginDir of FEISHU_PLUGIN_DIRS) {
11451
- const dst = node_path.default.join(extDst, pluginDir);
11452
- if (node_fs.default.existsSync(dst)) {
11453
- node_fs.default.rmSync(dst, {
11454
- recursive: true,
11455
- force: true
11456
- });
11457
- log(` deleted: extensions/${pluginDir}`);
11458
- }
11459
- }
11460
- const configBackup = node_path.default.join(backupDir, "openclaw.json");
11461
- if (node_fs.default.existsSync(configBackup)) {
11462
- node_fs.default.copyFileSync(configBackup, configPath);
11463
- log(` restored: openclaw.json`);
11464
- } else log(` skipped restore: openclaw.json (not in backup — was not present before upgrade)`);
11465
- for (const pluginDir of FEISHU_PLUGIN_DIRS) {
11466
- const backupSrc = node_path.default.join(backupDir, "extensions", pluginDir);
11467
- if (node_fs.default.existsSync(backupSrc)) {
11468
- node_fs.default.cpSync(backupSrc, node_path.default.join(extDst, pluginDir), { recursive: true });
11469
- log(` restored: extensions/${pluginDir}`);
11470
- } else log(` skipped restore: extensions/${pluginDir} (not in backup — was not present before upgrade)`);
11471
- }
11472
- return true;
11473
- } catch (e) {
11474
- log(` restore error: ${e.message}`);
11475
- return false;
11476
- }
11477
- }
11478
- function readPkgVersion(pkgPath) {
11479
- try {
11480
- const pkg = JSON.parse(node_fs.default.readFileSync(pkgPath, "utf-8"));
11481
- return typeof pkg.version === "string" ? pkg.version : null;
11482
- } catch {
11483
- return null;
11484
- }
11485
- }
11486
11791
  function snapshotVersions(cwd, log) {
11487
11792
  const ocResult = (0, node_child_process.spawnSync)("openclaw", ["--version"], {
11488
11793
  cwd,
@@ -11738,7 +12043,7 @@ function runUpgradeLark(opts) {
11738
12043
  log("── [1/6] 文件备份 ────────────────────────────────────────");
11739
12044
  log(`before-state: botCount=${countFeishuBots(configPath)}`);
11740
12045
  const t_backupStart = Date.now();
11741
- const backup = backupFiles(fsOpts);
12046
+ const backup = backupFeishuPlugins(fsOpts);
11742
12047
  timing.backupMs = Date.now() - t_backupStart;
11743
12048
  if (!backup.ok) {
11744
12049
  log(`ERROR: ${backup.error}`);
@@ -11765,27 +12070,11 @@ function runUpgradeLark(opts) {
11765
12070
  log("");
11766
12071
  log("── [3/6] npx install (@larksuite/openclaw-lark-tools update) ──");
11767
12072
  const t_npxStart = Date.now();
11768
- const npxResult = (0, node_child_process.spawnSync)("npx", [
11769
- "-y",
11770
- "@larksuite/openclaw-lark-tools",
11771
- "update"
11772
- ], {
12073
+ const { exitCode: npxExitCode, stdout: npxStdout, stderr: npxStderr } = runLarkToolsUpdate({
11773
12074
  cwd,
11774
- encoding: "utf-8",
11775
- stdio: [
11776
- "ignore",
11777
- "pipe",
11778
- "pipe"
11779
- ],
11780
- timeout: 6e5
12075
+ log
11781
12076
  });
11782
12077
  timing.npxInstallMs = Date.now() - t_npxStart;
11783
- const npxStdout = npxResult.stdout?.trim() ?? "";
11784
- const npxStderr = npxResult.stderr?.trim() ?? "";
11785
- const npxExitCode = npxResult.status ?? 1;
11786
- if (npxStdout) log(`npx stdout:\n${npxStdout}`);
11787
- if (npxStderr) log(`npx stderr:\n${npxStderr}`);
11788
- log(`npx exit: ${npxExitCode}${npxResult.error ? ` error: ${npxResult.error.message}` : ""}`);
11789
12078
  if (statusCheckDelayMs > 0) {
11790
12079
  log("");
11791
12080
  log(`── 等待 ${statusCheckDelayMs / 1e3}s(让 openclaw 服务完成重启) ─────────────`);
@@ -11794,7 +12083,7 @@ function runUpgradeLark(opts) {
11794
12083
  }
11795
12084
  const doRollback = (reason) => {
11796
12085
  log(`ERROR: ${reason}`);
11797
- const rollbackOk = restoreFiles(fsOpts);
12086
+ const rollbackOk = restoreFeishuPlugins(fsOpts);
11798
12087
  log(`rollback: ${rollbackOk ? "ok" : "FAILED"}`);
11799
12088
  return finalReturn({
11800
12089
  status: "failed",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/openclaw-scripts-diagnose-cli",
3
- "version": "0.1.17",
3
+ "version": "0.1.18-alpha.1",
4
4
  "description": "CLI for OpenClaw config diagnose and repair with JSON5 support",
5
5
  "main": "dist/index.cjs",
6
6
  "bin": {