@lark-apaas/openclaw-scripts-diagnose-cli 0.1.15-alpha.9 → 0.1.16-alpha.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 +539 -129
  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.16-alpha.0";
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.16-alpha.0`;
10606
10718
  }
10607
10719
  const COMMANDS = [
10608
10720
  {
@@ -11140,12 +11252,28 @@ function planCtxPopulate(opts) {
11140
11252
  //#endregion
11141
11253
  //#region src/slardar/reporter.ts
11142
11254
  let slardarInitialized = false;
11255
+ /**
11256
+ * 解析实际运行环境,映射到 Slardar 支持的 env 值。
11257
+ *
11258
+ * 仅处理 FORCE_FRAMEWORK_ENVIRONMENT(studio_server 在沙箱启动时注入):
11259
+ * boe / pre → "dev"(非生产环境,避免污染线上指标)
11260
+ * online → "online"
11261
+ * 未识别到有效值时返回 undefined,交由 openclaw-slardar 内部逻辑决定
11262
+ * (MIAODA_SLARDAR_ENV / 编译期常量 / 默认 online)。
11263
+ */
11264
+ function resolveSlardarEnv() {
11265
+ const ffe = (process.env.FORCE_FRAMEWORK_ENVIRONMENT ?? "").trim().toLowerCase();
11266
+ if (ffe === "online") return "online";
11267
+ if (ffe === "boe" || ffe === "pre") return "dev";
11268
+ }
11143
11269
  function initDiagnoseSlardar() {
11144
11270
  if (slardarInitialized) return;
11145
11271
  slardarInitialized = true;
11272
+ const env = resolveSlardarEnv();
11146
11273
  initSlardar({
11147
11274
  bid: "apaas_miaoda",
11148
- release: getVersion()
11275
+ release: getVersion(),
11276
+ ...env ? { env } : {}
11149
11277
  });
11150
11278
  process.on("beforeExit", flushDiagnoseSlardar);
11151
11279
  }
@@ -11188,42 +11316,6 @@ function reportDoctorRunToSlardar(opts) {
11188
11316
  }
11189
11317
  });
11190
11318
  }
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
11319
  /**
11228
11320
  * 向 Slardar 上报 upgrade-lark 运行结果(upgrade_lark_run 事件)。
11229
11321
  *
@@ -11234,13 +11326,17 @@ function readLogFileTail(filePath, maxBytes = 4e3) {
11234
11326
  * ## extraCategories(字符串维度)
11235
11327
  * - scene:调用方标识(如 PageUpgradeLark)
11236
11328
  * - 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 字节(过滤时间戳和分隔符行)
11329
+ * - status:与 UpgradeLarkResult.status 一致(success / skipped / failed)
11330
+ * - skip_reason:跳过原因(对应 UpgradeLarkResult.skipReason)
11331
+ * - upgrade_needed:仅 --check 模式,是否检测到需要升级(对应 UpgradeLarkResult.upgradeNeeded)
11332
+ * - exit_code:npx 子进程退出码(对应 UpgradeLarkResult.exitCode,跳过安装时为空)
11333
+ * - rollback_ok:回滚是否成功(对应 UpgradeLarkResult.rollbackOk,未触发回滚时为空)
11334
+ * - validation_error:安装后校验失败信息(对应 UpgradeLarkResult.validationError)
11335
+ * - error:命令级错误信息(对应 UpgradeLarkResult.error)
11336
+ * - port_check_ok:端口存活检测结果(对应 UpgradeLarkResult.portCheckOk,未执行时为空)
11337
+ * - fix_status:场景专属修复状态(对应 UpgradeLarkResult.fixStatus,非场景命令时为空)
11338
+ * - result:执行结果一行摘要(派生字段,见 buildUpgradeLarkResultSummary)
11339
+ * - log_file:日志文件绝对路径(对应 UpgradeLarkResult.logFile)
11244
11340
  *
11245
11341
  * ## extraMetrics(数值指标,单位毫秒)
11246
11342
  * 未执行的阶段上报 -1 作为哨兵值,便于与"运行了 0ms"区分。
@@ -11253,23 +11349,26 @@ function readLogFileTail(filePath, maxBytes = 4e3) {
11253
11349
  * 注:[7/7] 重启耗时写入日志但未单独上报,包含在 durationMs 总耗时中。
11254
11350
  */
11255
11351
  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 ?? ""}`);
11352
+ console.error(`[slardar] upgrade_lark_run scene=${opts.scene ?? ""} checkOnly=${opts.checkOnly} status=${opts.resultStatus} exitCode=${opts.exitCode ?? ""} rollbackOk=${opts.rollbackOk ?? ""}`);
11257
11353
  const t = opts.timing ?? {};
