dsh-m 0.10.0 → 0.10.2

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.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,18 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
8
8
 
9
9
  ## 中文
10
10
 
11
+ ### 0.10.2 修复:D3 自升级 post-write 伪失败账实改判 + 静默自升级(0.10.0→0.10.1 dogfood 实证轮)
12
+
13
+ - **需求(2026-10-18 凌晨,首次 D3 真机 dogfood——0.10.0→0.10.1 自升级事件)**:自升级报失败(`application=failed、error=already-installed`)但三面账(manifest/node_modules/lock)均已写入 0.10.1——宿主对自身 bundle 换装存在 **post-write 伪失败**:installBundle 写穿后 manifest 含该名,宿主完成路径自触守卫(自指操作伪影;守卫源码 `specPackageName` 对 tarball URL 同样可解析包名)。另:0.10.1 引入的自升级确认框破坏既有「点击即升级」体验(主人裁决撤除)。
14
+ - **方案**:① **D3 终态失败一律账实复读改判**——实读 listInstalled:目标版已写入 = 升级事实成立,按成功改判(needsRestart=true 提示重启激活,output 留改判痕);未到目标 = 真失败原语义;profileDir 自 host-api/两处缝透传。② **静默自升级**——确认框整体撤除(受控态/Modal/文案键/SSR 钉),角标点击直接升级,结局由成功/失败横幅如实报告。ADR-0022 增补决议 7(spike 推断修正 + 改判 + UX 裁决入册)。
15
+ - **验证**:typecheck 零错误;全量测试(新增 2 钉 T-D1 改判成功/T-D2 真失败双形态,静默结构钉替换确认框钉;18 例既有失败 028 本机环境清单零新增);真机事件本身即第一份 dogfood 数据:0.10.1 经「重启激活」到位,重启后守卫锚点干净。
16
+
17
+ ### 0.10.1 修复:0.10.0 真机 E2E 实证缺陷三连——enable 竞速重试普适化 + 回装失败账实复读 + 守卫命中一律两步化
18
+
19
+ - **需求(2026-10-17,0.10.0 装机当日 E2E 实证,真机 skip-auth@0.4.1 与卸载→重装两个事件)**:①插件树重载窗口的 enable 阶段激活竞速(`pending (waiting for service: webRuntime)`)会让两步升级的第二腿、兜底回装腿与 D3 自升级通道直接终态失败——0.9.94 时代的「enable 失败自动重试一次(重试即愈)」本地增强只覆盖了首腿;②回装腿报失败 ≠ 真缺席——pnpm 可能已写入账面(激活阶段失败),此时建议「手动重装旧版」会误导用户降级;③卸载后同会话重装:宿主活跃态/manifest 字面仍视其在装(守卫命中),而 previousVersion 探测口径为「未安装」,0.10.0 的 previousVersion 前置门使守卫命中原样透传 already-installed。
20
+ - **方案**:① enable 竞速自动重试普适化——首腿、两步第二腿、兜底回装腿、D3 通道同享受限单次重试(`installOnceRetryingEnable`,退避与豁免形态不变);② 宣称缺席前实读账面(listInstalled)——已写入 → 「请重启 Desktop 完成激活」(details.installedVersion 如实透出),真缺席 → 重装指引,两条路径零误导性降级建议;③ install 路径守卫命中一律两步化(remove+add 即官方语义,去掉 previousVersion 前置门),无旧版可回装时失败走同款账实复读。附带:check-release 门禁行分割容忍 CRLF(本机工作树双语顶条恒找不到的修复)。
21
+ - **验证**:typecheck 零错误;全量 1595 例(新增 8 钉:T-A1/A2/A3 enable 竞速三腿 + T-B1/B2 账实复读两形态 + T-C1/C2/C3 无旧版守卫两步化三态;18 例既有失败为 028 本机环境清单零新增);真机恢复实录:skip-auth 0.4.1 经「重启激活」恢复(零重装),重启后守卫锚点干净。
22
+
11
23
  ### 0.10.0 特性+修复:宿主 0.2.1 already-installed 适配——第三方两步升级 + 兜底回装降级恢复 + 自升级专用通道(D3)
12
24
 
13
25
  - **需求(2026-10-17,主人)**:新版 DSH(0.2.1-rc.1/rc.2)中 dsh-m 无法升级与自升级任何包。根因定谳:宿主 `installBundle()` 新增 already-installed 安装守卫——spec 解析出的包名已在 profile manifest 就先于 pnpm 拒绝,官方升级语义改为「先卸载再安装」;tarball/git 形态 spec 包名在 pnpm 前不可知、不受守卫约束(真机 spike 实证:URL 覆盖安装已装 dsh-m 成功、激活形态 restart-required)。影响面:产品内 desktop 升级/自升级全断;卸载、开关、全新安装、web 路径无恙。跳过 0.9.95+ 直接发 0.10.0。
