dsh-m 0.9.1 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +13 -0
- package/README.md +13 -0
- package/lib/client.js +19 -4
- package/lib/core/registry.js +15 -4
- package/package.json +1 -1
- package/registry.json +3 -2
package/README.en.md
CHANGED
|
@@ -48,6 +48,19 @@ The default registry includes DSH Skins, ModSearch, the Lark / QQ / Weixin / WeC
|
|
|
48
48
|
|
|
49
49
|
The restart chain below applies to **Web only** (Desktop takes effect via the official app lifecycle: after installing, quit and reopen the Desktop app — see the 0.9.0 section). After any mutation, the already-open Market and Installed views refresh the profile state together, keeping badges and cards in sync without closing and reopening the marketplace; a "⚡ Restart" banner appears — under systemd, the DSH launcher's `appExit` hook hands the restart back to a unit configured with `Restart=on-failure` or `Restart=always`, avoiding a `systemctl` helper inside the unit cgroup that is about to stop; if `appExit` is unavailable, the fallback uses a manager-owned transient `systemd-run` service and only then a detached helper. The client confirms the replacement by boot id and dismisses the banner, leaving DSH Web's own background connection recovery in control; it does not force a full-page reload during the auth/route handoff. The restart chain has been live-verified on the current DSH Web `0.2.0-rc.2` (2026-10-01): dsh-m probes the unit's `Restart=` policy, hands the stop back via `appExit`, the `status=75/TEMPFAIL` exit is caught by the Restart policy and auto-started, and after recovery the page reconnects in the background with the panel fully functional. Historical scope: the `0.1.5-rc.1` era covered live `/dshm` ping and authenticated `303 → 200` checks; `0.1.2-rc.1` is covered by contract checks plus runtime shape-detection fallback (see the verified array) — with the deployment moved on, no live E2E is chased on retired generations; the transient `systemd-run` fallback only applies to hosts without `appExit` (all supported generations provide it) and stays a documented fallback rather than a release gate; repeated install/uninstall is covered by the profile-transaction test surface (compensation, post-install guard, 813 cases) plus the 0.4.0→0.4.2 consecutive-release evidence. Installs stream live pnpm progress (resolve → download → link → build).
|
|
50
50
|
|
|
51
|
+
### Fixed in 0.9.3 — Windows native support (verified on a real Windows host)
|
|
52
|
+
|
|
53
|
+
- **Registry atomic writes**: temp file names were derived with `split('/')`, but Windows paths separate with `\` — the "basename" came out as the entire path, the temp-file open always failed silently, and every cache / accepted-metadata write silently failed; now uses `basename()`, identical on all three platforms.
|
|
54
|
+
- **Local-file registry address parsing**: `C:\…` drive paths and `\\server\share` UNC were misread as URL schemes (with a misleading "HTTPS only" error), and `file://C:/…` normalized to a drive-less dangling path that always failed with ENOENT; all three local forms (POSIX absolute / Windows drive / UNC) now parse correctly as file kind.
|
|
55
|
+
- **Build**: `node_modules/.bin/tsc` is a POSIX sh shim; on Windows `spawnSync` failed with ENOENT — the build now runs TypeScript's JS entry with the current Node, identical on all platforms.
|
|
56
|
+
- **Test surface**: on a real Windows host the full 829-case suite went from 41 failures to **0** (821 pass / 8 skipped); POSIX-only contracts (process-group/SIGTERM timing, symlink privileges, platform path assertions, fixed-sleep timing) are now labeled skips, platform-neutral, or poll-based — test contracts unchanged.
|
|
57
|
+
|
|
58
|
+
### Fixed in 0.9.2 — Header chip stuck on the old version after a service restart
|
|
59
|
+
|
|
60
|
+
- **In-place refresh once the restart is confirmed**: the moment the boot id confirms DSH Web is back, the panel re-fetches ping — the `dsh-m vX.Y.Z` chip and the profile chip sync to the new process's data instead of lingering on the old version (observed on 0.9.1: chip upgrade + restart still showed v0.9.0).
|
|
61
|
+
- **Refetch when the page becomes visible again**: with the panel left open while the service is restarted externally, returning to the tab refetches ping via `visibilitychange` (zero polling cost).
|
|
62
|
+
- **Chip version guard**: a self-check verdict whose version doesn't match the current process (stale verdict from the old process) is treated as silent, preventing "v0.9.1 ⬆ v0.9.1"-style misrenders.
|
|
63
|
+
|
|
51
64
|
### New in 0.9.1 — Header version chip upgrade notice (lights up only when an update exists)
|
|
52
65
|
|
|
53
66
|
- **Silent by default**: the panel-header `dsh-m vX.Y.Z` chip keeps the static look introduced in 0.7.5 — up to date, failed check, or a local dev build ahead of npm (ahead) never interrupt (ahead only shows a "local dev build" hint in the hover title).
|
package/README.md
CHANGED
|
@@ -48,6 +48,19 @@
|
|
|
48
48
|
|
|
49
49
|
以下重启链路仅适用于 **Web**(Desktop 的生效走官方应用生命周期:安装完成后退出并重新打开 Desktop 应用即可,见 0.9.0 节)。安装 / 卸载 / 升级完成后,当前已打开的市场页与已装页会一起重新读取 profile 状态并同步徽标/卡片,不需要关闭后重新打开插件市场;随后出现「⚡ 一键重启」横幅——受 systemd 管理时通过 DSH launcher 的 `appExit` 交给服务的 `Restart` 策略,避免在待停止 unit 的 cgroup 内启动 `systemctl` helper;无 `appExit` 的 systemd 兜底改用 manager-owned transient `systemd-run`,最后才退回 detached-helper。客户端按 boot id 确认新进程已恢复后关闭横幅,交由 DSH Web 自身的后台连接重试恢复页面,不强制整页刷新,避免认证/路由切换期间白屏。重启链路已在当前 DSH Web `0.2.0-rc.2` 实机核验(2026-10-01):dsh-m 探测 unit `Restart=` 策略后经 `appExit` 交还 systemd,`status=75/TEMPFAIL` 退出由 Restart 策略接住自动拉起,服务恢复后页面后台重连、面板全功能可用。历史口径:`0.1.5-rc.1` 时代已核验 `/dshm` ping 与带认证 `303 → 200`;`0.1.2-rc.1` 按契约核对 + 形态探测兜底收录(verified 数组),部署面演进后不再追旧代 live E2E;transient `systemd-run` 兜底仅适用于无 `appExit` 的宿主(受支持代际均提供 `appExit`),保持设计兜底而非发布门槛;连续安装/卸载由 profile 事务测试面(补偿事务、装后守卫等 813 用例)与 0.4.0→0.4.2 连续发布实证覆盖。安装过程实时显示 pnpm 进度(解析 → 下载 → 链接 → 构建)。
|
|
50
50
|
|
|
51
|
+
### 0.9.3 修复:Windows 原生适配(Windows 宿主实机回归)
|
|
52
|
+
|
|
53
|
+
- **registry 原子写**:临时文件名此前用 `split('/')` 从绝对路径取尾段,而 Windows 路径分隔符是 `\`——取到的是整条路径(内嵌 `\` 即目录分隔符),临时文件 open 必败且被吞,cache 与 accepted metadata 写入**静默全失败**;改用 `basename()`,三平台一致。
|
|
54
|
+
- **本地文件清单地址解析**:`C:\…` 盘符与 `\\server\share` UNC 此前被误当 URL scheme(报「只允许 HTTPS」的误导性错误),`file://C:/…` 会归一成无盘符悬空路径必 ENOENT;现 POSIX 绝对 / Windows 盘符 / UNC 三形态均正确解析为 file kind。
|
|
55
|
+
- **构建**:`node_modules/.bin/tsc` 是 POSIX sh shim,Windows 下 `spawnSync ENOENT`;改用当前 Node 直跑 TypeScript 的 JS 入口,`npm run build` 三平台一致。
|
|
56
|
+
- **测试面**:Windows 实机全量 829 用例 41 败 → **0**(821 pass / 8 skipped);POSIX 进程组/SIGTERM 时序、symlink 权限、平台路径断言、固定 sleep 时序假设逐项标注 skip 或平台无关化/有界轮询,测试契约不变。
|
|
57
|
+
|
|
58
|
+
### 0.9.2 修复:跨服务重启后头部角标停留旧版本
|
|
59
|
+
|
|
60
|
+
- **一键重启确认新进程后就地刷新**:boot id 确认 DSH Web 已恢复的瞬间,面板立即重取 ping——`dsh-m vX.Y.Z` 角标与 profile chip 同步到新进程数据,不再停留旧版本(0.9.1 实测:芯片升级 + 重启后仍显 v0.9.0)。
|
|
61
|
+
- **页面回前台时重取**:面板常开、服务在后台被外部重启的场景,`visibilitychange` 回前台即重取 ping(零轮询成本)。
|
|
62
|
+
- **角标版本护栏**:self-check 判定携带的版本与当前进程不符(旧进程残留判定)时一律静默,杜绝「v0.9.1 ⬆ v0.9.1」式误渲染。
|
|
63
|
+
|
|
51
64
|
### 0.9.1 新增:头部版本角标升级提示(有更新才点亮)
|
|
52
65
|
|
|
53
66
|
- **静默口径**:面板头部 `dsh-m vX.Y.Z` 角标常态维持 0.7.5 起的静态展示——已是最新、检查失败、本地 dev 版领先 npm(ahead)一律不打扰(ahead 仅在悬停 title 里提示「本地开发版」)。
|
package/lib/client.js
CHANGED
|
@@ -1017,6 +1017,7 @@ function clearSelfCheckCache(storage) {
|
|
|
1017
1017
|
}
|
|
1018
1018
|
function deriveChipState(check, version) {
|
|
1019
1019
|
if (!check || typeof check !== "object") return { kind: "idle" };
|
|
1020
|
+
if (typeof version === "string" && version !== "" && check.current !== version) return { kind: "idle" };
|
|
1020
1021
|
if (check.outdated === true && typeof check.latest === "string" && check.latest !== "") {
|
|
1021
1022
|
return { kind: "outdated", latest: check.latest };
|
|
1022
1023
|
}
|
|
@@ -2001,7 +2002,7 @@ function TwoStepButton({ label, confirmLabel, className, onConfirm, disabled })
|
|
|
2001
2002
|
arm ? confirmLabel : label
|
|
2002
2003
|
);
|
|
2003
2004
|
}
|
|
2004
|
-
function RestartBanner({ note, onDone, desktop }) {
|
|
2005
|
+
function RestartBanner({ note, onDone, desktop, onRestarted }) {
|
|
2005
2006
|
const [phase, setPhase] = useState("idle");
|
|
2006
2007
|
const [err, setErr] = useState(null);
|
|
2007
2008
|
const restart = useCallback(async () => {
|
|
@@ -2030,12 +2031,18 @@ function RestartBanner({ note, onDone, desktop }) {
|
|
|
2030
2031
|
if (nextRestartWait2({ phase: "after-ping", bootChanged }) === "done") break;
|
|
2031
2032
|
}
|
|
2032
2033
|
setPhase("idle");
|
|
2034
|
+
if (onRestarted) {
|
|
2035
|
+
try {
|
|
2036
|
+
onRestarted();
|
|
2037
|
+
} catch {
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2033
2040
|
onDone(true);
|
|
2034
2041
|
} catch (e) {
|
|
2035
2042
|
setPhase("idle");
|
|
2036
2043
|
setErr(String(e && e.message || e));
|
|
2037
2044
|
}
|
|
2038
|
-
}, [onDone]);
|
|
2045
|
+
}, [onDone, onRestarted]);
|
|
2039
2046
|
return h(
|
|
2040
2047
|
"div",
|
|
2041
2048
|
{ className: "dshm-banner" },
|
|
@@ -3475,7 +3482,7 @@ function MarketPanel({ onClose }) {
|
|
|
3475
3482
|
};
|
|
3476
3483
|
const [pluginVersion, setPluginVersion] = useState(null);
|
|
3477
3484
|
const [profile, setProfile] = useState(null);
|
|
3478
|
-
|
|
3485
|
+
const reloadPing = useCallback(() => {
|
|
3479
3486
|
let live = true;
|
|
3480
3487
|
api("ping").then((r) => {
|
|
3481
3488
|
if (!live) return;
|
|
@@ -3487,6 +3494,14 @@ function MarketPanel({ onClose }) {
|
|
|
3487
3494
|
live = false;
|
|
3488
3495
|
};
|
|
3489
3496
|
}, []);
|
|
3497
|
+
useEffect(() => reloadPing(), [reloadPing]);
|
|
3498
|
+
useEffect(() => {
|
|
3499
|
+
const onVis = () => {
|
|
3500
|
+
if (typeof document !== "undefined" && !document.hidden) reloadPing();
|
|
3501
|
+
};
|
|
3502
|
+
document.addEventListener("visibilitychange", onVis);
|
|
3503
|
+
return () => document.removeEventListener("visibilitychange", onVis);
|
|
3504
|
+
}, [reloadPing]);
|
|
3490
3505
|
const marketReloadAll = useCallback(
|
|
3491
3506
|
(force) => {
|
|
3492
3507
|
marketCommunity.reload(force);
|
|
@@ -3648,7 +3663,7 @@ function MarketPanel({ onClose }) {
|
|
|
3648
3663
|
{ className: toast.kind === "err" ? "dshm-banner err" : "dshm-banner" },
|
|
3649
3664
|
h("span", { className: "dshm-banner-text" }, toast.text)
|
|
3650
3665
|
) : null,
|
|
3651
|
-
banner ? h(RestartBanner, { note: banner.text, onDone: () => setBanner(null), desktop: banner.desktop === true }) : null
|
|
3666
|
+
banner ? h(RestartBanner, { note: banner.text, onDone: () => setBanner(null), desktop: banner.desktop === true, onRestarted: reloadPing }) : null
|
|
3652
3667
|
)
|
|
3653
3668
|
);
|
|
3654
3669
|
}
|
package/lib/core/registry.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { createHash, randomUUID } from 'node:crypto';
|
|
7
7
|
import { createRequire } from 'node:module';
|
|
8
|
-
import { isAbsolute, join, normalize } from 'node:path';
|
|
8
|
+
import { basename, isAbsolute, join, normalize } from 'node:path';
|
|
9
9
|
import { constants as fsConstants, mkdirSync } from 'node:fs';
|
|
10
10
|
import { lstat, mkdir, open, readFile, readdir, realpath, rename, rm } from 'node:fs/promises';
|
|
11
11
|
import { WEB_PROFILE, cacheRoot } from './env.js';
|
|
@@ -44,7 +44,8 @@ function normalizeLocalPath(raw) {
|
|
|
44
44
|
const p = normalize(raw);
|
|
45
45
|
if (!isAbsolute(p))
|
|
46
46
|
throw new Error('本地 registry 必须是绝对路径或 file:// URL');
|
|
47
|
-
|
|
47
|
+
// Windows 原生分隔符是 \:只查 '/' 会把盘根 '\'、'C:\dir\' 误当文件放行
|
|
48
|
+
if (p.endsWith('/') || p.endsWith('\\'))
|
|
48
49
|
throw new Error('本地 registry 不能指向目录');
|
|
49
50
|
return p;
|
|
50
51
|
}
|
|
@@ -67,6 +68,10 @@ function parseFileUrl(input) {
|
|
|
67
68
|
catch {
|
|
68
69
|
throw new Error('file:// registry 路径百分号编码无效');
|
|
69
70
|
}
|
|
71
|
+
// Windows:WHATWG 把盘符路径统一表成 /C:/...(file://C:/x 同样归一到此前缀),
|
|
72
|
+
// 原生路径须剥掉这个前导斜杠,否则 normalize 出 '\C:\x' 悬空路径(无盘符)必 ENOENT
|
|
73
|
+
if (/^\/[a-zA-Z]:[/\\]/.test(pathname))
|
|
74
|
+
pathname = pathname.slice(1);
|
|
70
75
|
if (CONTROL_RE.test(pathname))
|
|
71
76
|
throw new Error('registry 路径包含控制字符');
|
|
72
77
|
const normalized = normalizeLocalPath(pathname);
|
|
@@ -106,7 +111,10 @@ export function parseRegistryAddress(raw) {
|
|
|
106
111
|
throw new Error('registry 地址包含控制字符');
|
|
107
112
|
if (/^file:/i.test(input))
|
|
108
113
|
return parseFileUrl(input);
|
|
109
|
-
|
|
114
|
+
// 本地路径三形态:POSIX 绝对(/…)、Windows 盘符(C:\… 或 C:/…)、Windows UNC(\\server\share…)。
|
|
115
|
+
// 盘符/UNC 判定必须先于 scheme 正则——'C:' 会被 /^[a-z][a-z0-9+.-]*:/i 误认成 URL scheme,
|
|
116
|
+
// 落进 parseHttpUrl 后抛误导性的「只允许 HTTPS」(Windows 下自定义文件清单的常规形态)。
|
|
117
|
+
if (input.startsWith('/') || input.startsWith('\\') || /^[a-zA-Z]:[/\\]/.test(input)) {
|
|
110
118
|
const normalized = normalizeLocalPath(input);
|
|
111
119
|
return { kind: 'file', input, normalized, cacheKey: stableKey(`file:${normalized}`) };
|
|
112
120
|
}
|
|
@@ -404,7 +412,10 @@ async function atomicWriteJson(target, dir, value, profile = WEB_PROFILE) {
|
|
|
404
412
|
}
|
|
405
413
|
if (existing && existing.isSymbolicLink())
|
|
406
414
|
return false;
|
|
407
|
-
|
|
415
|
+
// 临时文件名必须取 basename:Windows 分隔符是 \,用 split('/') 解析绝对路径会得到
|
|
416
|
+
// 整条路径(内嵌 \ 即目录分隔符),open 必败且被下方 catch 吞掉——全部 cache/metadata
|
|
417
|
+
// 写入静默失败(Windows 实测 2026-10-01:cache 恒缺失、metadataCommitted 恒 false)。
|
|
418
|
+
const tmp = join(dir, `.${basename(target)}.tmp-${process.pid}-${randomUUID().slice(0, 8)}`);
|
|
408
419
|
const fh = await open(tmp, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_NOFOLLOW, 0o600);
|
|
409
420
|
try {
|
|
410
421
|
await fh.write(Buffer.from(JSON.stringify(value, null, 2), 'utf8'));
|
package/package.json
CHANGED
package/registry.json
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
{
|
|
28
28
|
"id": "dsh-skins",
|
|
29
29
|
"name": "DSH Skins",
|
|
30
|
-
"description": "DSH Web 皮肤插件:多套品牌皮肤侧栏热切换,一键回到官方界面。已适配 0.1.7-rc.2 与 0.2.0-rc.1,详见仓库。",
|
|
30
|
+
"description": "DSH Web 皮肤插件:多套品牌皮肤侧栏热切换,一键回到官方界面。已适配 0.1.7-rc.2 与 0.2.0-rc.1/rc.2,详见仓库。",
|
|
31
31
|
"category": "ui",
|
|
32
32
|
"tags": [
|
|
33
33
|
"皮肤",
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"verified": [
|
|
38
38
|
"0.1.2-rc.1",
|
|
39
39
|
"0.1.7-rc.2",
|
|
40
|
-
"0.2.0-rc.1"
|
|
40
|
+
"0.2.0-rc.1",
|
|
41
|
+
"0.2.0-rc.2"
|
|
41
42
|
],
|
|
42
43
|
"source": "npm",
|
|
43
44
|
"npm": "@iasiv5/dsh-skins",
|