11258
- const logContent = readLogFileTail(opts.logFile);
11259
11354
  reportTask({
11260
11355
  eventName: "upgrade_lark_run",
11261
11356
  durationMs: opts.durationMs,
11262
- status: opts.success ? "success" : "failed",
11357
+ status: opts.resultStatus === "failed" || opts.upgradeNeeded ? "failed" : "success",
11263
11358
  extraCategories: {
11264
11359
  scene: opts.scene ?? "",
11265
11360
  check_only: String(opts.checkOnly),
11266
- skipped: String(opts.skipped ?? false),
11361
+ status: opts.resultStatus,
11267
11362
  skip_reason: opts.skipReason ?? "",
11363
+ upgrade_needed: String(opts.upgradeNeeded ?? ""),
11268
11364
  exit_code: String(opts.exitCode ?? ""),
11269
- rollback_ok: opts.rollbackOk != null ? String(opts.rollbackOk) : "",
11365
+ rollback_ok: String(opts.rollbackOk ?? ""),
11270
11366
  validation_error: opts.validationError ?? "",
11271
- error_msg: opts.error ?? "",
11272
- log_content: logContent
11367
+ error: opts.error ?? "",
11368
+ port_check_ok: String(opts.portCheckOk ?? ""),
11369
+ fix_status: opts.fixStatus ?? "",
11370
+ result: opts.resultSummary,
11371
+ log_file: opts.logFile
11273
11372
  },
11274
11373
  extraMetrics: {
11275
11374
  pre_probe_ms: t.preProbeMs ?? -1,
@@ -11277,10 +11376,28 @@ function reportUpgradeLarkToSlardar(opts) {
11277
11376
  backup_ms: t.backupMs ?? -1,
11278
11377
  npx_install_ms: t.npxInstallMs ?? -1,
11279
11378
  post_probe_ms: t.postProbeMs ?? -1,
11280
- doctor_fix_ms: t.doctorFixMs ?? -1
11379
+ doctor_fix_ms: t.doctorFixMs ?? -1,
11380
+ port_check_ms: t.portCheckMs ?? -1
11281
11381
  }
11282
11382
  });
11283
11383
  }
11384
+ /**
11385
+ * 将 upgrade-lark 运行结果归纳为一行摘要字符串,用于 Slardar result 字段。
11386
+ *
11387
+ * 格式:
11388
+ * "check: upgrade needed" / "check: no upgrade needed"
11389
+ * "skipped: <skipReason>"
11390
+ * "success: upgrade installed"
11391
+ * "failed: <error> (rolled back)" / "failed: <error> (rollback FAILED)"
11392
+ */
11393
+ function buildUpgradeLarkResultSummary(result, checkOnly) {
11394
+ if (result.status === "skipped") {
11395
+ if (checkOnly) return result.upgradeNeeded ? "check: upgrade needed" : "check: no upgrade needed";
11396
+ return `skipped: ${result.skipReason ?? "pre-check gate"}`;
11397
+ }
11398
+ if (result.status === "success") return "success: upgrade installed";
11399
+ return `failed: ${result.error ?? result.validationError ?? "unknown error"}${result.rollbackOk === false ? " (rollback FAILED)" : result.rollbackOk ? " (rolled back)" : ""}`;
11400
+ }
11284
11401
  //#endregion
11285
11402
  //#region src/upgrade-lark.ts
11286
11403
  /** 升级前需备份的 extensions/ 下的插件目录 */
@@ -11419,6 +11536,82 @@ function probeChannels(label, log, timeoutMs) {
11419
11536
  };
11420
11537
  }
11421
11538
  }
