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 +24 -0
- package/lib/client.js +2 -40
- package/lib/core/host-api.js +6 -4
- package/lib/core/profile-ops.js +123 -36
- 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.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
|
|
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
|
-
|
|
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
|
{
|
package/lib/core/host-api.js
CHANGED
|
@@ -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
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
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,
|
package/lib/core/profile-ops.js
CHANGED
|
@@ -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
|
|
399
|
-
//
|
|
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
|
-
//
|
|
428
|
+
// 0.10.1(E2E 实证 ③):去掉 previousVersion 前置门——卸载后同会话重装时,宿主活跃态/manifest
|
|
429
|
+
// 字面仍视其在装(守卫命中),而探测口径为「未安装」;守卫命中即 remove+add(官方语义),
|
|
430
|
+
// 无旧版可回装时失败走账实复读给恢复指引。
|
|
423
431
|
let twoStepNote;
|
|
424
|
-
if (isAlreadyInstalledGuard(change) &&
|
|
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
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
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.
|
|
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"
|