@coclaw/openclaw-coclaw 0.26.2 → 0.26.3

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.
@@ -2,21 +2,26 @@
2
2
  * worker.js — 由 updater-spawn 以 detached 进程启动
3
3
  *
4
4
  * 用法:node worker.js --pluginDir <dir> --fromVersion <ver> --toVersion <ver>
5
- * --pluginId <id> --pkgName <name>
5
+ * --pluginId <id> --pkgName <name> [--baselineVersion <ver>]
6
6
  *
7
- * 流程:备份 → openclaw plugins update 等待 gateway 重启验证 成功清理/失败回滚
7
+ * 流程:备份 → inflightopenclaw plugins updateL2 结局核对(inspect 安装记录)
8
+ * → 等待 gateway 重启 → 验证 → 成功清理/失败回滚/未推进 no-op 跳过
9
+ * → 终态记账(recordUpgradeTerminal 原子写 lastUpgrade + 清 inflight)
8
10
  *
9
11
  * 注意:
10
12
  * - 本模块作为独立 node 进程运行,与 gateway 进程隔离
11
13
  * - state dir 通过 OPENCLAW_STATE_DIR 环境变量由 spawner 传入
12
14
  * - shell 仅在 Windows 启用(openclaw 全局安装生成 .cmd 包装器,需 shell 解析)
15
+ * - 所有 state 写入非致命:独立 try/catch + 日志,失败继续流程(账目兜底交
16
+ * scheduler 的 inflight 对账);worker 是独立子进程,禁 remoteLog
13
17
  */
14
18
 
15
19
  import { execFile as nodeExecFile } from 'node:child_process';
16
20
  import { parseArgs } from 'node:util';
17
21
  import { createBackup, restoreFromBackup, removeBackup } from './worker-backup.js';
18
- import { verifyUpgrade, triggerGatewayRestart } from './worker-verify.js';
19
- import { addSkippedVersion, updateLastUpgrade, appendLog } from './state.js';
22
+ import { verifyUpgrade, triggerGatewayRestart, isVersionReached } from './worker-verify.js';
23
+ import { inspectPluginInstall } from './updater-check.js';
24
+ import { appendLog, recordUpgradeTerminal, updateInflight, writeInflight } from './state.js';
20
25
  import { getCurrentNpmRegistry, pickFallbackRegistry } from './registry-fallback.js';
21
26
 
22
27
  const SEMVER_RE = /^\d+\.\d+\.\d+(-[\w.-]+)?$/;
@@ -25,21 +30,55 @@ const UPDATE_TIMEOUT_MS = 10 * 60 * 1000;
25
30
  // 回滚兜底重装旧版本走的是同一条 npm 下载链路,且触发前置本身是"备份已丢"的异常态,
26
31
  // 此时尽量兜住比快速失败更重要,与 UPDATE_TIMEOUT_MS 对齐
27
32
  const FALLBACK_INSTALL_TIMEOUT_MS = 10 * 60 * 1000;
28
- const FALLBACK_UNINSTALL_TIMEOUT_MS = 60 * 1000;
33
+ // 子命令双流各取尾部上限:真因(prerelease 拒装等)通常在输出尾部
34
+ const CMD_OUTPUT_TAIL_CHARS = 500;
29
35
 