@@ -667,6 +679,18 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
667
679
 
668
680
  ## English
669
681
 
682
+ ### Fixes in 0.10.2 — account-reality reconciliation for D3 self-upgrade post-write pseudo-failure + silent self-upgrade (first D3 dogfood round)
683
+
684
+ - **Need (2026-10-18 early morning, first real-machine D3 dogfood — the 0.10.0→0.10.1 self-upgrade event)**: the self-upgrade reported failure (`application=failed、error=already-installed`) while all three ledgers (manifest/node_modules/lock) had already been written to 0.10.1 — the host has a **post-write pseudo-failure** for its own bundle swap: installBundle writes through, the manifest then contains the name, and the host's completion path trips its own guard (self-referential artifact; the guard's `specPackageName` resolves package names from tarball URLs too). Separately: the 0.10.1 self-upgrade confirmation dialog broke the existing "click to upgrade" experience (owner ordered its removal).
685
+ - **Approach**: ① **reconcile on any D3 terminal failure** — re-read listInstalled: if the target version is already written, the upgrade factually happened and the result is re-judged as success (needsRestart=true prompting the activating restart, with the reconciliation noted in output); if the target is absent, the genuine-failure semantics stand; profileDir is threaded from host-api and both seams. ② **silent self-upgrade** — the confirmation dialog is removed wholesale (controlled state / modal / copy keys / SSR pin); the chip click upgrades directly and outcomes are still reported truthfully via banners. ADR-0022 decision 7 records the spike-inference correction, the reconciliation, and the UX ruling.
686
+ - **Verification**: typecheck clean; full suite (2 new pins T-D1 reconciled-success / T-D2 genuine-failure, silent structure pins replacing the confirmation pins; 18 pre-existing failures are the 028 machine-environment set, zero new); the real-machine event itself is the first dogfood datum: 0.10.1 landed via "restart to activate", guard journal anchors clean after restart.
687
+
688
+ ### Fixes in 0.10.1 — three defects proven by same-day real-machine E2E of 0.10.0: enable-race retry universalized + account-reality re-read on rollback failure + guard hit always two-steps
689
+
690
+ - **Need (2026-10-17, proven during the same-day real-machine E2E of 0.10.0 — the skip-auth@0.4.1 upgrade event and the uninstall→reinstall event)**: ① the enable-stage activation race during plugin-tree reload windows (`pending (waiting for service: webRuntime)`) made the two-step upgrade's second leg, the rollback reinstall leg, and the D3 self-upgrade channel fail terminally — the 0.9.94-era "retry enable failures once (retry heals)" local enhancement covered only the first leg; ② a failed reinstall leg ≠ true absence — pnpm may have already written the package (activation failed afterwards), where advising "manually reinstall the old version" would mislead the user into a downgrade; ③ uninstall followed by a same-session reinstall: the host's live state / literal manifest still considers the package installed (guard fires) while the previousVersion probe reports "not installed", so 0.10.0's previousVersion precondition let the raw already-installed error surface.
691
+ - **Approach**: ① universalize the enable-race auto-retry — the first leg, the two-step second leg, the rollback reinstall leg, and the D3 channel all share the limited single retry (`installOnceRetryingEnable`; backoff and exempted failure shapes unchanged); ② re-read the ledger (listInstalled) before claiming absence — if written → "restart Desktop to finish activation" (with `details.installedVersion` surfaced truthfully), if truly absent → reinstall guidance; neither path suggests a misleading downgrade; ③ on the install path a guard hit always two-steps (remove+add is the official semantics; the previousVersion precondition is removed), and a failure without an old version to roll back to goes through the same ledger re-read. Incidental: the check-release gate now splits lines CRLF-tolerantly (fixing the bilingual top-entry false negative on CRLF working trees).
692
+ - **Verification**: typecheck clean; full suite 1595 tests (8 new pins: T-A1/A2/A3 enable-race on three legs + T-B1/B2 both ledger-re-read shapes + T-C1/C2/C3 three states of guard-hit two-step without an old version; 18 pre-existing failures are the 028 machine-environment set, zero new); real-machine recovery record: skip-auth 0.4.1 recovered via "restart to activate" (zero reinstall), guard journal anchors clean after restart.
693
+
670
694
  ### Feature + fix in 0.10.0 — host 0.2.1 already-installed adaptation: third-party two-step upgrade + rollback-reinstall degraded restore + dedicated self-upgrade channel (D3)
