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

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 +517 -128
  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.9";
55
+ return "0.1.15";
56
56
  }
57
57
  //#endregion
58
58
  //#region src/rule-engine/base.ts
@@ -1770,6 +1770,96 @@ SessionPersistenceRule = __decorate([Rule({
1770
1770
  level: "silent"
1771
1771
  })], SessionPersistenceRule);
1772
1772
  //#endregion
1773
+ //#region src/rules/tools-allow-also-allow-conflict.ts
1774
+ /**
1775
+ * 检测 tools 配置中 allow 与 alsoAllow 同时非空的冲突。
1776
+ *
1777
+ * openclaw 的 Zod schema 在配置加载时会拒绝这种组合,原因是语义互斥:
1778
+ * - allow:完全替换式白名单,只允许列出的工具
1779
+ * - alsoAllow:追加式,在 profile 或默认基线上叠加额外工具
1780
+ * 两者同时存在会导致 openclaw 服务启动失败。
1781
+ *
1782
+ * 修复策略:将 alsoAllow 的条目去重合并进 allow,删除 alsoAllow。
1783
+ * 这是最保守的修复:保留原 allow 的精确白名单,不扩大权限范围。
1784
+ *
1785
+ * 检测范围:
1786
+ * - 顶层 tools
1787
+ * - tools.byProvider.*
1788
+ * - agents.list[].tools
1789
+ * - agents.list[].tools.byProvider.*
1790
+ */
1791
+ let ToolsAllowAlsoAllowConflictRule = class ToolsAllowAlsoAllowConflictRule extends DiagnoseRule {
1792
+ validate(ctx) {
1793
+ const conflicts = [];
1794
+ visitAllScopes(ctx.config, (scope, label) => {
1795
+ if (hasConflict(scope)) conflicts.push(label);
1796
+ });
1797
+ if (conflicts.length === 0) return { pass: true };
1798
+ return {
1799
+ pass: false,
1800
+ message: `tools allow 与 alsoAllow 冲突(${conflicts.length} 处):${conflicts.join(";")}。修复方式:将 alsoAllow 合并进 allow 并删除 alsoAllow`
1801
+ };
1802
+ }
1803
+ repair(ctx) {
1804
+ visitAllScopes(ctx.config, (scope) => mergeInScope(scope));
1805
+ }
1806
+ };
1807
+ ToolsAllowAlsoAllowConflictRule = __decorate([Rule({
1808
+ key: "tools_allow_also_allow_conflict",
1809
+ description: "tools 配置中 allow 与 alsoAllow 同时设置会导致 openclaw 启动失败;自动将 alsoAllow 合并进 allow(实验性)",
1810
+ dependsOn: ["config_syntax_check"],
1811
+ repairMode: "standard",
1812
+ level: "critical",
1813
+ profile: "experimental"
1814
+ })], ToolsAllowAlsoAllowConflictRule);
1815
+ /**
1816
+ * 遍历所有 tools-policy scope,对每个 scope 调用 callback。
1817
+ * validate 和 repair 共用同一套遍历逻辑,确保两者覆盖的 scope 始终一致。
1818
+ */
1819
+ function visitAllScopes(config, cb) {
1820
+ const topTools = asRecord(asRecord(config)?.tools);
1821
+ if (topTools) {
1822
+ cb(topTools, "tools");
1823
+ const byProvider = asRecord(topTools.byProvider);
1824
+ if (byProvider) for (const key of Object.keys(byProvider)) {
1825
+ const scope = asRecord(byProvider[key]);
1826
+ if (scope) cb(scope, `tools.byProvider.${key}`);
1827
+ }
1828
+ }
1829
+ const agents = asRecord(asRecord(config)?.agents);
1830
+ const list = Array.isArray(agents?.list) ? agents.list : [];
1831
+ for (let i = 0; i < list.length; i++) {
1832
+ const agent = asRecord(list[i]);
1833
+ const agentId = typeof agent?.id === "string" ? agent.id : String(i);
1834
+ const tools = asRecord(agent?.tools);
1835
+ if (tools) {
1836
+ cb(tools, `agents.list[${agentId}].tools`);
1837
+ const byProvider = asRecord(tools.byProvider);
1838
+ if (byProvider) for (const key of Object.keys(byProvider)) {
1839
+ const scope = asRecord(byProvider[key]);
1840
+ if (scope) cb(scope, `agents.list[${agentId}].tools.byProvider.${key}`);
1841
+ }
1842
+ }
1843
+ }
1844
+ }
1845
+ /** scope 中 allow 与 alsoAllow 同时非空 */
1846
+ function hasConflict(scope) {
1847
+ const allow = scope.allow;
1848
+ const alsoAllow = scope.alsoAllow;
1849
+ return Array.isArray(allow) && allow.length > 0 && Array.isArray(alsoAllow) && alsoAllow.length > 0;
1850
+ }
1851
+ /**
1852
+ * 将 alsoAllow 去重合并进 allow,删除 alsoAllow。
1853
+ * 仅保留字符串元素,过滤掉格式异常的非字符串条目,避免写入损坏配置。
1854
+ */
1855
+ function mergeInScope(scope) {
1856
+ if (!hasConflict(scope)) return;
1857
+ const allow = scope.allow.filter((v) => typeof v === "string");
1858
+ const alsoAllow = scope.alsoAllow.filter((v) => typeof v === "string");
1859
+ scope.allow = [...new Set([...allow, ...alsoAllow])];
1860
+ delete scope.alsoAllow;
1861
+ }
1862
+ //#endregion
1773
1863
  //#region src/rules/feishu-default-account.ts
1774
1864
  /**
1775
1865
  * Owns the multi-agent feishu-channel migration: turns legacy v1/v2
@@ -2544,6 +2634,19 @@ const SECRETS_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-openclaw-s
2544
2634
  /** Absolute path to the openclaw config JSON. */
2545
2635
  const CONFIG_PATH = `${WORKSPACE_DIR}/openclaw.json`;
2546
2636
  /**
2637
+ * upgrade-lark 场景专属修复状态的信号文件目录。
2638
+ * fixStatus 有值时在此目录下创建同名文件(如 /tmp/event/PORT_FIX_READY),
2639
+ * 文件内容为完整的 UpgradeLarkResult JSON,供外部进程轮询感知升级结果。
2640
+ */
2641
+ const FIX_EVENT_DIR = "/tmp/event";
2642
+ /**
2643
+ * 安装指令互斥锁文件路径。
2644
+ * upgrade-lark / install-openclaw / install-extension / install-cli / reset --worker
2645
+ * 共享此锁,同一时刻只允许一个安装指令运行。
2646
+ * 锁文件内容:{ pid, command, startedAt }。
2647
+ */
2648
+ const INSTALL_LOCK_FILE = `${DIAGNOSE_DIR}/install.lock`;
2649
+ /**
2547
2650
  * upgrade-lark 每次运行的日志文件路径,含时间戳便于按时间排序定位。
2548
2651
  * checkOnly=true 时文件名含 "-check" 后缀,便于与正式安装日志区分。
2549
2652
  */
