dsh-m 0.9.20 → 0.9.22
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/docs/DESIGN.md +13 -0
- package/lib/cli.js +2 -1
- package/lib/client.js +16 -4
- package/lib/core/activation.js +160 -0
- package/lib/core/market.js +15 -1
- package/lib/core/profile-ops.js +16 -1
- package/lib/core/ustar.js +101 -0
- package/lib/tools.js +9 -3
- package/package.json +1 -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.9.22 变更:升级生效判定——tarball 差异三态分类 + 三端重启提示分流(ADR-0007)
|
|
12
|
+
|
|
13
|
+
- **问题(0.9.21 实证)**:`@iasiv5/dsh-skins` 1.2.3→1.3.0 升级后未重启即已生效(客户端 bundle rev 热更),三端仍无条件提示「需要重启」——警报疲劳会侵蚀提示的权威性,重启本身也有真实成本(web 服务瞬断 / desktop 手动重开)。
|
|
14
|
+
- **生效判定(GLOSSARY「生效判定/纯客户端更新」)**:npm 源升级成功点拉取新旧两版 tarball(并行、总 deadline 10s、单包 8MiB、无缓存),零依赖 ustar 只读解析(支持 pax 长名)+ 逐文件 sha256 diff,分类 `activation: 'client-only' | 'restart-required' | 'unknown'`。规则五条:client 集合 = `exports['./client']` 目标;`dsh.bundle.patch` 声明的补丁目标变更 → 宿主;`package.json` 忽略顶层 version 后语义比较(dependencies 等字段变化照常算宿主);其余差异按路径归属;client 指向变化保守判宿主。**fail-open**:一切异常 → unknown → 现状提示,绝不影响升级成功态。
|
|
15
|
+
- **三端分流**:agent 工具消息 client-only 明示「刷新页面即可生效,不要询问 dshm_restart」、unknown 保守建议重启;GUI toast 后缀 + `needsRestart` 门(client-only 不亮重启横幅;纯函数 `upgradeNotify` 可单测);CLI 行文案换挡。`needsRestart` 源头放宽 boolean(TS2430 规避),`UpgradeResult`/desktop 升级结果新增 `activation` 字段;接线点 `upgradePluginLocked` + `desktopUpgradeLocked`,selfUpgrade/install/uninstall/github 源维持现状。
|
|
16
|
+
- **测试与文档**:新增 ustar 解析 11 例、分类规则+fail-open 18 例、升级接线 7 例(npm/github/selfUpgrade/desktop)、renderUpgrade 4 例、upgradeNotify 3 例;既有升级替身统一补 `classifyActivation` 缝(防隐式出网)。决策与已知局限全文见 `docs/adr/0007-activation-classification.md`(client chunk require 图不追踪;docs 类随版差异保守判宿主侧)。
|
|
17
|
+
|
|
18
|
+
### 0.9.21 变更:设置页「强制刷新」显示连带修复 + registry.json 变更自动清 jsDelivr 缓存
|
|
19
|
+
|
|
20
|
+
- **设置页强刷显示修复(实机 2026-10-03 实证)**:设置页「强制刷新」只重载 `registry` 接口(force 同步强刷全链路),但面板展示的 registryState **优先读 `registry-config` 的挂载时快照**——于是出现「toast 报已强制刷新、生效来源/更新时间/条目数纹丝不动」的假死,重开设置页才对齐。现在强刷成功后连带重载 `registry-config`(force 已更新 controller 内存快照,零成本取新值),显示即时跟随生效数据。
|
|
21
|
+
- **jsDelivr 自动清缓存(`registry.yml` 新增 `purge-jsdelivr` job)**:默认链「线路粘性 + CDN 恒 200 即成功」会让 jsDelivr 的滞后快照**无限钉死**精选清单——实证:0.9.18 收录 DSH Market(18→19)后,粘性在 jsDelivr 的本机持续拉回 18 条旧版,raw 主线路永远轮不到,用户侧强制刷新也无解(唯有手动 `purge.jsdelivr.net`)。现在 push 到 main 且 `registry.json` 实际变更时(`github.event.before` diff 判定),`validate` 全绿后自动调 purge 接口清缓存并回读 cdn 验证条目数,结果写入 step summary;registry.json 未变的 push 与 PR 全部跳过。
|
|
22
|
+
|
|
11
23
|
### 0.9.20 变更:latest 探测缓存退回纯内存(重启即失效)+ mutation 定向失效(ADR-0006)
|
|
12
24
|
|
|
13
25
|
- **事故复盘落地(2026-10-03 凌晨)**:0.9.14 起 latest 探测缓存 write-through 落盘、跨重启存活,失效通道只有 TTL 一条——00:22:58 缓存写入后,00:27–01:05 连发四版、01:07 重启 DSH、01:13 重开面板全部吃到陈旧值,卡片与 `dshm_outdated` 双双误报「已是最新」。本次推翻该设计,决策与取舍全文见 `docs/adr/0006-latest-cache-memory-only.md`。
|
|
@@ -230,6 +242,18 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
|
|
|
230
242
|
|
|
231
243
|
## English
|
|
232
244
|
|
|
245
|
+
### Changed in 0.9.22 — upgrade activation classification (tarball diff, three states) + three-surface restart-hint split (ADR-0007)
|
|
246
|
+
|
|
247
|
+
- **Problem (observed in 0.9.21)**: after upgrading `@iasiv5/dsh-skins` 1.2.3→1.3.0 the plugin was already live without a restart (client bundle rev hot-reload), yet all three surfaces still said "restart required" — the noise trains users to ignore the hint, and restarts have real costs (web service blip / manual desktop reopen).
|
|
248
|
+
- **Activation classification** (GLOSSARY「生效判定 / 纯客户端更新」): npm-source upgrades now fetch both tarballs at the success point (parallel, 10s total deadline, 8MiB per tarball, no cache), parse them with a zero-dependency read-only ustar reader (pax long names supported) and classify by per-file sha256 diff into `activation: 'client-only' | 'restart-required' | 'unknown'`. Five rules: client set = `exports['./client']` target; a changed `dsh.bundle.patch`-declared patch file → host; `package.json` compared semantically with the top-level `version` ignored (dependency changes still count as host); every other added/removed/changed file attributed by path; a changed client pointer conservatively counts as host. **Fail-open**: any error → unknown → current conservative hint, never affecting the upgrade's success.
|
|
249
|
+
- **Three-surface split**: the agent tool message for `client-only` says a page refresh suffices and explicitly forbids offering `dshm_restart`; the GUI toast gains a suffix and its restart banner gate follows `needsRestart` (pure helper `upgradeNotify`, unit-tested); the CLI line switches accordingly. `needsRestart` widened to boolean at the source (TS2430 avoidance); `UpgradeResult` and the desktop upgrade result carry the new `activation` field; wired at `upgradePluginLocked` + `desktopUpgradeLocked`; selfUpgrade/install/uninstall/github sources unchanged.
|
|
250
|
+
- **Tests & docs**: 11 new ustar parser cases, 18 classifier rule/fail-open cases, 7 upgrade-wiring cases (npm/github/selfUpgrade/desktop), 4 renderUpgrade cases, 3 upgradeNotify cases; existing upgrade fixtures gained a `classifyActivation` seam (no implicit network). Full decision record and known limitations in `docs/adr/0007-activation-classification.md` (no client chunk require-graph tracking; docs-only diffs conservatively count as host).
|
|
251
|
+
|
|
252
|
+
### Changed in 0.9.21 — Settings force-refresh display fix + automatic jsDelivr purge on registry.json changes
|
|
253
|
+
|
|
254
|
+
- **Settings force-refresh display fix (observed live 2026-10-03)**: the Settings tab's "Force refresh" only reloaded the `registry` API (synchronous full-chain force fetch), but the panel renders the registryState **from the `registry-config` snapshot taken at mount** — so the toast said "Registry force-refreshed" while source / fetched-at / entry-count stayed frozen until the settings tab was remounted. A successful force refresh now also reloads `registry-config` (force already updated the controller's in-memory snapshot, so this costs nothing) and the display follows the live data immediately.
|
|
255
|
+
- **Automatic jsDelivr purge (new `purge-jsdelivr` job in `registry.yml`)**: the default chain's "sticky route + CDN always answers 200" combo lets a lagging jsDelivr snapshot **pin the curated list indefinitely** — observed: after 0.9.18 added DSH Market (18→19), a machine sticky to jsDelivr kept receiving the 18-entry snapshot; the raw primary route was never retried, and even the user-facing force refresh could not escape (only a manual `purge.jsdelivr.net` call fixed it). Now a push to main that actually changes `registry.json` (diffed against `github.event.before`) triggers a CDN purge **after** `validate` goes green, re-reads the CDN to verify the entry count, and writes both into the step summary; pushes without registry.json changes and PRs skip the job entirely.
|
|
256
|
+
|
|
233
257
|
### Changed in 0.9.20 — latest probe cache back to memory-only (restart invalidates) + mutation-scoped invalidation (ADR-0006)
|
|
234
258
|
|
|
235
259
|
- **Post-mortem landed (night of 2026-10-03)**: since 0.9.14 the latest-version probe cache was written through to disk and survived restarts, with TTL as the only invalidation channel — after a cache write at 00:22:58, four releases (00:27–01:05), a DSH restart (01:07) and a panel reopen (01:13) all served the stale value; cards and `dshm_outdated` both wrongly reported "up to date". That design is overturned; the full decision record lives in `docs/adr/0006-latest-cache-memory-only.md`.
|
package/docs/DESIGN.md
CHANGED
|
@@ -120,6 +120,7 @@ npm 安装 / GitHub 安装 / 升级 / 自升级 / 卸载是**同一个事务模
|
|
|
120
120
|
- **卸载**(事务定序写死):validate(严格读取:NOT_INSTALLED / NOT_DSH_PLUGIN / PLUGIN_METADATA_UNREADABLE,全部零写入零快照)→ 快照 → live-disable(先让 client bundle 下线,避免 404;回滚时尽力反向)→ 摘除该包补丁条目(`pnpm-workspace.yaml` 顶层 `patchedDependencies` 与 `package.json#pnpm.patchedDependencies`;残留条目会令 pnpm 以 `ERR_PNPM_UNUSED_PATCH` 整单失败,只精确匹配 `pkg` / `pkg@ver`,补丁文件本体保留并计入残留报告)→ remove → verify gone(仍在则回滚)。**不清理插件产生的数据/配置**,但把检测到的疑似残留路径(如 `~/.dsh/<plugin>.json`)列出报告。
|
|
121
121
|
- **安装(Desktop profile 委派,0.9.0+)**:官方 pluginManager `installBundle(enabled: true)` → ChangeResult 以 application/stage 为准判定:cancelled 拒绝;build-blocked 走结构化审批重试(pending 名单原样回传);minimumReleaseAge 翻译成诚实等待指引(策略正确工作非故障);enable 失败按 `packageResult` 如实区分「包已写入 / 未能写入 / 官方未回传(未知)」——enable 先行失败(pnpm 未跑)时不得断言「包已写入」→ `listBundles` 复读在装(不冒充成功)。**enable 阶段失败自动重试一次**(本地增强 2026-10-02,未发版):应用侧 enable 与高频装卸/插件树重载竞速返回 `operation-error`(实机实证 dsh-copilot-auth 三装卸两败,失败 operation 的 pnpm.log 均为 0 字节),单次退避重试即愈;其余失败形态不重试。
|
|
122
122
|
- **升级**:**按需检查**(`dshm_outdated` / `dshm_list` 时实时比对本地版本 vs npm latest / GitHub release,半自动——展示升级计划,确认后执行,即重开一次安装事务)。**不做后台定时器**。
|
|
123
|
+
- **生效判定(0.9.22,[ADR-0007](./adr/0007-activation-classification.md),术语见 GLOSSARY「生效判定/纯客户端更新」)**:npm 源升级成功点后拉取新旧两版 tarball(并行、总 deadline 10s、单包 8MiB、无缓存),零依赖 ustar 只读解析 + 逐文件 sha256 diff,分类 `activation: 'client-only' | 'restart-required' | 'unknown'`——规则:client 集合 = `exports['./client']` 目标;`dsh.bundle.patch` 声明的补丁目标变更 → 宿主;`package.json` 忽略顶层 version 后语义比较;其余差异按路径归属;client 指向变化保守判宿主。`needsRestart` 仅 client-only 覆写 false(`InstallResult.needsRestart` 源头放宽 boolean,TS2430 规避)。三端分流:工具消息 client-only 明示「不要询问 dshm_restart」、GUI toast 后缀 + 横幅门(`needsRestart=false` 不亮)、CLI 行文案换挡。fail-open:一切异常 → unknown → 现状提示。接线点 `upgradePluginLocked` + `desktopUpgradeLocked`;selfUpgrade/install/uninstall/github 源不产出。已知局限(保守方向误判,安全):client chunk require 图不追踪、docs 类随版差异判宿主侧。
|
|
123
124
|
- **自更新**:dsh-m 对自己同样做版本比对 + 提示升级(设置页呈现);执行即 `install-npm` 事务(integrity 缺失直接拒绝,不进事务)。
|
|
124
125
|
- **开关(0.4.0,术语见 GLOSSARY.md「开关/行覆盖/Bundle 选择/委派降级」)**:已装插件运行状态的可逆切换,**不经 Profile 变更事务**。粒度自动路由——包内补丁层单 insert 行且无配置补丁行 → 行覆盖(`cordis.patch.yml` 保注释 `disabled` 编辑,官方 `writePluginEnabled` 同款语义,启用写显式 `disabled: false`);多行/不可解析 → Bundle 选择(`dsh.profile.bundles` 数组增删)。写路径委派优先:运行时探测官方 `pluginManager` 服务(`ctx.get`,按存在性不判版本号)→ `setPluginEnabled`/`setBundleEnabled`(白拿保护判定、unaddressable 识别、hmr 活体重组;applied 以复读 `listPlugins()` 为准);服务缺席降级 loader `entry.update()` 直操作 + 锁内文件编辑(bundle 级恒 restart-required)。**必须持官方同款锁**(`withFileLock(<profile>/package.json)`)防与官方 UI 并发丢更新。架构决策见 [ADR-0001](./adr/0001-delegate-with-fallback-for-plugin-manager.md)。
|
|
125
126
|
- **运行相位(0.4.0)**:loader fiber 状态投影(active/failed/pending/loading/unloading;fiber 缺失即已停用),读路径始终自读 loader entries,读路径零服务依赖。
|
|
@@ -139,6 +140,18 @@ npm 安装 / GitHub 安装 / 升级 / 自升级 / 卸载是**同一个事务模
|
|
|
139
140
|
4. pnpm 构建脚本被拦时,先按 pnpm 待决名单**逐键精确放行**(ADR-0002),名单不可读才 `dangerouslyAllowAllBuilds` 全量兜底,且**必须明确报告**放行了哪些包/是否兜底;
|
|
140
141
|
5. 不做签名/验签体系(自用,明确不做)。
|
|
141
142
|
|
|
143
|
+
### 3.3 生效判定三态分流(0.9.22,[ADR-0007](./adr/0007-activation-classification.md))
|
|
144
|
+
|
|
145
|
+
判定本体(五条规则、10s/8MiB 预算、fail-open、已知局限)见 §3.2「生效判定」条目与 ADR-0007,此处只列三端分流表:
|
|
146
|
+
|
|
147
|
+
| activation | agent 工具消息 | GUI | CLI |
|
|
148
|
+
|---|---|---|---|
|
|
149
|
+
| `client-only` | 明示刷新生效,**不询问** dshm_restart | toast 后缀「刷新页面即可生效」;重启横幅不亮(`needsRestart=false`) | 「纯客户端更新:刷新页面即生效,无需重启」 |
|
|
150
|
+
| `unknown` | 保守建议重启并询问 dshm_restart | 现状重启横幅 + toast 后缀「判定未完成」 | 「生效判定未完成;建议重启」 |
|
|
151
|
+
| `restart-required` / 字段缺席(github 源等) | 现状「需要重启」并询问 | 现状重启横幅 | 现状行 |
|
|
152
|
+
|
|
153
|
+
与 `application`(Profile 变更事务词汇)的边界:`activation` 只是「新版本如何生效」的操作建议,不是事务状态——事务四态(applied/restart-required/overridden/failed)描述**变更本身**,`activation` 描述**生效方式**;两者不互相推导(GLOSSARY Avoid 行同源)。
|
|
154
|
+
|
|
142
155
|
## 4. GUI(旗舰,v1 必须做好)
|
|
143
156
|
|
|
144
157
|
0.4.0 增补:已装卡右上 `dshm-switch` 开关(受 `toggleable` 控制,锁因 title 提示);sub 行相位点 `● active · v1.0.8 · npm`(相位点只映射 phase 五值,「已停用」归 Switch,两输入源各管各的);开关结果通知按 `applied` 分流(live → 绿 toast「即时生效」;restart-required → 沿用重启横幅 + 一键重启);安装遇兼容拦截(409 + issue)弹「仍要安装」确认(红字风险 + peers 清单 → `forceIncompatible` 重发)。
|
package/lib/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ import { togglePlugin as coreTogglePlugin } from './core/toggle.js';
|
|
|
10
10
|
import { loadRegistry } from './core/registry.js';
|
|
11
11
|
import { COMMUNITY_CATEGORY_LABELS } from './core/community.js';
|
|
12
12
|
import { scheduleRestart } from './core/restart.js';
|
|
13
|
+
import { upgradeEffectLine } from './core/activation.js';
|
|
13
14
|
import { pathToFileURL } from 'node:url';
|
|
14
15
|
import { resolve } from 'node:path';
|
|
15
16
|
/** 分类中文标签(0.7.0 Task 6;0.9.16 策展五桶):精选策展桶本地表 + 社区已知标签单一事实源;未知 slug 原样。 */
|
|
@@ -366,7 +367,7 @@ async function runCliDispatch(argv, deps, io) {
|
|
|
366
367
|
const to = res.version ? `v${res.version}` : res.sha ? res.sha.slice(0, 7) : '最新';
|
|
367
368
|
out(`✅ 已升级 ${res.pkg}(${from}${to})`);
|
|
368
369
|
outBuildsNote(res, out);
|
|
369
|
-
out(
|
|
370
|
+
out(upgradeEffectLine(res.activation));
|
|
370
371
|
return 0;
|
|
371
372
|
}
|
|
372
373
|
case 'toggle': {
|
package/lib/client.js
CHANGED
|
@@ -710,8 +710,14 @@ __export(operations_exports, {
|
|
|
710
710
|
createOpsPump: () => createOpsPump,
|
|
711
711
|
drainRestored: () => drainRestored,
|
|
712
712
|
opAppliesTo: () => opAppliesTo,
|
|
713
|
-
restoreRecords: () => restoreRecords
|
|
713
|
+
restoreRecords: () => restoreRecords,
|
|
714
|
+
upgradeNotify: () => upgradeNotify
|
|
714
715
|
});
|
|
716
|
+
function upgradeNotify(activation) {
|
|
717
|
+
if (activation === "client-only") return { needsRestart: false, suffixKey: "notify.upgraded.clientonly" };
|
|
718
|
+
if (activation === "unknown") return { needsRestart: true, suffixKey: "notify.upgraded.activationUnknown" };
|
|
719
|
+
return { needsRestart: true, suffixKey: null };
|
|
720
|
+
}
|
|
715
721
|
function safeParse(raw) {
|
|
716
722
|
try {
|
|
717
723
|
const v = JSON.parse(raw);
|
|
@@ -1163,7 +1169,7 @@ var { toggleViewModel: toggleViewModel2, toggleNoticeKeys: toggleNoticeKeys2 } =
|
|
|
1163
1169
|
var { pickPayload: pickPayload2, parseToolArgs: parseToolArgs2 } = (init_tool_view(), __toCommonJS(tool_view_exports));
|
|
1164
1170
|
var { RESTART_POLL_MS: RESTART_POLL_MS2, RESTART_DEADLINE_MS: RESTART_DEADLINE_MS2, nextRestartWait: nextRestartWait2, isAmbiguousRestartRequestError: isAmbiguousRestartRequestError2 } = (init_restart_wait(), __toCommonJS(restart_wait_exports));
|
|
1165
1171
|
var { refreshAfterMutation: refreshAfterMutation2 } = (init_view_refresh(), __toCommonJS(view_refresh_exports));
|
|
1166
|
-
var { createOperationsStore: createOperationsStore2, restoreRecords: restoreRecords2, createOpsPump: createOpsPump2, opAppliesTo: opAppliesTo2, TERMINAL_CLEARABLE: TERMINAL_CLEARABLE2 } = (init_operations(), __toCommonJS(operations_exports));
|
|
1172
|
+
var { createOperationsStore: createOperationsStore2, restoreRecords: restoreRecords2, createOpsPump: createOpsPump2, opAppliesTo: opAppliesTo2, TERMINAL_CLEARABLE: TERMINAL_CLEARABLE2, upgradeNotify: upgradeNotify2 } = (init_operations(), __toCommonJS(operations_exports));
|
|
1167
1173
|
var { createFavoritesStore: createFavoritesStore2, partitionStale: partitionStale2 } = (init_favorites(), __toCommonJS(favorites_exports));
|
|
1168
1174
|
var { readSelfCheckCache: readSelfCheckCache2, writeSelfCheckCache: writeSelfCheckCache2, clearSelfCheckCache: clearSelfCheckCache2, deriveChipState: deriveChipState2 } = (init_self_check(), __toCommonJS(self_check_exports));
|
|
1169
1175
|
var { shouldShowInstallCmd: shouldShowInstallCmd2 } = (init_install_cmd(), __toCommonJS(install_cmd_exports));
|
|
@@ -1390,6 +1396,8 @@ var ZH = {
|
|
|
1390
1396
|
"notify.livedisabled": "\uFF08\u5DF2\u5148\u4E0B\u7EBF\u8FD0\u884C\u4E2D\u7684\u754C\u9762\uFF09",
|
|
1391
1397
|
"notify.leftovers": "\uFF1B\u68C0\u6D4B\u5230\u7591\u4F3C\u6B8B\u7559\u6570\u636E\uFF1A{paths}",
|
|
1392
1398
|
"notify.upgraded": "\u5DF2\u5347\u7EA7 {pkg}\uFF08{from} \u2192 {to}\uFF09",
|
|
1399
|
+
"notify.upgraded.clientonly": "\u3002\u7EAF\u5BA2\u6237\u7AEF\u66F4\u65B0\uFF1A\u5237\u65B0\u9875\u9762\u5373\u53EF\u751F\u6548\uFF0C\u65E0\u9700\u91CD\u542F",
|
|
1400
|
+
"notify.upgraded.activationUnknown": "\u3002\u751F\u6548\u5224\u5B9A\u672A\u5B8C\u6210\uFF1A\u4E3A\u786E\u4FDD\u751F\u6548\u8BF7\u91CD\u542F DSH Web",
|
|
1393
1401
|
"notify.upgradehint": "\uFF08\u6CE8\u610F\uFF1A\u8BE5\u63D2\u4EF6\u6267\u884C\u4E86\u6784\u5EFA\u811A\u672C\uFF09",
|
|
1394
1402
|
"notify.selfupgraded": "dsh-m \u5DF2\u5347\u7EA7\uFF08v{from} \u2192 v{to}\uFF09",
|
|
1395
1403
|
"notify.selfupgraded.failed": "dsh-m \u5347\u7EA7\u5931\u8D25\uFF1A{err}",
|
|
@@ -1650,6 +1658,8 @@ var EN = {
|
|
|
1650
1658
|
"notify.livedisabled": " (live UI disabled first)",
|
|
1651
1659
|
"notify.leftovers": "; possible leftover data: {paths}",
|
|
1652
1660
|
"notify.upgraded": "Upgraded {pkg} ({from} \u2192 {to})",
|
|
1661
|
+
"notify.upgraded.clientonly": ". Client-only update: refresh the page to take effect \u2014 no restart needed",
|
|
1662
|
+
"notify.upgraded.activationUnknown": ". Activation classification unavailable: restart DSH Web to ensure the new version is live",
|
|
1653
1663
|
"notify.upgradehint": " (note: this plugin ran build scripts)",
|
|
1654
1664
|
"notify.selfupgraded": "dsh-m upgraded (v{from} \u2192 v{to})",
|
|
1655
1665
|
"notify.selfupgraded.failed": "dsh-m upgrade failed: {err}",
|
|
@@ -2962,14 +2972,15 @@ function InstalledTab({ notify, installed, onMutation, ops }) {
|
|
|
2962
2972
|
const doUpgrade = async (it) => {
|
|
2963
2973
|
try {
|
|
2964
2974
|
const res = await ops.runOp("upgrade", it.pkg, () => api("upgrade", { pkg: it.pkg }));
|
|
2975
|
+
const note = upgradeNotify2(res.activation);
|
|
2965
2976
|
notify({
|
|
2966
2977
|
kind: "ok",
|
|
2967
|
-
needsRestart:
|
|
2978
|
+
needsRestart: note.needsRestart,
|
|
2968
2979
|
text: lookup("notify.upgraded", {
|
|
2969
2980
|
pkg: res.pkg,
|
|
2970
2981
|
from: res.fromVersion ? `v${res.fromVersion}` : "\u2014",
|
|
2971
2982
|
to: res.version ? `v${res.version}` : res.sha ? res.sha.slice(0, 7) : "latest"
|
|
2972
|
-
}) + (res.buildApprovals && res.buildApprovals.length ? lookup("notify.builds", { names: res.buildApprovals.join(", ") }) : res.fallbackAllBuilds ? lookup("notify.builds.fallback") : "")
|
|
2983
|
+
}) + (res.buildApprovals && res.buildApprovals.length ? lookup("notify.builds", { names: res.buildApprovals.join(", ") }) : res.fallbackAllBuilds ? lookup("notify.builds.fallback") : "") + (note.suffixKey ? lookup(note.suffixKey) : "")
|
|
2973
2984
|
});
|
|
2974
2985
|
await (onMutation ? onMutation() : reload());
|
|
2975
2986
|
} catch (e) {
|
|
@@ -3122,6 +3133,7 @@ function SettingsTab({ notify, onRegistryChanged }) {
|
|
|
3122
3133
|
setBusy(true);
|
|
3123
3134
|
try {
|
|
3124
3135
|
await reg.reload(true);
|
|
3136
|
+
await reloadRegistryState().catch(() => void 0);
|
|
3125
3137
|
notify({ kind: "ok", text: lookup("registry.refreshed"), needsRestart: false });
|
|
3126
3138
|
} finally {
|
|
3127
3139
|
setBusy(false);
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 生效判定(GLOSSARY:生效判定 Activation Classification,0.9.22):
|
|
3
|
+
* 对 npm 源升级的新旧两版 tarball 做逐文件 sha256 diff,把「新版本如何生效」分类为
|
|
4
|
+
* 'client-only'(纯客户端更新:刷新页面即生效)/ 'restart-required'(需重启)/ 'unknown'。
|
|
5
|
+
* 任何异常(元数据缺失、下载失败、超时、超限、tar/JSON 解析)一律 fail-open 到 'unknown',
|
|
6
|
+
* 绝不影响升级成功态。判定预算独立于 pnpm 事务:总 deadline 默认 10s、单 tarball 上限 8MiB、
|
|
7
|
+
* 无缓存(升级是一次性操作,结果随升级返回渲染完即弃——ADR-0006「缓存是优化不是功能」精神)。
|
|
8
|
+
* 分类规则五条与已知局限见 docs/adr/0007-activation-classification.md。
|
|
9
|
+
*/
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
11
|
+
import { gunzipSync } from 'node:zlib';
|
|
12
|
+
import { fetchBytesLimited } from './httpx.js';
|
|
13
|
+
import { npmVersion } from './versions.js';
|
|
14
|
+
import { parseTarEntries } from './ustar.js';
|
|
15
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
16
|
+
const DEFAULT_MAX_TARBALL_BYTES = 8 * 1024 * 1024;
|
|
17
|
+
function stripPackagePrefix(name) {
|
|
18
|
+
return name.startsWith('package/') ? name.slice('package/'.length) : name;
|
|
19
|
+
}
|
|
20
|
+
function normalizeTarget(target) {
|
|
21
|
+
return target.replace(/^\.\//, '');
|
|
22
|
+
}
|
|
23
|
+
function sha256(buf) {
|
|
24
|
+
return createHash('sha256').update(buf).digest('hex');
|
|
25
|
+
}
|
|
26
|
+
function fileMap(tarBuf) {
|
|
27
|
+
const raw = parseTarEntries(gunzipSync(tarBuf));
|
|
28
|
+
const map = new Map();
|
|
29
|
+
for (const [name, content] of raw)
|
|
30
|
+
map.set(stripPackagePrefix(name), content);
|
|
31
|
+
return map;
|
|
32
|
+
}
|
|
33
|
+
/** 深比较(键序无关):package.json 忽略顶层 version 后的语义 diff 用。 */
|
|
34
|
+
function deepEqual(a, b) {
|
|
35
|
+
if (a === b)
|
|
36
|
+
return true;
|
|
37
|
+
if (a === null || b === null || typeof a !== typeof b)
|
|
38
|
+
return false;
|
|
39
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
40
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
|
|
41
|
+
return false;
|
|
42
|
+
return a.every((v, i) => deepEqual(v, b[i]));
|
|
43
|
+
}
|
|
44
|
+
if (typeof a === 'object') {
|
|
45
|
+
const ka = Object.keys(a).sort();
|
|
46
|
+
const kb = Object.keys(b).sort();
|
|
47
|
+
if (!deepEqual(ka, kb))
|
|
48
|
+
return false;
|
|
49
|
+
return ka.every((k) => deepEqual(a[k], b[k]));
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
function parsePackageJson(buf) {
|
|
54
|
+
if (!buf)
|
|
55
|
+
throw new Error('tar 缺少 package.json');
|
|
56
|
+
const parsed = JSON.parse(buf.toString('utf8'));
|
|
57
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
58
|
+
throw new Error('package.json 不是对象');
|
|
59
|
+
return parsed;
|
|
60
|
+
}
|
|
61
|
+
/** 规则 3:package.json 忽略顶层 version 后语义比较(依赖条目里的版本串照常计入);非法 JSON 抛错。 */
|
|
62
|
+
function packageJsonMeaningfulDiff(oldBuf, newBuf) {
|
|
63
|
+
const a = parsePackageJson(oldBuf);
|
|
64
|
+
delete a.version;
|
|
65
|
+
const b = parsePackageJson(newBuf);
|
|
66
|
+
delete b.version;
|
|
67
|
+
return !deepEqual(a, b);
|
|
68
|
+
}
|
|
69
|
+
function exportClientTarget(pkg) {
|
|
70
|
+
const exports = pkg.exports;
|
|
71
|
+
const target = exports?.['./client'];
|
|
72
|
+
return typeof target === 'string' ? normalizeTarget(target) : null;
|
|
73
|
+
}
|
|
74
|
+
function patchTarget(pkg) {
|
|
75
|
+
const dsh = pkg.dsh;
|
|
76
|
+
const patch = dsh?.bundle?.patch;
|
|
77
|
+
return typeof patch === 'string' ? normalizeTarget(patch) : null;
|
|
78
|
+
}
|
|
79
|
+
/** 规则引擎(五条,见 ADR-0007);输入已解包归一,异常由调用方 fail-open。 */
|
|
80
|
+
function classifyDiff(oldFiles, newFiles) {
|
|
81
|
+
const oldPkgRaw = oldFiles.get('package.json');
|
|
82
|
+
const newPkgRaw = newFiles.get('package.json');
|
|
83
|
+
const oldPkg = parsePackageJson(oldPkgRaw);
|
|
84
|
+
const newPkg = parsePackageJson(newPkgRaw);
|
|
85
|
+
const clientOld = exportClientTarget(oldPkg);
|
|
86
|
+
const clientNew = exportClientTarget(newPkg);
|
|
87
|
+
if (clientOld !== clientNew)
|
|
88
|
+
return 'restart-required'; // 规则 5:client 指向变化保守判宿主
|
|
89
|
+
const patchOld = patchTarget(oldPkg);
|
|
90
|
+
const patchNew = patchTarget(newPkg);
|
|
91
|
+
const names = new Set([...oldFiles.keys(), ...newFiles.keys()]);
|
|
92
|
+
for (const name of names) {
|
|
93
|
+
const o = oldFiles.get(name);
|
|
94
|
+
const n = newFiles.get(name);
|
|
95
|
+
if (o && n && sha256(o) === sha256(n))
|
|
96
|
+
continue;
|
|
97
|
+
if (name === 'package.json') {
|
|
98
|
+
// 规则 3:仅顶层 version 差异不算宿主侧;其余字段差异算
|
|
99
|
+
if (packageJsonMeaningfulDiff(oldPkgRaw, newPkgRaw))
|
|
100
|
+
return 'restart-required';
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (name === clientNew || name === clientOld)
|
|
104
|
+
continue; // 规则 1/4:client 集合内
|
|
105
|
+
if (name === patchNew || name === patchOld)
|
|
106
|
+
return 'restart-required'; // 规则 2:强调项(被规则 4 覆盖)
|
|
107
|
+
return 'restart-required'; // 规则 4:其余一切差异归宿主侧
|
|
108
|
+
}
|
|
109
|
+
return 'client-only';
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* 生效判定主入口:拉取 from/to 两版元数据与 tarball(并行),解包 diff 后分类。
|
|
113
|
+
* 任何异常返回 'unknown'(含被 opts.signal 外部中止)——调用方无需 try/catch。
|
|
114
|
+
*/
|
|
115
|
+
export async function classifyUpgradeActivation(pkg, fromVersion, toVersion, opts = {}, deps = {}) {
|
|
116
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
117
|
+
const maxBytes = opts.maxTarballBytes ?? DEFAULT_MAX_TARBALL_BYTES;
|
|
118
|
+
const ac = new AbortController();
|
|
119
|
+
const onOuterAbort = () => ac.abort(opts.signal?.reason);
|
|
120
|
+
if (opts.signal) {
|
|
121
|
+
if (opts.signal.aborted)
|
|
122
|
+
onOuterAbort();
|
|
123
|
+
else
|
|
124
|
+
opts.signal.addEventListener('abort', onOuterAbort, { once: true });
|
|
125
|
+
}
|
|
126
|
+
const timer = setTimeout(() => ac.abort(new Error('生效判定超时')), timeoutMs);
|
|
127
|
+
try {
|
|
128
|
+
const fetchVersionMeta = deps.fetchVersionMeta ??
|
|
129
|
+
(async (p, v, signal) => ({ tarball: (await npmVersion(p, v, timeoutMs, signal)).tarball }));
|
|
130
|
+
const fetchTarball = deps.fetchTarball ??
|
|
131
|
+
(async (url, max, signal) => (await fetchBytesLimited(url, { maxBytes: max, timeoutMs, signal })).bytes);
|
|
132
|
+
const [oldMeta, newMeta] = await Promise.all([
|
|
133
|
+
fetchVersionMeta(pkg, fromVersion, ac.signal),
|
|
134
|
+
fetchVersionMeta(pkg, toVersion, ac.signal),
|
|
135
|
+
]);
|
|
136
|
+
if (!oldMeta?.tarball || !newMeta?.tarball)
|
|
137
|
+
return 'unknown';
|
|
138
|
+
const [oldTar, newTar] = await Promise.all([
|
|
139
|
+
fetchTarball(oldMeta.tarball, maxBytes, ac.signal),
|
|
140
|
+
fetchTarball(newMeta.tarball, maxBytes, ac.signal),
|
|
141
|
+
]);
|
|
142
|
+
return classifyDiff(fileMap(oldTar), fileMap(newTar));
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return 'unknown';
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
clearTimeout(timer);
|
|
149
|
+
if (opts.signal)
|
|
150
|
+
opts.signal.removeEventListener('abort', onOuterAbort);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** CLI 行文案(tools/GUI 有各自的专用措辞;此处只服务 cli.ts upgrade case)。 */
|
|
154
|
+
export function upgradeEffectLine(activation) {
|
|
155
|
+
if (activation === 'client-only')
|
|
156
|
+
return '纯客户端更新:刷新页面即生效,无需重启';
|
|
157
|
+
if (activation === 'unknown')
|
|
158
|
+
return '生效判定未完成;建议重启 DSH Web 生效:dshm restart --yes';
|
|
159
|
+
return '需要重启 DSH Web 生效:dshm restart --yes';
|
|
160
|
+
}
|
package/lib/core/market.js
CHANGED
|
@@ -22,6 +22,7 @@ import { COMMUNITY_CATEGORY_LABELS, fetchCommunityCatalog as defaultFetchCommuni
|
|
|
22
22
|
import { adaptCommunityCatalog } from './community-adapter.js';
|
|
23
23
|
import { normalizeSearchText, relevanceScore, tokenizeSearchText } from './search-relevance.js';
|
|
24
24
|
import { GithubBudgetExhaustedError, createGithubRequestBudget, isExactVersion } from './versions.js';
|
|
25
|
+
import { classifyUpgradeActivation } from './activation.js';
|
|
25
26
|
import { HttpError } from './httpx.js';
|
|
26
27
|
import { ensureLatestCacheSwept, invalidateLatestCache, latestCacheKey, latestItemId, readLatestCache, writeLatestCache } from './latest-cache.js';
|
|
27
28
|
import { verifyInstalledAdditions } from './install-guard.js';
|
|
@@ -1301,7 +1302,20 @@ async function upgradePluginLocked(pkg, cfg = {}, opts = {}, deps) {
|
|
|
1301
1302
|
// 0.5.1 修复:直调 installEntryLocked——upgradePlugin 已在 mutation session 区间内,
|
|
1302
1303
|
// 再经 installEntry 二次获取 session 会自死锁(session 非重入,见 withMutationSession 契约)。
|
|
1303
1304
|
const result = await installEntryLocked(entry, cfg, opts, deps);
|
|
1304
|
-
|
|
1305
|
+
// 0.9.22 生效判定:npm 源升级在成功点分类;任何异常 fail-open 到 unknown,绝不影响升级成功态
|
|
1306
|
+
let activation;
|
|
1307
|
+
let needsRestart = result.needsRestart;
|
|
1308
|
+
if (entry.source === 'npm' && entry.npm && target.version && result.version) {
|
|
1309
|
+
try {
|
|
1310
|
+
activation = await (deps?.classifyActivation ?? classifyUpgradeActivation)(entry.npm, target.version, result.version);
|
|
1311
|
+
if (activation === 'client-only')
|
|
1312
|
+
needsRestart = false;
|
|
1313
|
+
}
|
|
1314
|
+
catch {
|
|
1315
|
+
activation = 'unknown';
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
return { ...result, fromVersion: target.version, needsRestart, ...(activation ? { activation } : {}) };
|
|
1305
1319
|
}
|
|
1306
1320
|
/**
|
|
1307
1321
|
* 社区条目按收录 id 查找(0.7.1 安装修复):合并市场展示的社区条目 id = 社区目录合成 id
|
package/lib/core/profile-ops.js
CHANGED
|
@@ -28,6 +28,7 @@ import { adaptCommunityCatalog } from './community-adapter.js';
|
|
|
28
28
|
import { listInstalledPlugins as defaultListInstalled } from './installed.js';
|
|
29
29
|
import { precheckNpmCompat, IncompatibleError } from './compat-check.js';
|
|
30
30
|
import { npmLatest as defaultNpmLatest, githubLatestTag as defaultGithubLatestTag } from './versions.js';
|
|
31
|
+
import { classifyUpgradeActivation } from './activation.js';
|
|
31
32
|
import { togglePlugin, ToggleError } from './toggle.js';
|
|
32
33
|
import { describeReleaseAgeFailure, releaseAgePrecheck } from './release-age.js';
|
|
33
34
|
// 0.9.19:等待期失败翻译移入 release-age.ts(describeReleaseAgeFailure)——全量违规解析 +
|
|
@@ -406,6 +407,19 @@ async function desktopUpgradeLocked(pkg, cfg, opts, deps) {
|
|
|
406
407
|
}
|
|
407
408
|
throw err;
|
|
408
409
|
}
|
|
410
|
+
// 0.9.22 生效判定:npm 源升级在成功点分类;任何异常 fail-open 到 unknown,绝不影响升级成功态
|
|
411
|
+
let activation;
|
|
412
|
+
let needsRestart = true;
|
|
413
|
+
if (entry.source === 'npm' && entry.npm && target.version && version !== undefined) {
|
|
414
|
+
try {
|
|
415
|
+
activation = await (deps.classifyActivation ?? classifyUpgradeActivation)(entry.npm, target.version, version);
|
|
416
|
+
if (activation === 'client-only')
|
|
417
|
+
needsRestart = false;
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
activation = 'unknown';
|
|
421
|
+
}
|
|
422
|
+
}
|
|
409
423
|
return {
|
|
410
424
|
id: entry.id,
|
|
411
425
|
pkg,
|
|
@@ -417,10 +431,11 @@ async function desktopUpgradeLocked(pkg, cfg, opts, deps) {
|
|
|
417
431
|
fallbackAllBuilds: false,
|
|
418
432
|
...(compat !== null ? { compat } : {}),
|
|
419
433
|
...(compatSkipped ? { compatSkipped } : {}),
|
|
420
|
-
needsRestart
|
|
434
|
+
needsRestart,
|
|
421
435
|
output: `official pluginManager: application=${change.application ?? 'applied'}, bundle=${bundleName}${change.changed === false ? ', changed=false' : ''}`,
|
|
422
436
|
via: 'desktop-manager',
|
|
423
437
|
...(change.application === 'overridden' ? { overridden: true } : {}),
|
|
424
438
|
fromVersion: target.version,
|
|
439
|
+
...(activation ? { activation } : {}),
|
|
425
440
|
};
|
|
426
441
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ustar/pax 只读解析器(0.9.22 生效判定专用):把(已解压的)tar 字节流解析为
|
|
3
|
+
* 「条目路径 → 内容」的 Map。支持:常规文件('0'/NUL)、目录('5',跳过)、
|
|
4
|
+
* pax 扩展头('x',path= 记录应用于下一条件目)、GNU 长名('L')、全局头('g',跳过)。
|
|
5
|
+
* 任何结构异常(截断、坏 size、校验和不符)抛 Error——调用方 fail-open 到 unknown。
|
|
6
|
+
* 只读不落盘;零第三方依赖(npm-integrity 同款纪律)。npm registry 产出的 tar 足够规整,
|
|
7
|
+
* 不支持 base-256 超长 size 编码。
|
|
8
|
+
*/
|
|
9
|
+
const BLOCK = 512;
|
|
10
|
+
function isZeroBlock(block) {
|
|
11
|
+
for (const b of block)
|
|
12
|
+
if (b !== 0)
|
|
13
|
+
return false;
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
/** 八进制字段:容忍 NUL/space 填充;空串返回 0;非八进制返回 null。 */
|
|
17
|
+
function parseOctal(field) {
|
|
18
|
+
const text = field.toString('utf8');
|
|
19
|
+
const cleaned = text.replace(/[\0 ].*$/, '').trim();
|
|
20
|
+
if (cleaned === '') {
|
|
21
|
+
// 全 NUL/space 的 size 视为 0(目录/空文件常态);完全空白的 checksum 视为非法
|
|
22
|
+
return /^[\0 ]*$/.test(text) && text.includes('\0') ? 0 : null;
|
|
23
|
+
}
|
|
24
|
+
if (!/^[0-7]+$/.test(cleaned))
|
|
25
|
+
return null;
|
|
26
|
+
const value = Number.parseInt(cleaned, 8);
|
|
27
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
28
|
+
}
|
|
29
|
+
function cString(field) {
|
|
30
|
+
const end = field.indexOf(0);
|
|
31
|
+
return field.subarray(0, end === -1 ? field.length : end).toString('utf8');
|
|
32
|
+
}
|
|
33
|
+
/** ustar 头:name(100)@0 + prefix(155)@345(magic 为 ustar 时拼接)。 */
|
|
34
|
+
function entryName(header) {
|
|
35
|
+
const name = cString(header.subarray(0, 100));
|
|
36
|
+
const magic = cString(header.subarray(257, 263));
|
|
37
|
+
const prefix = magic === 'ustar' ? cString(header.subarray(345, 500)) : '';
|
|
38
|
+
return prefix !== '' ? `${prefix}/${name}` : name;
|
|
39
|
+
}
|
|
40
|
+
/** pax 'x' 负载:每行 "%d key=value\n";只关心 path= 记录(取最后一次出现的值)。 */
|
|
41
|
+
function paxPathRecord(content) {
|
|
42
|
+
let found = null;
|
|
43
|
+
for (const line of content.toString('utf8').split('\n')) {
|
|
44
|
+
const m = /^\d+ path=(.*)$/.exec(line);
|
|
45
|
+
if (m)
|
|
46
|
+
found = m[1] ?? null;
|
|
47
|
+
}
|
|
48
|
+
return found;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 解析 tar 字节流。键为条目原始路径(保留 npm 的 `package/` 前缀,归一是调用方职责);
|
|
52
|
+
* 仅收录常规文件;目录与元数据条目不进 Map。
|
|
53
|
+
*/
|
|
54
|
+
export function parseTarEntries(tar) {
|
|
55
|
+
const entries = new Map();
|
|
56
|
+
let offset = 0;
|
|
57
|
+
let pendingName = null; // 来自 pax 'x' / GNU 'L'
|
|
58
|
+
while (offset + BLOCK <= tar.length) {
|
|
59
|
+
const header = tar.subarray(offset, offset + BLOCK);
|
|
60
|
+
if (isZeroBlock(header))
|
|
61
|
+
break;
|
|
62
|
+
// 校验和:计算时 chksum 字段(148-156)按 8 个空格计
|
|
63
|
+
let sum = 0;
|
|
64
|
+
for (let i = 0; i < BLOCK; i++)
|
|
65
|
+
sum += i >= 148 && i < 156 ? 0x20 : header[i];
|
|
66
|
+
const stored = parseOctal(header.subarray(148, 156));
|
|
67
|
+
if (stored === null || stored !== sum)
|
|
68
|
+
throw new Error('tar 头校验和不符');
|
|
69
|
+
const size = parseOctal(header.subarray(124, 136));
|
|
70
|
+
if (size === null || size < 0)
|
|
71
|
+
throw new Error('tar size 字段非法');
|
|
72
|
+
const typeflag = String.fromCharCode(header[156] ?? 0x30);
|
|
73
|
+
const contentStart = offset + BLOCK;
|
|
74
|
+
const contentEnd = contentStart + size;
|
|
75
|
+
if (contentEnd > tar.length)
|
|
76
|
+
throw new Error('tar 条目内容截断');
|
|
77
|
+
const content = tar.subarray(contentStart, contentEnd);
|
|
78
|
+
if (typeflag === 'x') {
|
|
79
|
+
const path = paxPathRecord(content);
|
|
80
|
+
if (path !== null)
|
|
81
|
+
pendingName = path;
|
|
82
|
+
}
|
|
83
|
+
else if (typeflag === 'L') {
|
|
84
|
+
pendingName = cString(content);
|
|
85
|
+
}
|
|
86
|
+
else if (typeflag === 'g') {
|
|
87
|
+
// 全局扩展头:整段跳过
|
|
88
|
+
}
|
|
89
|
+
else if (typeflag === '0' || typeflag === '\0') {
|
|
90
|
+
const name = pendingName ?? entryName(header);
|
|
91
|
+
pendingName = null;
|
|
92
|
+
entries.set(name, Buffer.from(content)); // 拷贝,避免 subarray 挂住整块底层 buffer
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
// 目录/链接等其他类型:长名只属于紧贴其后的条目,未消费即弃用(防跨条目错名归属)
|
|
96
|
+
pendingName = null;
|
|
97
|
+
}
|
|
98
|
+
offset = contentEnd + ((BLOCK - (size % BLOCK)) % BLOCK);
|
|
99
|
+
}
|
|
100
|
+
return entries;
|
|
101
|
+
}
|
package/lib/tools.js
CHANGED
|
@@ -392,7 +392,7 @@ export function registerTools(ctx, cfg, deps = {}, profile = resolveActiveProfil
|
|
|
392
392
|
}));
|
|
393
393
|
ctx.tools.register(defineTool({
|
|
394
394
|
name: 'dshm_upgrade',
|
|
395
|
-
description: 'Upgrade an installed DSH plugin to the latest version (npm 拉最新精确版 / github 重新锁 HEAD)。pkg 来自 dshm_list 或 dshm_outdated。用户确认升级哪一个之后再调用。After success
|
|
395
|
+
description: 'Upgrade an installed DSH plugin to the latest version (npm 拉最新精确版 / github 重新锁 HEAD)。pkg 来自 dshm_list 或 dshm_outdated。用户确认升级哪一个之后再调用。After success follow the result\'s activation field: \'client-only\' → tell the user a page refresh suffices and do NOT offer dshm_restart; \'unknown\' or \'restart-required\' (or the field being absent) → tell the user it needs a restart and offer dshm_restart.',
|
|
396
396
|
parameters: {
|
|
397
397
|
pkg: { type: 'string', required: true, description: '包名 from dshm_list / dshm_outdated' },
|
|
398
398
|
force: { type: 'boolean', description: 'peer 兼容预检不通过且用户已确认风险后置 true(等价 forceIncompatible,跳过 @deepseek-ai/dsh(-*) peer 拦截);守卫拦截(guard)无 force 通道,不受此参数影响。' },
|
|
@@ -577,7 +577,7 @@ function renderOutdated(out) {
|
|
|
577
577
|
head.push('询问用户要升级哪个,确认后调 dshm_upgrade(pkg)。');
|
|
578
578
|
return head.join('\n');
|
|
579
579
|
}
|
|
580
|
-
function renderUpgrade(out) {
|
|
580
|
+
export function renderUpgrade(out) {
|
|
581
581
|
// 0.8.5 修复:升级工具会命中装后守卫拦截(复用 installEntry,link/file prior 不可回退等),
|
|
582
582
|
// execute 返回 guard 形态结果——旧 render 没有该分支,把拦截渲染成「✅ undefined 已升级」假成功。
|
|
583
583
|
if (out.guard === true) {
|
|
@@ -592,7 +592,13 @@ function renderUpgrade(out) {
|
|
|
592
592
|
const from = out.fromVersion ? `v${out.fromVersion} → ` : '';
|
|
593
593
|
const to = out.version ? `v${out.version}` : out.sha ? out.sha.slice(0, 7) : '最新';
|
|
594
594
|
const extra = buildsNote(out);
|
|
595
|
-
|
|
595
|
+
// 0.9.22 生效判定三态分流:client-only 明确不问重启;unknown 保守建议重启;其余现状
|
|
596
|
+
const tail = out.activation === 'client-only'
|
|
597
|
+
? '纯客户端更新:刷新页面即可生效,无需重启——不要询问 dshm_restart。'
|
|
598
|
+
: out.activation === 'unknown'
|
|
599
|
+
? '生效判定未完成(网络或解析失败):为确保生效建议重启——询问是否 dshm_restart。'
|
|
600
|
+
: '需要重启生效——询问是否 dshm_restart。';
|
|
601
|
+
return `✅ ${out.pkg} 已升级(${from}${to})。${extra}${tail}`;
|
|
596
602
|
}
|
|
597
603
|
function clamp(n, min, max) {
|
|
598
604
|
return Math.min(max, Math.max(min, n));
|