30
36
  /**
31
- * 执行 openclaw plugins update
37
+ * 把子命令失败的真因拼进错误消息。
38
+ * 上游错误 outcome 走 console.log(stdout)而 execFile err.message 只附 stderr,
39
+ * 必须双流都收。先脱敏再截尾:截断可能把凭据切半导致正则漏匹配。
40
+ * @param {string} prefix - 错误前缀(如 "plugins update failed")
41
+ * @param {Error} err - execFile 回调的 err
42
+ * @param {string|Buffer} stdout
43
+ * @param {string|Buffer} stderr
44
+ * @returns {string}
45
+ */
46
+ export function formatCmdFailure(prefix, err, stdout, stderr) {
47
+ const scrub = (s) => String(s ?? '')
48
+ // registry URL 的 userinfo(https://user:pass@host)
49
+ .replace(/:\/\/[^@\s/]+@/g, '://***@')
50
+ // .npmrc 风格 token(//host/:_authToken=xxx)
51
+ .replace(/(_authToken\s*=\s*)\S+/gi, '$1***');
52
+ const tail = (s) => {
53
+ const t = s.trim();
54
+ return t.length > CMD_OUTPUT_TAIL_CHARS ? t.slice(-CMD_OUTPUT_TAIL_CHARS) : t;
55
+ };
56
+ /* c8 ignore next -- ?? fallback */
57
+ const parts = [`${prefix}: ${tail(scrub(err?.message ?? String(err)))}`];
58
+ const out = tail(scrub(stdout));
59
+ if (out) parts.push(`stdout: ${out}`);
60
+ const errOut = tail(scrub(stderr));
61
+ if (errOut) parts.push(`stderr: ${errOut}`);
62
+ return parts.join(' | ');
63
+ }
64
+
65
+ /**
66
+ * 执行 openclaw plugins update(裸 npm 包名)
32
67
  *
33
68
  * 仅支持 source === "npm" 的安装(updater 已做前置过滤)。
69
+ * 用裸包名而非插件 id:裸包名 update 走包名匹配产生 specOverride,成功后把
70
+ * 安装记录的 spec 重写为裸名——一次 update 同时"装 latest + 解钉"。否则
71
+ * fallback install 留下的 `pkg@x.y.z` 精确 spec 会让 update 永远 resolve
72
+ * 同一版本,自动升级被永久钉死。
34
73
  * env 由调用方决定:缺省时子进程继承当前 process.env(含用户 .npmrc 自动生效);
35
74
  * 显式传入时用于覆盖 registry 等 npm 配置以做兜底重试。
36
- * @param {string} pluginId - 插件 ID
75
+ * @param {string} pkgName - npm 包名
37
76
  * @param {object} [opts]
38
77
  * @param {Function} [opts.execFileFn]
39
78
  * @param {NodeJS.ProcessEnv} [opts.env]
40
79
  * @returns {Promise<void>}
41
80
  */