671
695
 
672
696
  - **Need (2026-10-17, the owner)**: on the new DSH (0.2.1-rc.1/rc.2) dsh-m could no longer upgrade or self-upgrade any package. Root cause: the host's `installBundle()` gained an already-installed guard — when the package name resolved from a registry spec already exists in the profile manifest it is rejected before pnpm runs, and the official upgrade semantics became "uninstall first, then install"; tarball/git specs are exempt because their package name is unknowable before pnpm (verified on a real machine: installing a tarball URL over the installed dsh-m succeeded, activation shape restart-required). Scope: in-product desktop upgrade/self-upgrade fully broken; uninstall, toggle, fresh install, and the web path unaffected. Version 0.9.95+ skipped straight to 0.10.0.
package/lib/client.js CHANGED
@@ -2351,9 +2351,6 @@ var ZH = {
2351
2351
  "op.degraded": "\u5347\u7EA7\u5931\u8D25\uFF1A\u65B0\u7248\u5B89\u88C5\u672A\u6210\u529F\uFF0C\u5DF2\u81EA\u52A8\u6062\u590D\u65E7\u7248 {restoredTo}\u2014\u2014\u63D2\u4EF6\u5F53\u524D\u53EF\u7528\uFF0C\u53EF\u7A0D\u540E\u91CD\u8BD5\u3002",
2352
2352
  "notify.selfupgraded": "dsh-m \u5DF2\u5347\u7EA7\uFF08v{from} \u2192 v{to}\uFF09",
2353
2353
  "notify.selfupgraded.failed": "dsh-m \u5347\u7EA7\u5931\u8D25\uFF1A{err}",
2354
- "selfupgrade.confirm": "\u5C06\u901A\u8FC7 tarball \u76F4\u88C5\u901A\u9053\u628A dsh-m \u5347\u7EA7\u5230 {version}\uFF1A\u4E0D\u7ECF\u8FC7\u5378\u8F7D\uFF0C\u5931\u8D25\u65F6\u5F53\u524D\u7248\u672C\u4E0D\u53D7\u5F71\u54CD\uFF1B\u5B8C\u6210\u540E\u9700\u91CD\u542F Desktop \u751F\u6548\u3002",
2355
- "selfupgrade.confirm.noversion": "\u5C06\u901A\u8FC7 tarball \u76F4\u88C5\u901A\u9053\u5347\u7EA7 dsh-m\uFF1A\u4E0D\u7ECF\u8FC7\u5378\u8F7D\uFF0C\u5931\u8D25\u65F6\u5F53\u524D\u7248\u672C\u4E0D\u53D7\u5F71\u54CD\uFF1B\u5B8C\u6210\u540E\u9700\u91CD\u542F Desktop \u751F\u6548\u3002",
2356
- "selfupgrade.go": "\u5347\u7EA7",
2357
2354
  "failed.install": "\u5B89\u88C5\u5931\u8D25\uFF1A{err}",
2358
2355
  "failed.uninstall": "\u5378\u8F7D\u5931\u8D25\uFF1A{err}",
2359
2356
  "failed.upgrade": "\u5347\u7EA7\u5931\u8D25\uFF1A{err}",
@@ -2655,9 +2652,6 @@ var EN = {
2655
2652
  "op.degraded": "Upgrade failed: the new version did not install \u2014 rolled back to {restoredTo}. The plugin still works; you can retry later.",
2656
2653
  "notify.selfupgraded": "dsh-m upgraded (v{from} \u2192 v{to})",
2657
2654
  "notify.selfupgraded.failed": "dsh-m upgrade failed: {err}",
2658
- "selfupgrade.confirm": "Upgrade dsh-m to {version} via the tarball direct-install channel: no uninstall involved, the current version stays intact on failure; a Desktop restart is needed afterwards.",
2659
- "selfupgrade.confirm.noversion": "Upgrade dsh-m via the tarball direct-install channel: no uninstall involved, the current version stays intact on failure; a Desktop restart is needed afterwards.",
2660
- "selfupgrade.go": "Upgrade",
2661
2655
  "failed.install": "Install failed: {err}",
2662
2656
  "failed.uninstall": "Uninstall failed: {err}",
2663
2657
  "failed.upgrade": "Upgrade failed: {err}",
@@ -5191,12 +5185,11 @@ var TABS = [
5191
5185
  ["installed", "tab.installed", "installed"],
5192
5186
  ["settings", "tab.settings", null]
5193
5187
  ];
5194
- function DshmVersionChip({ version, notify, initialCheck, ops, initialConfirm }) {
5188
+ function DshmVersionChip({ version, notify, initialCheck, ops }) {
5195
5189
  const [check, setCheck] = useState(
5196
5190
  () => initialCheck !== void 0 ? initialCheck : typeof window !== "undefined" && window.localStorage ? readSelfCheckCache2(window.localStorage, { version }) : null
5197
5191
  );
5198
5192
  const [busy, setBusy] = useState(false);
5199
- const [confirming, setConfirming] = useState(initialConfirm === true);
5200
5193
  useEffect(() => {
5201
5194
  if (initialCheck !== void 0) return;
5202
5195
  let live = true;
@@ -5214,11 +5207,10 @@ function DshmVersionChip({ version, notify, initialCheck, ops, initialConfirm })
5214
5207
  }, [initialCheck]);
5215
5208
  const doUpgrade = async () => {
5216
5209
  if (busy) return;
5217
- setConfirming(true);
5210
+ return performUpgrade();
5218
5211
  };
5219
5212
  const performUpgrade = async () => {
5220
5213
  if (busy) return;
5221
- setConfirming(false);
5222
5214
  setBusy(true);
5223
5215
  try {
5224
5216
  const res = await (ops ? ops.runOp("upgrade", "dsh-m", async () => {
@@ -5247,36 +5239,6 @@ function DshmVersionChip({ version, notify, initialCheck, ops, initialConfirm })
5247
5239
  };
5248
5240
  const state = deriveChipState2(check, version);
5249
5241
  if (state.kind === "outdated") {
5250
- if (confirming) {
5251
- return h(
5252
- "div",
5253
- { className: "dshm-compat-overlay", ...backdropCloseHandlers2(() => setConfirming(false)) },
5254
- h(
5255
- "div",
5256
- { className: "dshm-compat-dialog", onClick: (e) => e.stopPropagation() },
5257
- h("div", { className: "dshm-compat-title" }, lookup("selfupdate.available", { v: state.latest || "?" })),
5258
- h(
5259
- "div",
5260
- { className: "dshm-compat-body" },
5261
- state.latest ? lookup("selfupgrade.confirm", { version: `v${state.latest}` }) : lookup("selfupgrade.confirm.noversion")
5262
- ),
5263
- h(
5264
- "div",
5265
- { className: "dshm-actions" },
5266
- h(
5267
- "button",
5268
- { className: "dshm-btn", onClick: () => setConfirming(false) },
5269
- lookup("common.later")
5270
- ),
5271
- h(
5272
- "button",
5273
- { className: "dshm-btn primary", onClick: performUpgrade, disabled: busy },
5274
- lookup("selfupgrade.go")
5275
- )
5276
- )
5277
- )
5278
- );
5279
- }
5280
5242
  return h(
5281
5243
  "button",
5282
5244
  {
@@ -302,14 +302,16 @@ export function createApiDispatcher(ctx) {
302
302
  }
303
303
  case 'self-upgrade': {
304
304
  assertWriteAllowed(profile, 'self-upgrade');
305
- // 0.10.0 D3 自升级专用通道(desktop):tarball 直覆盖。desktopInstall 的 registry spec 在
306
- // 0.2.1 宿主必撞 already-installed 守卫,而 remove+add 对调用者自身是破坏性路径(R6:自升级
307
- // 绝不落入通用两步——改道唯一执行点在 core 缝,此处调 D3 入口即语义同源);tarball spec
308
- // 不受守卫约束(spike 实证)。恒 panel 语义零外部日志(ADR-0018),intent 预写/收口在 core 层。
305
+ // 0.10.0 D3 自升级专用通道(desktop):tarball 直装。0.10.2 实证修订:0.2.1 守卫经
306
+ // specPackageName 对 tarball URL 同样解析包名(自升级写穿后宿主完成路径自触守卫 = post-write
307
+ // 伪失败),由 D3 的账实复读改判兜住;通道分治不变——remove+registry-spec 通用两步仍不达自升级。
308
+ // 恒 panel 语义零外部日志(ADR-0018),intent 预写/收口在 core 层。
309
309
  if (profile.kind !== 'web') {
310
310
  const managed = await d.desktopSelfUpgradeViaTarball(ctx.pkg.name, ctx.pkg.version, {
311
311
  timeoutMs: cfg().timeoutMs,
312
312
  signal,
313
+ // 0.10.2:账实复读(post-write 伪失败改判)需要 profile 目录
314
+ profileDir: profile.dir,
313
315
  }, {
314
316
  getService: d.getService,
315
317
  ensureService: d.ensureService,
@@ -194,7 +194,7 @@ async function desktopInstallLocked(id, cfg, opts, deps) {
194
194
  // 0.10.0 R6(install 臂):dsh-m 已装态下的 install 调用实为升级(守卫必拦)→ 改道 D3;
195
195
  // 全新安装(未装)走既有单步——守卫不触发,R1「新装路径不动」。
196
196
  if (pkg === SELF_PKG && previousVersion !== undefined) {
197
- return desktopSelfUpgradeViaTarballLocked(pkg, previousVersion, { timeoutMs, signal: opts.signal }, {
197
+ return desktopSelfUpgradeViaTarballLocked(pkg, previousVersion, { timeoutMs, signal: opts.signal, profileDir: opts.profileDir }, {
198
198
  getService: () => service,
199
199
  npmLatest: deps.npmLatest,
200
200
  npmTarballUrl: deps.npmTarballUrl,
@@ -340,6 +340,18 @@ async function runManagedInstall(service, ctx, deps = {}) {
340
340
  enabled: true,
341
341
  ...(ctx.approvedBuilds && ctx.approvedBuilds.length > 0 ? { approvedBuilds: ctx.approvedBuilds } : {}),
342
342
  }));
343
+ // enable 阶段竞速自动重试(2026-10-02 本地增强的普适化,0.10.1 E2E 实证扩面):
344
+ // 高频装卸/插件树重载窗口下 enable 阶段与应用侧竞速(实机实证「重试即愈」,skip-auth@0.4.1 事件二度实证)——
345
+ // 首腿、两步第二腿、兜底回装腿同享受限单次重试;cancelled / build-blocked / release-age 等其他失败形态不重试。
346
+ const installOnceRetryingEnable = async (spec) => {
347
+ const first = await installOnce(spec);
348
+ if (first.stage === 'enable' && (first.application === 'failed' || first.error)) {
349
+ if (enableRetryDelayMs > 0)
350
+ await sleepMs(enableRetryDelayMs);
351
+ return installOnce(spec);
352
+ }
353
+ return first;
354
+ };
343
355
  /** 失败映射(既有语义不变:cancelled / build-blocked 审批 / release-age 等待 / enable / 通用)。 */
344
356
  const mapFailure = async (change) => {
345
357
  if (change.application === 'cancelled') {
@@ -395,14 +407,8 @@ async function runManagedInstall(service, ctx, deps = {}) {
395
407
  }
396
408
  return { change, bundleName };
397
409
  };
398
- let change = await installOnce();
399
- // 本地增强:enable 阶段失败自动重试一次(受限单次 + 短退避;与高频装卸/插件树重载的竞速重试即愈;
400
- // cancelled / build-blocked / release-age 等其他失败形态不重试)
401
- if (change.stage === 'enable' && (change.application === 'failed' || change.error)) {
402
- if (enableRetryDelayMs > 0)
403
- await sleepMs(enableRetryDelayMs);
404
- change = await installOnce();
405
- }
410
+ let change = await installOnceRetryingEnable(ctx.spec);
411
+ // (enable 阶段竞速自动重试已普适化进 installOnceRetryingEnable——首腿/两步第二腿/兜底回装腿/D3 同享)
406
412
  // ADR-0009 挂点3:双码失败 → 治理 → changed 则重试该命令一次(每次调用至多一次;
407
413
  // cancelled / build-blocked 不治理不重试)。仍败走既有 mapFailure(含账实分裂复读);
408
414
  // 挂点3 已落盘的合并以短语并入失败 message 与成功 output(留痕契约含挂点3)。
@@ -419,49 +425,96 @@ async function runManagedInstall(service, ctx, deps = {}) {
419
425
  // (removeBundle 旧版 → 重试安装同一 spec,至多一次、无循环);第二腿终态失败 → 兜底回装旧版
420
426
  // (remove 后旧名已不在 manifest,守卫不适用):回装成功 = 降级恢复结局(degraded,R4);
421
427
  // cancelled / build-blocked 语义优先(不冒充降级);回装失败 = 缺席硬错误(rollback-failed)。
422
- // 无 previousVersion 不两步化——R3 兜底网的存在前提(无旧版即无回装能力)。
428
+ // 0.10.1(E2E 实证 ③):去掉 previousVersion 前置门——卸载后同会话重装时,宿主活跃态/manifest
429
+ // 字面仍视其在装(守卫命中),而探测口径为「未安装」;守卫命中即 remove+add(官方语义),
430
+ // 无旧版可回装时失败走账实复读给恢复指引。
423
431
  let twoStepNote;
424
- if (isAlreadyInstalledGuard(change) && ctx.previousVersion && typeof service.removeBundle === 'function') {
432
+ if (isAlreadyInstalledGuard(change) && typeof service.removeBundle === 'function') {
425
433
  twoStepNote = 'already-installed → two-step';
426
434
  const removeChange = changeOf(await service.removeBundle(ctx.pkg));
427
435
  if (isTerminalFailure(removeChange)) {
428
436
  // remove 腿失败短路(不回装):旧版尚未移除,状态以官方插件页为准
429
437
  throw new DesktopOpsError('remove-failed', `两步升级第一步(卸载旧版)失败(${ctx.pkg};application=${removeChange.application ?? 'failed'}${removeChange.error?.code ? `、error=${removeChange.error.code}` : ''}):未执行升级,请到官方 Desktop 插件页确认 ${ctx.pkg} 状态`);
430
438
  }
431
- change = await installOnce();
439
+ change = await installOnceRetryingEnable(ctx.spec);
432
440
  if (isTerminalFailure(change)) {
433
441
  const pendingAdd = pendingOf(change);
434
442
  const addDiagnostic = `${change.error?.diagnostic ?? ''}${change.error?.code ? `\n${change.error.code}` : ''}`.trim();
435
443
  const addCancelled = change.application === 'cancelled';
436
444
  const addBuildBlocked = change.packageResult?.kind === 'build-blocked' && pendingAdd.length > 0;
437
- // 兜底回装旧版(R3):回装腿失败(含其自身 build-blocked)= 缺席硬错误,降级失败真相优先
438
- const rollbackChange = await installOnce(`${ctx.pkg}@${ctx.previousVersion}`);
439
- const pendingRollback = pendingOf(rollbackChange);
440
- if (isTerminalFailure(rollbackChange) || (rollbackChange.packageResult?.kind === 'build-blocked' && pendingRollback.length > 0)) {
441
- throw new DesktopOpsError('rollback-failed', `升级失败且自动回装旧版也失败(${ctx.pkg}@${ctx.previousVersion}):插件当前可能缺席,请在官方 Desktop 插件页手动重装 ${ctx.pkg}@${ctx.previousVersion}${addDiagnostic !== '' ? `;升级失败原因:${addDiagnostic}` : ''}`, {
442
- pkg: ctx.pkg,
443
- previousVersion: ctx.previousVersion,
444
- ...(addDiagnostic !== '' ? { addDiagnostic } : {}),
445
- ...(pendingRollback.length > 0 ? { pendingBuilds: pendingRollback } : {}),
446
- });
445
+ if (ctx.previousVersion) {
446
+ // 兜底回装旧版(R3):回装腿失败(含其自身 build-blocked)= 缺席硬错误,降级失败真相优先
447
+ const rollbackChange = await installOnceRetryingEnable(`${ctx.pkg}@${ctx.previousVersion}`);
448
+ const pendingRollback = pendingOf(rollbackChange);
449
+ if (isTerminalFailure(rollbackChange) || (rollbackChange.packageResult?.kind === 'build-blocked' && pendingRollback.length > 0)) {
450
+ // 0.10.1(E2E 实证,skip-auth@0.4.1 事件):回装腿失败 ≠ 一定缺席——pnpm 可能已写入账面
451
+ // (激活阶段失败)。宣称缺席前实读账面,按实况分派恢复指引(账实相符优先,零误导性降级建议)。
452
+ let installedVersion;
453
+ if (ctx.profileDir && ctx.listInstalled) {
454
+ try {
455
+ const row = (await ctx.listInstalled(ctx.profileDir)).items.find((it) => it.pkg === ctx.pkg);
456
+ installedVersion = row?.version;
457
+ }
458
+ catch {
459
+ // 复读失败按未知处理:保留既有缺席文案(含「可能」字样,不断言)
460
+ }
461
+ }
462
+ const message = installedVersion !== undefined
463
+ ? `升级失败且自动回装未完成,但账面显示 ${ctx.pkg}@${installedVersion} 已写入(激活阶段可能未完成):请重启 Desktop 完成激活;如重启后仍异常,请在官方 Desktop 插件页手动处理${addDiagnostic !== '' ? `;升级失败原因:${addDiagnostic}` : ''}`
464
+ : `升级失败且自动回装旧版也失败(${ctx.pkg}@${ctx.previousVersion}):插件当前可能缺席,请在官方 Desktop 插件页手动重装 ${ctx.pkg}@${ctx.previousVersion}${addDiagnostic !== '' ? `;升级失败原因:${addDiagnostic}` : ''}`;
465
+ throw new DesktopOpsError('rollback-failed', message, {
466
+ pkg: ctx.pkg,
467
+ previousVersion: ctx.previousVersion,
468
+ ...(addDiagnostic !== '' ? { addDiagnostic } : {}),
469
+ ...(pendingRollback.length > 0 ? { pendingBuilds: pendingRollback } : {}),
470
+ ...(installedVersion !== undefined ? { installedVersion } : {}),
471
+ });
472
+ }
473
+ const verifiedRollback = await verifyAndBundle(rollbackChange);
474
+ if (addBuildBlocked) {
475
+ // build-blocked 语义优先:旧版已兜住,仍抛审批请求(不冒充降级成功)
476
+ throw new DesktopBuildApprovalNeeded(pendingAdd);
477
+ }
478
+ if (addCancelled) {
479
+ // 取消不冒充降级成功:已恢复旧版、未做升级
480
+ throw new DesktopOpsError('install-refused', `升级已被取消(${ctx.pkg}):已恢复旧版 ${ctx.previousVersion},未做升级`);
481
+ }
482
+ // 降级恢复(R4):成功形态 + degraded 标记;跳过挂点2 登记——实际在装的是旧版,登记新版本会是假账
483
+ return {
484
+ ...verifiedRollback,
485
+ ...(governRetryNote !== undefined ? { governRetryNote } : {}),
486
+ degraded: true,
487
+ restoredTo: ctx.previousVersion,
488
+ twoStepNote,
489
+ };
447
490
  }
448
- const verifiedRollback = await verifyAndBundle(rollbackChange);
491
+ // 0.10.1(E2E 实证 ③):无 previousVersion——remove 已按官方语义执行、无旧版可回装。
492
+ // build-blocked / cancelled 语义照旧;其余按账实复读给恢复指引(缺席硬错误族)。
449
493
  if (addBuildBlocked) {
450
- // build-blocked 语义优先:旧版已兜住,仍抛审批请求(不冒充降级成功)
451
494
  throw new DesktopBuildApprovalNeeded(pendingAdd);
452
495
  }
453
496
  if (addCancelled) {
454
- // 取消不冒充降级成功:已恢复旧版、未做升级
455
- throw new DesktopOpsError('install-refused', `升级已被取消(${ctx.pkg}):已恢复旧版 ${ctx.previousVersion},未做升级`);
497
+ throw new DesktopOpsError('install-refused', `安装已被取消(${ctx.pkg}):未安装`);
498
+ }
499
+ let installedVersion;
500
+ if (ctx.profileDir && ctx.listInstalled) {
501
+ try {
502
+ const row = (await ctx.listInstalled(ctx.profileDir)).items.find((it) => it.pkg === ctx.pkg);
503
+ installedVersion = row?.version;
504
+ }
505
+ catch {
506
+ // 复读失败按未知处理:缺席文案含「可能」字样,不断言
507
+ }
456
508
  }
457
- // 降级恢复(R4):成功形态 + degraded 标记;跳过挂点2 登记——实际在装的是旧版,登记新版本会是假账
458
- return {
459
- ...verifiedRollback,
460
- ...(governRetryNote !== undefined ? { governRetryNote } : {}),
461
- degraded: true,
462
- restoredTo: ctx.previousVersion,
463
- twoStepNote,
464
- };
509
+ const message = installedVersion !== undefined
510
+ ? `安装未完成,但账面显示 ${ctx.pkg}@${installedVersion} 已写入(激活阶段可能未完成):请重启 Desktop 完成激活;如重启后仍异常,请在官方 Desktop 插件页手动处理${addDiagnostic !== '' ? `;失败原因:${addDiagnostic}` : ''}`
511
+ : `安装失败且无法自动恢复(${ctx.pkg}):插件当前可能缺席,请在官方 Desktop 插件页手动重装${addDiagnostic !== '' ? `;失败原因:${addDiagnostic}` : ''}`;
512
+ throw new DesktopOpsError('rollback-failed', message, {
513
+ pkg: ctx.pkg,
514
+ ...(addDiagnostic !== '' ? { addDiagnostic } : {}),
515
+ ...(pendingAdd.length > 0 ? { pendingBuilds: pendingAdd } : {}),
516
+ ...(installedVersion !== undefined ? { installedVersion } : {}),
517
+ });
465
518
  }
466
519
  }
467
520
  if (change.application === 'failed' || change.application === 'cancelled' || change.error) {
@@ -610,8 +663,42 @@ async function desktopSelfUpgradeViaTarballLocked(pkgName, currentVersion, opts,
610
663
  });
611
664
  const resolveIntent = deps.resolveUpgradeIntent ?? defaultResolveUpgradeIntent;
612
665
  try {
613
- const change = changeOf(await service.installBundle(spec, { enabled: true }));
666
+ let change = changeOf(await service.installBundle(spec, { enabled: true }));
667
+ // 0.10.1:enable 阶段竞速自动重试(与 runManagedInstall 各腿同款;skip-auth@0.4.1 真机事件实证竞速窗口真实)
668
+ if (change.stage === 'enable' && (change.application === 'failed' || change.error)) {
669
+ if (enableRetryDelayMs > 0)
670
+ await sleepMs(enableRetryDelayMs);
671
+ change = changeOf(await service.installBundle(spec, { enabled: true }));
672
+ }
614
673
  if (isTerminalFailure(change)) {
674
+ // 0.10.2(D3 dogfood 实证,0.10.0→0.10.1 真机事件):宿主对自身 bundle 换装存在 post-write 伪失败——
675
+ // installBundle 已写穿三面账(manifest/node_modules/lock 均为目标版)却回传 failed/already-installed
676
+ // (自指操作伪影:写完后 manifest 含该名,宿主完成路径自触守卫)。终态失败一律实读账面:
677
+ // 目标版已写入 = 升级事实成立,按成功改判(needsRestart=true,重启激活);否则真失败(原语义)。
678
+ let installedVersion;
679
+ if (opts.profileDir && deps.listInstalled) {
680
+ try {
681
+ const row = (await deps.listInstalled(opts.profileDir)).items.find((it) => it.pkg === pkgName);
682
+ installedVersion = row?.version;
683
+ }
684
+ catch {
685
+ // 复读失败按真失败处理
686
+ }
687
+ }
688
+ if (installedVersion === latest.version) {
689
+ await resolveIntent({ ok: true, installedVersion: latest.version });
690
+ return {
691
+ id: pkgName,
692
+ pkg: pkgName,
693
+ spec,
694
+ version: latest.version,
695
+ buildApprovals: Array.isArray(change.approvedBuilds) ? change.approvedBuilds : [],
696
+ fallbackAllBuilds: false,
697
+ needsRestart: true,
698
+ output: `official pluginManager (self-upgrade via tarball): application=${change.application ?? 'failed'}、error=${change.error?.code ?? 'terminal-failure'} 经账实复读改判成功——目标版 ${latest.version} 已写入(宿主自升级 post-write 伪失败),重启 Desktop 激活`,
699
+ via: 'desktop-manager',
700
+ };
701
+ }
615
702
  const pending = pendingOf(change);
616
703
  if (change.application === 'cancelled') {
617
704
  throw new DesktopOpsError('install-refused', `自升级已被取消(${pkgName}):当前版本 ${currentVersion} 不受影响`);
@@ -670,7 +757,7 @@ async function desktopUpgradeLocked(pkg, cfg, opts, deps) {
670
757
  const selfTarget = items.find((it) => it.pkg === pkg);
671
758
  if (!selfTarget)
672
759
  throw new DesktopOpsError('install-refused', `desktop profile 未安装该插件: ${pkg}(升级目标必须是已装插件)`);
673
- return desktopSelfUpgradeViaTarballLocked(pkg, selfTarget.version, { timeoutMs, signal: opts.signal }, {
760
+ return desktopSelfUpgradeViaTarballLocked(pkg, selfTarget.version, { timeoutMs, signal: opts.signal, profileDir: opts.profileDir }, {
674
761
  getService: () => service,
675
762
  npmLatest: deps.npmLatest,
676
763
  npmTarballUrl: deps.npmTarballUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-m",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "description": "可自定义收录清单的 DSH 插件市场:收录、安装、卸载、升级一站完成,附 agent 工具与 dshm CLI。",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -46,6 +46,7 @@
46
46
  "build": "node scripts/build.mjs",
47
47
  "typecheck": "tsc -p tsconfig.json --noEmit",
48
48
  "test": "node --test tests/*.test.mjs",
49
+ "check-release": "node scripts/check-release.mjs",
49
50
  "pretest": "npm run build",
50
51
  "prepare": "npm run build",
51
52
  "capture:readme": "node scripts/capture-readme.mjs"