@@ -3223,6 +3326,13 @@ function execCaptureErr(cmd) {
3223
3326
  throw new Error(stderrStr ? `${base}\nstderr: ${stderrStr}` : base);
3224
3327
  }
3225
3328
  }
3329
+ /**
3330
+ * Synchronous sleep using Atomics.wait on a shared buffer.
3331
+ * Works on the main thread (unlike setTimeout which requires an event loop tick).
3332
+ */
3333
+ function sleepSync(ms) {
3334
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
3335
+ }
3226
3336
  /** POSIX single-quote shell escape. Paths with embedded quotes are rare but
3227
3337
  * the token-file path conventions in sandboxes don't guarantee cleanliness. */
3228
3338
  function shellQuote(s) {
@@ -4070,9 +4180,10 @@ let FeishuBotChannelConfigRule = class FeishuBotChannelConfigRule extends Diagno
4070
4180
  };
4071
4181
  }
4072
4182
  /** Check a single bot entry (either an account object or the feishu channel itself).
4073
- * appSecret is validated based on its current type:
4074
4183
  * - object → must match canonical provider-ref
4075
4184
  * - string → must match larkApps plaintext
4185
+ * - undefined/null → missing
4186
+ * - other → unexpected type
4076
4187
  */
4077
4188
  checkBot(label, bot, larkApp, issues) {
4078
4189
  const creatorOpenID = larkApp.creatorOpenID;
@@ -4085,7 +4196,8 @@ let FeishuBotChannelConfigRule = class FeishuBotChannelConfigRule extends Diagno
4085
4196
  if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) issues.push(`${label} appSecret is a provider-ref but not the canonical one`);
4086
4197
  } else if (typeof secret === "string") {
4087
4198
  if (secret !== larkApp.appSecret) issues.push(`${label} appSecret plaintext mismatch`);
4088
- } else issues.push(`${label} appSecret has unexpected type ${typeof secret}`);
4199
+ } else if (secret === void 0 || secret === null) issues.push(`${label} appSecret is missing`);
4200
+ else issues.push(`${label} appSecret has unexpected type ${typeof secret}`);
4089
4201
  }
4090
4202
  repair(ctx) {
4091
4203
  const larkApps = ctx.vars.larkApps;
@@ -4109,9 +4221,8 @@ let FeishuBotChannelConfigRule = class FeishuBotChannelConfigRule extends Diagno
4109
4221
  }
4110
4222
  }
4111
4223
  /** Fix a single bot entry in-place.
4112
- * appSecret is repaired based on its current type:
4113
- * - object → fix to canonical provider-ref
4114
- * - string → fix to larkApps plaintext
4224
+ * - object (provider-ref) fix to canonical
4225
+ * - otherwise → fix to larkApps plaintext
4115
4226
  */
4116
4227
  fixBot(bot, larkApp) {
4117
4228
  const creatorOpenID = larkApp.creatorOpenID;
@@ -4125,9 +4236,7 @@ let FeishuBotChannelConfigRule = class FeishuBotChannelConfigRule extends Diagno
4125
4236
  const secret = bot.appSecret;
4126
4237
  if (typeof secret === "object" && secret !== null && !Array.isArray(secret)) {
4127
4238
  if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) bot.appSecret = { ...DEFAULT_FEISHU_APP_SECRET };
4128
- } else if (typeof secret === "string") {
4129
- if (secret !== larkApp.appSecret) bot.appSecret = larkApp.appSecret;
4130
- }
4239
+ } else if (secret !== larkApp.appSecret) bot.appSecret = larkApp.appSecret;
4131
4240
  }
4132
4241
  };