42
- function runPluginUpdate(pluginId, opts) {
81
+ function runPluginUpdate(pkgName, opts) {
43
82
  /* c8 ignore next -- ?./?? fallback */
44
83
  const doExecFile = opts?.execFileFn ?? nodeExecFile;
45
84
  return new Promise((resolve, reject) => {
@@ -49,53 +88,43 @@ function runPluginUpdate(pluginId, opts) {
49
88
  };
50
89
  // 不传 env 时让 Node 默认继承父进程;显式 env 才覆盖
51
90
  if (opts?.env) execOpts.env = opts.env;
52
- doExecFile('openclaw', ['plugins', 'update', pluginId], execOpts, (err) => {
53
- if (err) reject(new Error(`plugins update failed: ${err.message}`));
91
+ doExecFile('openclaw', ['plugins', 'update', pkgName], execOpts, (err, stdout, stderr) => {
92
+ if (err) reject(new Error(formatCmdFailure('plugins update failed', err, stdout, stderr)));
54
93
  else resolve();
55
94
  });
56
95
  });
57
96
  }
58
97
 
59
98
  /**
60
- * 尝试通过 npm 安装旧版本进行兜底回滚
61
- *
62
- * openclaw plugins install 不支持覆盖已安装插件,因此需先 uninstall
63
- * uninstall 失败不阻断流程(插件可能已处于异常状态)。
99
+ * 尝试通过 npm 安装旧版本进行兜底回滚。
100
+ * 单命令 `plugins install <pkg>@<ver> --force`:--force 即覆盖装(上游映射
101
+ * mode=update,绕开 "already exists" 拒绝),不再需要 uninstall 前置——
102
+ * 旧的 uninstall 在非 TTY 下要求交互确认必失败且错误被静默吞。
64
103
  *
65
104
  * @param {string} pkgName - npm 包名
66
105
  * @param {string} version
67
- * @param {string} pluginId - 插件 ID
68
106
  * @param {object} [opts]
69
107
  * @param {Function} [opts.execFileFn]
70
108
  * @returns {Promise<void>}
71
109
  */
72
- // 回滚兜底:当物理备份恢复失败时,尝试从 npm 重新安装旧版本
73
- async function fallbackInstallOldVersion(pkgName, version, pluginId, opts) {
110
+ async function fallbackInstallOldVersion(pkgName, version, opts) {
74
111
  // version 来自 package.json,正常不会有异常值,但 shell: true 下做防御校验
75
112
  if (!SEMVER_RE.test(version)) {
76
113
  throw new Error(`invalid version format: ${version}`);
77
114
  }
78
115
  /* c8 ignore next -- ?./?? fallback */
79
116
  const doExecFile = opts?.execFileFn ?? nodeExecFile;
80
- const run = (args, timeout) => new Promise((resolve, reject) => {
81
- doExecFile('openclaw', args, { timeout, shell: process.platform === 'win32' }, (err) => {
82
- if (err) reject(err);
83
- else resolve();
84
- });
117
+ return new Promise((resolve, reject) => {
118
+ doExecFile(
119
+ 'openclaw',
120
+ ['plugins', 'install', `${pkgName}@${version}`, '--force'],
121
+ { timeout: FALLBACK_INSTALL_TIMEOUT_MS, shell: process.platform === 'win32' },
122
+ (err, stdout, stderr) => {
123
+ if (err) reject(new Error(formatCmdFailure('fallback install failed', err, stdout, stderr)));
124
+ else resolve();
125
+ },
126
+ );
85
127
  });
86
-
87
- // 先卸载:install 不支持覆盖已安装插件
88
- try {
89
- await run(['plugins', 'uninstall', pluginId], FALLBACK_UNINSTALL_TIMEOUT_MS);
90
- } catch {
91
- // uninstall 失败不阻断,继续尝试 install
92
- }
93
-
94
- try {
95
- await run(['plugins', 'install', `${pkgName}@${version}`], FALLBACK_INSTALL_TIMEOUT_MS);
96
- } catch (err) {
97
- throw new Error(`fallback install failed: ${err.message}`);
98
- }
99
128
  }
100
129
 
101
130
  /**
@@ -104,6 +133,7 @@ async function fallbackInstallOldVersion(pkgName, version, pluginId, opts) {
104
133
  * @param {string} params.pluginDir - 插件安装目录
105
134
  * @param {string} params.fromVersion - 当前版本
106
135
  * @param {string} params.toVersion - 目标版本
136
+ * @param {string} [params.baselineVersion] - 升级前权威安装记录的版本(L2 基线;可缺)
107
137
  * @param {string} params.pluginId - 插件 ID
108
138
  * @param {string} params.pkgName - npm 包名
109
139
  * @param {object} [params.opts] - 测试注入选项
@@ -112,7 +142,7 @@ async function fallbackInstallOldVersion(pkgName, version, pluginId, opts) {
112
142
  * @param {number} [params.opts.pollIntervalMs]
113
143
  * @param {Function} [params.logger] - 日志函数
114
144
  */
115
- export async function runUpgrade({ pluginDir, fromVersion, toVersion, pluginId, pkgName, opts, logger }) {
145
+ export async function runUpgrade({ pluginDir, fromVersion, toVersion, baselineVersion, pluginId, pkgName, opts, logger }) {
116
146
  const log = logger ?? console.log;
117
147
 
118
148
  log(`[upgrade-worker] Starting upgrade: ${fromVersion} → ${toVersion}`);
@@ -120,14 +150,23 @@ export async function runUpgrade({ pluginDir, fromVersion, toVersion, pluginId,
120
150
 
121
151
  // 1. 备份
122
152
  log('[upgrade-worker] Creating backup...');
123
- await createBackup(pluginDir);
153
+ await createBackup(pluginDir, pluginId);
124
154
  log('[upgrade-worker] Backup created');
125
155
 
156
+ // 进入改盘阶段前写 inflight:worker 若没活到终态记账(典型:被自己触发的
157
+ // 网关重启杀死),scheduler 下轮锁空闲时据此对账补记终态
158
+ try {
159
+ await writeInflight({ from: fromVersion, to: toVersion, verifyTarget: toVersion, pluginDir, phase: 'update' });
160
+ }
161
+ catch (e) {
162
+ log(`[upgrade-worker] Failed to write inflight marker (non-fatal): ${e.message}`);
163
+ }
164
+
126
165
  // 2. 执行升级(首次按用户原 env,失败后用反向 mirror 重试一次)
127
166
  log('[upgrade-worker] Running plugins update...');
128
167
  let updateErr = null;
129
168
  try {
130
- await runPluginUpdate(pluginId, opts);
169
+ await runPluginUpdate(pkgName, opts);
131
170
  log('[upgrade-worker] Update command completed');
132
171
  }
133
172
  catch (firstErr) {
@@ -143,7 +182,7 @@ export async function runUpgrade({ pluginDir, fromVersion, toVersion, pluginId,
143
182
  const retryEnv = { ...process.env };
144
183
  delete retryEnv.NPM_CONFIG_REGISTRY;
145
184
  retryEnv.npm_config_registry = fallback;
146
- await runPluginUpdate(pluginId, { ...opts, env: retryEnv });
185
+ await runPluginUpdate(pkgName, { ...opts, env: retryEnv });
147
186
  log('[upgrade-worker] Update command completed on retry');
148
187
  updateErr = null;
149
188
  }
@@ -162,24 +201,106 @@ export async function runUpgrade({ pluginDir, fromVersion, toVersion, pluginId,
162
201
  return;
163
202
  }
164
203
 
165
- // 3. 等待 gateway 重启并验证
204
+ // 3. L2 结局核对:update exit 0 不代表真升级(老 host 出错也 exit 0、path/缺记录干净
205
+ // skip 也 exit 0、registry 假成功、latest-compatible 封顶)。经权威 inspect 读升级后
206
+ // 安装记录分流结局;stdout 文本无契约承诺,仅记日志不作判据。
207
+ // worker 是独立子进程、无 bridge 连接,禁 remoteLog——诊断只写本地日志,
208
+ // 结局经 lastUpgrade 接 scheduler 下轮上报链。
209
+ log('[upgrade-worker] Inspecting install record (post-update)...');
210
+ // inspectPluginInstall 契约是永不抛,但注入实现可能同步抛错;裸抛会 fatal exit 1
211
+ // (留备份、无状态记录),故归一化为 inspect 自身失败,走下方保守分支
212
+ let inspected;
213
+ try {
214
+ inspected = await inspectPluginInstall(pluginId, opts);
215
+ }
216
+ catch (err) {
217
+ /* c8 ignore next -- ?? fallback:err 字段缺省的兜底分支不强制覆盖 */
218
+ const msg = err?.message ?? String(err);
219
+ inspected = { ok: false, reason: `inspect threw: ${msg}` };
220
+ }
221
+
222
+ let verifyTarget = toVersion;
223
+ // record 推进但未达标:按实装版本验证健康,成功后对 toVersion 记跳过(已知到不了)
224
+ let advancedShortfall = false;
225
+ if (inspected.ok && typeof inspected.install?.version === 'string') {
226
+ const recordVersion = inspected.install.version;
227
+ if (isVersionReached(recordVersion, toVersion)) {
228
+ // 达标(等于或更新,覆盖 dist-tag 前移):真升级,走现行 restart + 健康轮询流
229
+ log(`[upgrade-worker] Install record reached target: ${recordVersion}`);
230
+ }
231
+ else if (!baselineVersion) {
232
+ // 基线不可得:无从判断 record 是否推进,退化为现行流(restart + verify(toVersion))
233
+ log(`[upgrade-worker] Record version ${recordVersion} below target but baseline unknown; proceeding with standard verify`);
234
+ }
235
+ else if (recordVersion === baselineVersion) {
236
+ // record 未推进:update 干净 skip / registry 假成功——磁盘什么都没变。
237
+ // no-op:不重启、不回滚,删备份,立即记 skipVersion 停止每小时空转重试
238
+ // (瞬时故障在新 host 上走 exit≠0 原路径,不会落到这里被永久跳过)
239
+ log(`[upgrade-worker] Install record did not advance (still ${recordVersion}); no-op skip for ${toVersion}`);
240
+ try { await removeBackup(pluginId); }
241
+ catch (e) { log(`[upgrade-worker] Backup cleanup failed (non-fatal): ${e.message}`); }
242
+ try {
243
+ await recordUpgradeTerminal({
244
+ from: fromVersion, to: toVersion, result: 'noop-skip', skipVersion: toVersion,
245
+ });
246
+ }
247
+ /* c8 ignore next -- 状态写入 catch:测试中 stub 不会失败 */
248
+ catch (e) { log(`[upgrade-worker] Failed to record terminal state (non-fatal): ${e.message}`); }
249
+ log(`[upgrade-worker] No-op skip complete. Version ${toVersion} added to skipped list`);
250
+ return;
251
+ }
252
+ else {
253
+ // record 推进但未达标(latest-compatible 封顶等):磁盘确已换代,
254
+ // 必须健康验证实装版本,避免未经验证的新副本在下次自然重启时静默激活
255
+ log(`[upgrade-worker] Install record advanced to ${recordVersion} (target ${toVersion} not reached); verifying actual version`);
256
+ verifyTarget = recordVersion;
257
+ advancedShortfall = true;
258
+ }
259
+ }
260
+ else {
261
+ // inspect 自身失败 / 记录缺版本:保守按"真升级"处理,进现行 restart + verify
262
+ //(健康检查 + 回滚兜底),避免工具故障静默压制激活
263
+ const reason = inspected.ok ? 'install record missing version' : inspected.reason;
264
+ log(`[upgrade-worker] Post-update inspect unavailable (${reason}); proceeding with standard verify`);
265
+ }
266
+
267
+ // 4. 等待 gateway 重启并验证;phase/verifyTarget 推进进 inflight,
268
+ // 中断时 scheduler 对账据 verifyTarget 判定成败(advancedShortfall 语义同步)
269
+ try {
270
+ await updateInflight({ phase: 'verify', verifyTarget });
271
+ }
272
+ catch (e) {
273
+ log(`[upgrade-worker] Failed to update inflight marker (non-fatal): ${e.message}`);
274
+ }
166
275
  log('[upgrade-worker] Verifying upgrade...');
167
- const result = await verifyUpgrade(pluginDir, toVersion, opts, log);
276
+ const result = await verifyUpgrade(pluginDir, verifyTarget, opts, log);
168
277
 
169
278
  if (result.ok) {
170
279
  // 4a. 成功
171
280
  log(`[upgrade-worker] Upgrade verified. Version: ${result.version}`);
172
281
  try {
173
- await removeBackup(pluginDir);
282
+ await removeBackup(pluginId);
174
283
  }
175
284
  catch (e) {
176
285
  log(`[upgrade-worker] Backup cleanup failed (non-fatal): ${e.message}`);
177
286
  }
178
287
  // 记录真实装上的版本而非目标版本——dist-tag 前移窗口下两者可能不同。
179
288
  // 不加 fallback:若 result.ok 时 version 缺失,说明上游契约被破坏,
180
- // 宁可让状态里直接暴露 undefined 便于排障,也不要用 toVersion 糊过去
181
- await updateLastUpgrade({ from: fromVersion, to: result.version, result: 'ok' });
182
- await appendLog({ from: fromVersion, to: result.version, result: 'ok' });
289
+ // 宁可让状态里直接暴露 undefined 便于排障,也不要用 toVersion 糊过去。
290
+ // advancedShortfall:实装版本健康但 toVersion 在本 host 装不上 记跳过,
291
+ // 停止注定不达标的重试
292
+ try {
293
+ await recordUpgradeTerminal({
294
+ from: fromVersion, to: result.version, result: 'ok',
295
+ skipVersion: advancedShortfall ? toVersion : undefined,
296
+ });
297
+ }
298
+ catch (e) {
299
+ log(`[upgrade-worker] Failed to record terminal state (non-fatal): ${e.message}`);
300
+ }
301
+ if (advancedShortfall) {
302
+ log(`[upgrade-worker] Version ${toVersion} added to skipped list (host capped below target)`);
303
+ }
183
304
  log('[upgrade-worker] Upgrade complete');
184
305
  } else {
185
306
  // 4b. 失败,回滚
@@ -192,52 +313,83 @@ export async function runUpgrade({ pluginDir, fromVersion, toVersion, pluginId,
192
313
  }
193
314
 
194
315
  /**
195
- * 回滚处理
316
+ * 回滚处理。
317
+ * result=rollback 指**文件态已恢复**(restore 或 fallback install 任一成功),
318
+ * 运行态恢复交还重启/上游 watcher;两路都失败记 rollback-failed(error 带真因)。
319
+ * 记账整体在 triggerGatewayRestart **之前**:回滚后文件态已定,记账不依赖重启,
320
+ * 且 worker 在不可脱逃形态下可能被自己触发的重启杀死。
196
321
  */
197
322
  async function handleRollback({ pluginDir, fromVersion, toVersion, pluginId, pkgName, error, skipVersion, opts, log }) {
198
323
  log('[upgrade-worker] Attempting rollback...');
324
+ try {
325
+ await updateInflight({ phase: 'rollback' });
326
+ }
327
+ catch (e) {
328
+ log(`[upgrade-worker] Failed to update inflight marker (non-fatal): ${e.message}`);
329
+ }
199
330
 
200
- // 首选 mv 备份目录
331
+ // 首选 rename 备份目录
201
332
  let restored = false;
333
+ let fallbackOk = false;
334
+ let rollbackErrMsg = '';
202
335
  try {
203
- restored = await restoreFromBackup(pluginDir);
336
+ restored = await restoreFromBackup(pluginDir, pluginId, { log });
204
337
  } catch (restoreErr) {
338
+ rollbackErrMsg = `restore: ${restoreErr.message}`;
205
339
  log(`[upgrade-worker] Backup restore error: ${restoreErr.message}`);
206
340
  }
207
341
 
208
342
  if (restored) {
209
343
  log('[upgrade-worker] Restored from backup');
210
344
  } else {
211
- // 兜底:先卸载再从 npm 安装旧版本
345
+ // 兜底:从 npm 覆盖安装旧版本
212
346
  log('[upgrade-worker] Backup restore failed, falling back to npm install');
213
347
  try {
214
- await fallbackInstallOldVersion(pkgName, fromVersion, pluginId, opts);
348
+ await fallbackInstallOldVersion(pkgName, fromVersion, opts);
349
+ fallbackOk = true;
215
350
  log('[upgrade-worker] Fallback install completed');
216
351
  }
217
352
  catch (fallbackErr) {
353
+ rollbackErrMsg = rollbackErrMsg ? `${rollbackErrMsg}; ${fallbackErr.message}` : fallbackErr.message;
218
354
  log(`[upgrade-worker] Fallback install also failed: ${fallbackErr.message}`);
219
355
  }
220
356
  }
221
357
 
222
- // 触发 gateway 重启让老版本回到运行态(尽力而为,不验证结果)
223
- log('[upgrade-worker] Triggering gateway restart after rollback...');
224
- await triggerGatewayRestart(opts);
358
+ const rollbackOk = restored || fallbackOk;
359
+ // rollback-failed error 带真因:!rollbackOk 必经 fallback 抛错,rollbackErrMsg 非空
360
+ let finalError = error;
361
+ if (!rollbackOk) {
362
+ finalError = `${error}; rollback failed: ${rollbackErrMsg}`;
363
+ }
225
364
 
226
- // 记录状态(顺序执行因共享 state 文件,但各自 try/catch 避免单个失败阻断其余)
365
+ // 记录状态(重启前完成;失败非致命,inflight 未清交 scheduler 对账兜底)
227
366
  // 仅验证失败(新版本确实被加载并发现有问题)才标记为 skipped;
228
367
  // update 命令失败可能是瞬态故障(网络、磁盘等),不应永久跳过该版本
229
- if (skipVersion) {
230
- try { await addSkippedVersion(toVersion); }
231
- /* c8 ignore next -- 状态写入 catch:测试中 stub 不会失败 */
232
- catch (e) { log(`[upgrade-worker] Failed to record skipped version (non-fatal): ${e.message}`); }
368
+ try {
369
+ await recordUpgradeTerminal({
370
+ from: fromVersion, to: toVersion,
371
+ result: rollbackOk ? 'rollback' : 'rollback-failed',
372
+ error: finalError,
373
+ skipVersion: skipVersion ? toVersion : undefined,
374
+ });
233
375
  }
234
- try { await updateLastUpgrade({ from: fromVersion, to: toVersion, result: 'rollback' }); }
235
- /* c8 ignore next -- 状态写入 catch */
236
- catch (e) { log(`[upgrade-worker] Failed to update lastUpgrade (non-fatal): ${e.message}`); }
237
- try { await appendLog({ from: fromVersion, to: toVersion, result: 'rollback', error }); }
238
- /* c8 ignore next -- 状态写入 catch */
239
- catch (e) { log(`[upgrade-worker] Failed to append log (non-fatal): ${e.message}`); }
240
- if (skipVersion) {
376
+ catch (e) {
377
+ log(`[upgrade-worker] Failed to record terminal state (non-fatal): ${e.message}`);
378
+ }
379
+
380
+ // 触发 gateway 重启让老版本回到运行态(尽力而为,不验证结果)
381
+ log('[upgrade-worker] Triggering gateway restart after rollback...');
382
+ const restartOk = await triggerGatewayRestart(opts);
383
+ if (!restartOk) {
384
+ // 文件态已恢复但运行态未刷新;worker 禁 remoteLog,只落 jsonl 事件供诊断
385
+ log('[upgrade-worker] Gateway restart command failed after rollback');
386
+ try { await appendLog({ event: 'rollback-restart-failed', from: fromVersion, to: toVersion }); }
387
+ catch (e) { log(`[upgrade-worker] Failed to append log (non-fatal): ${e.message}`); }
388
+ }
389
+
390
+ if (!rollbackOk) {
391
+ log(`[upgrade-worker] Rollback failed. Version ${toVersion} may still be active on disk`);
392
+ } else if (skipVersion) {
241
393
  log(`[upgrade-worker] Rollback complete. Version ${toVersion} added to skipped list`);
242
394
  } else {
243
395
  log(`[upgrade-worker] Rollback complete. Version ${toVersion} not skipped (transient failure)`);
@@ -252,20 +404,21 @@ async function main() {
252
404
  pluginDir: { type: 'string' },
253
405
  fromVersion: { type: 'string' },
254
406
  toVersion: { type: 'string' },
407
+ baselineVersion: { type: 'string' }, // 可缺:缺时按"基线不可得"退化处理
255
408
  pluginId: { type: 'string' },
256
409
  pkgName: { type: 'string' },
257
410
  },
258
411
  strict: true,
259
412
  });
260
413
 
261
- const { pluginDir, fromVersion, toVersion, pluginId, pkgName } = values;
414
+ const { pluginDir, fromVersion, toVersion, baselineVersion, pluginId, pkgName } = values;
262
415
  if (!pluginDir || !fromVersion || !toVersion || !pluginId || !pkgName) {
263
- console.error('Usage: node worker.js --pluginDir <dir> --fromVersion <ver> --toVersion <ver> --pluginId <id> --pkgName <name>');
416
+ console.error('Usage: node worker.js --pluginDir <dir> --fromVersion <ver> --toVersion <ver> --pluginId <id> --pkgName <name> [--baselineVersion <ver>]');
264
417
  process.exit(1);
265
418
  }
266
419
 
267
420
  try {
268
- await runUpgrade({ pluginDir, fromVersion, toVersion, pluginId, pkgName });
421
+ await runUpgrade({ pluginDir, fromVersion, toVersion, baselineVersion, pluginId, pkgName });
269
422
  process.exit(0);
270
423
  }
271
424
  catch (err) {
@@ -1,7 +1,9 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import nodePath from 'node:path';
3
3
 
4
- // 延迟读取 + 缓存:避免模块加载时 package.json 损坏导致插件整体无法注册
4
+ // 延迟读取 + 缓存:避免模块加载时 package.json 损坏导致插件整体无法注册。
5
+ // 仅供 info 类展示;upgradeHealth / inflight 对账的判定禁用本原语,
6
+ // 必须用 src/auto-upgrade/updater.js 的加载时刻快照(getLoadedPluginVersion)。
5
7
  let __pluginVersion = null;
6
8
  export async function getPluginVersion() {
7
9
  if (__pluginVersion) return __pluginVersion;