11539
+ /**
11540
+ * 根据 scene、最终状态和端口检测结果,计算场景专属修复状态。
11541
+ * scene 不在已知场景列表时返回 undefined。
11542
+ */
11543
+ function computeFixStatus(scene, status, portCheckOk = void 0) {
11544
+ if (scene === "FromPreviewFailed") return status === "success" && portCheckOk !== false ? "PORT_FIX_READY" : "PORT_FIX_FAILED";
11545
+ if (scene === "FromChannelFailed") return status === "success" ? "CHANNEL_FIX_READY" : "CHANNEL_FIX_FAILED";
11546
+ }
11547
+ /**
11548
+ * 轮询端口检测脚本,确认 openclaw-gateway 在重启后监听指定端口。
11549
+ *
11550
+ * 逻辑:
11551
+ * 1. 先等待 initialWaitMs(让服务有时间完成重启)
11552
+ * 2. 循环执行 `bash ${BUILTIN_PATH}/tool/port_check.sh <port>`,至多 maxAttempts 次:
11553
+ * - 输出为空 → 服务尚未就绪,等 intervalMs 后重试
11554
+ * - 输出含端口号 → 端口存活,返回 ok=true
11555
+ * - 输出非空但不含端口号 → 端口异常,立即返回 ok=false
11556
+ * 3. 超过 maxAttempts 仍无响应 → 返回 ok=false
11557
+ *
11558
+ * 注意:${BUILTIN_PATH} 是沙箱 shell 环境变量,通过 `bash -c` 让 shell 自行展开,
11559
+ * Node 侧不读取、不拼接该路径。
11560
+ */
11561
+ function pollPortCheck(opts) {
11562
+ const { port, initialWaitMs, intervalMs, maxAttempts, log } = opts;
11563
+ const portStr = String(port);
11564
+ const cmd = "bash ${BUILTIN_PATH}/tool/port_check.sh " + portStr;
11565
+ log(` waiting ${initialWaitMs / 1e3}s before first port-check poll...`);
11566
+ if (initialWaitMs > 0) sleepSync(initialWaitMs);
11567
+ for (let i = 1; i <= maxAttempts; i++) {
11568
+ const r = (0, node_child_process.spawnSync)("bash", ["-c", cmd], {
11569
+ encoding: "utf-8",
11570
+ stdio: [
11571
+ "ignore",
11572
+ "pipe",
11573
+ "pipe"
11574
+ ],
11575
+ timeout: 1e4
11576
+ });
11577
+ const output = (r.stdout ?? "").trim();
11578
+ const errout = (r.stderr ?? "").trim();
11579
+ log(` port-check [${i}/${maxAttempts}]: exit=${r.status ?? "null"} output=${JSON.stringify(output)}${errout ? ` stderr=${JSON.stringify(errout)}` : ""}`);
11580
+ if (!output) {
11581
+ sleepSync(intervalMs);
11582
+ continue;
11583
+ }
11584
+ if (output.includes(portStr)) {
11585
+ log(` port-check: SUCCESS — port ${port} confirmed in output`);
11586
+ return {
11587
+ ok: true,
11588
+ output
11589
+ };
11590
+ }
11591
+ log(` port-check: FAILED — output non-empty but port ${port} not found`);
11592
+ return {
11593
+ ok: false,
11594
+ output
11595
+ };
11596
+ }
11597
+ log(` port-check: FAILED — no response after ${maxAttempts} attempts`);
11598
+ return { ok: false };
11599
+ }
11600
+ /**
11601
+ * 将 fixStatus 信号文件写入 /tmp/event/<fixStatus>,内容为完整的 UpgradeLarkResult JSON。
11602
+ * 外部进程(如服务端诊断流程)通过轮询此文件感知升级结果。
11603
+ * 写入失败只记录日志,不影响主流程返回值。
11604
+ */
11605
+ function writeFixStatusEvent(fixStatus, result, log) {
11606
+ const filePath = `${FIX_EVENT_DIR}/${fixStatus}`;
11607
+ try {
11608
+ node_fs.default.mkdirSync(FIX_EVENT_DIR, { recursive: true });
11609
+ node_fs.default.writeFileSync(filePath, JSON.stringify(result, null, 2));
11610
+ log(`[fix-event] written: ${filePath}`);
11611
+ } catch (e) {
11612
+ log(`[fix-event] write failed: ${filePath} — ${e.message}`);
11613
+ }
11614
+ }
11422
11615
  function runUpgradeLark(opts) {
11423
11616
  const cwd = opts.cwd ?? "/home/gem/workspace/agent";
11424
11617
  const configPath = opts.configPath ?? CONFIG_PATH;
@@ -11438,6 +11631,26 @@ function runUpgradeLark(opts) {
11438
11631
  log(` configPath : ${configPath}`);
11439
11632
  log(`${"=".repeat(60)}`);
11440
11633
  const timing = {};
11634
+ for (const s of [
11635
+ "PORT_FIX_READY",
11636
+ "PORT_FIX_FAILED",
11637
+ "CHANNEL_FIX_READY",
11638
+ "CHANNEL_FIX_FAILED"
11639
+ ]) {
11640
+ const f = `${FIX_EVENT_DIR}/${s}`;
11641
+ try {
11642
+ if (node_fs.default.existsSync(f)) {
11643
+ node_fs.default.rmSync(f);
11644
+ log(`[fix-event] cleared stale: ${f}`);
11645
+ }
11646
+ } catch (e) {
11647
+ log(`[fix-event] clear failed: ${f} — ${e.message}`);
11648
+ }
11649
+ }
11650
+ const finalReturn = (r) => {
11651
+ if (r.fixStatus !== void 0) writeFixStatusEvent(r.fixStatus, r, log);
11652
+ return r;
11653
+ };
11441
11654
  log("");
11442
11655
  log("── [Pre-check A] channels probe(升级前)────────────────");
11443
11656
  const t_preProbeStart = Date.now();
@@ -11476,14 +11689,14 @@ function runUpgradeLark(opts) {
11476
11689
  log(`${"=".repeat(60)}`);
11477
11690
  log("upgrade-lark skipped (pre-check gate)");
11478
11691
  log(`${"=".repeat(60)}`);
11479
- return {
11480
- ok: true,
11481
- skipped: true,
11692
+ return finalReturn({
11693
+ status: "skipped",
11482
11694
  skipReason: reason,
11483
11695
  upgradeNeeded: false,
11484
11696
  timing,
11485
- logFile
11486
- };
11697
+ logFile,
11698
+ fixStatus: computeFixStatus(opts.scene, "skipped")
11699
+ });
11487
11700
  }
11488
11701
  if (beforeChannels.anyAccountWorking) {
11489
11702
  const reason = "channels are working — upgrade not needed (issue detected but system is functional)";
@@ -11491,14 +11704,14 @@ function runUpgradeLark(opts) {
11491
11704
  log(`${"=".repeat(60)}`);
11492
11705
  log("upgrade-lark skipped (pre-check gate)");
11493
11706
  log(`${"=".repeat(60)}`);
11494
- return {
11495
- ok: true,
11496
- skipped: true,
11707
+ return finalReturn({
11708
+ status: "skipped",
11497
11709
  skipReason: reason,
11498
11710
  upgradeNeeded: false,
11499
11711
  timing,
11500
- logFile
11501
- };
11712
+ logFile,
11713
+ fixStatus: computeFixStatus(opts.scene, "skipped")
11714
+ });
11502
11715
  }
11503
11716
  log(` PROCEED: requiresLarkUpgrade=true (version=${versionIncompatible}, feishuConfig=${feishuConfigInvalid}) AND channels not working → running upgrade`);
11504
11717
  if (opts.checkOnly) {
@@ -11506,14 +11719,13 @@ function runUpgradeLark(opts) {
11506
11719
  log(`${"=".repeat(60)}`);
11507
11720
  log("upgrade-lark check complete");
11508
11721
  log(`${"=".repeat(60)}`);
11509
- return {
11510
- ok: true,
11511
- skipped: true,
11722
+ return finalReturn({
11723
+ status: "skipped",
11512
11724
  skipReason: "check",
11513
11725
  upgradeNeeded: true,
11514
11726
  timing,
11515
11727
  logFile
11516
- };
11728
+ });
11517
11729
  }
11518
11730
  log("");
11519
11731
  log("── [1/6] 文件备份 ────────────────────────────────────────");
@@ -11523,12 +11735,13 @@ function runUpgradeLark(opts) {
11523
11735
  timing.backupMs = Date.now() - t_backupStart;
11524
11736
  if (!backup.ok) {
11525
11737
  log(`ERROR: ${backup.error}`);
11526
- return {
11527
- ok: false,
11738
+ return finalReturn({
11739
+ status: "failed",
11528
11740
  error: backup.error,
11529
11741
  timing,
11530
- logFile
11531
- };
11742
+ logFile,
11743
+ fixStatus: computeFixStatus(opts.scene, "failed")
11744
+ });
11532
11745
  }
11533
11746
  log("backup: ok");
11534
11747
  logVersionSnapshot("before-versions", snapshotVersions(cwd, log), log);
@@ -11569,15 +11782,15 @@ function runUpgradeLark(opts) {
11569
11782
  if (statusCheckDelayMs > 0) {
11570
11783
  log("");
11571
11784
  log(`── 等待 ${statusCheckDelayMs / 1e3}s(让 openclaw 服务完成重启) ─────────────`);
11572
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, statusCheckDelayMs);
11785
+ sleepSync(statusCheckDelayMs);
11573
11786
  log("wait done");
11574
11787
  }
11575
11788
  const doRollback = (reason) => {
11576
11789
  log(`ERROR: ${reason}`);
11577
11790
  const rollbackOk = restoreFiles(fsOpts);
11578
11791
  log(`rollback: ${rollbackOk ? "ok" : "FAILED"}`);
11579
- return {
11580
- ok: false,
11792
+ return finalReturn({
11793
+ status: "failed",
11581
11794
  error: reason,
11582
11795
  validationError: reason,
11583
11796
  stdout: npxStdout,
@@ -11585,8 +11798,9 @@ function runUpgradeLark(opts) {
11585
11798
  exitCode: npxExitCode,
11586
11799
  rollbackOk,
11587
11800
  timing,
11588
- logFile
11589
- };
11801
+ logFile,
11802
+ fixStatus: computeFixStatus(opts.scene, "failed")
11803
+ });
11590
11804
  };
11591
11805
  log("");
11592
11806
  log("── [4/5] 安装后诊断校验 ─────────────────────────────────");
@@ -11612,7 +11826,7 @@ function runUpgradeLark(opts) {
11612
11826
  timing.postProbeMs = Date.now() - t_postProbeStart;
11613
11827
  log(` feishu config invalid after: ${afterChannels.feishuConfigInvalid}`);
11614
11828
  const stillNeedsUpgrade = (afterVersionIncompatible || afterChannels.feishuConfigInvalid) && !afterChannels.anyAccountWorking;
11615
- const isNewDefaultOnly = !beforeChannels.anyAccountWorking && isDefaultOnlyState(afterChannels) && !afterVersionIncompatible;
11829
+ const isNewDefaultOnly = !afterVersionIncompatible && !afterChannels.feishuConfigInvalid && !beforeChannels.anyAccountWorking && isDefaultOnlyState(afterChannels);
11616
11830
  log(` post-check: stillNeedsUpgrade=${stillNeedsUpgrade} (version=${afterVersionIncompatible}, feishuConfig=${afterChannels.feishuConfigInvalid}, channelsWorking=${afterChannels.anyAccountWorking}) isNewDefaultOnly=${isNewDefaultOnly}`);
11617
11831
  if (stillNeedsUpgrade && !isNewDefaultOnly) return doRollback(`post-install diagnosis still shows anomaly: versionIncompatible=${afterVersionIncompatible}, feishuConfigInvalid=${afterChannels.feishuConfigInvalid}, anyAccountWorking=${afterChannels.anyAccountWorking}`);
11618
11832
  if (isNewDefaultOnly) log(" post-install diagnosis: ok (new default account — plugin installed, awaiting configuration)");
@@ -11638,8 +11852,9 @@ function runUpgradeLark(opts) {
11638
11852
  if (fixResult.stderr?.trim()) log(`doctor(fix) stderr:\n${fixResult.stderr.trim()}`);
11639
11853
  log(`doctor(fix) exit: ${fixResult.status ?? "null"}${fixResult.error ? ` error: ${fixResult.error.message}` : ""}`);
11640
11854
  log("");
11641
- log("── [7/7] 重启 openclaw 服务 ──────────────────────────────");
11855
+ log("── [7/8] 重启 openclaw 服务 ──────────────────────────────");
11642
11856
  const restartScript = "/opt/force/bin/openclaw_scripts/restart.sh";
11857
+ let restartExecuted = false;
11643
11858
  if (opts.skipRestart) log(" skipped: --skip-restart");
11644
11859
  else if (node_fs.default.existsSync(restartScript)) {
11645
11860
  const t_restart = Date.now();
@@ -11656,24 +11871,133 @@ function runUpgradeLark(opts) {
11656
11871
  if (restartResult.stdout?.trim()) log(` restart stdout:\n${restartResult.stdout.trim()}`);
11657
11872
  if (restartResult.stderr?.trim()) log(` restart stderr:\n${restartResult.stderr.trim()}`);
11658
11873
  log(` restart.sh exit: ${restartResult.status ?? "null"} (${restartMs}ms)${restartResult.error ? ` error: ${restartResult.error.message}` : ""}`);
11874
+ restartExecuted = true;
11659
11875
  } else log(` skipped: ${restartScript} not found`);
11660
11876
  log("");
11877
+ log("── [8/8] 端口存活检测 ────────────────────────────────────");
11878
+ let portCheckOk;
11879
+ if (!restartExecuted) log(" skipped: restart was not executed");
11880
+ else {
11881
+ const t_portCheck = Date.now();
11882
+ const pcResult = pollPortCheck({
11883
+ port: 18789,
11884
+ initialWaitMs: opts.portCheckInitialWaitMs ?? 3e3,
11885
+ intervalMs: opts.portCheckIntervalMs ?? 1e3,
11886
+ maxAttempts: opts.portCheckMaxAttempts ?? 30,
11887
+ log
11888
+ });
11889
+ timing.portCheckMs = Date.now() - t_portCheck;
11890
+ portCheckOk = pcResult.ok;
11891
+ log(` port-check result: ${portCheckOk ? "ok" : "FAILED"} (${timing.portCheckMs}ms)`);
11892
+ }
11893
+ log("");
11661
11894
  log(`${"=".repeat(60)}`);
11662
11895
  log("upgrade-lark completed successfully");
11663
11896
  log(`${"=".repeat(60)}`);
11664
- return {
11665
- ok: true,
11897
+ return finalReturn({
11898
+ status: "success",
11666
11899
  stdout: npxStdout,
11667
11900
  stderr: npxStderr,
11668
11901
  exitCode: npxExitCode,
11902
+ portCheckOk,
11669
11903
  timing,
11670
- logFile
11904
+ logFile,
11905
+ fixStatus: computeFixStatus(opts.scene, "success", portCheckOk)
11906
+ });
11907
+ }
11908
+ //#endregion
11909
+ //#region src/install-lock.ts
11910
+ /**
11911
+ * 检查指定 PID 是否仍在运行(向进程发 signal 0,不影响其运行状态)。
11912
+ * 跨平台:Linux/macOS 均支持;Windows 不在目标平台范围内。
11913
+ */
11914
+ function isPidAlive(pid) {
11915
+ try {
11916
+ process.kill(pid, 0);
11917
+ return true;
11918
+ } catch (e) {
11919
+ if (e.code === "EPERM") return true;
11920
+ return false;
11921
+ }
11922
+ }
11923
+ /**
11924
+ * 尝试获取安装操作互斥锁。
11925
+ *
11926
+ * 利用 open(O_CREAT | O_EXCL) 的 POSIX 原子性确保同一时刻只有一个安装指令运行。
11927
+ * 覆盖的安装指令:upgrade-lark / install-openclaw / install-extension / install-cli / reset --worker。
11928
+ *
11929
+ * 返回 null 表示成功获取锁;返回错误信息字符串表示锁被其他进程持有。
11930
+ * 与抛异常相比,返回值让调用方可以输出符合各自格式的 result,而不依赖 catch handler 统一处理。
11931
+ *
11932
+ * 成功获取后自动注册 process.once('exit') 释放锁,无需调用方手动清理。
11933
+ */
11934
+ function acquireInstallLock(command) {
11935
+ node_fs.default.mkdirSync(DIAGNOSE_DIR, { recursive: true });
11936
+ const info = {
11937
+ pid: process.pid,
11938
+ command,
11939
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
11671
11940
  };
11941
+ const content = JSON.stringify(info);
11942
+ while (true) try {
11943
+ 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);
11944
+ node_fs.default.writeSync(fd, content);
11945
+ node_fs.default.closeSync(fd);
11946
+ process.once("exit", releaseInstallLock);
11947
+ console.error(`[install-lock] acquired command=${command} pid=${process.pid}`);
11948
+ return null;
11949
+ } catch (e) {
11950
+ if (e.code !== "EEXIST") throw e;
11951
+ let existing = null;
11952
+ try {
11953
+ existing = JSON.parse(node_fs.default.readFileSync(INSTALL_LOCK_FILE, "utf-8"));
11954
+ } catch {
11955
+ try {
11956
+ node_fs.default.rmSync(INSTALL_LOCK_FILE, { force: true });
11957
+ } catch {}
11958
+ continue;
11959
+ }
11960
+ if (!isPidAlive(existing.pid)) {
11961
+ console.error(`[install-lock] stale lock detected (pid=${existing.pid} command=${existing.command}), removing`);
11962
+ try {
11963
+ node_fs.default.rmSync(INSTALL_LOCK_FILE, { force: true });
11964
+ } catch {}
11965
+ continue;
11966
+ }
11967
+ const conflictMsg = `另一个安装指令正在运行,请等待其完成后重试。\n 占用指令: ${existing.command}\n 进程 PID : ${existing.pid}\n 开始时间: ${existing.startedAt}\n 锁文件 : ${INSTALL_LOCK_FILE}`;
11968
+ console.error(`[install-lock] conflict: command=${command} blocked by pid=${existing.pid} command=${existing.command}`);
11969
+ return conflictMsg;
11970
+ }
11971
+ }
11972
+ /**
11973
+ * 释放安装操作互斥锁。
11974
+ * 仅删除由本进程创建的锁文件,避免误删其他进程的锁。
11975
+ * 通常由 process.once('exit') 自动调用,不需要手动调用。
11976
+ */
11977
+ function releaseInstallLock() {
11978
+ try {
11979
+ const raw = node_fs.default.readFileSync(INSTALL_LOCK_FILE, "utf-8");
11980
+ const info = JSON.parse(raw);
11981
+ if (info.pid === process.pid) {
11982
+ node_fs.default.rmSync(INSTALL_LOCK_FILE, { force: true });
11983
+ console.error(`[install-lock] released command=${info.command} pid=${process.pid}`);
11984
+ }
11985
+ } catch {}
11672
11986
  }
11673
11987
  //#endregion
11674
11988
  //#region src/index.ts
11989
+ /** 锁冲突时各 install 命令的统一处理:输出 { ok: false } 并退出 */
11990
+ function handleInstallLockConflict(lockErr) {
11991
+ console.log(JSON.stringify({
11992
+ ok: false,
11993
+ error: lockErr
11994
+ }));
11995
+ node_process.default.exit(1);
11996
+ }
11675
11997
  const args = node_process.default.argv.slice(2);
11676
11998
  const mode = args.find((a) => !a.startsWith("-"));
11999
+ const t0 = Date.now();
12000
+ const scene = getFlag(args, "scene");
11677
12001
  /**
11678
12002
  * Pull the first non-flag positional after the mode name.
11679
12003
  * (The mode itself is args[0] in the filtered set, so we skip index 0.)
@@ -11757,13 +12081,11 @@ async function main() {
11757
12081
  }
11758
12082
  const caller = getFlag(args, "caller");
11759
12083
  const traceId = getFlag(args, "trace-id");
11760
- const scene = getFlag(args, "scene");
11761
12084
  const profile = getFlag(args, "profile") === "experimental" ? "experimental" : "standard";
11762
12085
  const rc = setRunContext({
11763
12086
  caller,
11764
12087
  traceId
11765
12088
  });
11766
- const t0 = Date.now();
11767
12089
  console.error(`${mode}: begin argv=[${args.join(" ")}] version=${getVersion()} traceId=${traceId ?? "-"} caller=${caller ?? "-"} runIdGenerated=${rc.generated}`);
11768
12090
  switch (mode) {
11769
12091
  case "check": {
@@ -11871,6 +12193,17 @@ async function main() {
11871
12193
  node_process.default.exit(1);
11872
12194
  }
11873
12195
  const resultFile = resetResultFile(taskId);
12196
+ const resetLockErr = acquireInstallLock("reset");
12197
+ if (resetLockErr) {
12198
+ try {
12199
+ node_fs.default.writeFileSync(resultFile, JSON.stringify({
12200
+ status: "failed",
12201
+ error: resetLockErr
12202
+ }));
12203
+ } catch {}
12204
+ node_process.default.exitCode = 1;
12205
+ return;
12206
+ }
11874
12207
  const raw = await fetchCtxViaInnerApi({
11875
12208
  populate: planCtxPopulate({ command: "reset" }),
11876
12209
  caller,
@@ -11914,6 +12247,8 @@ async function main() {
11914
12247
  console.error("Usage: install-openclaw <tag> [--oss_file_map=<base64>]");
11915
12248
  node_process.default.exit(1);
11916
12249
  }
12250
+ const installOcLockErr = acquireInstallLock("install-openclaw");
12251
+ if (installOcLockErr) handleInstallLockConflict(installOcLockErr);
11917
12252
  const ossFileMapFlag = getFlag(args, "oss_file_map");
11918
12253
  let installOssFileMap;
11919
12254
  let rawForTelemetry;
@@ -11956,6 +12291,8 @@ async function main() {
11956
12291
  console.error("Usage: install-extension <tag> (--all | --extension=<name>...) [--home_base=<dir>] [--config_path=<path>] [--skip-config-update] [--oss_file_map=<base64>]");
11957
12292
  node_process.default.exit(1);
11958
12293
  }
12294
+ const installExtLockErr = acquireInstallLock("install-extension");
12295
+ if (installExtLockErr) handleInstallLockConflict(installExtLockErr);
11959
12296
  const all = args.includes("--all");
11960
12297
  const names = getMultiFlag(args, "extension");
11961
12298
  const homeBase = getFlag(args, "home_base");
@@ -12019,6 +12356,8 @@ async function main() {
12019
12356
  console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--oss_file_map=<base64>]");
12020
12357
  node_process.default.exit(1);
12021
12358
  }
12359
+ const installCliLockErr = acquireInstallLock("install-cli");
12360
+ if (installCliLockErr) handleInstallLockConflict(installCliLockErr);
12022
12361
  const homeBase = getFlag(args, "home_base");
12023
12362
  const ossFileMapFlag = getFlag(args, "oss_file_map");
12024
12363
  let installOssFileMap;
@@ -12162,6 +12501,32 @@ async function main() {
12162
12501
  }
12163
12502
  case "upgrade-lark": {
12164
12503
  const checkOnly = args.includes("--check");
12504
+ if (!checkOnly) {
12505
+ const upgradeLockErr = acquireInstallLock("upgrade-lark");
12506
+ if (upgradeLockErr) {
12507
+ const fixStatus = computeFixStatus(scene, "failed");
12508
+ const failResult = {
12509
+ status: "failed",
12510
+ error: upgradeLockErr,
12511
+ logFile: "",
12512
+ fixStatus
12513
+ };
12514
+ if (fixStatus !== void 0) writeFixStatusEvent(fixStatus, failResult, console.error);
12515
+ console.log(JSON.stringify(failResult));
12516
+ reportUpgradeLarkToSlardar({
12517
+ scene,
12518
+ checkOnly,
12519
+ durationMs: Date.now() - t0,
12520
+ resultStatus: "failed",
12521
+ error: upgradeLockErr,
12522
+ logFile: "",
12523
+ fixStatus,
12524
+ resultSummary: buildUpgradeLarkResultSummary(failResult, checkOnly)
12525
+ });
12526
+ node_process.default.exitCode = 1;
12527
+ return;
12528
+ }
12529
+ }
12165
12530
  const skipRestart = args.includes("--skip-restart");
12166
12531
  const result = runUpgradeLark({
12167
12532
  runId: rc.runId,
@@ -12175,14 +12540,17 @@ async function main() {
12175
12540
  scene,
12176
12541
  checkOnly,
12177
12542
  durationMs: upgradeDurationMs,
12178
- success: result.ok,
12179
- skipped: result.skipped,
12543
+ resultStatus: result.status,
12544
+ upgradeNeeded: result.upgradeNeeded,
12180
12545
  skipReason: result.skipReason,
12181
12546
  logFile: result.logFile,
12547
+ resultSummary: buildUpgradeLarkResultSummary(result, checkOnly),
12182
12548
  exitCode: result.exitCode,
12183
12549
  rollbackOk: result.rollbackOk,
12184
12550
  validationError: result.validationError,
12185
12551
  error: result.error,
12552
+ portCheckOk: result.portCheckOk,
12553
+ fixStatus: result.fixStatus,
12186
12554
  timing: result.timing
12187
12555
  });
12188
12556
  try {
@@ -12194,14 +12562,14 @@ async function main() {
12194
12562
  durationMs: upgradeDurationMs,
12195
12563
  caller: rc.caller,
12196
12564
  traceId: rc.traceId,
12197
- success: result.ok,
12565
+ success: result.status !== "failed",
12198
12566
  result,
12199
- error: result.ok ? void 0 : { message: result.error ?? "upgrade-lark failed" }
12567
+ error: result.status === "failed" ? { message: result.error ?? "upgrade-lark failed" } : void 0
12200
12568
  });
12201
12569
  } catch (e) {
12202
12570
  console.error(`[telemetry] reportCliRun failed: ${e.message}`);
12203
12571
  }
12204
- if (!result.ok || checkOnly && result.upgradeNeeded) {
12572
+ if (result.status === "failed" || checkOnly && result.upgradeNeeded) {
12205
12573
  node_process.default.exitCode = 1;
12206
12574
  return;
12207
12575
  }
@@ -12225,6 +12593,48 @@ main().catch((err) => {
12225
12593
  const msg = err instanceof Error ? err.message : String(err);
12226
12594
  console.error(`${mode ?? "<no-mode>"}: error message=${msg}`);
12227
12595
  node_process.default.stderr.write(`Error: ${msg}\n`);
12596
+ if (mode) {
12597
+ const durationMs = Date.now() - t0;
12598
+ if (mode === "upgrade-lark") {
12599
+ const checkOnly = args.includes("--check");
12600
+ const fixStatus = computeFixStatus(scene, "failed");
12601
+ const failResult = {
12602
+ status: "failed",
12603
+ error: msg,
12604
+ logFile: "",
12605
+ fixStatus
12606
+ };
12607
+ if (fixStatus !== void 0) writeFixStatusEvent(fixStatus, failResult, console.error);
12608
+ console.log(JSON.stringify(failResult));
12609
+ reportUpgradeLarkToSlardar({
12610
+ scene,
12611
+ checkOnly,
12612
+ durationMs,
12613
+ resultStatus: "failed",
12614
+ error: msg,
12615
+ logFile: "",
12616
+ fixStatus,
12617
+ resultSummary: buildUpgradeLarkResultSummary(failResult, checkOnly)
12618
+ });
12619
+ } else if ([
12620
+ "doctor",
12621
+ "check",
12622
+ "repair",
12623
+ "install-openclaw",
12624
+ "install-extension",
12625
+ "install-cli",
12626
+ "download-resource",
12627
+ "reset",
12628
+ "lark-cli-init"
12629
+ ].includes(mode)) reportDoctorRunToSlardar({
12630
+ command: mode,
12631
+ scene,
12632
+ profile: getFlag(args, "profile") === "experimental" ? "experimental" : "standard",
12633
+ fix: args.includes("--fix"),
12634
+ durationMs,
12635
+ success: false
12636
+ });
12637
+ }
12228
12638
  node_process.default.exitCode = 1;
12229
12639
  });
12230
12640
  //#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.16-alpha.0",
4
4
  "description": "CLI for OpenClaw config diagnose and repair with JSON5 support",
5
5
  "main": "dist/index.cjs",
6
6
  "bin": {