4133
4242
  FeishuBotChannelConfigRule = __decorate([Rule({
@@ -7013,45 +7122,56 @@ function fixBotChannelConfig(configPath, larkApps, log) {
7013
7122
  return;
7014
7123
  }
7015
7124
  const config = loadJSON5().parse(node_fs.default.readFileSync(configPath, "utf-8"));
7016
- const accounts = asRecord(getNestedMap(config, "channels", "feishu")?.accounts);
7017
- if (!accounts) {
7018
- log("no feishu accounts in config, skip bot channel config fix");
7125
+ const feishu = asRecord(getNestedMap(config, "channels", "feishu"));
7126
+ if (!feishu) {
7127
+ log("no feishu channel in config, skip bot channel config fix");
7019
7128
  return;
7020
7129
  }
7021
7130
  let fixCount = 0;
7022
- for (const [, account] of Object.entries(accounts)) {
7131
+ const accounts = asRecord(feishu.accounts);
7132
+ if (accounts) for (const [, account] of Object.entries(accounts)) {
7023
7133
  const bot = asRecord(account);
7024
7134
  if (!bot) continue;
7025
7135
  const appId = bot.appId;
7026
7136
  if (typeof appId !== "string" || !appId.startsWith("cli_")) continue;
7027
7137
  const larkApp = larkApps.find((e) => e.larkAppID === appId);
7028
7138
  if (!larkApp) continue;
7029
- const creatorOpenID = larkApp.creatorOpenID;
7030
- if (typeof creatorOpenID === "string" && creatorOpenID !== "") {
7031
- const allowFrom = Array.isArray(bot.allowFrom) ? [...bot.allowFrom] : [];
7032
- if (!allowFrom.includes(creatorOpenID)) {
7033
- allowFrom.push(creatorOpenID);
7034
- bot.allowFrom = allowFrom;
7035
- fixCount++;
7036
- }
7037
- }
7038
- const secret = bot.appSecret;
7039
- let needsFix = false;
7040
- if (typeof secret === "object" && secret !== null && !Array.isArray(secret)) {
7041
- if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) needsFix = true;
7042
- } else if (typeof secret === "string") {
7043
- if (secret !== larkApp.appSecret) needsFix = true;
7044
- } else needsFix = true;
7045
- if (needsFix) {
7046
- bot.appSecret = { ...DEFAULT_FEISHU_APP_SECRET };
7047
- fixCount++;
7048
- }
7139
+ fixCount += fixBot(bot, larkApp);
7140
+ }
7141
+ const singleAppId = feishu.appId;
7142
+ if (typeof singleAppId === "string" && singleAppId.startsWith("cli_") && !accounts) {
7143
+ const larkApp = larkApps.find((e) => e.larkAppID === singleAppId);
7144
+ if (larkApp) fixCount += fixBot(feishu, larkApp);
7049
7145
  }
7050
7146
  if (fixCount > 0) {
7051
7147
  node_fs.default.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
7052
7148
  log(`fixed ${fixCount} bot channel config issue(s) (allowFrom/appSecret)`);
7053
7149
  } else log("bot channel config ok, no fixes needed");
7054
7150
  }
7151
+ /** Fix a single bot entry in-place. Returns number of fixes applied. */
7152
+ function fixBot(bot, larkApp) {
7153
+ let fixCount = 0;
7154
+ const creatorOpenID = larkApp.creatorOpenID;
7155
+ if (typeof creatorOpenID === "string" && creatorOpenID !== "") {
7156
+ const allowFrom = Array.isArray(bot.allowFrom) ? [...bot.allowFrom] : [];
7157
+ if (!allowFrom.includes(creatorOpenID)) {
7158
+ allowFrom.push(creatorOpenID);
7159
+ bot.allowFrom = allowFrom;
7160
+ fixCount++;
7161
+ }
7162
+ }
7163
+ const secret = bot.appSecret;
7164
+ if (typeof secret === "object" && secret !== null && !Array.isArray(secret)) {
7165
+ if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) {
7166
+ bot.appSecret = { ...DEFAULT_FEISHU_APP_SECRET };
7167
+ fixCount++;
7168
+ }
7169
+ } else if (secret !== larkApp.appSecret) {
7170
+ bot.appSecret = larkApp.appSecret;
7171
+ fixCount++;
7172
+ }
7173
+ return fixCount;
7174
+ }
7055
7175
  /**
7056
7176
  * Step 7: Verify startup scripts landed in configDir/scripts/.
7057
7177
  *
@@ -7275,14 +7395,6 @@ function getResetTask(taskId) {
7275
7395
  progress: "等待中..."
7276
7396
  };
7277
7397
  }
7278
- /**
7279
- * Synchronous sleep using Atomics.wait on a shared buffer.
7280
- */
7281
- function sleepSync(ms) {
7282
- const buf = new SharedArrayBuffer(4);
7283
- const arr = new Int32Array(buf);
7284
- Atomics.wait(arr, 0, 0, ms);
7285
- }
7286
7398
  //#endregion
7287
7399
  //#region src/oss/resolveOssFileMap.ts
7288
7400
  /**
@@ -10602,7 +10714,7 @@ async function reportCliRun(opts) {
10602
10714
  //#region src/help.ts
10603
10715
  const BIN = "mclaw-diagnose";
10604
10716
  function versionBanner() {
10605
- return `v0.1.15-alpha.9`;
10717
+ return `v0.1.15`;
10606
10718
  }
10607
10719
  const COMMANDS = [
10608
10720
  {
@@ -11188,42 +11300,6 @@ function reportDoctorRunToSlardar(opts) {
11188
11300
  }
11189
11301
  });
11190
11302
  }
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
- let fd = -1;
11200
- try {
11201
- fd = node_fs.default.openSync(filePath, "r");
11202
- const { size } = node_fs.default.fstatSync(fd);
11203
- const readOffset = size > maxBytes ? size - maxBytes : 0;
11204
- const readSize = size - readOffset;
11205
- const buf = Buffer.allocUnsafe(readSize);
11206
- node_fs.default.readSync(fd, buf, 0, readSize, readOffset);
11207
- let start = 0;
11208
- while (start < buf.length && buf[start] !== 10) start++;
11209
- return (start < buf.length ? buf.subarray(start + 1).toString("utf-8") : buf.toString("utf-8")).split("\n").map((line) => line.replace(LOG_PREFIX_RE, "")).filter((line) => !/^=+$/.test(line.trim())).join("\n").trim();
11210
- } catch {
11211
- return "";
11212
- } finally {
11213
- if (fd !== -1) try {
11214
- node_fs.default.closeSync(fd);
11215
- } catch {}
11216
- }
11217
- }
11218
- /**
11219
- * 向 Slardar 上报 upgrade-lark 运行结果(upgrade_lark_run 事件)。
11220
- *
11221
- * extraCategories 记录字符串维度:scene、exit_code、rollback_ok、
11222
- * validation_error、error_msg、log_content(日志文件末尾 4000 字节,含关键结果)。
11223
- *
11224
- * extraMetrics 记录各阶段耗时(毫秒);未执行的阶段上报 -1 作为哨兵值,
11225
- * 便于在 Slardar 查询时区分"未运行"和"运行了 0ms"。
11226
- */
11227
11303
  /**
11228
11304
  * 向 Slardar 上报 upgrade-lark 运行结果(upgrade_lark_run 事件)。
11229
11305
  *
@@ -11234,13 +11310,16 @@ function readLogFileTail(filePath, maxBytes = 4e3) {
11234
11310
  * ## extraCategories(字符串维度)
11235
11311
  * - scene:调用方标识(如 PageUpgradeLark)
11236
11312
  * - check_only:是否为 --check 仅诊断模式
11237
- * - skipped:"true" 表示前置门控未触发跳过安装(含 --check 模式),"false" 表示执行了安装
11238
- * - skip_reason:跳过原因描述(skipped=true 时有值,"check" 表示 --check 模式)
11239
- * - exit_code:npx 子进程退出码(跳过安装时为空)
11240
- * - rollback_ok:回滚是否成功(未触发回滚时为空)
11241
- * - validation_error:安装后校验失败的错误信息
11242
- * - error_msg:命令级错误信息
11243
- * - log_content:日志文件末尾 4000 字节(过滤时间戳和分隔符行)
11313
+ * - status:与 UpgradeLarkResult.status 一致(success / skipped / failed)
11314
+ * - skip_reason:跳过原因(对应 UpgradeLarkResult.skipReason)
11315
+ * - upgrade_needed:仅 --check 模式,是否检测到需要升级(对应 UpgradeLarkResult.upgradeNeeded)
11316
+ * - exit_code:npx 子进程退出码(对应 UpgradeLarkResult.exitCode,跳过安装时为空)
11317
+ * - rollback_ok:回滚是否成功(对应 UpgradeLarkResult.rollbackOk,未触发回滚时为空)
11318
+ * - validation_error:安装后校验失败信息(对应 UpgradeLarkResult.validationError)
11319
+ * - error:命令级错误信息(对应 UpgradeLarkResult.error)
11320
+ * - port_check_ok:端口存活检测结果(对应 UpgradeLarkResult.portCheckOk,未执行时为空)
11321
+ * - result:执行结果一行摘要(派生字段,见 buildUpgradeLarkResultSummary)
11322
+ * - log_file:日志文件绝对路径(对应 UpgradeLarkResult.logFile)
11244
11323
  *
11245
11324
  * ## extraMetrics(数值指标,单位毫秒)
11246
11325
  * 未执行的阶段上报 -1 作为哨兵值,便于与"运行了 0ms"区分。
@@ -11253,23 +11332,25 @@ function readLogFileTail(filePath, maxBytes = 4e3) {
11253
11332
  * 注:[7/7] 重启耗时写入日志但未单独上报,包含在 durationMs 总耗时中。
11254
11333
  */
11255
11334
  function reportUpgradeLarkToSlardar(opts) {
11256
- console.error(`[slardar] upgrade_lark_run scene=${opts.scene ?? ""} checkOnly=${opts.checkOnly} skipped=${opts.skipped ?? false} success=${opts.success} exitCode=${opts.exitCode ?? ""} rollbackOk=${opts.rollbackOk ?? ""}`);
11335
+ console.error(`[slardar] upgrade_lark_run scene=${opts.scene ?? ""} checkOnly=${opts.checkOnly} status=${opts.resultStatus} exitCode=${opts.exitCode ?? ""} rollbackOk=${opts.rollbackOk ?? ""}`);
11257
11336
  const t = opts.timing ?? {};
11258
- const logContent = readLogFileTail(opts.logFile);
11259
11337
  reportTask({
11260
11338
  eventName: "upgrade_lark_run",
11261
11339
  durationMs: opts.durationMs,
11262
- status: opts.success ? "success" : "failed",
11340
+ status: opts.resultStatus === "failed" || opts.upgradeNeeded ? "failed" : "success",
11263
11341
  extraCategories: {
11264
11342
  scene: opts.scene ?? "",
11265
11343
  check_only: String(opts.checkOnly),
11266
- skipped: String(opts.skipped ?? false),
11344
+ status: opts.resultStatus,
11267
11345
  skip_reason: opts.skipReason ?? "",
11346
+ upgrade_needed: String(opts.upgradeNeeded ?? ""),
11268
11347
  exit_code: String(opts.exitCode ?? ""),
11269
- rollback_ok: opts.rollbackOk != null ? String(opts.rollbackOk) : "",
11348
+ rollback_ok: String(opts.rollbackOk ?? ""),
11270
11349
  validation_error: opts.validationError ?? "",
11271
- error_msg: opts.error ?? "",
11272
- log_content: logContent
11350
+ error: opts.error ?? "",
11351
+ port_check_ok: String(opts.portCheckOk ?? ""),
11352
+ result: opts.resultSummary,
11353
+ log_file: opts.logFile
11273
11354
  },
11274
11355
  extraMetrics: {
11275
11356
  pre_probe_ms: t.preProbeMs ?? -1,
@@ -11277,10 +11358,28 @@ function reportUpgradeLarkToSlardar(opts) {
11277
11358
  backup_ms: t.backupMs ?? -1,
11278
11359
  npx_install_ms: t.npxInstallMs ?? -1,
11279
11360
  post_probe_ms: t.postProbeMs ?? -1,
11280
- doctor_fix_ms: t.doctorFixMs ?? -1
11361
+ doctor_fix_ms: t.doctorFixMs ?? -1,
11362
+ port_check_ms: t.portCheckMs ?? -1
11281
11363
  }
11282
11364
  });
11283
11365
  }
11366
+ /**
11367
+ * 将 upgrade-lark 运行结果归纳为一行摘要字符串,用于 Slardar result 字段。
11368
+ *
11369
+ * 格式:
11370
+ * "check: upgrade needed" / "check: no upgrade needed"
11371
+ * "skipped: <skipReason>"
11372
+ * "success: upgrade installed"
11373
+ * "failed: <error> (rolled back)" / "failed: <error> (rollback FAILED)"
11374
+ */
11375
+ function buildUpgradeLarkResultSummary(result, checkOnly) {
11376
+ if (result.status === "skipped") {
11377
+ if (checkOnly) return result.upgradeNeeded ? "check: upgrade needed" : "check: no upgrade needed";
11378
+ return `skipped: ${result.skipReason ?? "pre-check gate"}`;
11379
+ }
11380
+ if (result.status === "success") return "success: upgrade installed";
11381
+ return `failed: ${result.error ?? result.validationError ?? "unknown error"}${result.rollbackOk === false ? " (rollback FAILED)" : result.rollbackOk ? " (rolled back)" : ""}`;
11382
+ }
11284
11383
  //#endregion
11285
11384
  //#region src/upgrade-lark.ts
11286
11385
  /** 升级前需备份的 extensions/ 下的插件目录 */
@@ -11419,6 +11518,82 @@ function probeChannels(label, log, timeoutMs) {
11419
11518
  };
11420
11519
  }
11421
11520
  }
11521
+ /**
11522
+ * 根据 scene、最终状态和端口检测结果,计算场景专属修复状态。
11523
+ * scene 不在已知场景列表时返回 undefined。
11524
+ */
11525
+ function computeFixStatus(scene, status, portCheckOk = void 0) {
11526
+ if (scene === "FromPreviewFailed") return status === "success" && portCheckOk !== false ? "PORT_FIX_READY" : "PORT_FIX_FAILED";
11527
+ if (scene === "FromChannelFailed") return status === "success" ? "CHANNEL_FIX_READY" : "CHANNEL_FIX_FAILED";
11528
+ }
11529
+ /**
11530
+ * 轮询端口检测脚本,确认 openclaw-gateway 在重启后监听指定端口。
11531
+ *
11532
+ * 逻辑:
11533
+ * 1. 先等待 initialWaitMs(让服务有时间完成重启)
11534
+ * 2. 循环执行 `bash ${BUILTIN_PATH}/tool/port_check.sh <port>`,至多 maxAttempts 次:
11535
+ * - 输出为空 → 服务尚未就绪,等 intervalMs 后重试
11536
+ * - 输出含端口号 → 端口存活,返回 ok=true
11537
+ * - 输出非空但不含端口号 → 端口异常,立即返回 ok=false
11538
+ * 3. 超过 maxAttempts 仍无响应 → 返回 ok=false
11539
+ *
11540
+ * 注意:${BUILTIN_PATH} 是沙箱 shell 环境变量,通过 `bash -c` 让 shell 自行展开,
11541
+ * Node 侧不读取、不拼接该路径。
11542
+ */
11543
+ function pollPortCheck(opts) {
11544
+ const { port, initialWaitMs, intervalMs, maxAttempts, log } = opts;
11545
+ const portStr = String(port);
11546
+ const cmd = "bash ${BUILTIN_PATH}/tool/port_check.sh " + portStr;
11547
+ log(` waiting ${initialWaitMs / 1e3}s before first port-check poll...`);
11548
+ if (initialWaitMs > 0) sleepSync(initialWaitMs);
11549
+ for (let i = 1; i <= maxAttempts; i++) {
11550
+ const r = (0, node_child_process.spawnSync)("bash", ["-c", cmd], {
11551
+ encoding: "utf-8",
11552
+ stdio: [
11553
+ "ignore",
11554
+ "pipe",
11555
+ "pipe"
11556
+ ],
11557
+ timeout: 1e4
11558
+ });
11559
+ const output = (r.stdout ?? "").trim();
11560
+ const errout = (r.stderr ?? "").trim();
11561
+ log(` port-check [${i}/${maxAttempts}]: exit=${r.status ?? "null"} output=${JSON.stringify(output)}${errout ? ` stderr=${JSON.stringify(errout)}` : ""}`);
11562
+ if (!output) {
11563
+ sleepSync(intervalMs);
11564
+ continue;
11565
+ }
11566
+ if (output.includes(portStr)) {
11567
+ log(` port-check: SUCCESS — port ${port} confirmed in output`);
11568
+ return {
11569
+ ok: true,
11570
+ output
11571
+ };
11572
+ }
11573
+ log(` port-check: FAILED — output non-empty but port ${port} not found`);
11574
+ return {
11575
+ ok: false,
11576
+ output
11577
+ };
11578
+ }
11579
+ log(` port-check: FAILED — no response after ${maxAttempts} attempts`);
11580
+ return { ok: false };
11581
+ }
11582
+ /**
11583
+ * 将 fixStatus 信号文件写入 /tmp/event/<fixStatus>,内容为完整的 UpgradeLarkResult JSON。
11584
+ * 外部进程(如服务端诊断流程)通过轮询此文件感知升级结果。
11585
+ * 写入失败只记录日志,不影响主流程返回值。
11586
+ */
11587
+ function writeFixStatusEvent(fixStatus, result, log) {
11588
+ const filePath = `${FIX_EVENT_DIR}/${fixStatus}`;
11589
+ try {
11590
+ node_fs.default.mkdirSync(FIX_EVENT_DIR, { recursive: true });
11591
+ node_fs.default.writeFileSync(filePath, JSON.stringify(result, null, 2));
11592
+ log(`[fix-event] written: ${filePath}`);
11593
+ } catch (e) {
11594
+ log(`[fix-event] write failed: ${filePath} — ${e.message}`);
11595
+ }
11596
+ }
11422
11597
  function runUpgradeLark(opts) {
11423
11598
  const cwd = opts.cwd ?? "/home/gem/workspace/agent";
11424
11599
  const configPath = opts.configPath ?? CONFIG_PATH;
@@ -11438,6 +11613,26 @@ function runUpgradeLark(opts) {
11438
11613
  log(` configPath : ${configPath}`);
11439
11614
  log(`${"=".repeat(60)}`);
11440
11615
  const timing = {};
11616
+ for (const s of [
11617
+ "PORT_FIX_READY",
11618
+ "PORT_FIX_FAILED",
11619
+ "CHANNEL_FIX_READY",
11620
+ "CHANNEL_FIX_FAILED"
11621
+ ]) {
11622
+ const f = `${FIX_EVENT_DIR}/${s}`;
11623
+ try {
11624
+ if (node_fs.default.existsSync(f)) {
11625
+ node_fs.default.rmSync(f);
11626
+ log(`[fix-event] cleared stale: ${f}`);
11627
+ }
11628
+ } catch (e) {
11629
+ log(`[fix-event] clear failed: ${f} — ${e.message}`);
11630
+ }
11631
+ }
11632
+ const finalReturn = (r) => {
11633
+ if (r.fixStatus !== void 0) writeFixStatusEvent(r.fixStatus, r, log);
11634
+ return r;
11635
+ };
11441
11636
  log("");
11442
11637
  log("── [Pre-check A] channels probe(升级前)────────────────");
11443
11638
  const t_preProbeStart = Date.now();
@@ -11476,14 +11671,14 @@ function runUpgradeLark(opts) {
11476
11671
  log(`${"=".repeat(60)}`);
11477
11672
  log("upgrade-lark skipped (pre-check gate)");
11478
11673
  log(`${"=".repeat(60)}`);
11479
- return {
11480
- ok: true,
11481
- skipped: true,
11674
+ return finalReturn({
11675
+ status: "skipped",
11482
11676
  skipReason: reason,
11483
11677
  upgradeNeeded: false,
11484
11678
  timing,
11485
- logFile
11486
- };
11679
+ logFile,
11680
+ fixStatus: computeFixStatus(opts.scene, "skipped")
11681
+ });
11487
11682
  }
11488
11683
  if (beforeChannels.anyAccountWorking) {
11489
11684
  const reason = "channels are working — upgrade not needed (issue detected but system is functional)";
@@ -11491,14 +11686,14 @@ function runUpgradeLark(opts) {
11491
11686
  log(`${"=".repeat(60)}`);
11492
11687
  log("upgrade-lark skipped (pre-check gate)");
11493
11688
  log(`${"=".repeat(60)}`);
11494
- return {
11495
- ok: true,
11496
- skipped: true,
11689
+ return finalReturn({
11690
+ status: "skipped",
11497
11691
  skipReason: reason,
11498
11692
  upgradeNeeded: false,
11499
11693
  timing,
11500
- logFile
11501
- };
11694
+ logFile,
11695
+ fixStatus: computeFixStatus(opts.scene, "skipped")
11696
+ });
11502
11697
  }
11503
11698
  log(` PROCEED: requiresLarkUpgrade=true (version=${versionIncompatible}, feishuConfig=${feishuConfigInvalid}) AND channels not working → running upgrade`);
11504
11699
  if (opts.checkOnly) {
@@ -11506,14 +11701,13 @@ function runUpgradeLark(opts) {
11506
11701
  log(`${"=".repeat(60)}`);
11507
11702
  log("upgrade-lark check complete");
11508
11703
  log(`${"=".repeat(60)}`);
11509
- return {
11510
- ok: true,
11511
- skipped: true,
11704
+ return finalReturn({
11705
+ status: "skipped",
11512
11706
  skipReason: "check",
11513
11707
  upgradeNeeded: true,
11514
11708
  timing,
11515
11709
  logFile
11516
- };
11710
+ });
11517
11711
  }
11518
11712
  log("");
11519
11713
  log("── [1/6] 文件备份 ────────────────────────────────────────");
@@ -11523,12 +11717,13 @@ function runUpgradeLark(opts) {
11523
11717
  timing.backupMs = Date.now() - t_backupStart;
11524
11718
  if (!backup.ok) {
11525
11719
  log(`ERROR: ${backup.error}`);
11526
- return {
11527
- ok: false,
11720
+ return finalReturn({
11721
+ status: "failed",
11528
11722
  error: backup.error,
11529
11723
  timing,
11530
- logFile
11531
- };
11724
+ logFile,
11725
+ fixStatus: computeFixStatus(opts.scene, "failed")
11726
+ });
11532
11727
  }
11533
11728
  log("backup: ok");
11534
11729
  logVersionSnapshot("before-versions", snapshotVersions(cwd, log), log);
@@ -11569,15 +11764,15 @@ function runUpgradeLark(opts) {
11569
11764
  if (statusCheckDelayMs > 0) {
11570
11765
  log("");
11571
11766
  log(`── 等待 ${statusCheckDelayMs / 1e3}s(让 openclaw 服务完成重启) ─────────────`);
11572
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, statusCheckDelayMs);
11767
+ sleepSync(statusCheckDelayMs);
11573
11768
  log("wait done");
11574
11769
  }
11575
11770
  const doRollback = (reason) => {
11576
11771
  log(`ERROR: ${reason}`);
11577
11772
  const rollbackOk = restoreFiles(fsOpts);
11578
11773
  log(`rollback: ${rollbackOk ? "ok" : "FAILED"}`);
11579
- return {
11580
- ok: false,
11774
+ return finalReturn({
11775
+ status: "failed",
11581
11776
  error: reason,
11582
11777
  validationError: reason,
11583
11778
  stdout: npxStdout,
@@ -11585,8 +11780,9 @@ function runUpgradeLark(opts) {
11585
11780
  exitCode: npxExitCode,
11586
11781
  rollbackOk,
11587
11782
  timing,
11588
- logFile
11589
- };
11783
+ logFile,
11784
+ fixStatus: computeFixStatus(opts.scene, "failed")
11785
+ });
11590
11786
  };
11591
11787
  log("");
11592
11788
  log("── [4/5] 安装后诊断校验 ─────────────────────────────────");
@@ -11612,7 +11808,7 @@ function runUpgradeLark(opts) {
11612
11808
  timing.postProbeMs = Date.now() - t_postProbeStart;
11613
11809
  log(` feishu config invalid after: ${afterChannels.feishuConfigInvalid}`);
11614
11810
  const stillNeedsUpgrade = (afterVersionIncompatible || afterChannels.feishuConfigInvalid) && !afterChannels.anyAccountWorking;
11615
- const isNewDefaultOnly = !beforeChannels.anyAccountWorking && isDefaultOnlyState(afterChannels) && !afterVersionIncompatible;
11811
+ const isNewDefaultOnly = !afterVersionIncompatible && !afterChannels.feishuConfigInvalid && !beforeChannels.anyAccountWorking && isDefaultOnlyState(afterChannels);
11616
11812
  log(` post-check: stillNeedsUpgrade=${stillNeedsUpgrade} (version=${afterVersionIncompatible}, feishuConfig=${afterChannels.feishuConfigInvalid}, channelsWorking=${afterChannels.anyAccountWorking}) isNewDefaultOnly=${isNewDefaultOnly}`);
11617
11813
  if (stillNeedsUpgrade && !isNewDefaultOnly) return doRollback(`post-install diagnosis still shows anomaly: versionIncompatible=${afterVersionIncompatible}, feishuConfigInvalid=${afterChannels.feishuConfigInvalid}, anyAccountWorking=${afterChannels.anyAccountWorking}`);
11618
11814
  if (isNewDefaultOnly) log(" post-install diagnosis: ok (new default account — plugin installed, awaiting configuration)");
@@ -11638,8 +11834,9 @@ function runUpgradeLark(opts) {
11638
11834
  if (fixResult.stderr?.trim()) log(`doctor(fix) stderr:\n${fixResult.stderr.trim()}`);
11639
11835
  log(`doctor(fix) exit: ${fixResult.status ?? "null"}${fixResult.error ? ` error: ${fixResult.error.message}` : ""}`);
11640
11836
  log("");
11641
- log("── [7/7] 重启 openclaw 服务 ──────────────────────────────");
11837
+ log("── [7/8] 重启 openclaw 服务 ──────────────────────────────");
11642
11838
  const restartScript = "/opt/force/bin/openclaw_scripts/restart.sh";
11839
+ let restartExecuted = false;
11643
11840
  if (opts.skipRestart) log(" skipped: --skip-restart");
11644
11841
  else if (node_fs.default.existsSync(restartScript)) {
11645
11842
  const t_restart = Date.now();
@@ -11656,24 +11853,133 @@ function runUpgradeLark(opts) {
11656
11853
  if (restartResult.stdout?.trim()) log(` restart stdout:\n${restartResult.stdout.trim()}`);
11657
11854
  if (restartResult.stderr?.trim()) log(` restart stderr:\n${restartResult.stderr.trim()}`);
11658
11855
  log(` restart.sh exit: ${restartResult.status ?? "null"} (${restartMs}ms)${restartResult.error ? ` error: ${restartResult.error.message}` : ""}`);
11856
+ restartExecuted = true;
11659
11857
  } else log(` skipped: ${restartScript} not found`);
11660
11858
  log("");
11859
+ log("── [8/8] 端口存活检测 ────────────────────────────────────");
11860
+ let portCheckOk;
11861
+ if (!restartExecuted) log(" skipped: restart was not executed");
11862
+ else {
11863
+ const t_portCheck = Date.now();
11864
+ const pcResult = pollPortCheck({
11865
+ port: 18789,
11866
+ initialWaitMs: opts.portCheckInitialWaitMs ?? 3e3,
11867
+ intervalMs: opts.portCheckIntervalMs ?? 1e3,
11868
+ maxAttempts: opts.portCheckMaxAttempts ?? 30,
11869
+ log
11870
+ });
11871
+ timing.portCheckMs = Date.now() - t_portCheck;
11872
+ portCheckOk = pcResult.ok;
11873
+ log(` port-check result: ${portCheckOk ? "ok" : "FAILED"} (${timing.portCheckMs}ms)`);
11874
+ }
11875
+ log("");
11661
11876
  log(`${"=".repeat(60)}`);
11662
11877
  log("upgrade-lark completed successfully");
11663
11878
  log(`${"=".repeat(60)}`);
11664
- return {
11665
- ok: true,
11879
+ return finalReturn({
11880
+ status: "success",
11666
11881
  stdout: npxStdout,
11667
11882
  stderr: npxStderr,
11668
11883
  exitCode: npxExitCode,
11884
+ portCheckOk,
11669
11885
  timing,
11670
- logFile
11886
+ logFile,
11887
+ fixStatus: computeFixStatus(opts.scene, "success", portCheckOk)
11888
+ });
11889
+ }
11890
+ //#endregion
11891
+ //#region src/install-lock.ts
11892
+ /**
11893
+ * 检查指定 PID 是否仍在运行(向进程发 signal 0,不影响其运行状态)。
11894
+ * 跨平台:Linux/macOS 均支持;Windows 不在目标平台范围内。
11895
+ */
11896
+ function isPidAlive(pid) {
11897
+ try {
11898
+ process.kill(pid, 0);
11899
+ return true;
11900
+ } catch (e) {
11901
+ if (e.code === "EPERM") return true;
11902
+ return false;
11903
+ }
11904
+ }
11905
+ /**
11906
+ * 尝试获取安装操作互斥锁。
11907
+ *
11908
+ * 利用 open(O_CREAT | O_EXCL) 的 POSIX 原子性确保同一时刻只有一个安装指令运行。
11909
+ * 覆盖的安装指令:upgrade-lark / install-openclaw / install-extension / install-cli / reset --worker。
11910
+ *
11911
+ * 返回 null 表示成功获取锁;返回错误信息字符串表示锁被其他进程持有。
11912
+ * 与抛异常相比,返回值让调用方可以输出符合各自格式的 result,而不依赖 catch handler 统一处理。
11913
+ *
11914
+ * 成功获取后自动注册 process.once('exit') 释放锁,无需调用方手动清理。
11915
+ */
11916
+ function acquireInstallLock(command) {
11917
+ node_fs.default.mkdirSync(DIAGNOSE_DIR, { recursive: true });
11918
+ const info = {
11919
+ pid: process.pid,
11920
+ command,
11921
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
11671
11922
  };
11923
+ const content = JSON.stringify(info);
11924
+ while (true) try {
11925
+ const fd = node_fs.default.openSync(INSTALL_LOCK_FILE, node_fs.default.constants.O_CREAT | node_fs.default.constants.O_EXCL | node_fs.default.constants.O_WRONLY, 420);
11926
+ node_fs.default.writeSync(fd, content);
11927
+ node_fs.default.closeSync(fd);
11928
+ process.once("exit", releaseInstallLock);
11929
+ console.error(`[install-lock] acquired command=${command} pid=${process.pid}`);
11930
+ return null;
11931
+ } catch (e) {
11932
+ if (e.code !== "EEXIST") throw e;
11933
+ let existing = null;
11934
+ try {
11935
+ existing = JSON.parse(node_fs.default.readFileSync(INSTALL_LOCK_FILE, "utf-8"));
11936
+ } catch {
11937
+ try {
11938
+ node_fs.default.rmSync(INSTALL_LOCK_FILE, { force: true });
11939
+ } catch {}
11940
+ continue;
11941
+ }
11942
+ if (!isPidAlive(existing.pid)) {
11943
+ console.error(`[install-lock] stale lock detected (pid=${existing.pid} command=${existing.command}), removing`);
11944
+ try {
11945
+ node_fs.default.rmSync(INSTALL_LOCK_FILE, { force: true });
11946
+ } catch {}
11947
+ continue;
11948
+ }
11949
+ const conflictMsg = `另一个安装指令正在运行,请等待其完成后重试。\n 占用指令: ${existing.command}\n 进程 PID : ${existing.pid}\n 开始时间: ${existing.startedAt}\n 锁文件 : ${INSTALL_LOCK_FILE}`;
11950
+ console.error(`[install-lock] conflict: command=${command} blocked by pid=${existing.pid} command=${existing.command}`);
11951
+ return conflictMsg;
11952
+ }
11953
+ }
11954
+ /**
11955
+ * 释放安装操作互斥锁。
11956
+ * 仅删除由本进程创建的锁文件,避免误删其他进程的锁。
11957
+ * 通常由 process.once('exit') 自动调用,不需要手动调用。
11958
+ */
11959
+ function releaseInstallLock() {
11960
+ try {
11961
+ const raw = node_fs.default.readFileSync(INSTALL_LOCK_FILE, "utf-8");
11962
+ const info = JSON.parse(raw);
11963
+ if (info.pid === process.pid) {
11964
+ node_fs.default.rmSync(INSTALL_LOCK_FILE, { force: true });
11965
+ console.error(`[install-lock] released command=${info.command} pid=${process.pid}`);
11966
+ }
11967
+ } catch {}
11672
11968
  }
11673
11969
  //#endregion
11674
11970
  //#region src/index.ts
11971
+ /** 锁冲突时各 install 命令的统一处理:输出 { ok: false } 并退出 */
11972
+ function handleInstallLockConflict(lockErr) {
11973
+ console.log(JSON.stringify({
11974
+ ok: false,
11975
+ error: lockErr
11976
+ }));
11977
+ node_process.default.exit(1);
11978
+ }
11675
11979
  const args = node_process.default.argv.slice(2);
11676
11980
  const mode = args.find((a) => !a.startsWith("-"));
11981
+ const t0 = Date.now();
11982
+ const scene = getFlag(args, "scene");
11677
11983
  /**
11678
11984
  * Pull the first non-flag positional after the mode name.
11679
11985
  * (The mode itself is args[0] in the filtered set, so we skip index 0.)
@@ -11757,13 +12063,11 @@ async function main() {
11757
12063
  }
11758
12064
  const caller = getFlag(args, "caller");
11759
12065
  const traceId = getFlag(args, "trace-id");
11760
- const scene = getFlag(args, "scene");
11761
12066
  const profile = getFlag(args, "profile") === "experimental" ? "experimental" : "standard";
11762
12067
  const rc = setRunContext({
11763
12068
  caller,
11764
12069
  traceId
11765
12070
  });
11766
- const t0 = Date.now();
11767
12071
  console.error(`${mode}: begin argv=[${args.join(" ")}] version=${getVersion()} traceId=${traceId ?? "-"} caller=${caller ?? "-"} runIdGenerated=${rc.generated}`);
11768
12072
  switch (mode) {
11769
12073
  case "check": {
@@ -11871,6 +12175,17 @@ async function main() {
11871
12175
  node_process.default.exit(1);
11872
12176
  }
11873
12177
  const resultFile = resetResultFile(taskId);
12178
+ const resetLockErr = acquireInstallLock("reset");
12179
+ if (resetLockErr) {
12180
+ try {
12181
+ node_fs.default.writeFileSync(resultFile, JSON.stringify({
12182
+ status: "failed",
12183
+ error: resetLockErr
12184
+ }));
12185
+ } catch {}
12186
+ node_process.default.exitCode = 1;
12187
+ return;
12188
+ }
11874
12189
  const raw = await fetchCtxViaInnerApi({
11875
12190
  populate: planCtxPopulate({ command: "reset" }),
11876
12191
  caller,
@@ -11914,6 +12229,8 @@ async function main() {
11914
12229
  console.error("Usage: install-openclaw <tag> [--oss_file_map=<base64>]");
11915
12230
  node_process.default.exit(1);
11916
12231
  }
12232
+ const installOcLockErr = acquireInstallLock("install-openclaw");
12233
+ if (installOcLockErr) handleInstallLockConflict(installOcLockErr);
11917
12234
  const ossFileMapFlag = getFlag(args, "oss_file_map");
11918
12235
  let installOssFileMap;
11919
12236
  let rawForTelemetry;
@@ -11956,6 +12273,8 @@ async function main() {
11956
12273
  console.error("Usage: install-extension <tag> (--all | --extension=<name>...) [--home_base=<dir>] [--config_path=<path>] [--skip-config-update] [--oss_file_map=<base64>]");
11957
12274
  node_process.default.exit(1);
11958
12275
  }
12276
+ const installExtLockErr = acquireInstallLock("install-extension");
12277
+ if (installExtLockErr) handleInstallLockConflict(installExtLockErr);
11959
12278
  const all = args.includes("--all");
11960
12279
  const names = getMultiFlag(args, "extension");
11961
12280
  const homeBase = getFlag(args, "home_base");
@@ -12019,6 +12338,8 @@ async function main() {
12019
12338
  console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--oss_file_map=<base64>]");
12020
12339
  node_process.default.exit(1);
12021
12340
  }
12341
+ const installCliLockErr = acquireInstallLock("install-cli");
12342
+ if (installCliLockErr) handleInstallLockConflict(installCliLockErr);
12022
12343
  const homeBase = getFlag(args, "home_base");
12023
12344
  const ossFileMapFlag = getFlag(args, "oss_file_map");
12024
12345
  let installOssFileMap;
@@ -12162,6 +12483,31 @@ async function main() {
12162
12483
  }
12163
12484
  case "upgrade-lark": {
12164
12485
  const checkOnly = args.includes("--check");
12486
+ if (!checkOnly) {
12487
+ const upgradeLockErr = acquireInstallLock("upgrade-lark");
12488
+ if (upgradeLockErr) {
12489
+ const fixStatus = computeFixStatus(scene, "failed");
12490
+ const failResult = {
12491
+ status: "failed",
12492
+ error: upgradeLockErr,
12493
+ logFile: "",
12494
+ fixStatus
12495
+ };
12496
+ if (fixStatus !== void 0) writeFixStatusEvent(fixStatus, failResult, console.error);
12497
+ console.log(JSON.stringify(failResult));
12498
+ reportUpgradeLarkToSlardar({
12499
+ scene,
12500
+ checkOnly,
12501
+ durationMs: Date.now() - t0,
12502
+ resultStatus: "failed",
12503
+ error: upgradeLockErr,
12504
+ logFile: "",
12505
+ resultSummary: buildUpgradeLarkResultSummary(failResult, checkOnly)
12506
+ });
12507
+ node_process.default.exitCode = 1;
12508
+ return;
12509
+ }
12510
+ }
12165
12511
  const skipRestart = args.includes("--skip-restart");
12166
12512
  const result = runUpgradeLark({
12167
12513
  runId: rc.runId,
@@ -12175,14 +12521,16 @@ async function main() {
12175
12521
  scene,
12176
12522
  checkOnly,
12177
12523
  durationMs: upgradeDurationMs,
12178
- success: result.ok,
12179
- skipped: result.skipped,
12524
+ resultStatus: result.status,
12525
+ upgradeNeeded: result.upgradeNeeded,
12180
12526
  skipReason: result.skipReason,
12181
12527
  logFile: result.logFile,
12528
+ resultSummary: buildUpgradeLarkResultSummary(result, checkOnly),
12182
12529
  exitCode: result.exitCode,
12183
12530
  rollbackOk: result.rollbackOk,
12184
12531
  validationError: result.validationError,
12185
12532
  error: result.error,
12533
+ portCheckOk: result.portCheckOk,
12186
12534
  timing: result.timing
12187
12535
  });
12188
12536
  try {
@@ -12194,14 +12542,14 @@ async function main() {
12194
12542
  durationMs: upgradeDurationMs,
12195
12543
  caller: rc.caller,
12196
12544
  traceId: rc.traceId,
12197
- success: result.ok,
12545
+ success: result.status !== "failed",
12198
12546
  result,
12199
- error: result.ok ? void 0 : { message: result.error ?? "upgrade-lark failed" }
12547
+ error: result.status === "failed" ? { message: result.error ?? "upgrade-lark failed" } : void 0
12200
12548
  });
12201
12549
  } catch (e) {
12202
12550
  console.error(`[telemetry] reportCliRun failed: ${e.message}`);
12203
12551
  }
12204
- if (!result.ok || checkOnly && result.upgradeNeeded) {
12552
+ if (result.status === "failed" || checkOnly && result.upgradeNeeded) {
12205
12553
  node_process.default.exitCode = 1;
12206
12554
  return;
12207
12555
  }
@@ -12225,6 +12573,47 @@ main().catch((err) => {
12225
12573
  const msg = err instanceof Error ? err.message : String(err);
12226
12574
  console.error(`${mode ?? "<no-mode>"}: error message=${msg}`);
12227
12575
  node_process.default.stderr.write(`Error: ${msg}\n`);
12576
+ if (mode) {
12577
+ const durationMs = Date.now() - t0;
12578
+ if (mode === "upgrade-lark") {
12579
+ const checkOnly = args.includes("--check");
12580
+ const fixStatus = computeFixStatus(scene, "failed");
12581
+ const failResult = {
12582
+ status: "failed",
12583
+ error: msg,
12584
+ logFile: "",
12585
+ fixStatus
12586
+ };
12587
+ if (fixStatus !== void 0) writeFixStatusEvent(fixStatus, failResult, console.error);
12588
+ console.log(JSON.stringify(failResult));
12589
+ reportUpgradeLarkToSlardar({
12590
+ scene,
12591
+ checkOnly,
12592
+ durationMs,
12593
+ resultStatus: "failed",
12594
+ error: msg,
12595
+ logFile: "",
12596
+ resultSummary: buildUpgradeLarkResultSummary(failResult, checkOnly)
12597
+ });
12598
+ } else if ([
12599
+ "doctor",
12600
+ "check",
12601
+ "repair",
12602
+ "install-openclaw",
12603
+ "install-extension",
12604
+ "install-cli",
12605
+ "download-resource",
12606
+ "reset",
12607
+ "lark-cli-init"
12608
+ ].includes(mode)) reportDoctorRunToSlardar({
12609
+ command: mode,
12610
+ scene,
12611
+ profile: getFlag(args, "profile") === "experimental" ? "experimental" : "standard",
12612
+ fix: args.includes("--fix"),
12613
+ durationMs,
12614
+ success: false
12615
+ });
12616
+ }
12228
12617
  node_process.default.exitCode = 1;
12229
12618
  });
12230
12619
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/openclaw-scripts-diagnose-cli",
3
- "version": "0.1.15-alpha.9",
3
+ "version": "0.1.15",
4
4
  "description": "CLI for OpenClaw config diagnose and repair with JSON5 support",
5
5
  "main": "dist/index.cjs",
6
6
  "bin": {