dsh-wx-skin 0.1.5 → 0.2.0

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.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # 🎨 dsh-wx-skin
2
2
 
3
- **DeepSeek Harness(DSH)Web GUI 皮肤插件** —— 侧栏「皮肤」面板,自选本地图片或图片 URL 作为全屏磨砂背景,支持预设、暗化、模糊与透出调节,明暗主题自动适配,跨刷新持久化。
3
+ **DeepSeek Harness(DSH)Web GUI 皮肤插件** —— 在 **设置 → 皮肤** 里指定图片文件夹或本地图片作为全屏磨砂背景,支持暗化、模糊与透出调节,明暗主题自动适配,跨刷新持久化。
4
4
 
5
5
  ![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)
6
6
  ![npm version](https://img.shields.io/npm/v/dsh-wx-skin.svg)
@@ -13,27 +13,40 @@
13
13
 
14
14
  ## ✨ 功能特性
15
15
 
16
- - **侧栏「皮肤」入口**,点击弹出设置面板(浮层,不遮挡聊天内容);旁边另有「下一张」入口,不打开面板也能一键换图。
17
- - **本地图片**:浏览器原生文件对话框选择 PNG / JPEG / WebP / GIF / BMP,**不限文件大小**;按显示需要编码(≤4096px 保持原始分辨率,更大的自动缩放以保证浏览器可绘制、可持久化),选中后点「应用」生效。
18
- - **图片 URL**:粘贴 `http(s)://` 图片地址直接应用。
16
+ - **设置里的「皮肤」页**:所有皮肤设置挂进 harness **自带设置面板**的官方加法页位 `settings.section`——点侧栏 ⚙ 设置,左栏多出「皮肤」一项,内容列按设置面板既有的内边距与滚动渲染,不再有会超出窗口的浮层。
17
+ - **侧栏底部换图快捷键**:「上一张」「下一张」以**一次注册**挂进官方 `sidebar.footer.action` 插槽(设置行上方),不打开设置也能一键换图;侧栏收成 56px 图标轨时自动变成两个图标按钮。
18
+ - **两行路径,一个模型**:设置页只有「文件夹」和「本地文件」两行,不再有「背景来源」下拉框。
19
+ - **文件夹**:当前文件夹的路径(可用 harness 宿主目录选择器,也可手填绝对路径)。
20
+ - **本地文件**:当前背景图片的**完整路径**;背景来自 URL 时这里显示 URL;用浏览器文件对话框选图且浏览器不给目录时显示文件名并给出提示。
21
+ - **选文件自动带出文件夹**:在「本地文件」行粘贴一个图片绝对路径并点「应用」,插件读盘扫描它所在的文件夹,把该文件设为当前背景,并**按目录顺序从它继续**切换。
22
+ - **对话框选图也会接上文件夹**:浏览器文件对话框不给目录,但只要**已加载的文件夹里恰有一张同名图片**,点「应用」时就会定位到它——背景变成那张、完整路径显示在「本地文件」行、「下一张 / 上一张」从它继续(选 bg129,下一张就是 bg130)。同名图片不止一张(递归扫到的不同子文件夹)时不做猜测,退回编码图并给出提示。
23
+ - **选文件夹自动带出文件**:加载文件夹后,当前那张的完整路径会显示在「本地文件」行。
24
+ - **本地图片**:浏览器原生文件对话框选择 PNG / JPEG / WebP / GIF / BMP,**不限文件大小**;按显示需要编码(≤4096px 保持原始分辨率,更大的自动缩放以保证浏览器可绘制、可持久化),选中后点「应用」生效。⚠️ 浏览器(含 Electron 43)的文件对话框**不提供目录**,所以这条路本身拿不到路径;文件夹已加载时按上面的「同名匹配」接上轮播,否则可把完整路径粘到「本地文件」行。
25
+ - **图片 URL**:在「本地文件」行粘贴 `http(s)://` 图片地址再点「应用」,即按 URL 作为背景(该行同时就是当前图片路径 / URL 的显示位)。
19
26
  - **文件夹轮播**:指定一个本地文件夹(可调用 harness 的宿主目录选择器,也可手填绝对路径),插件读盘列出其中所有图片并**缓存路径**;「下一张」按你选的模式换图:
20
- - **顺序**:按文件名自然排序(数值感知,`img2` 在 `img10` 之前)依次推进,到底回绕;
27
+ - **顺序**:按文件名自然排序(数值感知,`img2` 在 `img10` 之前)**逐张推进,两端回绕**——选到第 10 张,「下一张」就是第 11 张,「上一张」就是第 9 张;
21
28
  - **随机**:从未切换的图片里等概率抽取;
22
- - **一轮内不重复**:已切换过的图片会被记录,只有本轮图片全部切过之后才重置队列;重置后的第一张不会与当前这张重复(只有一张时除外)。
23
- - 可选「包含子文件夹」;单次最多缓存 2000 张、遍历 20000 项、递归深度 6,达上限时面板会提示已截断。
24
- - **上一张**:回到上一张图片——顺序模式按文件名回退(首张回绕到末张),随机模式回到**上一次显示过的那张**;无路可退时按钮与侧栏入口置灰。
25
- - **自动同步**:回到页面/切回标签页、每 60 秒、以及打开面板时都会重新读盘扫描该文件夹;新增的图片直接进入未切换队列,**当前图片与本轮记录不会被重置**;已删除的图片会被移出(当前那张被删时按原序号就近落位)。
26
- - **背景来源下拉框**:面板顶部一个下拉框在「图片」与「文件夹」之间切换,分别只显示「选择图片 / 图片 URL / 预设」或「文件夹路径 / 顺序随机 / 下一张 / 本轮进度」,面板不再把所有控件堆在一起(效果滑杆与「恢复默认」始终可见)。
27
- - **当前图片名称**:面板在「当前:」下方显示正在使用的图片名称——文件夹显示当前那张的文件名、本地图片显示所选文件名、URL 显示路径末段(如 `photo.jpg`)、预设显示预设名;过长自动省略,悬停可看到完整路径 / URL;未启用或没有背景时不显示这一行。侧栏「下一张」入口的悬停提示也带上当前名称。
28
- - **预设皮肤**:墨蓝 / 石板 / 暖沙 / 落日渐变 / 深海渐变 / 极光渐变。
29
+ - **随机模式一轮内不重复**:已切换过的图片会被记录,只有本轮图片全部切过之后才重置队列;重置后的第一张不会与当前这张重复(只有一张时除外)。顺序模式按目录顺序走,天然不会在回绕前重复。
30
+ - 可选「包含子文件夹」;单次最多缓存 2000 张、遍历 20000 项、递归深度 6,达上限时设置页会提示已截断。
31
+ - **上一张**:顺序模式回到目录里的上一张(首张回绕到末张),随机模式回到**上一次显示过的那张**;无路可退时按钮与侧栏快捷键置灰。
32
+ - **自动同步**:回到页面/切回标签页、每 60 秒、以及打开设置页时都会重新读盘扫描该文件夹;新增的图片进入列表,**当前图片不会被重置**;已删除的图片会被移出(当前那张被删时按原序号就近落位)。
33
+ - **背景预览**:设置页顶部一块大预览(`height: min(36vh, 340px)` + `background-size: contain`),**完整显示**当前背景整张图(不裁切,两边留白),背景未启用时显示「未启用」。
34
+ - **当前图片名称**:设置页在「当前:」下方显示正在使用的图片名称——文件夹显示当前那张的文件名、本地图片显示所选文件名、URL 显示路径末段(如 `photo.jpg`);过长自动省略,悬停可看到完整路径 / URL;未启用或没有背景时不显示这一行。侧栏「下一张」快捷键的悬停提示也带上当前名称。
35
+ - **适合度**:背景图如何铺满窗口,和 Windows 壁纸的「选择适合度」同款语义:
36
+ - **填充**(默认):铺满窗口,超出的裁掉;
37
+ - **适应**:完整显示整张图,按比例缩放并留白;
38
+ - **拉伸**:拉满窗口,比例可能变形;
39
+ - **平铺**:按图片原始尺寸重复铺满;
40
+ - **居中**:按原始尺寸居中,四周留白。
41
+ (Windows 还多一个「跨区」,那是多显示器概念——浏览器只有一个视口,因此不提供。)
29
42
  - **效果调节**:
30
43
  - 暗化 0–80%(黑色遮罩,保证文字可读);
31
44
  - 模糊 0–24px(背景毛玻璃);
32
45
  - **透出 0–100%**(表面不透明度,越低背景越明显,默认 72%;可读性由「暗化」配合)。
33
46
  - **启用开关 + 恢复默认**,一键关闭皮肤。
34
47
  - **持久化**:设置同时存于 `localStorage` 与宿主副本 `~/.dsh/dsh-wx-skin.settings.json`,刷新、重启、桌面端换端口后均自动恢复;文件夹轮播的图片列表、当前序号、本轮已用记录同样持久化(只存路径,不存图片本体,不占 localStorage 配额)。
35
- - **明暗主题适配**:随 `body[data-ds-dark-theme]` 自动切换两套半透明配色;设置面板自身也跟随明暗主题(暗色模式为不透明深色卡片 + 浅色文字),`color-scheme` 让面板内的勾选框、滑杆、滚动条与面板同色系。
36
- - **参与 harness 生命周期**:浏览器半区通过 `ctx.effect` 注册挂载,client-hmr 重载或插件行停用时会被完整卸载(入口、面板、背景层、样式表全部移除,不残留、不重复)。
48
+ - **明暗主题适配**:随 `body[data-ds-dark-theme]` 自动切换两套半透明配色;设置页自身也跟随明暗主题(暗色模式为深色控件 + 浅色文字),`color-scheme` 让页内的勾选框、滑杆、滚动条与页面同色系。
49
+ - **参与 harness 生命周期**:浏览器半区通过 `ctx.effect` 注册挂载,设置页与侧栏入口都通过官方插槽注册;client-hmr 重载或插件行停用时会被完整卸载(插槽注册、背景层、样式表全部移除,不残留、不重复)。
37
50
  - **完全独立**:纯浏览器端 client 插件,不修改 DSH 仓库,不影响主界面与其它插件。
38
51
 
39
52
  ---
@@ -47,19 +60,19 @@
47
60
  插件已发布到 npm,一条命令装齐(在 **DSH 源码 checkout 目录**执行):
48
61
 
49
62
  ```sh
50
- pnpm dsh plugin --profile web add dsh-wx-skin@0.1.5
63
+ pnpm dsh plugin --profile web add dsh-wx-skin@0.2.0
51
64
  # 若 `dsh` 已加入 PATH,也可直接:
52
- dsh plugin --profile web add dsh-wx-skin@0.1.5
65
+ dsh plugin --profile web add dsh-wx-skin@0.2.0
53
66
  ```
54
67
 
55
68
  ### 方式二:使用发布包(tarball)
56
69
 
57
- 下载 `dsh-wx-skin-0.1.5.tgz`(或通过 GitHub Releases 获取):
70
+ 下载 `dsh-wx-skin-0.2.0.tgz`(或通过 GitHub Releases 获取):
58
71
 
59
72
  ```sh
60
73
  pnpm dsh plugin --profile web add file:<tgz 的绝对路径>
61
74
  # 例如:
62
- pnpm dsh plugin --profile web add file:C:/Users/you/Downloads/dsh-wx-skin-0.1.5.tgz
75
+ pnpm dsh plugin --profile web add file:C:/Users/you/Downloads/dsh-wx-skin-0.2.0.tgz
63
76
  ```
64
77
 
65
78
  ### 方式三:克隆源码构建(开发者)
@@ -76,7 +89,7 @@ pnpm dsh plugin --profile web add link:<本目录绝对路径>
76
89
 
77
90
  ### 完成安装后
78
91
 
79
- **重启 `dsh web`**,刷新 `http://127.0.0.1:3080`,侧栏出现「皮肤」入口即可使用。
92
+ **重启 `dsh web`**,刷新 `http://127.0.0.1:3080`,打开 **设置 → 皮肤** 即可使用(侧栏底部另有「上一张 / 下一张」换图快捷键)。
80
93
 
81
94
  验证是否挂载:
82
95
 
@@ -88,14 +101,13 @@ pnpm dsh --profile web --dump-config # 应看到 "# == dsh-wx-skin" 层
88
101
 
89
102
  ## 🚀 使用
90
103
 
91
- 1. 点击侧栏「皮肤」打开面板。
92
- 2. **选背景**:先用顶部「背景来源」下拉框选择「图片」或「文件夹」,面板只显示该分组对应的控件(效果滑杆与恢复默认始终可见):
93
- - 点「选择图片」→ 选择本地图片 → 显示缩略图 → 点「应用」;
94
- - 或粘贴图片 URL → 点「应用」;
95
- - 或**加载文件夹**:点「选择文件夹」(调用宿主目录选择器)或直接粘贴绝对路径 → 点「加载」→ 面板显示 `第 n/N 张 · 本轮已用 m/N`;
96
- - 或直接点一个预设色板。
97
- 3. **换图**:点面板里的「下一张」,或侧栏「下一张」入口(无需打开面板);切换方式在「顺序 / 随机」之间选;「重新加载」重新读盘以纳入新增/删除的文件,「重置本轮」清空已用记录;点「上一张」回退(顺序模式按文件名回退并首尾回绕,随机模式回到上一次显示的那张)。**未加载任何文件夹时「下一张」什么都不做**(侧栏入口置灰并提示先加载文件夹),不会弹出任何面板或对话框。 文件夹内容变化会被自动同步(回到页面、切回标签页或每 60 秒),也可点「重新加载」立即重扫。
98
- 4. **调效果**:拖动「暗化」「模糊」「透出」滑杆实时预览。
104
+ 1. 打开侧栏的 **⚙ 设置**,在左栏选 **皮肤**。
105
+ 2. **选背景**:页面只有「文件夹」和「本地文件」两行(效果滑杆与恢复默认始终可见):
106
+ - **选文件夹**:点「选择文件夹」(调用宿主目录选择器)或直接粘贴绝对路径 → 点「加载」;加载后当前那张的完整路径会显示在「本地文件」行,状态行显示 `第 n/N 张`;
107
+ - **选本地文件**:点「选择图片」用系统文件对话框 → 显示缩略图 → 点「应用」——若已加载的文件夹里有同名图片,会自动定位到它并从这里继续切换;或在「本地文件」行**粘贴图片的绝对路径**再点「应用」,插件会切到该文件所在文件夹,并从这个文件开始按目录顺序切换;
108
+ - **图片 URL**:把 `http(s)://` 地址粘贴到「本地文件」行 → 点「应用」。
109
+ 3. **换图**:点设置页里的「下一张」,或侧栏底部的「下一张」快捷键(无需打开设置);切换方式在「顺序 / 随机」之间选;「重新加载」重新读盘以纳入新增/删除的文件(随机模式下可用「重置本轮」清空本轮记录);点「上一张」回退——**顺序模式就是目录里的上一张/下一张**(选到第 10 张,下一张 11、上一张 9),随机模式回到上一次显示的那张。**未加载任何文件夹时「下一张」什么都不做**(快捷键置灰并提示先加载文件夹)。 文件夹内容变化会被自动同步(回到页面、切回标签页或每 60 秒),也可点「重新加载」立即重扫。
110
+ 4. **调效果**:拖动「暗化」「模糊」「透出」滑杆实时预览;上面一行「适合度」下拉框决定背景图如何铺满窗口(填充 / 适应 / 拉伸 / 平铺 / 居中)。
99
111
  5. 「启用皮肤」开关控制总开关;「恢复默认」一键还原(同时清空文件夹缓存)。
100
112
 
101
113
  > ⚠️ 文件夹轮播需要**宿主半区**读盘,因此首次升级到 0.1.x 后要**重启一次 `dsh web`**;只改浏览器半区时才可依赖 client-hmr 热重载。
@@ -108,26 +120,33 @@ pnpm dsh --profile web --dump-config # 应看到 "# == dsh-wx-skin" 层
108
120
  pnpm dsh plugin --profile web remove dsh-wx-skin
109
121
  ```
110
122
 
111
- 重启 `dsh web` 后入口消失,皮肤设置一并清除。
123
+ 重启 `dsh web` 后设置页与快捷键消失,皮肤设置一并清除。
112
124
 
113
125
  ---
114
126
 
115
127
  ## ⚙️ 工作原理
116
128
 
117
- - **形态**:外部 client 插件(参考 [dsh-web-ui](https://github.com/zhu1090093659/dsh-web-ui) 模式)——`package.json` 声明 `dsh.client`(浏览器半区)+ `dsh.bundle.patch`(`cordis.patch.yml` 插入加载行),构建产物经 tsdown 输出为 `lib/client.js`,由 DSH 的 client-modules 经 `/plugins/??<id>/client.js&rev=<rev>` 组合端点提供(DSH 0.1.5 起带 `rev`;0.1.6 起 `rev` 为启动期不透明版本号,并由 client-hmr 轮询 bundle 变更后热重载)。
118
- - **生命周期**:`src/client/index.ts` 的 `apply(ctx)` 用 `ctx.effect(() => mountSkin(), 'dsh-wx-skin: skin mount')` 注册挂载。harness 的客户端热重载(client-hmr)与插件行停用都是“先卸载旧 fiber(跑 disposer)、删掉该插件拥有的 `<style data-plugin>`,再重新 apply”,因此必须把清理交给 fiber:否则每次重载都会多出一个「皮肤」入口并泄漏 body 级 MutationObserver。挂载失败时记录日志并 `teardownSkinDom()` 清理半成品,绝不外抛。
119
- - **背景层**:注入全屏 `div[data-wx-skin-layer]`(`position: fixed; z-index: 0; pointer-events: none`),并将应用根 `#root` 抬到 `z-index: 1`。⚠️ 实测 **`z-index: -1` 的 fixed 图层在 DSH shell 中不绘制**(落在 canvas 背景之下),这是早期版本"能选图但背景不显示"的根因,故采用 `z-index: 0` + `#root` 抬升方案。
120
- - **半透明表面**:以独立 `<style>` + `!important` 覆盖十余个 alias 表面 token(`--dsw-alias-bg-*`、`--dsw-specific-*`、`--dsw-alias-markdown-*` 等),明暗两套值;透明度由 `--wx-skin-surface` 变量统一控制(不依赖 `color-mix()`,任意现代浏览器可用)。该样式表带 `data-plugin="dsh-wx-skin"` 归属标记(与官方 CSS loader 一致):client-modules 会把未标记的 `<style>` 认领给下一个物化的插件,client-hmr 又按此属性删除插件拥有的样式,不标记就可能被别的插件连带删除。
129
+ - **形态**:外部 client 插件(参考 [dsh-web-ui](https://github.com/zhu1090093659/dsh-web-ui) 模式)——`package.json` 声明 `dsh.client`(浏览器半区)+ `dsh.bundle.patch`(`cordis.patch.yml` 插入加载行),构建产物经 tsdown 输出为 `lib/client.js`(闭包工厂 `window.__ModuleLoader__.load({id, factory})`),由 DSH 的 client-modules 提供(DSH 0.1.5 起 bundle 引用带 `rev`;0.1.7 起该引用为**文档相对**形式 `plugins/??<id>/client.js&rev=<rev>`,由 `<base href="./">` 解析到当前挂载,并由 client-hmr 轮询 bundle 变更后热重载)。
130
+ - **设置页**:皮肤设置作为**独立一页**挂进 harness 官方设置面板的加法页位 `settings.section`(`ctx.slots.inject` + `ctx.slots.register({name, id: 'skin', order: 30, label: '皮肤'}, SkinSettings)`)。shell 拥有这个 800×800 面板:左栏按 `order` 列出各注册页并解析 `label`,内容列按既有内边距(`padding: 0 24px 24px`)与滚动渲染选中页,占用者收到 owner props `{ close }`(本插件不用——关闭走面板自身的关闭按钮 / Esc)。**皮肤不再自带浮层**:侧栏脚部的锚点离窗口底边太近,浮层必然越出视口,把设置放进设置面板才是最稳的落点。
131
+ - **侧栏换图快捷键**:「上一张 / 下一张」以**一次注册**挂进官方 `sidebar.footer.action` 插槽(`{name, id: 'skin-slideshow', order: 10, label: '换图'}`),由 shell 负责位置、排序与卸载;占用者收到 owner props `{ wide }`——`wide` 为真渲染整行两个等分按钮(图标 + 文字),为假(56px 图标轨)渲染两个 28px 图标。插件**不再**查询 `sidebarCol` / `logoRow` / `newSession` 等 shell 内部类名,也不再向侧栏 DOM 注入或挂 MutationObserver。
132
+ - **为什么是一次注册**:该插槽的每一项都渲染成同一 flex 行的直接子节点,而随 DSH 发布的 `cordis-panel`(ui-cordis)占用者用 `width: 100%; flex: none` 独占整行;若注册两个同级项,排在其后的项会被压成 0 宽。整组一个占用者即把布局收回自己手里。
133
+ - **并排共存**:分组挂载时把自己所在的那一行设为 `flex-wrap: wrap`(只改 shell 交给它的那个父元素,卸载时还原),这样多个占满整行的占用者各占一行、互不裁剪。
134
+ - **生命周期**:`src/client/index.ts` 的 `apply(ctx)` 用 `ctx.effect(() => …)` 建立控制器并在其中注册两个插槽。harness 的客户端热重载(client-hmr)与插件行停用都是“先卸载旧 fiber(跑 disposer)、删掉该插件拥有的 `<style data-plugin>`,再重新 apply”,因此必须把清理交给 fiber:否则每次重载都会多出一份设置页与一组快捷键。挂载失败时记录日志并 `teardownSkinDom()` 清理半成品,绝不外抛。
135
+ - **作用域可选依赖**:`slots` 与 `uiWorkspace` 都用 `ctx.inject([...], scope => …)` 的**子作用域**形式声明,而不是插件的 `inject`——`uiWorkspace` 要等客户端↔宿主连接就绪才 provide(比 `apply` 晚),而 `inject: ['slots']` 会让整个浏览器半区(含背景层与持久化)一起等待,并多一个启动审计失败面。`settings.section` / `sidebar.footer.action` 的**声明**到达时才建立注册,ui-settings / ui-sidebar 热重载(声明 epoch 变化)会自动反向清理并按新声明重建。
136
+ - **背景层**:注入全屏 `div[data-wx-skin-layer]`(`position: fixed; z-index: 0; pointer-events: none`),并将应用根 `#root` 抬到 `z-index: 1`。⚠️ 实测 **`z-index: -1` 的 fixed 图层在 DSH shell 中不绘制**(落在 canvas 背景之下),这是早期版本"能选图但背景不显示"的根因,故采用 `z-index: 0` + `#root` 抬升方案。铺图方式由「适合度」决定:applier 把模式投影成 `--wx-skin-bg-size` / `--wx-skin-bg-repeat` / `--wx-skin-bg-position` 三个变量,图层用它们取 `background-size/repeat/position`(默认值即「填充」)。
137
+ - **半透明表面**:以独立 `<style>` + `!important` 覆盖二十个表面 token(`--dsw-alias-bg-*`、`--dsw-specific-*`、`--dsw-alias-markdown-*`,含 0.1.7 改成字面量 rgba 的 `--dsw-specific-menu`),明暗两套值;透明度由 `--wx-skin-surface` 变量统一控制(不依赖 `color-mix()`,任意现代浏览器可用)。该样式表带 `data-plugin="dsh-wx-skin"` 归属标记(与官方 CSS loader 一致):client-modules 会把未标记的 `<style>` 认领给下一个物化的插件,client-hmr 又按此属性删除插件拥有的样式,不标记就可能被别的插件连带删除。
121
138
  - **图片管线**:canvas 解码 → 编码为 JPEG data URL;原始分辨率 ≤4096px 时保持原样,更大或超出浏览器存储容量时静默缩小;编码结果做有效性校验,异常自动降档重编——**永不因图片大小报错**。
122
- - **宿主持久化**:`src/index.ts` 通过 `webServer` 服务注册 `/dsh-wx-skin/load` 与 `/dsh-wx-skin/save` 两条回环路由,把设置原子写入 DSH home(`$DSH_HOME` 或 `~/.dsh`)下的 `dsh-wx-skin.settings.json`;桌面端每次启动端口变化、localStorage 清空时,客户端自动从宿主副本恢复——背景图片跨会话不丢失。
139
+ - **宿主持久化**:`src/index.ts` 通过 `webServer` 服务注册 `/dsh-wx-skin/load` 与 `/dsh-wx-skin/save` 两条回环路由(注册键保持根绝对),把设置原子写入 DSH home(`$DSH_HOME` 或 `~/.dsh`)下的 `dsh-wx-skin.settings.json`;桌面端每次启动端口变化、localStorage 清空时,客户端自动从宿主副本恢复——背景图片跨会话不丢失。浏览器侧只使用**文档相对**引用(`dsh-wx-skin/load`),经 shell 注入的 `<base href="./">` 解析到当前挂载,因此在带前缀剥离代理的部署下同样命中。
123
140
  - **文件夹轮播**:
124
141
  - `POST /dsh-wx-skin/folder` 读盘扫描(可选递归),只收 `png/jpg/jpeg/webp/gif/bmp`,跳过隐藏项与符号链接,按 `rel` 自然排序后返回路径列表;上限 2000 张 / 20000 项 / 深度 6,超限返回 `truncated`。客户端把列表、当前序号、本轮已用路径一起持久化(只存路径)。
125
142
  - `GET /dsh-wx-skin/image?p=<绝对路径>&v=<mtime>` 逐个回传图片字节,**只服务上一次成功扫描的根(或设置文件里的 `folderPath`)之内的文件**:`realpath` 归一 + 分隔符前缀比较(Windows 忽略大小写)+ 扩展名白名单 + 必须是文件;`v` 用 mtime 做缓存令牌,文件改动即换 URL。跨站请求(`Sec-Fetch-Site: cross-site`)直接拒绝,响应带 `X-Content-Type-Options: nosniff`。
126
- - 切换算法在 `src/client/skin-folder.ts`(纯函数、注入 rng):顺序模式按索引向后推进,随机模式在**未切换集合**中等概率抽取;集合为空时才开启新一轮,并排除当前这张以避免立即重复。已切换记录按路径而非下标保存,文件夹内容变化时自动丢弃失效项。
127
- - 「选择文件夹」按钮调用 harness 客户端服务 `uiWorkspace.pickDirectory()`。该服务由 `ui-workspace` 提供,而它自己 inject 了 `remote.directoryPicker` 等依赖,即**只有客户端↔宿主连接就绪后才会 provide**——比本插件的 `apply` 晚。因此插件用 `ctx.inject(['uiWorkspace'], scope => …)` 把它当作**作用域可选依赖**(不是插件的 `inject`,插件不会因此等待),并通过 `src/client/skin-picker.ts` 的桥**懒读取**:服务到达/离开时通知面板重渲染,按钮始终可见,未就绪时置灰并提示;缺失或不可用时仍可用手填路径。
128
- - 面板内「下一张」在未加载文件夹时禁用;侧栏「下一张」入口同样置灰(`data-empty`)且点击不产生任何副作用。
143
+ - 切换算法在 `src/client/skin-folder.ts`(纯函数、注入 rng):**顺序模式按目录顺序 +1 / -1 并两端回绕**(选到第 10 张就继续 11 / 回到 9),随机模式在**未切换集合**中等概率抽取;集合为空时才开启新一轮,并排除当前这张以避免立即重复。本轮记录只服务随机模式,按路径而非下标保存,文件夹内容变化时自动丢弃失效项。
144
+ - 同一文件里的 `matchFolderImageByName` / `focusFolderImage` 负责「按文件名把对话框选中的图接回文件夹」:唯一同名才定位,同名歧义直接放弃(宁可显示编码图,也不默默显示错的那张);定位会同时清掉不再使用的 data URL / URL,持久化里不会残留两套来源。
145
+ - 路径词汇在 `src/client/skin-path.ts`(纯字符串):判断 URL / 绝对路径、「本地文件」行的路径属于哪个文件夹(`parentDirectory`)、以及宿主规范化路径与用户输入是否为同一文件(`samePath`,Windows 忽略大小写与分隔符)。
146
+ - 「选择文件夹」按钮调用 harness 客户端服务 `uiWorkspace.pickDirectory()`。该服务由 `ui-workspace` 提供,而它自己 inject 了 `remote.directoryPicker` 等依赖,即**只有客户端↔宿主连接就绪后才会 provide**——比本插件的 `apply` 晚。因此插件用 `ctx.inject(['uiWorkspace'], scope => …)` 把它当作**作用域可选依赖**(不是插件的 `inject`,插件不会因此等待),并通过 `src/client/skin-picker.ts` 的桥**懒读取**:服务到达/离开时通知设置页重渲染,按钮始终可见,未就绪时置灰并提示;缺失或不可用时仍可用手填路径。
147
+ - 设置页里的「下一张」在未加载文件夹时禁用;侧栏「下一张」快捷键同样置灰(`data-empty`)且点击不产生任何副作用。
129
148
  - **不依赖 `ctx.theme` 服务**:皮肤完全独立于 DSH 主题系统,关闭时样式惰性、默认主题不受影响。
130
- - **面板配色**:设置面板是不透明浮层,自带一套语义变量 `--wx-panel-*`(浅色默认 + `body[data-ds-dark-theme]` 覆盖),面板区**刻意不引用 `--dsw-alias-*`**:alias 令牌会随主题反转(暗色下 `--dsw-alias-label-primary` 近白、`--dsw-alias-border-l1` 近透明),而面板底色固定,二者叠加就会出现"浅底浅字"。每个变量都带与官方 `--dsw-static-*` 令牌等值的字面量回退,并由 `tests/panel-theme.spec.ts` 校验两套调色板的文字对比度(≥4.5:1)与结构。
149
+ - **设置页配色**:设置页自带一套语义变量 `--wx-panel-*`(浅色默认 + `body[data-ds-dark-theme]` 覆盖),页面区**刻意不引用 `--dsw-alias-*`**:alias 令牌会随主题反转(暗色下 `--dsw-alias-label-primary` 近白、`--dsw-alias-border-l1` 近透明),而页内控件的底色调过,二者叠加就会出现"浅底浅字"。每个变量都带与官方 `--dsw-static-*` 令牌等值的字面量回退,并由 `tests/panel-theme.spec.ts` 校验两套调色板的文字对比度(≥4.5:1)与结构。
131
150
 
132
151
  ---
133
152
 
@@ -136,7 +155,7 @@ pnpm dsh plugin --profile web remove dsh-wx-skin
136
155
  ```sh
137
156
  npm install # 安装依赖
138
157
  npm run typecheck # tsc 类型检查(宿主 + 客户端)
139
- npm run test # vitest 单元测试(skin-store / skin-folder / folder-scan / skin-host / client-lifecycle / panel-theme)
158
+ npm run test # vitest 单元测试(skin-store / skin-folder / folder-scan / skin-host / skin-picker / image-pipeline / client-lifecycle / panel-theme / global-skin-css / route-resolution)
140
159
  npm run build # tsc 宿主 lib + tsdown client bundle
141
160
  npm pack # 产出发布包 dsh-wx-skin-<version>.tgz
142
161
  ```
@@ -157,17 +176,21 @@ dsh-wx-skin/
157
176
  │ ├── folder.ts # 宿主:文件夹扫描 / 位图白名单 / 路径收敛(io 注入,可测)
158
177
  │ ├── core/types.ts # 共享类型(SkinSettings / SkinFolderImage)
159
178
  │ └── client/
160
- │ ├── index.ts # 浏览器半区入口(ctx.effect 注册挂载 + 失败清理)
161
- │ ├── mount.tsx # 侧栏「皮肤」/「下一张」入口 + 面板宿主(自愈挂载、单一 commit)
162
- │ ├── SkinPanel.tsx # 受控设置面板(背景来源下拉:图片 / 文件夹,分组显示)
163
- │ ├── skin-folder.ts # 纯逻辑:排序、图片 URL、去重切换算法(注入 rng)
164
- │ ├── skin-store.ts # 纯逻辑:默认值、localStorage、预设、CSS 变量映射
165
- │ ├── skin-host.ts # 宿主调用(load / save / folder,loopback fetch,可注入)
179
+ │ ├── index.ts # 浏览器半区入口(控制器 + 两个官方插槽注册 + 作用域可选依赖 + 失败清理)
180
+ │ ├── skin-controller.tsx # 皮肤控制器:设置状态与应用、轮播、持久化、重扫、订阅快照
181
+ │ ├── skin-settings.tsx # 设置页(注册进官方 settings.section 插槽)
182
+ │ ├── entries.tsx # 侧栏脚部换图快捷键(注册进官方 sidebar.footer.action 插槽)
183
+ │ ├── SkinPanel.tsx # 受控设置页内容(文件夹 / 本地文件两行 + 切换 + 效果)
184
+ │ ├── skin-path.ts # 纯逻辑:URL / 绝对路径判定、所在文件夹、路径同一性
185
+ │ ├── skin-folder.ts # 纯逻辑:排序、图片 URL、顺序/随机切换算法(注入 rng)
186
+ │ ├── skin-store.ts # 纯逻辑:默认值、localStorage、CSS 变量映射
187
+ │ ├── skin-host.ts # 宿主调用(load / save / folder,文档相对 fetch,可注入)
188
+ │ ├── skin-picker.ts # 目录选择器桥(作用域可选依赖,懒读取)
166
189
  │ ├── image-pipeline.ts # 图片 → 降采样 → data URL(依赖注入,可测)
167
190
  │ ├── skin-dom.ts # 全局样式 / 背景层 / 应用与卸载到文档
168
191
  │ ├── global-skin-css.ts # 皮肤全局样式表(背景层 + 半透明表面 + 明暗适配)
169
- │ └── skin.module.css # 面板 / 侧栏入口样式
170
- ├── tests/ # vitest 测试(含 client-lifecycle 挂载契约与 panel-theme 调色板守卫)
192
+ │ └── skin.module.css # 设置页 / 侧栏脚部快捷键样式
193
+ ├── tests/ # vitest 测试(插槽生命周期、路由解析守卫、调色板守卫等)
171
194
  ├── cordis.patch.yml # 插件加载行补丁
172
195
  └── package.json # dsh.client / dsh.bundle.patch 声明
173
196
  ```
@@ -176,8 +199,14 @@ dsh-wx-skin/
176
199
 
177
200
  ## 📋 兼容性与注意事项
178
201
 
179
- - **DSH 版本**:适配基准为 DSH `0.1.6-alpha.1`(checkout `dsh-v0.1.6-alpha.1-5-g0d1f50007f`,2026-10)。逐项核对过的契约:闭包工厂产物 `window.__ModuleLoader__.load({id,factory})`、平台模块表种子词(`react` / `react/jsx-runtime` / `react-dom/client`)、combo 端点 `/plugins/??<id>/client.js&rev=<rev>`、`dsh.client.platform === 'web'` 与 `dsh.bundle.patch` 清单、`webServer.register({kind:'exact'})`、cordis `ctx.effect` / `ctx.logger`、`body[data-ds-dark-theme]`、`#root` 挂载根,以及本插件覆盖的全部 `--dsw-*` 令牌。侧栏入口另依赖 shell 的 DOM 结构(`[class*="sidebarCol"]` / `[class*="logoRow"]` / `[class*="newSession"]`,CSS Modules 哈希命名保证子串可匹配)。若 DSH 版本变更导致入口未出现,只会记录日志、不影响 GUI,可反馈后调整选择器。
180
- - **生命周期约定**:插件必须通过 `ctx.effect` 归还副作用。client-hmr 重载与插件行停用都会先卸载旧 fiber,只有 fiber 上的 disposer 会执行;`dsh-wx-skin` 的入口、面板、背景层、样式表和文档属性都在这条路径上回收。
202
+ - **DSH 版本**:适配基准为 DSH `0.1.7-alpha.2`(checkout tag `dsh-v0.1.7-alpha.2`,commit `00102833df`,2026-09)。逐项核对过的契约:
203
+ - **客户端模块系统**:闭包工厂产物 `window.__ModuleLoader__.load({id, factory})`;平台模块表种子词与官方 `packages/client/web/src/platform.ts` 的 `PLATFORM_MODULES` 逐字一致(`react` / `react/jsx-runtime` / `react-dom` / `react-dom/client` / `@deepseek-ai/cordis` / `dsh-client-store` / `ui-slots` / `ui-primitives` / `ui-dockkit`);0.1.7 起 bundle 引用为文档相对(`plugins/??<id>/client.js&rev=<rev>`)、`rev` 由 bundle 文件元数据(mtime/ctime/size)生成,均由 shell 侧生成,插件不自行拼 URL。
204
+ - **插槽注册**:客户端服务名 `slots`;`ctx.slots.inject(key, cb)` 等待槽**声明**并把回调作为子 fiber effect 运行;`ctx.slots.register({name, id, order, label}, Component)` 的 list 槽按 `order` 升序渲染、**不额外包 DOM**。本插件使用两个官方加法页位:`settings.section`(`kind: 'list'`、`scope: 'root'`、`replaceRisk: 'none'`,owner props `{ close }`;内容列由 shell 提供内边距与滚动)与 `sidebar.footer.action`(同 kind/scope,owner props `{ wide }`;单次注册承载整组快捷键,该行的既有占用者会独占整行,见上文)。
205
+ - **宿主契约**:`webServer.register({kind:'exact', path, handler})`;cordis `ctx.effect` / `ctx.inject` / `ctx.logger`;`uiWorkspace.pickDirectory(): Promise<string | null>`(无 chooser 时 reject,设置页已吞异常)。
206
+ - **样式契约**:`body[data-ds-dark-theme]` 明暗开关、`#root` 挂载根、本文覆盖的全部 `--dsw-*` 表面令牌(含 0.1.7 改为字面量 rgba 的 `--dsw-specific-menu`)。**不再依赖** `sidebarCol` / `logoRow` / `newSession` 等 shell 内部类名。
207
+ - **路由约定**:目标为应用自有路由的浏览器引用一律文档相对(对齐官方 `verify-client-route-resolution` 门禁),`tests/route-resolution.spec.ts` 在本地执行同类静态扫描。`--dsw-alias-bg-document-preview` 有意不覆盖(仅文档/PDF 预览正文使用,阅读需要不透明底)。
208
+ - **生命周期约定**:插件必须通过 `ctx.effect` 归还副作用。client-hmr 重载与插件行停用都会先卸载旧 fiber,只有 fiber 上的 disposer 会执行;`dsh-wx-skin` 的两个插槽注册(React 组件由 shell 卸载)、背景层、样式表和文档属性都在这条路径上回收。
209
+ - **入口的降级**:设置页依赖 ui-settings 声明 `settings.section`,快捷键依赖 ui-sidebar 声明 `sidebar.footer.action`。若某个部署没有对应外壳(槽永不声明),插件**不会**因此等待或报错:背景、设置数据与持久化照常工作,只是那处入口不出现(这也是不用 `inject: ['slots']` 的原因)。侧栏折叠为图标轨时快捷键自动变成两个 28px 图标。
181
210
  - **存储**:本地图片以 data URL 存于 `localStorage`(浏览器缓存/回退),并镜像到宿主副本 `~/.dsh/dsh-wx-skin.settings.json`(跨端口持久化);超大图会自动缩放至可持久化尺寸。文件夹轮播只持久化路径列表与序号,不占图片配额。
182
211
  - **格式**:仅接受位图(PNG / JPEG / WebP / GIF / BMP);SVG 等矢量格式不支持(canvas 管线只处理位图,文件夹扫描也按白名单过滤,安全可预测)。
183
212
  - **文件夹读取范围**:图片路由只服务「上一次成功扫描的根」或设置文件里 `folderPath` 之内的位图文件,回环来源 + 非跨站才放行,无法被用作任意文件读取;宿主进程与文件夹必须在同一台机器(`dsh web` 的 GUI 与目录同机)。