dsh-m 0.9.94 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/lib/client.js +70 -10
- package/lib/core/active-profile.js +1 -1
- package/lib/core/host-api.js +69 -16
- package/lib/core/packument.js +11 -0
- package/lib/core/profile-ops.js +283 -14
- package/lib/core/upgrade-intent.js +108 -0
- package/lib/core/versions.js +1 -1
- package/package.json +2 -1
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.1 修复:0.10.0 真机 E2E 实证缺陷三连——enable 竞速重试普适化 + 回装失败账实复读 + 守卫命中一律两步化
|
|
12
|
+
|
|
13
|
+
- **需求(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。
|
|
14
|
+
- **方案**:① enable 竞速自动重试普适化——首腿、两步第二腿、兜底回装腿、D3 通道同享受限单次重试(`installOnceRetryingEnable`,退避与豁免形态不变);② 宣称缺席前实读账面(listInstalled)——已写入 → 「请重启 Desktop 完成激活」(details.installedVersion 如实透出),真缺席 → 重装指引,两条路径零误导性降级建议;③ install 路径守卫命中一律两步化(remove+add 即官方语义,去掉 previousVersion 前置门),无旧版可回装时失败走同款账实复读。附带:check-release 门禁行分割容忍 CRLF(本机工作树双语顶条恒找不到的修复)。
|
|
15
|
+
- **验证**:typecheck 零错误;全量 1595 例(新增 8 钉:T-A1/A2/A3 enable 竞速三腿 + T-B1/B2 账实复读两形态 + T-C1/C2/C3 无旧版守卫两步化三态;18 例既有失败为 028 本机环境清单零新增);真机恢复实录:skip-auth 0.4.1 经「重启激活」恢复(零重装),重启后守卫锚点干净。
|
|
16
|
+
|
|
17
|
+
### 0.10.0 特性+修复:宿主 0.2.1 already-installed 适配——第三方两步升级 + 兜底回装降级恢复 + 自升级专用通道(D3)
|
|
18
|
+
|
|
19
|
+
- **需求(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。
|
|
20
|
+
- **方案(ADR-0022,grill 决议 R1–R15)**:① **install-first 自适应两步**——不探测宿主版本,首腿收到 `already-installed` 错误码才转 remove+重试(至多一次、无循环);install 路径撞守卫同样静默两步化(已装态 install 即升级),全新安装零行为变化(removeBundle 恒零调用);release-age/build-blocked/cancelled 语义先于守卫判定。② **兜底回装**——两步化后安装腿终态失败自动回装旧版:回装成功 = **降级恢复**新结局(成功形态 + degraded/restoredTo;客户端 warn 投影不入「升级成功」臂、操作泵复用 warned 状态落账、设置页红点沿用既有 failed||warned 口径、× 删除照旧);回装再失败 = rollback-failed 缺席硬错误(回装腿自身 build-blocked 时 details.pendingBuilds 如实透出);cancelled 抛「已恢复旧版/未做升级」不冒充降级,build-blocked 回装兜住后仍抛审批请求。③ **通道分治**——第三方走官方两步;dsh-m 自升级独享 tarball 直覆盖专用通道(精确版本 → 生效 registry `dist.tarball` → intent 预写 → `installBundle(tarballUrl)` 直覆盖 → 就地收口),core 缝单点改道(SELF_PKG='dsh-m'),host-api/Agent 工具/客户端零特判,自升级绝不落入 remove+add;入口全面保留:头部角标新增确认框(不经过卸载、失败时当前版本不受影响、完成后需重启 Desktop 生效),needsRestart 忠实透传宿主 application(applied→无横幅、restart-required→提示重启)。④ **开机对账**——自升级 intent ledger(`$DSH_HOME/dshm/self-upgrade-intent.json`;ADR-0018 面板通道零外部日志不变量不破)作崩溃兜底网:正常路径就地收口,进程死于升级途中时由下一次 self-check 每进程一次比对实际版本收口。⑤ WRITE_GUIDANCE 自升级失配文案修复(不再让用户「回 DSH Web」,改指官方 Settings → Plugins remove + install 精确版本)。GLOSSARY 七词条入册(两步升级/宿主安装守卫/install-first 自适应/兜底回装/降级恢复/自升级专用通道/开机对账)。
|
|
21
|
+
- **验证**:typecheck 零错误;全量 1585 测试(028 本机环境既有失败 18 例零新增——干净树对照为同一集合:symlink EPERM 家族 + client-lightbox ESM scheme + CRLF 源锚 + 网络 fetch);新增钉例覆盖计划 T1–T17(intent ledger 存储五态 / npmTarballUrl 三钉 / 两步化-兜底-降级边界九钉(含 remove-failed 短路、cancelled 文案、回装 build-blocked 透 pendingBuilds)/ D3 与 SELF_PKG 缝改道五钉 / host-api 开机对账与 upgrade(dsh-m) 改道端到端 / client 降级投影-两站点接线-确认框结构序-操作泵落账);ADR-0022 立项、分层 commit(feat core/client + docs adr/glossary + chore release)。
|
|
22
|
+
|
|
11
23
|
### 0.9.94 重构+修复:变更结局描述客户端单源化(op-outcome)+ 补丁映射 YAML 引号宽容与重复键根治
|
|
12
24
|
|
|
13
25
|
- **需求(2026-10-17 架构巡查 + grill 九决议)**:变更结局的呈现无 owning module——守卫拦截文案装配 main.jsx 三处逐字重复(0.9.93 toWire 收敛了线格式、client 侧呈现是已知残尾,`repairBasis` 后加时的多点位手补复发已实证)、升级成功文案两处逐字、builds 后缀四处拼装、superseded 五处;另:事务补偿对 workspace `patchedDependencies` **非双引号在场形态**(pnpm 的单引号/无引号书写)逐字判重失明——逐字判重可自造重复键文件、复读检查可假 committed(同包链经 strip 先摘在生产不可达,本项属防御面收敛与 fail-closed 补洞)。
|
|
@@ -661,6 +673,18 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
|
|
|
661
673
|
|
|
662
674
|
## English
|
|
663
675
|
|
|
676
|
+
### 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
|
|
677
|
+
|
|
678
|
+
- **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.
|
|
679
|
+
- **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).
|
|
680
|
+
- **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.
|
|
681
|
+
|
|
682
|
+
### 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)
|
|
683
|
+
|
|
684
|
+
- **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.
|
|
685
|
+
- **Approach (ADR-0022, grilled decisions R1–R15)**: ① **install-first adaptive two-step** — no host version probing; only when the first leg returns `already-installed` does it convert to remove + retry (at most once, no loops); the install path silently two-steps on the guard too (installing an installed package is an upgrade), while fresh installs keep zero `removeBundle` calls; release-age/build-blocked/cancelled shapes take precedence over the guard check. ② **Rollback reinstall** — when the install leg terminates in failure the old version is reinstalled automatically: success yields the new **degraded restore** outcome (success shape + degraded/restoredTo; the client projects a warn that never enters the "upgraded vX→vY" arm, the operation pump reuses the existing `warned` state, the settings red dot keeps the existing failed||warned rule, and ✕ dismissal is unchanged); a failed reinstall is the hard `rollback-failed` absence error (with `details.pendingBuilds` surfaced truthfully when the reinstall leg itself is build-blocked); cancellation reports "restored the old version / no upgrade was made" and never masquerades as degraded; a build-blocked install leg still raises the approval request after the rollback has secured the old version. ③ **Channel separation** — third-party plugins use the official two-step; dsh-m's own self-upgrade uses the dedicated tarball direct-install channel (exact version → live-registry `dist.tarball` → intent pre-write → `installBundle(tarballUrl)` → in-place settle), rerouted at a single core seam (`SELF_PKG='dsh-m'`) with zero special-casing in host-api/agent tools/client, and never falling into remove+add; all entries preserved: the header chip gains a confirmation dialog (no uninstall involved, the current version stays intact on failure, a Desktop restart is needed afterwards), and `needsRestart` faithfully passes through the host's `application` (applied → no banner, restart-required → restart prompt). ④ **Boot reconciliation** — a self-upgrade intent ledger (`$DSH_HOME/dshm/self-upgrade-intent.json`; ADR-0018's panel-channel-zero-external-logging invariant intact) serves as a crash safety net: the normal path settles in-process, and only a process that died mid-upgrade is reconciled by the next self-check, once per process, against the actually installed version. ⑤ The mismatched self-upgrade WRITE_GUIDANCE copy no longer sends users "back to DSH Web", pointing to the official Settings → Plugins remove + install (exact version) instead. Seven GLOSSARY terms registered (two-step upgrade / already-installed guard / install-first adaptation / rollback reinstall / degraded restore / dedicated self-upgrade channel / boot reconciliation).
|
|
686
|
+
- **Verification**: typecheck clean; full suite 1585 tests (18 pre-existing environment-bound failures per know-how 028, zero new — the clean-tree comparison yields the same set: symlink-EPERM family + client-lightbox ESM scheme + CRLF source anchor + network fetch); new pins cover plan T1–T17 (intent-ledger storage five states / npmTarballUrl three pins / two-step-rollback-degraded boundary nine pins including remove-failed short-circuit, cancelled copy, and reinstall build-blocked pendingBuilds passthrough / D3 and the SELF_PKG seam rerouting five pins / host-api boot reconciliation plus the upgrade(dsh-m) rerouting end-to-end / client degraded projection, two-site wiring, confirmation-dialog ordering, and pump accounting); ADR-0022 filed, layered commits (feat core/client + docs adr/glossary + chore release).
|
|
687
|
+
|
|
664
688
|
### Refactor + fix in 0.9.94 — client-side mutation-outcome copy single-sourced (op-outcome) + patch-mapping YAML quote tolerance and duplicate-key elimination
|
|
665
689
|
|
|
666
690
|
- **Need (2026-10-17 architecture walk + nine grilled decisions)**: the presentation of mutation outcomes had no owning module — the guard-interception copy was assembled verbatim three times in main.jsx (0.9.93's toWire converged the wire format while the client-side presentation remained the known leftover; the multi-point hand-patching recurrence when `repairBasis` was added had already been demonstrated once), the upgrade-success copy twice verbatim, the builds suffix four times, superseded five times; separately: the transaction's compensation was blind to **non-double-quoted in-place forms** of workspace `patchedDependencies` (pnpm's single-quoted/unquoted spellings) via verbatim duplicate checks — the verbatim check could fabricate a duplicate-key file and the read-back check could false-pass (the same-package chain is unreachable in production because strip removes it first; this item is defensive-surface convergence and a fail-closed gap-fill).
|
package/lib/client.js
CHANGED
|
@@ -1654,10 +1654,12 @@ function createOpsPump(store, getCtx) {
|
|
|
1654
1654
|
if (!err) {
|
|
1655
1655
|
const opMeta = value && typeof value === "object" && value.opMeta && typeof value.opMeta === "object" ? value.opMeta : null;
|
|
1656
1656
|
const cur = opMeta ? store.list().find((x) => x.id === queued.id) : null;
|
|
1657
|
+
const degraded = value && typeof value === "object" && value.degraded === true ? { degraded: true, restoredTo: value.restoredTo } : null;
|
|
1658
|
+
const mergedMeta = opMeta || degraded ? { ...cur && cur.meta || {}, ...opMeta || {}, ...degraded || {} } : null;
|
|
1657
1659
|
finalize({
|
|
1658
|
-
status: value && value.opWarning ? "warned" : "done",
|
|
1660
|
+
status: value && value.opWarning || degraded ? "warned" : "done",
|
|
1659
1661
|
warning: value && value.opWarning,
|
|
1660
|
-
...
|
|
1662
|
+
...mergedMeta ? { meta: mergedMeta } : {}
|
|
1661
1663
|
});
|
|
1662
1664
|
if (waitersEntry) waitersEntry.resolve(value);
|
|
1663
1665
|
} else {
|
|
@@ -2030,12 +2032,13 @@ function createOpOutcome(lookup2) {
|
|
|
2030
2032
|
}) + buildsSuffix(res) + (note.suffixKey ? lookup2(note.suffixKey) : "")
|
|
2031
2033
|
};
|
|
2032
2034
|
};
|
|
2035
|
+
const degradedOutcome = (res) => res && res.degraded === true ? { kind: "warn", text: lookup2("op.degraded", { restoredTo: res.restoredTo || "\u2014" }) } : null;
|
|
2033
2036
|
const mutationFailed = (e, { verb, target }) => {
|
|
2034
2037
|
if (e && e.opSuperseded) return { kind: "ok", text: lookup2("op.superseded.note", { target }) };
|
|
2035
2038
|
if (e && e.guard) return { kind: "err", text: guardBlockedText(e.guard) };
|
|
2036
2039
|
return { kind: "err", text: lookup2(PLAIN_FAIL_KEY[verb], { err: e && e.message || e }) };
|
|
2037
2040
|
};
|
|
2038
|
-
return { guardBlockedText, buildsSuffix, upgradeOkText, mutationFailed };
|
|
2041
|
+
return { guardBlockedText, buildsSuffix, upgradeOkText, degradedOutcome, mutationFailed };
|
|
2039
2042
|
}
|
|
2040
2043
|
var PLAIN_FAIL_KEY;
|
|
2041
2044
|
var init_op_outcome = __esm({
|
|
@@ -2345,8 +2348,12 @@ var ZH = {
|
|
|
2345
2348
|
"notify.upgraded.clientonly": "\u3002\u7EAF\u5BA2\u6237\u7AEF\u66F4\u65B0\uFF1A\u5237\u65B0\u9875\u9762\u5373\u53EF\u751F\u6548\uFF0C\u65E0\u9700\u91CD\u542F",
|
|
2346
2349
|
"notify.upgraded.activationUnknown": "\u3002\u751F\u6548\u5224\u5B9A\u672A\u5B8C\u6210\uFF1A\u4E3A\u786E\u4FDD\u751F\u6548\u8BF7\u91CD\u542F DSH Web",
|
|
2347
2350
|
"notify.upgradehint": "\uFF08\u6CE8\u610F\uFF1A\u8BE5\u63D2\u4EF6\u6267\u884C\u4E86\u6784\u5EFA\u811A\u672C\uFF09",
|
|
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",
|
|
2348
2352
|
"notify.selfupgraded": "dsh-m \u5DF2\u5347\u7EA7\uFF08v{from} \u2192 v{to}\uFF09",
|
|
2349
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",
|
|
2350
2357
|
"failed.install": "\u5B89\u88C5\u5931\u8D25\uFF1A{err}",
|
|
2351
2358
|
"failed.uninstall": "\u5378\u8F7D\u5931\u8D25\uFF1A{err}",
|
|
2352
2359
|
"failed.upgrade": "\u5347\u7EA7\u5931\u8D25\uFF1A{err}",
|
|
@@ -2645,8 +2652,12 @@ var EN = {
|
|
|
2645
2652
|
"notify.upgraded.clientonly": ". Client-only update: refresh the page to take effect \u2014 no restart needed",
|
|
2646
2653
|
"notify.upgraded.activationUnknown": ". Activation classification unavailable: restart DSH Web to ensure the new version is live",
|
|
2647
2654
|
"notify.upgradehint": " (note: this plugin ran build scripts)",
|
|
2655
|
+
"op.degraded": "Upgrade failed: the new version did not install \u2014 rolled back to {restoredTo}. The plugin still works; you can retry later.",
|
|
2648
2656
|
"notify.selfupgraded": "dsh-m upgraded (v{from} \u2192 v{to})",
|
|
2649
2657
|
"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",
|
|
2650
2661
|
"failed.install": "Install failed: {err}",
|
|
2651
2662
|
"failed.uninstall": "Uninstall failed: {err}",
|
|
2652
2663
|
"failed.upgrade": "Upgrade failed: {err}",
|
|
@@ -4164,9 +4175,15 @@ function MarketTab({ notify, markets, onMutation, ops, favorites, profileKind, o
|
|
|
4164
4175
|
const m = opUpgradeMeta2(r);
|
|
4165
4176
|
return m ? { ...r, opMeta: m } : r;
|
|
4166
4177
|
});
|
|
4167
|
-
const
|
|
4168
|
-
|
|
4169
|
-
|
|
4178
|
+
const degraded = oc.degradedOutcome(res);
|
|
4179
|
+
if (degraded) {
|
|
4180
|
+
notify({ kind: "warn", text: degraded.text });
|
|
4181
|
+
setInstallNote({ id: it.id, kind: "warn", text: degraded.text });
|
|
4182
|
+
} else {
|
|
4183
|
+
const ok = oc.upgradeOkText(res);
|
|
4184
|
+
notify({ kind: "ok", needsRestart: ok.needsRestart, text: ok.text });
|
|
4185
|
+
setInstallNote({ id: it.id, kind: "ok", text: ok.text });
|
|
4186
|
+
}
|
|
4170
4187
|
await (onMutation ? onMutation() : void 0);
|
|
4171
4188
|
} catch (e) {
|
|
4172
4189
|
const m = oc.mutationFailed(e, { verb: "upgrade", target: it.installedPkg });
|
|
@@ -4560,8 +4577,13 @@ function InstalledTab({ notify, installed, updates, onMutation, ops }) {
|
|
|
4560
4577
|
const m = opUpgradeMeta2(r);
|
|
4561
4578
|
return m ? { ...r, opMeta: m } : r;
|
|
4562
4579
|
});
|
|
4563
|
-
const
|
|
4564
|
-
|
|
4580
|
+
const degraded = oc.degradedOutcome(res);
|
|
4581
|
+
if (degraded) {
|
|
4582
|
+
notify({ kind: "warn", text: degraded.text });
|
|
4583
|
+
} else {
|
|
4584
|
+
const ok = oc.upgradeOkText(res);
|
|
4585
|
+
notify({ kind: "ok", needsRestart: ok.needsRestart, text: ok.text });
|
|
4586
|
+
}
|
|
4565
4587
|
await (onMutation ? onMutation() : reload());
|
|
4566
4588
|
} catch (e) {
|
|
4567
4589
|
const m = oc.mutationFailed(e, { verb: "upgrade", target: it.pkg });
|
|
@@ -5169,11 +5191,12 @@ var TABS = [
|
|
|
5169
5191
|
["installed", "tab.installed", "installed"],
|
|
5170
5192
|
["settings", "tab.settings", null]
|
|
5171
5193
|
];
|
|
5172
|
-
function DshmVersionChip({ version, notify, initialCheck, ops }) {
|
|
5194
|
+
function DshmVersionChip({ version, notify, initialCheck, ops, initialConfirm }) {
|
|
5173
5195
|
const [check, setCheck] = useState(
|
|
5174
5196
|
() => initialCheck !== void 0 ? initialCheck : typeof window !== "undefined" && window.localStorage ? readSelfCheckCache2(window.localStorage, { version }) : null
|
|
5175
5197
|
);
|
|
5176
5198
|
const [busy, setBusy] = useState(false);
|
|
5199
|
+
const [confirming, setConfirming] = useState(initialConfirm === true);
|
|
5177
5200
|
useEffect(() => {
|
|
5178
5201
|
if (initialCheck !== void 0) return;
|
|
5179
5202
|
let live = true;
|
|
@@ -5191,6 +5214,11 @@ function DshmVersionChip({ version, notify, initialCheck, ops }) {
|
|
|
5191
5214
|
}, [initialCheck]);
|
|
5192
5215
|
const doUpgrade = async () => {
|
|
5193
5216
|
if (busy) return;
|
|
5217
|
+
setConfirming(true);
|
|
5218
|
+
};
|
|
5219
|
+
const performUpgrade = async () => {
|
|
5220
|
+
if (busy) return;
|
|
5221
|
+
setConfirming(false);
|
|
5194
5222
|
setBusy(true);
|
|
5195
5223
|
try {
|
|
5196
5224
|
const res = await (ops ? ops.runOp("upgrade", "dsh-m", async () => {
|
|
@@ -5201,7 +5229,9 @@ function DshmVersionChip({ version, notify, initialCheck, ops }) {
|
|
|
5201
5229
|
setCheck({ current: version, latest: null, outdated: false, ahead: false });
|
|
5202
5230
|
notify({
|
|
5203
5231
|
kind: "ok",
|
|
5204
|
-
|
|
5232
|
+
// 0.10.0 R10:生效语义忠实透传宿主 application——applied/overridden → false(不弹重启横幅)、
|
|
5233
|
+
// restart-required → true;响应缺席时保守 true(旧行为兜底)
|
|
5234
|
+
needsRestart: res.needsRestart ?? true,
|
|
5205
5235
|
text: lookup("notify.selfupgraded", { from: version, to: res.version || "latest" }) + oc.buildsSuffix(res)
|
|
5206
5236
|
});
|
|
5207
5237
|
} catch (e) {
|
|
@@ -5217,6 +5247,36 @@ function DshmVersionChip({ version, notify, initialCheck, ops }) {
|
|
|
5217
5247
|
};
|
|
5218
5248
|
const state = deriveChipState2(check, version);
|
|
5219
5249
|
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
|
+
}
|
|
5220
5280
|
return h(
|
|
5221
5281
|
"button",
|
|
5222
5282
|
{
|
|
@@ -30,7 +30,7 @@ export const WRITE_GUIDANCE = {
|
|
|
30
30
|
'set-enabled': '',
|
|
31
31
|
upgrade: 'Desktop 暂不支持在市场内升级插件:请在官方 Desktop 的插件管理(Settings → Plugins)检查更新,或回到 DSH Web 使用 dsh-m 升级。',
|
|
32
32
|
uninstall: 'Desktop 暂不支持在市场内卸载插件:请使用官方 Desktop 的插件管理(Settings → Plugins)卸载。',
|
|
33
|
-
'self-upgrade': '
|
|
33
|
+
'self-upgrade': '当前宿主形态不支持面板内自更新:请在官方 Settings → Plugins 对 dsh-m 执行 remove + install(精确版本),或完全退出 Desktop 后用 `dsh plugin --profile desktop add dsh-m@<版本>`。',
|
|
34
34
|
restart: 'Desktop 不由 dsh-m 重启:请退出并重新打开 Desktop 应用(关闭窗口可能只是隐藏),让新安装的插件生效。',
|
|
35
35
|
};
|
|
36
36
|
export class ProfileUnsupportedError extends Error {
|
package/lib/core/host-api.js
CHANGED
|
@@ -13,9 +13,10 @@ import { runDoctor } from './doctor.js';
|
|
|
13
13
|
import { listInstalledWithMeta, listMarket, existingMarketIds, installFromRegistry, selfUpgrade, uninstallPlugin, upgradePlugin, InstallGuardError, } from './market.js';
|
|
14
14
|
import { runProfileTransaction, TransactionError } from './profile-transaction.js';
|
|
15
15
|
import { listExternalOperations, clearExternalOperations, removeExternalOperation } from './external-journal.js';
|
|
16
|
-
import {
|
|
16
|
+
import { readUpgradeIntent, resolveUpgradeIntent, writeUpgradeIntent } from './upgrade-intent.js';
|
|
17
|
+
import { readInstalledPluginReadme, listInstalledPlugins } from './installed.js';
|
|
17
18
|
import { loadPrimaryScreenshots } from './primary-screenshots.js';
|
|
18
|
-
import { isNewerVersion, npmLatest, npmPackumentReadme } from './versions.js';
|
|
19
|
+
import { isNewerVersion, npmLatest, npmTarballUrl, npmPackumentReadme } from './versions.js';
|
|
19
20
|
import { getCommunitySummary } from './community.js';
|
|
20
21
|
import { RegistryConfigError } from './registry-controller.js';
|
|
21
22
|
import { checkRegistryEntries } from './registry-check.js';
|
|
@@ -24,7 +25,7 @@ import { servingPort, scheduleRestart } from './restart.js';
|
|
|
24
25
|
import { togglePlugin, ToggleError } from './toggle.js';
|
|
25
26
|
import { IncompatibleError } from './compat-check.js';
|
|
26
27
|
import { ProfileUnsupportedError, assertWriteAllowed } from './active-profile.js';
|
|
27
|
-
import { DesktopOpsError, desktopInstallFromRegistry, desktopToggle, desktopUninstall, desktopUpgradeFromRegistry } from './profile-ops.js';
|
|
28
|
+
import { DesktopOpsError, desktopInstallFromRegistry, desktopToggle, desktopUninstall, desktopUpgradeFromRegistry, desktopSelfUpgradeViaTarball } from './profile-ops.js';
|
|
28
29
|
export class BadJsonError extends Error {
|
|
29
30
|
}
|
|
30
31
|
export class BodyTooLargeError extends Error {
|
|
@@ -226,12 +227,20 @@ export function createApiDispatcher(ctx) {
|
|
|
226
227
|
desktopToggle: ctx.deps?.desktopToggle ?? desktopToggle,
|
|
227
228
|
desktopUninstall: ctx.deps?.desktopUninstall ?? desktopUninstall,
|
|
228
229
|
desktopUpgrade: ctx.deps?.desktopUpgrade ?? desktopUpgradeFromRegistry,
|
|
230
|
+
desktopSelfUpgradeViaTarball: ctx.deps?.desktopSelfUpgradeViaTarball ?? desktopSelfUpgradeViaTarball,
|
|
231
|
+
readUpgradeIntent: ctx.deps?.readUpgradeIntent ?? readUpgradeIntent,
|
|
232
|
+
resolveUpgradeIntent: ctx.deps?.resolveUpgradeIntent ?? resolveUpgradeIntent,
|
|
233
|
+
writeUpgradeIntent: ctx.deps?.writeUpgradeIntent ?? writeUpgradeIntent,
|
|
234
|
+
npmTarballUrl: ctx.deps?.npmTarballUrl ?? npmTarballUrl,
|
|
235
|
+
listInstalled: ctx.deps?.listInstalled ?? listInstalledPlugins,
|
|
229
236
|
ensureService: ctx.deps?.ensureService,
|
|
230
237
|
listExternalOperations: ctx.deps?.listExternalOperations ?? listExternalOperations,
|
|
231
238
|
clearExternalOperations: ctx.deps?.clearExternalOperations ?? clearExternalOperations,
|
|
232
239
|
removeExternalOperation: ctx.deps?.removeExternalOperation ?? removeExternalOperation,
|
|
233
240
|
};
|
|
234
241
|
const cfg = () => ctx.controller.config;
|
|
242
|
+
// 0.10.0:开机对账每进程一次(dispatcher 生命周期 = 宿主一次 apply)
|
|
243
|
+
let selfCheckIntentDone = false;
|
|
235
244
|
// 0.9.0:宿主信任检查委派(含 ping 与未知 method,fail-closed);
|
|
236
245
|
// profile 为 host 生命周期内不可变对象(host.ts apply 期解析一次)
|
|
237
246
|
const rejectRequest = ctx.deps?.rejectRequest ?? (() => 403);
|
|
@@ -261,30 +270,55 @@ export function createApiDispatcher(ctx) {
|
|
|
261
270
|
};
|
|
262
271
|
break;
|
|
263
272
|
case 'self-check': {
|
|
273
|
+
// 0.10.0 开机对账(崩溃兜底网,每进程一次):上进程若死于自升级 promise 悬垂,delegated
|
|
274
|
+
// intent 遗留至此——与本进程实际版本比对收口;本进程内 D3 已就地 resolve 的(或无悬垂)
|
|
275
|
+
// 幂等 no-op。ADR-0018:读写独立 intent ledger,零外部日志。对账自身故障零涟漪。
|
|
276
|
+
let intentResolved;
|
|
277
|
+
if (!selfCheckIntentDone) {
|
|
278
|
+
selfCheckIntentDone = true;
|
|
279
|
+
try {
|
|
280
|
+
const rec = await d.readUpgradeIntent();
|
|
281
|
+
if (rec && rec.status === 'delegated') {
|
|
282
|
+
const match = rec.version === ctx.pkg.version;
|
|
283
|
+
await d.resolveUpgradeIntent({ ok: match, installedVersion: match ? ctx.pkg.version : undefined });
|
|
284
|
+
intentResolved = { ok: match, target: rec.target, ...(match ? { installedVersion: ctx.pkg.version } : {}) };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
// delegated 残留下次进程再收口;绝不阻塞 self-check
|
|
289
|
+
}
|
|
290
|
+
}
|
|
264
291
|
try {
|
|
265
292
|
const latest = await d.npmLatest(ctx.pkg.name, cfg().timeoutMs ?? 20_000);
|
|
266
293
|
const outdated = isNewerVersion(latest.version, ctx.pkg.version);
|
|
267
294
|
// ahead(0.8.0):本地 dev 版领先 npm 发布——设置页改显「本地开发版」而非误导性的旧「npm 最新」
|
|
268
295
|
const ahead = !outdated && latest.version !== ctx.pkg.version;
|
|
269
|
-
payload = { current: ctx.pkg.version, latest: latest.version, outdated, ahead };
|
|
296
|
+
payload = { current: ctx.pkg.version, latest: latest.version, outdated, ahead, ...(intentResolved ? { intentResolved } : {}) };
|
|
270
297
|
}
|
|
271
298
|
catch (err) {
|
|
272
|
-
payload = { current: ctx.pkg.version, latest: null, outdated: false, ahead: false, error: err instanceof Error ? err.message : String(err) };
|
|
299
|
+
payload = { current: ctx.pkg.version, latest: null, outdated: false, ahead: false, error: err instanceof Error ? err.message : String(err), ...(intentResolved ? { intentResolved } : {}) };
|
|
273
300
|
}
|
|
274
301
|
break;
|
|
275
302
|
}
|
|
276
303
|
case 'self-upgrade': {
|
|
277
304
|
assertWriteAllowed(profile, 'self-upgrade');
|
|
278
|
-
// 0.
|
|
279
|
-
//
|
|
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 层。
|
|
280
309
|
if (profile.kind !== 'web') {
|
|
281
|
-
const managed = await d.
|
|
282
|
-
|
|
283
|
-
profile: profile.name,
|
|
284
|
-
profileDir: profile.dir,
|
|
310
|
+
const managed = await d.desktopSelfUpgradeViaTarball(ctx.pkg.name, ctx.pkg.version, {
|
|
311
|
+
timeoutMs: cfg().timeoutMs,
|
|
285
312
|
signal,
|
|
286
|
-
|
|
287
|
-
|
|
313
|
+
}, {
|
|
314
|
+
getService: d.getService,
|
|
315
|
+
ensureService: d.ensureService,
|
|
316
|
+
npmLatest: d.npmLatest,
|
|
317
|
+
npmTarballUrl: d.npmTarballUrl,
|
|
318
|
+
writeUpgradeIntent: d.writeUpgradeIntent,
|
|
319
|
+
resolveUpgradeIntent: d.resolveUpgradeIntent,
|
|
320
|
+
listInstalled: d.listInstalled,
|
|
321
|
+
});
|
|
288
322
|
if ('needsBuildApproval' in managed) {
|
|
289
323
|
// build-blocked(自升级罕见):结构化透出,不冒充成功
|
|
290
324
|
payload = { ...managed };
|
|
@@ -295,7 +329,8 @@ export function createApiDispatcher(ctx) {
|
|
|
295
329
|
version: managed.version,
|
|
296
330
|
buildApprovals: managed.buildApprovals ?? [],
|
|
297
331
|
fallbackAllBuilds: false,
|
|
298
|
-
|
|
332
|
+
// R10:忠实透传宿主 application——applied/overridden → false;restart-required → true
|
|
333
|
+
needsRestart: managed.needsRestart,
|
|
299
334
|
};
|
|
300
335
|
break;
|
|
301
336
|
}
|
|
@@ -514,7 +549,16 @@ export function createApiDispatcher(ctx) {
|
|
|
514
549
|
profileDir: profile.dir,
|
|
515
550
|
signal,
|
|
516
551
|
channel: 'panel',
|
|
517
|
-
}, {
|
|
552
|
+
}, {
|
|
553
|
+
getService: d.getService,
|
|
554
|
+
ensureService: d.ensureService,
|
|
555
|
+
// 0.10.0:R5 previousVersion / SELF_PKG 缝改道注入透传(生产缺省 = 真实现;测试面经 overrides)
|
|
556
|
+
npmLatest: d.npmLatest,
|
|
557
|
+
listInstalled: d.listInstalled,
|
|
558
|
+
npmTarballUrl: d.npmTarballUrl,
|
|
559
|
+
writeUpgradeIntent: d.writeUpgradeIntent,
|
|
560
|
+
resolveUpgradeIntent: d.resolveUpgradeIntent,
|
|
561
|
+
})) };
|
|
518
562
|
break;
|
|
519
563
|
}
|
|
520
564
|
const version = typeof body.version === 'string' ? body.version : undefined;
|
|
@@ -572,7 +616,16 @@ export function createApiDispatcher(ctx) {
|
|
|
572
616
|
profileDir: profile.dir,
|
|
573
617
|
signal,
|
|
574
618
|
channel: 'panel',
|
|
575
|
-
}, {
|
|
619
|
+
}, {
|
|
620
|
+
getService: d.getService,
|
|
621
|
+
ensureService: d.ensureService,
|
|
622
|
+
// 0.10.0:两步化 previousVersion / SELF_PKG 缝改道 → D3 全链注入透传(生产缺省 = 真实现)
|
|
623
|
+
npmLatest: d.npmLatest,
|
|
624
|
+
listInstalled: d.listInstalled,
|
|
625
|
+
npmTarballUrl: d.npmTarballUrl,
|
|
626
|
+
writeUpgradeIntent: d.writeUpgradeIntent,
|
|
627
|
+
resolveUpgradeIntent: d.resolveUpgradeIntent,
|
|
628
|
+
})) };
|
|
576
629
|
break;
|
|
577
630
|
}
|
|
578
631
|
const result = await d.upgradePlugin(target, cfg(), {
|
package/lib/core/packument.js
CHANGED
|
@@ -255,6 +255,17 @@ export async function npmVersion(pkg, version, timeoutMs = 20_000, signal, regis
|
|
|
255
255
|
throw firstErr;
|
|
256
256
|
}
|
|
257
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* 精确版本 tarball URL(0.10.0,D3 自升级专用通道的取 URL 腿):
|
|
260
|
+
* `npmVersion` 薄封装——复用同一 L2① 履约阶梯(ADR-0012,含镜像 sync 腿与 npmjs 兜底),
|
|
261
|
+
* 不新增 packument 面。tarball 缺失 / 版本不存在 → 可读错误上抛。
|
|
262
|
+
*/
|
|
263
|
+
export async function npmTarballUrl(pkg, version, timeoutMs = 20_000, signal, registry, deps) {
|
|
264
|
+
const detail = await npmVersion(pkg, version, timeoutMs, signal, registry, deps);
|
|
265
|
+
if (!detail.tarball)
|
|
266
|
+
throw new Error(`${pkg}@${version} packument 缺 dist.tarball`);
|
|
267
|
+
return detail.tarball;
|
|
268
|
+
}
|
|
258
269
|
/**
|
|
259
270
|
* 升级探测(检测读)。L3 权威链(ADR-0012):新鲜度敏感——npmjs 权威源优先
|
|
260
271
|
* (首腿超时收紧 `Math.min(timeoutMs, 5_000)`),失败降级生效源(≠npmjs 时);
|
package/lib/core/profile-ops.js
CHANGED
|
@@ -28,7 +28,8 @@ import { loadRegistry as defaultLoadRegistry } from './registry.js';
|
|
|
28
28
|
import { fetchCommunityCatalog as defaultFetchCommunityCatalog } from './community.js';
|
|
29
29
|
import { listInstalledPlugins as defaultListInstalled } from './installed.js';
|
|
30
30
|
import { precheckNpmCompat, IncompatibleError } from './compat-check.js';
|
|
31
|
-
import { npmLatest as defaultNpmLatest, githubLatestTag as defaultGithubLatestTag } from './versions.js';
|
|
31
|
+
import { npmLatest as defaultNpmLatest, npmTarballUrl as defaultNpmTarballUrl, githubLatestTag as defaultGithubLatestTag } from './versions.js';
|
|
32
|
+
import { writeUpgradeIntent as defaultWriteUpgradeIntent, resolveUpgradeIntent as defaultResolveUpgradeIntent } from './upgrade-intent.js';
|
|
32
33
|
import { classifyUpgradeActivation } from './activation.js';
|
|
33
34
|
import { togglePlugin, ToggleError } from './toggle.js';
|
|
34
35
|
import { describeReleaseAgeFailure, releaseAgePrecheck } from './release-age.js';
|
|
@@ -177,12 +178,37 @@ async function desktopInstallLocked(id, cfg, opts, deps) {
|
|
|
177
178
|
if (gate.notice)
|
|
178
179
|
releaseAgeNotice = `;${gate.notice}`;
|
|
179
180
|
}
|
|
181
|
+
// 0.10.0 R5:install 目标已装(0.2.1 宿主 already-installed 守卫必拦)→ 预查旧版本号,
|
|
182
|
+
// 使 runManagedInstall 在守卫命中时可静默两步化(remove+add);全新包(未命中)不注入——
|
|
183
|
+
// T6 钉零 removeBundle。探测失败 fail-open(守卫命中且无 previousVersion 时走既有失败形态)。
|
|
184
|
+
let previousVersion;
|
|
185
|
+
if (opts.profileDir) {
|
|
186
|
+
try {
|
|
187
|
+
const { items } = await (deps.listInstalled ?? defaultListInstalled)(opts.profileDir);
|
|
188
|
+
previousVersion = items.find((it) => it.pkg === pkg)?.version;
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
previousVersion = undefined;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// 0.10.0 R6(install 臂):dsh-m 已装态下的 install 调用实为升级(守卫必拦)→ 改道 D3;
|
|
195
|
+
// 全新安装(未装)走既有单步——守卫不触发,R1「新装路径不动」。
|
|
196
|
+
if (pkg === SELF_PKG && previousVersion !== undefined) {
|
|
197
|
+
return desktopSelfUpgradeViaTarballLocked(pkg, previousVersion, { timeoutMs, signal: opts.signal }, {
|
|
198
|
+
getService: () => service,
|
|
199
|
+
npmLatest: deps.npmLatest,
|
|
200
|
+
npmTarballUrl: deps.npmTarballUrl,
|
|
201
|
+
writeUpgradeIntent: deps.writeUpgradeIntent,
|
|
202
|
+
resolveUpgradeIntent: deps.resolveUpgradeIntent,
|
|
203
|
+
listInstalled: deps.listInstalled,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
180
206
|
let change;
|
|
181
207
|
let bundleName;
|
|
182
208
|
let registerNote;
|
|
183
209
|
let managed;
|
|
184
210
|
try {
|
|
185
|
-
managed = await runManagedInstall(service, { pkg, spec, version, profileDir: opts.profileDir, approvedBuilds: opts.approvedBuilds, listInstalled: deps.listInstalled }, { governExclude: deps.governExclude, registerExclude: deps.registerExclude, packumentTimes: deps.packumentTimes });
|
|
211
|
+
managed = await runManagedInstall(service, { pkg, spec, version, ...(previousVersion !== undefined ? { previousVersion } : {}), profileDir: opts.profileDir, approvedBuilds: opts.approvedBuilds, listInstalled: deps.listInstalled }, { governExclude: deps.governExclude, registerExclude: deps.registerExclude, packumentTimes: deps.packumentTimes });
|
|
186
212
|
change = managed.change;
|
|
187
213
|
bundleName = managed.bundleName;
|
|
188
214
|
registerNote = managed.registerNote;
|
|
@@ -200,6 +226,9 @@ async function desktopInstallLocked(id, cfg, opts, deps) {
|
|
|
200
226
|
throw err;
|
|
201
227
|
}
|
|
202
228
|
const retryNote = managed.governRetryNote !== undefined && !governanceNote.includes(managed.governRetryNote) ? managed.governRetryNote : '';
|
|
229
|
+
const twoStepSuffix = managed.twoStepNote !== undefined ? `;${managed.twoStepNote}` : '';
|
|
230
|
+
const degradedRestoredTo = managed.degraded === true ? managed.restoredTo : undefined;
|
|
231
|
+
const degradedSuffix = degradedRestoredTo !== undefined ? `;升级失败已回装旧版 ${degradedRestoredTo}(降级恢复)` : '';
|
|
203
232
|
return {
|
|
204
233
|
id: entry.id,
|
|
205
234
|
pkg,
|
|
@@ -212,9 +241,12 @@ async function desktopInstallLocked(id, cfg, opts, deps) {
|
|
|
212
241
|
...(compat !== null ? { compat } : {}),
|
|
213
242
|
...(compatSkipped ? { compatSkipped } : {}),
|
|
214
243
|
needsRestart: true,
|
|
215
|
-
output: `official pluginManager: application=${change.application ?? 'applied'}, bundle=${bundleName}${change.changed === false ? ', changed=false' : ''}${governanceNote}${retryNote}${releaseAgeNotice}${registerNote ?? ''}`,
|
|
244
|
+
output: `official pluginManager: application=${change.application ?? 'applied'}, bundle=${bundleName}${change.changed === false ? ', changed=false' : ''}${governanceNote}${retryNote}${releaseAgeNotice}${registerNote ?? ''}${twoStepSuffix}${degradedSuffix}`,
|
|
216
245
|
via: 'desktop-manager',
|
|
217
246
|
...(change.application === 'overridden' ? { overridden: true } : {}),
|
|
247
|
+
...(degradedRestoredTo !== undefined ? { degraded: true, restoredTo: degradedRestoredTo } : {}),
|
|
248
|
+
// 0.10.0(评审 R1-2):降级恢复的 version 落账为回装后实际版本(账实相符);spec 字段保留尝试目标
|
|
249
|
+
...(degradedRestoredTo !== undefined ? { version: degradedRestoredTo } : {}),
|
|
218
250
|
};
|
|
219
251
|
}
|
|
220
252
|
/**
|
|
@@ -284,11 +316,42 @@ function isGovableFailure(change) {
|
|
|
284
316
|
return false;
|
|
285
317
|
return change.application === 'failed' || change.error !== undefined;
|
|
286
318
|
}
|
|
319
|
+
/** 官方 pendingBuilds 的鸭子读投影(strings-only)。 */
|
|
320
|
+
function pendingOf(change) {
|
|
321
|
+
return Array.isArray(change.pendingBuilds) ? change.pendingBuilds.filter((n) => typeof n === 'string') : [];
|
|
322
|
+
}
|
|
323
|
+
/** 终态失败(与 runManagedInstall 既有失败闸门同口径)。 */
|
|
324
|
+
function isTerminalFailure(change) {
|
|
325
|
+
return change.application === 'failed' || change.application === 'cancelled' || change.error !== undefined;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* 宿主 0.2.1 already-installed 安装守卫命中(0.10.0,R2):application=failed +
|
|
329
|
+
* error.code 精确匹配,且 release-age / build-blocked 形态先于本判定(等待期与审批语义优先)。
|
|
330
|
+
*/
|
|
331
|
+
function isAlreadyInstalledGuard(change) {
|
|
332
|
+
if (change.application !== 'failed' || change.error?.code !== 'already-installed')
|
|
333
|
+
return false;
|
|
334
|
+
if (change.packageResult?.kind === 'build-blocked' && pendingOf(change).length > 0)
|
|
335
|
+
return false;
|
|
336
|
+
return !/ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION|minimumReleaseAge/i.test(`${change.error?.diagnostic ?? ''}\n${change.error?.code ?? ''}`);
|
|
337
|
+
}
|
|
287
338
|
async function runManagedInstall(service, ctx, deps = {}) {
|
|
288
|
-
const installOnce = async () => changeOf(await service.installBundle(
|
|
339
|
+
const installOnce = async (spec = ctx.spec) => changeOf(await service.installBundle(spec, {
|
|
289
340
|
enabled: true,
|
|
290
341
|
...(ctx.approvedBuilds && ctx.approvedBuilds.length > 0 ? { approvedBuilds: ctx.approvedBuilds } : {}),
|
|
291
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
|
+
};
|
|
292
355
|
/** 失败映射(既有语义不变:cancelled / build-blocked 审批 / release-age 等待 / enable / 通用)。 */
|
|
293
356
|
const mapFailure = async (change) => {
|
|
294
357
|
if (change.application === 'cancelled') {
|
|
@@ -344,14 +407,8 @@ async function runManagedInstall(service, ctx, deps = {}) {
|
|
|
344
407
|
}
|
|
345
408
|
return { change, bundleName };
|
|
346
409
|
};
|
|
347
|
-
let change = await
|
|
348
|
-
//
|
|
349
|
-
// cancelled / build-blocked / release-age 等其他失败形态不重试)
|
|
350
|
-
if (change.stage === 'enable' && (change.application === 'failed' || change.error)) {
|
|
351
|
-
if (enableRetryDelayMs > 0)
|
|
352
|
-
await sleepMs(enableRetryDelayMs);
|
|
353
|
-
change = await installOnce();
|
|
354
|
-
}
|
|
410
|
+
let change = await installOnceRetryingEnable(ctx.spec);
|
|
411
|
+
// (enable 阶段竞速自动重试已普适化进 installOnceRetryingEnable——首腿/两步第二腿/兜底回装腿/D3 同享)
|
|
355
412
|
// ADR-0009 挂点3:双码失败 → 治理 → changed 则重试该命令一次(每次调用至多一次;
|
|
356
413
|
// cancelled / build-blocked 不治理不重试)。仍败走既有 mapFailure(含账实分裂复读);
|
|
357
414
|
// 挂点3 已落盘的合并以短语并入失败 message 与成功 output(留痕契约含挂点3)。
|
|
@@ -364,6 +421,102 @@ async function runManagedInstall(service, ctx, deps = {}) {
|
|
|
364
421
|
change = await installOnce();
|
|
365
422
|
}
|
|
366
423
|
}
|
|
424
|
+
// 0.10.0(宿主 0.2.1 already-installed 守卫适配,R2/R3):首腿守卫命中 → 静默两步化
|
|
425
|
+
// (removeBundle 旧版 → 重试安装同一 spec,至多一次、无循环);第二腿终态失败 → 兜底回装旧版
|
|
426
|
+
// (remove 后旧名已不在 manifest,守卫不适用):回装成功 = 降级恢复结局(degraded,R4);
|
|
427
|
+
// cancelled / build-blocked 语义优先(不冒充降级);回装失败 = 缺席硬错误(rollback-failed)。
|
|
428
|
+
// 0.10.1(E2E 实证 ③):去掉 previousVersion 前置门——卸载后同会话重装时,宿主活跃态/manifest
|
|
429
|
+
// 字面仍视其在装(守卫命中),而探测口径为「未安装」;守卫命中即 remove+add(官方语义),
|
|
430
|
+
// 无旧版可回装时失败走账实复读给恢复指引。
|
|
431
|
+
let twoStepNote;
|
|
432
|
+
if (isAlreadyInstalledGuard(change) && typeof service.removeBundle === 'function') {
|
|
433
|
+
twoStepNote = 'already-installed → two-step';
|
|
434
|
+
const removeChange = changeOf(await service.removeBundle(ctx.pkg));
|
|
435
|
+
if (isTerminalFailure(removeChange)) {
|
|
436
|
+
// remove 腿失败短路(不回装):旧版尚未移除,状态以官方插件页为准
|
|
437
|
+
throw new DesktopOpsError('remove-failed', `两步升级第一步(卸载旧版)失败(${ctx.pkg};application=${removeChange.application ?? 'failed'}${removeChange.error?.code ? `、error=${removeChange.error.code}` : ''}):未执行升级,请到官方 Desktop 插件页确认 ${ctx.pkg} 状态`);
|
|
438
|
+
}
|
|
439
|
+
change = await installOnceRetryingEnable(ctx.spec);
|
|
440
|
+
if (isTerminalFailure(change)) {
|
|
441
|
+
const pendingAdd = pendingOf(change);
|
|
442
|
+
const addDiagnostic = `${change.error?.diagnostic ?? ''}${change.error?.code ? `\n${change.error.code}` : ''}`.trim();
|
|
443
|
+
const addCancelled = change.application === 'cancelled';
|
|
444
|
+
const addBuildBlocked = change.packageResult?.kind === 'build-blocked' && pendingAdd.length > 0;
|
|
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
|
+
};
|
|
490
|
+
}
|
|
491
|
+
// 0.10.1(E2E 实证 ③):无 previousVersion——remove 已按官方语义执行、无旧版可回装。
|
|
492
|
+
// build-blocked / cancelled 语义照旧;其余按账实复读给恢复指引(缺席硬错误族)。
|
|
493
|
+
if (addBuildBlocked) {
|
|
494
|
+
throw new DesktopBuildApprovalNeeded(pendingAdd);
|
|
495
|
+
}
|
|
496
|
+
if (addCancelled) {
|
|
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
|
+
}
|
|
508
|
+
}
|
|
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
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
367
520
|
if (change.application === 'failed' || change.application === 'cancelled' || change.error) {
|
|
368
521
|
try {
|
|
369
522
|
await mapFailure(change);
|
|
@@ -391,7 +544,11 @@ async function runManagedInstall(service, ctx, deps = {}) {
|
|
|
391
544
|
// fail-open:登记失败不影响安装/升级成功态
|
|
392
545
|
}
|
|
393
546
|
}
|
|
394
|
-
return {
|
|
547
|
+
return {
|
|
548
|
+
...verified,
|
|
549
|
+
...(registerNote !== undefined || governRetryNote !== undefined ? { registerNote, governRetryNote } : {}),
|
|
550
|
+
...(twoStepNote !== undefined ? { twoStepNote } : {}),
|
|
551
|
+
};
|
|
395
552
|
}
|
|
396
553
|
/**
|
|
397
554
|
* Desktop 卸载(0.9.8,dsh-market 同策略):removeBundle 委派 + ChangeResult 判定 +
|
|
@@ -469,12 +626,118 @@ export async function desktopUpgradeFromRegistry(pkg, cfg = {}, opts = {}, deps
|
|
|
469
626
|
throw err;
|
|
470
627
|
}
|
|
471
628
|
}
|
|
629
|
+
/** 0.10.0 R6:自升级专用通道的目标包(core 缝改道唯一判据;host-api/tools/客户端零特判)。 */
|
|
630
|
+
export const SELF_PKG = 'dsh-m';
|
|
631
|
+
/**
|
|
632
|
+
* Desktop 自升级专用通道(0.10.0,R6/R7/D3):npmLatest 精确版本 → 生效 registry packument 的
|
|
633
|
+
* dist.tarball → 预写 intent → installBundle(tarballUrl, {enabled:true}) 直覆盖——tarball spec
|
|
634
|
+
* 的包名在 pnpm 前不可知,不受宿主 0.2.1 already-installed 守卫约束(本机 spike 实证)。
|
|
635
|
+
* - 恒 panel 语义零外部日志(ADR-0018):intent 预写落独立 ledger(upgrade-intent.ts),不入 external journal;
|
|
636
|
+
* - 同版本 → install-refused no-op(T10);URL 构造失败发生在预写之前 → 零副作用失败(T12);
|
|
637
|
+
* - install 终态失败 → 旧版不受影响(spike 实证)+ intent 收口 failed;
|
|
638
|
+
* - 成功(applied/restart-required/overridden)→ intent 收口 ok + installedVersion;
|
|
639
|
+
* - promise 悬垂(进程死于活交换)→ intent 保持 delegated,由 self-check 对账收口(崩溃兜底网)。
|
|
640
|
+
* 导出层自包 withMutationSession(market.ts 同惯例);core 缝改道发生在宿主 upgrade/install 的
|
|
641
|
+
* session 之内,直接调 Locked 内层避免嵌套。
|
|
642
|
+
*/
|
|
643
|
+
export async function desktopSelfUpgradeViaTarball(pkgName, currentVersion, opts = {}, deps = {}) {
|
|
644
|
+
return withMutationSession(() => desktopSelfUpgradeViaTarballLocked(pkgName, currentVersion, opts, deps));
|
|
645
|
+
}
|
|
646
|
+
async function desktopSelfUpgradeViaTarballLocked(pkgName, currentVersion, opts, deps) {
|
|
647
|
+
const service = await resolveManager(deps);
|
|
648
|
+
if (!service || typeof service.installBundle !== 'function') {
|
|
649
|
+
throw new DesktopOpsError('no-manager', '官方 pluginManager 服务不可用(desktop profile 的自升级必须由官方管理器执行);拒绝自升级(fail-closed)');
|
|
650
|
+
}
|
|
651
|
+
const timeoutMs = opts.timeoutMs ?? 20_000;
|
|
652
|
+
const latest = await (deps.npmLatest ?? defaultNpmLatest)(pkgName, timeoutMs, opts.signal);
|
|
653
|
+
if (latest.version === currentVersion) {
|
|
654
|
+
throw new DesktopOpsError('install-refused', `已是最新版本(${pkgName}@${currentVersion}),无需升级`);
|
|
655
|
+
}
|
|
656
|
+
const spec = await (deps.npmTarballUrl ?? defaultNpmTarballUrl)(pkgName, latest.version, timeoutMs, opts.signal);
|
|
657
|
+
// 预写 intent(崩溃兜底网):URL 构造失败已在此前抛出,零落盘(T12)
|
|
658
|
+
await (deps.writeUpgradeIntent ?? defaultWriteUpgradeIntent)({
|
|
659
|
+
target: pkgName,
|
|
660
|
+
version: latest.version,
|
|
661
|
+
...(currentVersion !== '' ? { fromVersion: currentVersion } : {}),
|
|
662
|
+
at: Date.now(),
|
|
663
|
+
});
|
|
664
|
+
const resolveIntent = deps.resolveUpgradeIntent ?? defaultResolveUpgradeIntent;
|
|
665
|
+
try {
|
|
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
|
+
}
|
|
673
|
+
if (isTerminalFailure(change)) {
|
|
674
|
+
const pending = pendingOf(change);
|
|
675
|
+
if (change.application === 'cancelled') {
|
|
676
|
+
throw new DesktopOpsError('install-refused', `自升级已被取消(${pkgName}):当前版本 ${currentVersion} 不受影响`);
|
|
677
|
+
}
|
|
678
|
+
if (change.packageResult?.kind === 'build-blocked' && pending.length > 0) {
|
|
679
|
+
throw new DesktopBuildApprovalNeeded(pending);
|
|
680
|
+
}
|
|
681
|
+
throw new DesktopOpsError('install-refused', `自升级失败(${pkgName}@${latest.version};application=${change.application ?? 'failed'}${change.error?.code ? `、error=${change.error.code}` : ''}):当前版本 ${currentVersion} 不受影响,可稍后重试`);
|
|
682
|
+
}
|
|
683
|
+
const bundleName = change.bundle ?? pkgName;
|
|
684
|
+
if (typeof service.listBundles === 'function') {
|
|
685
|
+
const bundles = await service.listBundles();
|
|
686
|
+
const found = (bundles ?? []).find((b) => b && b.name === bundleName);
|
|
687
|
+
if (!found || found.installed === false) {
|
|
688
|
+
throw new DesktopOpsError('verify-failed', `自升级复读失败(${pkgName}):官方结果为 ${change.application ?? 'unknown'} 但 listBundles 未見目标 bundle 在装——不冒充升级成功`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
await resolveIntent({ ok: true, installedVersion: latest.version });
|
|
692
|
+
return {
|
|
693
|
+
id: pkgName,
|
|
694
|
+
pkg: pkgName,
|
|
695
|
+
spec,
|
|
696
|
+
version: latest.version,
|
|
697
|
+
buildApprovals: Array.isArray(change.approvedBuilds) ? change.approvedBuilds : [],
|
|
698
|
+
fallbackAllBuilds: false,
|
|
699
|
+
// R10 忠实透传宿主 application:restart-required → true;applied/overridden → false
|
|
700
|
+
needsRestart: change.application === 'restart-required',
|
|
701
|
+
output: `official pluginManager (self-upgrade via tarball): application=${change.application ?? 'applied'}, bundle=${bundleName}`,
|
|
702
|
+
via: 'desktop-manager',
|
|
703
|
+
...(change.application === 'overridden' ? { overridden: true } : {}),
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
catch (err) {
|
|
707
|
+
// 未竟路径统一收口 intent 为 failed(成功臂已 resolve 时幂等 no-op);
|
|
708
|
+
// 收口失败不遮蔽原始错误——delegated 残留由 self-check 对账兜底。
|
|
709
|
+
try {
|
|
710
|
+
await resolveIntent({ ok: false });
|
|
711
|
+
}
|
|
712
|
+
catch {
|
|
713
|
+
/* 对账兜底仍在 */
|
|
714
|
+
}
|
|
715
|
+
throw err;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
472
718
|
async function desktopUpgradeLocked(pkg, cfg, opts, deps) {
|
|
473
719
|
const service = await resolveManager(deps);
|
|
474
720
|
if (!service || typeof service.installBundle !== 'function') {
|
|
475
721
|
throw new DesktopOpsError('no-manager', '官方 pluginManager 服务不可用(desktop profile 的升级必须由官方管理器执行覆盖安装);拒绝升级(fail-closed)');
|
|
476
722
|
}
|
|
477
723
|
const timeoutMs = cfg.timeoutMs ?? 20_000;
|
|
724
|
+
// 0.10.0 R6 自升级通道分治(唯一改道点):desktop 上「升级 dsh-m 自身」一律 D3 tarball 直覆盖——
|
|
725
|
+
// remove+add 对调用者自身是破坏性路径,绝不落入通用两步;host-api `case 'upgrade'`、tools
|
|
726
|
+
// `dshm_upgrade` 与一切经 core 的入口在此自动获得同语义(客户端与工具层零特判)。
|
|
727
|
+
if (pkg === SELF_PKG) {
|
|
728
|
+
const { items } = await (deps.listInstalled ?? defaultListInstalled)(opts.profileDir);
|
|
729
|
+
const selfTarget = items.find((it) => it.pkg === pkg);
|
|
730
|
+
if (!selfTarget)
|
|
731
|
+
throw new DesktopOpsError('install-refused', `desktop profile 未安装该插件: ${pkg}(升级目标必须是已装插件)`);
|
|
732
|
+
return desktopSelfUpgradeViaTarballLocked(pkg, selfTarget.version, { timeoutMs, signal: opts.signal }, {
|
|
733
|
+
getService: () => service,
|
|
734
|
+
npmLatest: deps.npmLatest,
|
|
735
|
+
npmTarballUrl: deps.npmTarballUrl,
|
|
736
|
+
writeUpgradeIntent: deps.writeUpgradeIntent,
|
|
737
|
+
resolveUpgradeIntent: deps.resolveUpgradeIntent,
|
|
738
|
+
listInstalled: deps.listInstalled,
|
|
739
|
+
});
|
|
740
|
+
}
|
|
478
741
|
const loaded = await (deps.loadRegistry ?? defaultLoadRegistry)(cfg, { namespace: opts.namespace ?? 'host', profile: opts.profile });
|
|
479
742
|
if (loaded.status === 'unavailable') {
|
|
480
743
|
throw new DesktopOpsError('install-refused', `收录清单不可用,无法升级 ${pkg};请检查 registry 配置或网络后重试`);
|
|
@@ -594,6 +857,9 @@ async function desktopUpgradeLocked(pkg, cfg, opts, deps) {
|
|
|
594
857
|
throw err;
|
|
595
858
|
}
|
|
596
859
|
const retryNote = managed.governRetryNote !== undefined && !governanceNote.includes(managed.governRetryNote) ? managed.governRetryNote : '';
|
|
860
|
+
const twoStepSuffix = managed.twoStepNote !== undefined ? `;${managed.twoStepNote}` : '';
|
|
861
|
+
const degradedRestoredTo = managed.degraded === true ? managed.restoredTo : undefined;
|
|
862
|
+
const degradedSuffix = degradedRestoredTo !== undefined ? `;升级失败已回装旧版 ${degradedRestoredTo}(降级恢复)` : '';
|
|
597
863
|
// 0.9.22 生效判定:npm 源升级在成功点分类;任何异常 fail-open 到 unknown,绝不影响升级成功态
|
|
598
864
|
let activation;
|
|
599
865
|
let needsRestart = true;
|
|
@@ -619,9 +885,12 @@ async function desktopUpgradeLocked(pkg, cfg, opts, deps) {
|
|
|
619
885
|
...(compat !== null ? { compat } : {}),
|
|
620
886
|
...(compatSkipped ? { compatSkipped } : {}),
|
|
621
887
|
needsRestart,
|
|
622
|
-
output: `official pluginManager: application=${change.application ?? 'applied'}, bundle=${bundleName}${change.changed === false ? ', changed=false' : ''}${governanceNote}${retryNote}${releaseAgeNotice}${registerNote ?? ''}`,
|
|
888
|
+
output: `official pluginManager: application=${change.application ?? 'applied'}, bundle=${bundleName}${change.changed === false ? ', changed=false' : ''}${governanceNote}${retryNote}${releaseAgeNotice}${registerNote ?? ''}${twoStepSuffix}${degradedSuffix}`,
|
|
623
889
|
via: 'desktop-manager',
|
|
624
890
|
...(change.application === 'overridden' ? { overridden: true } : {}),
|
|
891
|
+
...(degradedRestoredTo !== undefined ? { degraded: true, restoredTo: degradedRestoredTo } : {}),
|
|
892
|
+
// 0.10.0(评审 R1-2):降级恢复的 version 落账为回装后实际版本(账实相符);spec 字段保留尝试目标
|
|
893
|
+
...(degradedRestoredTo !== undefined ? { version: degradedRestoredTo } : {}),
|
|
625
894
|
fromVersion: target.version,
|
|
626
895
|
...(activation ? { activation } : {}),
|
|
627
896
|
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 自升级 intent ledger(0.10.0,ADR-0018 合规):panel 通道零外部日志 ⇒ 自升级 delegated
|
|
3
|
+
* 预写不进 external journal,落独立 ledger——定位为**崩溃兜底网**:D3 正常路径在本进程内
|
|
4
|
+
* 就地 resolve,仅当进程死于 promise 悬垂(活交换中进程死亡)时由下一次 self-check 对账
|
|
5
|
+
* 收口(host-api 挂载)。记录机器本地、单条覆盖写、不含凭据。
|
|
6
|
+
*
|
|
7
|
+
* - 存储文件:`$DSH_HOME/dshm/self-upgrade-intent.json`(`DSHM_UPGRADE_INTENT_FILE`
|
|
8
|
+
* 可整文件覆盖供测试;与 external-operations.json 同基址、不带 profile 维度)。
|
|
9
|
+
* - write:mkdir recursive 先行 → tmp+rename 原子替换(external-journal 同款惯例)。
|
|
10
|
+
* - read:文件缺失 = undefined;损坏 → 改名 `.corrupt-<ts>` 隔离留证后按空处理
|
|
11
|
+
* (best-effort,隔离失败同样不抛——读路径永不炸)。
|
|
12
|
+
* - resolve:仅收口悬垂(status='delegated')记录并落盘终态;无悬垂返回 undefined(幂等)。
|
|
13
|
+
*/
|
|
14
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
15
|
+
import { dirname, join } from 'node:path';
|
|
16
|
+
import { dshHome } from './env.js';
|
|
17
|
+
/** ledger 文件路径:env 整文件覆盖优先(测试隔离),缺省 $DSH_HOME/dshm/self-upgrade-intent.json。 */
|
|
18
|
+
export function upgradeIntentFile() {
|
|
19
|
+
return process.env.DSHM_UPGRADE_INTENT_FILE || join(dshHome(), 'dshm', 'self-upgrade-intent.json');
|
|
20
|
+
}
|
|
21
|
+
/** 显式字段白名单重组(超集字段一律不落盘)。 */
|
|
22
|
+
function normalizeRecord(record) {
|
|
23
|
+
return {
|
|
24
|
+
target: record.target,
|
|
25
|
+
version: record.version,
|
|
26
|
+
...(record.fromVersion !== undefined ? { fromVersion: record.fromVersion } : {}),
|
|
27
|
+
at: record.at,
|
|
28
|
+
status: record.status,
|
|
29
|
+
...(record.outcome !== undefined ? { outcome: record.outcome } : {}),
|
|
30
|
+
...(record.resolvedAt !== undefined ? { resolvedAt: record.resolvedAt } : {}),
|
|
31
|
+
...(record.installedVersion !== undefined ? { installedVersion: record.installedVersion } : {}),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async function atomicWrite(path, text) {
|
|
35
|
+
const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
36
|
+
await writeFile(tmp, text);
|
|
37
|
+
await rename(tmp, path);
|
|
38
|
+
}
|
|
39
|
+
/** 损坏文件隔离留证(best-effort,与 external-journal 同款两级重试;失败则放弃隔离按空继续)。 */
|
|
40
|
+
async function quarantine(file) {
|
|
41
|
+
try {
|
|
42
|
+
await rename(file, `${file}.corrupt-${Date.now()}`);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
try {
|
|
46
|
+
// 同毫秒重入等极小概率命名冲突:换一个带随机尾缀的名字再试一次;仍败则放弃隔离
|
|
47
|
+
await rename(file, `${file}.corrupt-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
/* 放弃隔离:按空继续 */
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** 读当前记录;文件缺失 → undefined;损坏/形态非法 → 隔离后按空处理(不抛)。 */
|
|
55
|
+
export async function readUpgradeIntent() {
|
|
56
|
+
const file = upgradeIntentFile();
|
|
57
|
+
let text;
|
|
58
|
+
try {
|
|
59
|
+
text = await readFile(file, 'utf8');
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
if (err.code === 'ENOENT')
|
|
63
|
+
return undefined;
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
let parsed;
|
|
67
|
+
try {
|
|
68
|
+
parsed = JSON.parse(text);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
await quarantine(file);
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
75
|
+
await quarantine(file);
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
return normalizeRecord(parsed);
|
|
79
|
+
}
|
|
80
|
+
/** 预写一条 delegated 意图(status 由 writer 盖章;调用方不传 status/outcome 等终态字段)。 */
|
|
81
|
+
export async function writeUpgradeIntent(input) {
|
|
82
|
+
const file = upgradeIntentFile();
|
|
83
|
+
await mkdir(dirname(file), { recursive: true });
|
|
84
|
+
const record = normalizeRecord({ ...input, status: 'delegated' });
|
|
85
|
+
await atomicWrite(file, `${JSON.stringify(record, null, 2)}\n`);
|
|
86
|
+
return record;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 收口悬垂记录:status → resolved、outcome 按 ok 盖章、resolvedAt 就地取值、
|
|
90
|
+
* installedVersion 可选回填;落盘后返回被收口的记录。无悬垂(缺文件/已 resolved)→
|
|
91
|
+
* undefined(幂等,不冒充收口)。
|
|
92
|
+
*/
|
|
93
|
+
export async function resolveUpgradeIntent(input) {
|
|
94
|
+
const current = await readUpgradeIntent();
|
|
95
|
+
if (!current || current.status !== 'delegated')
|
|
96
|
+
return undefined;
|
|
97
|
+
const resolved = normalizeRecord({
|
|
98
|
+
...current,
|
|
99
|
+
status: 'resolved',
|
|
100
|
+
outcome: input.ok ? 'ok' : 'failed',
|
|
101
|
+
resolvedAt: Date.now(),
|
|
102
|
+
...(input.installedVersion !== undefined ? { installedVersion: input.installedVersion } : {}),
|
|
103
|
+
});
|
|
104
|
+
const file = upgradeIntentFile();
|
|
105
|
+
await mkdir(dirname(file), { recursive: true });
|
|
106
|
+
await atomicWrite(file, `${JSON.stringify(resolved, null, 2)}\n`);
|
|
107
|
+
return resolved;
|
|
108
|
+
}
|
package/lib/core/versions.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { gt } from 'semver';
|
|
12
12
|
import { fetchJsonLimited, HttpError } from './httpx.js';
|
|
13
13
|
import { isExactVersion } from './packument.js';
|
|
14
|
-
export { npmVersion, npmPackument, npmPackumentReadme, npmLatest, extractGithubRepo, isExactVersion, } from './packument.js';
|
|
14
|
+
export { npmVersion, npmTarballUrl, npmPackument, npmPackumentReadme, npmLatest, extractGithubRepo, isExactVersion, } from './packument.js';
|
|
15
15
|
/** GitHub 匿名限额(60 次/小时/IP)用尽时返回可读提示(含重置等待分钟数),否则 null。 */
|
|
16
16
|
function githubRateLimitMessage(err) {
|
|
17
17
|
if (err instanceof HttpError && err.status === 403 && err.headers?.get('x-ratelimit-remaining') === '0') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-m",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
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"
|