dsh-any-background 0.3.10 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -82,9 +82,9 @@ controls (`z-index:-1`) and carries its own ink (`--dsh-any-caption-ink`), so th
82
82
  应用 / 编辑 menubar stay readable even when a low sidebar opacity makes the strip light — and 「标题条带文字」 says who wins when the strip's brightness and the theme's direction disagree (自动 derives it from the strip, 跟随主题 leaves the host's own label ink alone, 始终白色 / 始终黑色 pins it; see issue #30). The anchor is expressed in CSS (the row is the `conversation.session.header` slot's direct parent) instead of guessing a hashed class or tagging a host element at runtime; before `0.1.7-alpha.1` the host mounts that slot bare with no such element, so there the rule matches nothing and the part is simply inert. On the Windows desktop shell the same slider pair also drives the window caption band — except for the segment that sits above the left sidebar, which belongs to that sidebar's own pair (see **Left sidebar + the caption band above it**) — and the native minimize/maximize/close block is handed `transparent` so Windows paints no background of its own, leaving the band itself showing through. A blank session marks the row `headerBlank` (a placeholder left with only its padding) and the plugin skips claiming it.
83
83
  - **Sidebar "Theme" page (dual mode)** — The same five pages (Color / Interface / Font / Background / Profiles) register into two surfaces: without dsh-better-sidebar, a "Theme" card is contributed to the **official right Sidebar's guide page** through its public extension points (`sidebarRightTabs` + the `sidebar.right.pane.tab` keyed seat); with dsh-better-sidebar installed, the page registers in that plugin's sidebar instead and the official guide card withdraws itself, so the two never duplicate. Settings panel, official sidebar and better-sidebar all share one page implementation and one state store — a change in any of them shows up everywhere. The shell adapts to the panel width, and a narrow panel tightens padding and falls back to a single column. On a host without the right Sidebar the registration silently never happens.
84
84
  - **Conversation View Cards** — The message list is wrapped in a translucent card automatically, and the trajectory page gets whole-page opacity & blur controls, letting the wallpaper shine through the content.
85
- - **Theme Export / Import** — One-click export to a self-contained `dsh-any-theme.json` (config + wallpaper, video embedded as a data URL) and import to restore it anywhere.
86
- - **Appearance Presets & Profiles** — Six one-click presets (Default / Frosted glass / Minimal / Midnight / Cyber / Warm daylight) plus named profiles: save the current look and re-apply it anytime. A two-step confirm guards deletion.
87
- - **Wallpaper Rotation** — Add images to a rotation pool (thumbnail picker included) and let the wallpaper change by shuffle or order on every refresh, daily, or weekly. Advancing copies the chosen image into the active wallpaper slot, so export/import and color extraction keep working unchanged.
85
+ - **Theme Export / Import** — One-click export to a self-contained `dsh-any-theme.json` (config + wallpaper, video embedded as a data URL, and — from v0.4.0 — the whole rotation pool's image bytes) and import to restore it anywhere.
86
+ - **Appearance Presets & Profiles** — Six one-click presets (Default / Frosted glass / Minimal / Midnight / Cyber / Warm daylight) plus named profiles: save the current look and re-apply it anytime; profiles can be renamed and duplicated, and a two-step confirm guards deletion.
87
+ - **Wallpaper Rotation** — Add images to a rotation pool (thumbnail picker included) and let the wallpaper change by shuffle or order on every refresh, hourly, daily, or weekly — the cadence advances while the page stays open, not only on reload. Advancing copies the chosen image into the active wallpaper slot, so export/import and color extraction keep working unchanged.
88
88
  - **Day/Night Auto Switch** — Assign a day profile and a night profile; the plugin switches automatically at fixed clock times or by following the OS dark mode.
89
89
  - **Custom Font** — Upload a ttf / otf / woff / woff2 file (up to 100 MB) and apply it to the whole interface through `@font-face`; toggle it off or remove it at any time. Fonts stream as raw bytes and persist in the plugin data dir; code blocks keep their monospace stack.
90
90
  - **Per-part Text Outline** — The same surface groups as the interface page (now eleven, including the header popovers and the header bar), each with its own `-webkit-text-stroke`: width 0–4 px (0 = off) and a color of auto-contrast / gray / black / white / accent / custom. Code blocks, inline `code`, icons and the host's `background-clip: text` shimmer chrome (the "深度求索中" turn-status line and the turn-process rows) are exempted automatically, so multi-color syntax never smears and gradient text is never flattened into a stroke-coloured blob.
@@ -95,18 +95,19 @@ controls (`z-index:-1`) and carries its own ink (`--dsh-any-caption-ink`), so th
95
95
 
96
96
  ## Changelog (latest two releases)
97
97
 
98
- ### v0.3.10 (0.2.1-rc.3 verified: one more SUPPORTED_RELEASES row, no new facts)
98
+ ### v0.4.1 (fix: the "Every refresh" rotation cadence flipped the wallpaper every 30 seconds)
99
99
 
100
- - **`0.2.1-rc.3` is a checked build now, not a clamped guess.** `git diff rc.2 rc.3` over every file the `0.2.1` adapter speaks for comes back empty where it matters: `design-platform.css`, both menu stylesheets and `MenuGroup.tsx` are byte-identical, and so are `SidebarRight.module.css`, `TabLayout.tsx`, `base.css`, `gradient-shadow-text.css`, `AppFrame.*`, `InlineEditor.module.css`, `TextShimmer`, `JobListAction`, `TaskMenu`, `scoped-slots` and `PluginManagerPage.tsx`, and the five header slot keys stand. The adapter's facts hold, so rc.3 joins `v0-2-1` with a `SUPPORTED_RELEASES` row — its verdict reads `exact` — and the folder's header records what the tag did move: `Menu.tsx` is additive (an `aria-current` render and a portal-only `min-width` switch, with no stylesheet moving alongside), the guide and the sidebar panel list grow pin state and More menus AROUND the existing registrations while the tab registry in `shell/` is untouched, the session header's new View-row More menu mounts through `<Menu … portal>` to `document.body` and is therefore already claimed by the tagger's portaled arm, and `ConversationRoot.module.css` only re-lays the View tab row. `engines.dsh`, the six gated `@deepseek-ai/dsh-*` peers, `dsh.compatibility.dsh` and `dsh.compatibility.dshReleases` all name `0.2.1-rc.3`.
100
+ - **What happened.** v0.4.0 taught the 30-second schedule tick to advance a due rotation so an `Hourly` cadence works while the page stays open. But the due-math says "always due" for the `Every refresh` cadence — that is how a page load earns a fresh pick (the server advances it inside the `read` RPC). Wired into the tick, it meant: rotation enabled + `Every refresh` selected → the wallpaper switched itself every 30 seconds, and again on every OS dark/light flip or enabling the day/night schedule (both also drive the tick).
101
+ - **The fix.** Whether a cadence participates in live ticking is now a shared-math question: `rotationAdvancesLive()` in `src/shared/rotation.ts` (inlined into both bundles, like the rest of the cadence math) says only the time-based cadences — hourly / daily / weekly — do. `Every refresh` keeps its exact original semantics: one fresh pick per page load, plus the manual next button. Boot-time fallback and the enable-toggle behavior are unchanged.
101
102
 
102
- ### v0.3.9 (Issue #30 makes the caption ink a choice, and the header sliders reach every header popover)
103
+ ### v0.4.0 (persistence-integrity batch + rotation / export / profile features)
103
104
 
104
- - **The caption strip's ink is the user's call now (issue #30).** 0.3.8 derived it from what the strip is actually painted on, which fixed the invisible white toggle — and made the native menubar (「应用 / 编辑」) black under a dark theme whose sidebar is translucent over a bright wallpaper, because there the strip really IS light. The two reports of that one situation wanted opposite winners, so the 「标题栏」 card gained a 「标题条带文字」 selector: **自动** keeps the strip-following derivation, **跟随主题** emits no override at all (the menubar and the sidebar's fixed caption controls read the host's own label tokens, exactly as they did through 0.3.7), **始终白色 / 始终黑色** pin it. The key is stored and whitelisted on the host half too — the #28 lesson is that a key only one half knows is silently dropped at write time — and `npm test` round-trips it, including an unknown value collapsing to `auto`.
105
- - **In-place header popovers are claimed, and the claim no longer waits for a frame.** `header-tag.ts` accepted a popover only when its parent was `<body>`, so the session-log `…` menu — which `session-log-export/HeaderAction.tsx` renders WITHOUT the `portal` prop, inside `[data-slot="conversation.session.header.utilities"]` — was never tagged, and the only thing styling it at all was the header group's `[role="menu"][class*="_denseList"]:not([class*="_portal"])` arm. That arm dies the moment the host drops `dense` (or ships a header popover that is not a dense menu), and until then the panel followed the 「卡片」 slider while sitting in the title row. A candidate is now "portaled to `<body>`, **or** a descendant of one of the header's own slot anchors": a modal is still never claimed, and the composer's in-place lists stay with the card group that owns them. The observer's flush also moved off `requestAnimationFrame` onto the microtask queue, which is the other half of "still doesn't work": a queued frame does not run while a window is not painting (hidden tab, occluded or minimized desktop window, background Electron surface), so the tag simply never landed and the panel kept whatever the card group gave it — measured live in `dsh web`, an open header menu stayed untagged indefinitely on a non-painting page, and the same menu was tagged on the next microtask after this change. A microtask coalesces a mutation burst exactly as an animation frame did (the observer callback is already per-batch) with no dependency on the compositor, and it lands in the frame the popover mounts, so the panel no longer flashes the card look on open either.
106
- - **The 「标题栏」 row's frost stopped eating its own children's blur.** `backdrop-filter` on the row made the row a BACKDROP ROOT as well as a stacking context, so an in-place header popover could only ever blur what the row itself paints — a flat tint — and the header blur slider read dead on exactly the panel that hangs off the row. The frost now rides a `::before` underlay behind an `isolation:isolate` (a stacking context that is NOT a backdrop root), which is the recipe this plugin already uses for the frame, the composer capsule and every part column; the row keeps its compensating `z-index:10`, and the underlay's `position:relative` is written inline only while a blur exists and only when the host left the row static. Measured in the real `dsh web` app (the plugin linked from this working tree, driven through the actual 「顶栏选项」 sliders on the 界面 page): the in-place `…` menu and the portaled open-in-app menu both report `rgba(243,247,252,1)` at opacity 100 / blur 4 and `rgba(243,247,252,0.08)` at opacity 8 / blur 44. Measured again in a browser harness running the plugin's real client code over the host's own built CSS: the row reports `backdrop-filter: none`, its `::before` reports the slider's blur, and every header popup shape — portaled dense `MenuSurface`, in-place dense, in-place non-dense, the role-less job-list surface, the lineage tree — takes the 顶栏 pair's fill and frost instead of the card's.
107
- - **The interface-opacity sliders work on a fresh install now.** `readHostOpacityTokens()` sampled `getComputedStyle(:root)`, but `design-platform.css` declares this whole token set inside a `body { … }` block and a custom property is inherited downward only — so with no picked theme color every read came back empty, `applyCustomTokensNow` took its `surfaces === null` exit, and the 侧栏 / 卡片 / 输入框 / 顶栏选项 alphas were never emitted at all. The sample now comes off `body`, cached per host generation (its dark-mode marker plus the platform attribute), and the plugin's own `body{}` token sheet is switched off while reading — otherwise the second sample returns the plugin's own substituted literal and the surfaces freeze at whatever color they first had instead of following the host.
108
- - **A 0 on a menu blur slider now means no frost.** 0.3.7 introduced a `<html>` class gate (`dab-card-frost` / `dab-header-frost`) so a slider parked at 0 would not delete the host's own `--dsw-menu-backdrop-filter: blur(40px) saturate(150%)` — an element's own declaration always beats what it inherits, so the un-gated `var(--x, none)` really did clear it. The gate turned out to be the wrong trade: it made the slider's LOW end its HIGH end (0 → the host's 40 px, 1 → 1 px), so 「把模糊度关掉」 was unreachable and the control read as dead at exactly the position the user was testing. The frost token is now asserted at every value with the `none` fallback winning at 0, and the gate machinery is gone. Nothing loses the host look by accident: every part default is 30 px, so a stock install is frosted and only an explicit drag to 0 clears it.
109
- - **No release-table change.** The shapes above were re-checked against the installed `0.2.0-rc.1`, `0.2.1-alpha.3` and `0.2.1-rc.2` bundles: both utilities controls, `MenuSurface`'s `data-menu-material` + `.material` painter and the menu token set are identical across the three, so the `0.2.1` adapter's facts still hold.
105
+ - **Theme exports now carry the whole rotation pool (v3).** The old export carried the config and the active wallpaper only: the pool's metadata (file names + thumbnails) travelled with the config while the image bytes stayed on the source machine, so after an import on another machine every activation failed with `file missing`. A v3 export embeds each pool image's bytes (fetched from the new `/dsh-any-background/rotation/file` route, whose name goes through the same anti-traversal fence the config sanitizer uses) as `pool`; the import runs a new `rotationRestore` RPC that rebuilds the pool wholesale — bytes land under their exported file names, unreferenced files are dropped, and the cursor clamps into the new range. Dead entries in v2 files clear on import instead of failing on every activation.
106
+ - **Rotation gains an hourly cadence — and advances while the page stays open.** Advancing used to happen only inside the `read` RPC on page load, so a long-lived window never changed wallpaper; the 30-second schedule tick now re-checks it too (pure math gate — a no-op when nothing is due). The due/pick math moved into a shared module inlined into both bundles at build time (`src/shared/rotation.ts`), so the two halves can never disagree on a day/week boundary again.
107
+ - **Profiles can be renamed and duplicated.** Each row gains an inline rename (Enter commits) and a one-click duplicate (fresh id, `(2)` name suffix, same 20-entry oldest-eviction limit).
108
+ - **Persistence correctness fixes — each matching a failure mode this repo has already hit.** `readConfig`'s failure path handed back a shallow copy whose nested arrays rotation add/remove mutated in place, leaving ghost pool items in the module-level defaults forever (now `structuredClone`); a failed config write left the mtime-keyed cache describing changes the disk never got (cache dropped before writing); the font slot's stray-file adoption mislabelled MIME by file name, serving woff2 bytes as ttf (now sniffed from real magic bytes and persisted); raw wallpaper uploads skipped byte validation (now the same magic-number fence as the URL download and the font upload); rotation file names could collide within one millisecond (random suffix added); manual activation did not persist the cursor, so a reload jumped back to the old image (server-side read-modify-write, the same semantics as the auto-advance).
109
+ - **Structure & performance.** The browser half's four body-level MutationObservers merged into a single bus (`src/client/dom-watch.ts`, auto-disconnecting on the last unsubscribe); the parts pass moved from rAF to microtask coalescing — the v0.3.9 lesson (a queued frame never runs while the window is not painting), so a view or takeover card mounted in the background is styled immediately; `applyWp`'s fan-out deduplicated (the produced / header-popover passes ran twice per apply); a failed config write retries once (a transient Windows EBUSY from an antivirus scan) and then warns explicitly instead of being swallowed; `read` probes its three slots concurrently; a first run writes the config once, not twice; startup sweeps crash-leftover tmp files, excess `.corrupt-*` archives (keeping the newest 3) and orphaned pool files (10-minute grace); rotation advance/activate uses `copyFile`, so multi-MB wallpapers no longer pass through the heap whole.
110
+ - **i18n cleanup.** Four dead keys removed and embedded Chinese quotes in the English copy replaced; zh/en at 197/197 key parity.
110
111
 
111
112
  ## Installation
112
113
 
package/README.zh.md CHANGED
@@ -80,9 +80,9 @@
80
80
  - **标题栏(会话标题行)** — 会话顶部那条标题 / 操作 / 视图页签行终于有了自己的一对滑块。宿主自身不给它任何底色(中心列的底色直接透上来),所以壁纸一起来,标题与页签就直接压在图上、和周围被蒙住的表面割裂。现在它按「区块淡色」作画,配方与对话文本框那张卡一致:材质取插件主背景面的颜色、按本部件自己的透明度上色,读起来是压在列上的一条带,而不是叠上去的第二层颜色;模糊落在行前的一块 `::before` 垫层上(配合 `isolation:isolate`——它是层叠上下文,但**不是** backdrop root;0.3.8 之前霜直接压在行身上,而行一旦带上 `backdrop-filter` 就成了 backdrop root,恰恰把挂在行里的那些浮层的磨砂压成一片平色);而这道霜仍然把行变成层叠上下文,所以行同时带上补偿性的 `z-index:10`——行内挂出的浮层(直到 `0.2.1-alpha.1` 的后台任务列表、每个 release 上都可能就地的日程任务菜单与「会话日志」`…` 菜单)本来用 `z-index:100`,否则会被关在行里、压到对话之下。10 高于它会重叠到的列内部件(输入位 9、回到底部 8),又远低于所有页面级表面(100 / 1000 / 1100),因此打开菜单时这一条的颜色和模糊都不会变;`z-index` 无需 `position`——该行本是 flex 子项,`z-index` 天然生效,而垫层要用的 `position:relative` 只在模糊非 0、且宿主把这一行留在 static 时才以内联方式写入。详见 v0.3.7 / v0.3.9。Windows 上它驱动的条带与侧栏分段(见上一条);模态打开时条带会跟着宿主蒙版自行变暗(宿主蒙版故意停在标题区下方);生成出来的那一段只负责绘制——`pointer-events:none` 且不声明 app-region——因此不会吞掉本该给原生标题栏菜单的点击(issue #29)。它还画在控件之下(`z-index:-1`),并自带墨色(`--dsh-any-caption-ink`):即使侧栏透明度很低、条带发浅,开合按钮与「应用 / 编辑」菜单也清楚可读;而当条带的明暗与主题方向冲突时,由「标题条带文字」裁定谁获胜——「自动」沿用条带推导,「跟随主题」完全不写覆盖(回到宿主的标签墨色,即 0.3.7 及之前),「始终白色 / 始终黑色」直接钉死(issue #30)。这一行是**运行时认领**的:插件从 `conversation.session.header*` 槽位锚点向上找到最近的那个 `<header>` 再打上自己的类(与卡片、帧同款做法),不猜哈希类名、也不依赖宿主把槽位摆在哪个层级;`0.1.7-alpha.1` 之前宿主把槽位裸挂在会话根上、没有这一行元素,那种宿主上插件什么都不会认领,部件自然无效果。桌面壳(Windows)上这一对滑块同时驱动窗口标题条带——**但左侧栏正上方的那一段除外**,那一段归「侧栏」这对滑块管(见下条);原生最小化/最大化/关闭那块改由取色探针报 `transparent`,让 Windows 不画自己的背景,露出的正是条带本身。空会话时宿主把该行标为 `headerBlank`(一条只留 padding 的占位),插件跳过认领。
81
81
  - **侧边栏「主题」页面(双模式)** — 同一套五页(色彩 / 界面 / 字体 / 背景 / 配置)注册到两处:未安装 dsh-better-sidebar 时,通过官方右侧栏的公开扩展点(`sidebarRightTabs` + `sidebar.right.pane.tab`)在**官方侧边栏**的引导页挂一张「主题」卡片,点开即五页;安装了 dsh-better-sidebar 时,改为在它的侧边栏注册同名页面,官方引导页上的卡片自动撤下,避免重复。两处(加设置面板共三处)共用同一套页面代码与同一份状态,改一处处处同步;外壳按面板宽度自适应,窄面板收紧内边距并回到单列。宿主没有右侧栏扩展点时静默不注册,不影响任何其它功能。
82
82
  - **对话视图卡片** — 消息列表自动包裹为半透明卡片,轨迹页可整页调节透明度与模糊,让壁纸从内容后方透出来。
83
- - **主题导出 / 导入** — 一键导出为自包含的 `dsh-any-theme.json`(配置 + 壁纸,视频以 data URL 内嵌),可随时导入还原。
84
- - **外观预设与配置档案** — 六套一键预设(默认 / 毛玻璃 / 极简白 / 暗夜紫 / 赛博 / 暖阳),外加自定义命名档案:随时保存当前观感、随时套回,删除有二次确认保护。
85
- - **壁纸轮换** — 把多张图片加入轮换池(缩略图选择器),按随机或顺序、每次刷新 / 每天 / 每周的频率自动更换;切换时服务端把所选图片复制进当前壁纸槽,导出/导入与取色管线完全不用改。
83
+ - **主题导出 / 导入** — 一键导出为自包含的 `dsh-any-theme.json`(配置 + 壁纸,视频以 data URL 内嵌,v0.4.0 起连同整个轮换池的图片字节),可随时导入还原。
84
+ - **外观预设与配置档案** — 六套一键预设(默认 / 毛玻璃 / 极简白 / 暗夜紫 / 赛博 / 暖阳),外加自定义命名档案:随时保存当前观感、随时套回,支持重命名与复制,删除有二次确认保护。
85
+ - **壁纸轮换** — 把多张图片加入轮换池(缩略图选择器),按随机或顺序、每次刷新 / 每小时 / 每天 / 每周的频率自动更换,页面开着也会按时推进(不再只在刷新时更换);切换时服务端把所选图片复制进当前壁纸槽,导出/导入与取色管线完全不用改。
86
86
  - **昼夜自动切换** — 指定日间与夜间两套配置,按固定时段或跟随系统深色模式自动切换。
87
87
  - **自定义字体** — 上传一个 ttf / otf / woff / woff2 字体文件(上限 100 MB),通过 `@font-face` 应用到整个界面的文字;可随时停用或移除。字体按原始字节流式上传并持久化到插件数据目录,代码块仍保持等宽字体。
88
88
  - **分组文字描边** — 沿用界面页的分组(现为十一组,含顶栏选项与标题栏),为每个分组单独加 `-webkit-text-stroke` 描边:粗细 0–4 px(0 = 关闭),颜色支持自动反色 / 灰 / 黑 / 白 / 主题色 / 自定义。代码块、`inline code`、图标,以及宿主那类 `background-clip: text` 的流光文字(「深度求索中」状态行与轮次过程行)自动豁免——多色语法不会糊成一团,渐变文字也不会被描边压成一坨纯色。
@@ -93,18 +93,19 @@
93
93
 
94
94
  ## 更新日志(只保留最近两个版本)
95
95
 
96
- ### v0.3.10(0.2.1-rc.3 过检:SUPPORTED_RELEASES 多一行,事实零新增)
96
+ ### v0.4.1(修复:「每次刷新」档位壁纸每 30 秒自动切换)
97
97
 
98
- - **`0.2.1-rc.3` 从「收边夹取的猜测」变成「逐项核对过的构建」。** 对 `0.2.1` 适配层发言的每一个文件跑 `git diff rc.2 rc.3`:`design-platform.css`、两份菜单样式表、`MenuGroup.tsx` 逐字节一致,`SidebarRight.module.css`、`TabLayout.tsx`、`base.css`、`gradient-shadow-text.css`、`AppFrame.*`、`InlineEditor.module.css`、`TextShimmer`、`JobListAction`、`TaskMenu`、`scoped-slots`、`PluginManagerPage.tsx` 也全部未动,五个头部槽位键原样成立。事实不变,所以 rc.3 并入 `v0-2-1`、只添一条 `SUPPORTED_RELEASES` 行——判定从 `line` 升为 `exact`——该 tag 真正动了的东西记在目录头注释里:`Menu.tsx` 是纯增量(渲染 `aria-current`、portal 模式下写一个 `min-width`,没有任何样式表随行迁移);guide 与侧栏面板列表在**既有注册的外围**长出 pin 状态和 More 菜单,而 `native-tab.tsx` 对话的那个 tab 注册表(`shell/`)原封未动;会话头部新增的 View 行 More 菜单经 `<Menu … portal>` 挂到 `document.body`,标记器的 portal 分支本来就会认领它;`ConversationRoot.module.css` 只重排了 View 标签行。`engines.dsh`、六个受门禁的 `@deepseek-ai/dsh-*` peer、`dsh.compatibility.dsh` 与 `dsh.compatibility.dshReleases` 均已点名 `0.2.1-rc.3`。
98
+ - **发生了什么。** v0.4.0 让 30 秒一次的定时检查顺带推进到期的轮换,好让「每小时」档在页面长驻时也能换图。但到期数学对「每次刷新」档位是**恒为到期**——这正是页面加载能拿到新图的原因(服务端在 `read` RPC 里推进它)。把它接进定时器后就成了:轮换开启 + 选了「每次刷新」→ 壁纸每 30 秒自己换一张;系统明暗翻转、开启昼夜定时时也会跟着再切一次(这两处同样驱动该定时器)。
99
+ - **怎么修的。** 「哪些档位参与活推进」现在是共享数学的一部分:`src/shared/rotation.ts` 新增 `rotationAdvancesLive()`(与其余到期数学一样构建时内联进两个 bundle),只有时间档位——每小时 / 每天 / 每周——参与活推进。「每次刷新」回到它原本的语义:每次页面加载换一张,外加手动「换一张」按钮。boot 回退与开启开关时的立即换图行为不变。
99
100
 
100
- ### v0.3.9(issue #30:条带墨色改由用户裁定;顶栏那对滑块重新管住所有头部浮层)
101
+ ### v0.4.0(持久化正确性批次 + 轮换 / 导出 / 档案功能)
101
102
 
102
- - **「标题条带文字」现在是一个选项(issue #30)。** 0.3.8 让条带墨色跟随它实际被画在什么上,这修好了「白色开合按钮在亮条带上消失」——但也让暗色主题下、侧栏半透明压在亮壁纸上时,原生菜单条(「应用 / 编辑」)变成黑色,因为那一段条带确实是亮的。同一个场景,两份反馈要的是相反的赢家,所以这个取舍交给用户:「标题栏」卡片新增「标题条带文字」——**自动**保持 0.3.8 的推导;**跟随主题**完全不发射覆盖(菜单条与侧栏 fixed 在条带上的控件读宿主自己的标签令牌,即 0.3.7 及之前的样子);**始终白色 / 始终黑色**钉死。这个键同样补进了宿主半侧的白名单(#28 的教训是:只有一侧认识的键会在写盘时被静默丢弃),`npm test` 会把「落盘 → 重读」跑一遍,并断言未知值塌回 `auto`。
103
- - **就地的头部浮层现在会被认领,而且认领不再等一帧。** `header-tag.ts` 过去只接受父节点是 `<body>` 的浮层,而 `session-log-export/HeaderAction.tsx` 的那个 `…` 菜单**不带 `portal`**,就地挂在 `[data-slot="conversation.session.header.utilities"]` 里,于是永远拿不到标记;唯一还在给它上色的,是顶栏组的 `[role="menu"][class*="_denseList"]:not([class*="_portal"])` 分支——宿主一旦不再标 `dense`(或者新增一个非 dense 的头部浮层)这条分支就失效,而在此之前那块面板一直跟着「卡片」滑块走,尽管它就悬在标题行上。认领条件现在是「portal 到 `<body>`,**或者**是头部某个槽位锚点的后代」:模态依旧绝不认领,输入位自己的就地列表也继续归卡片组。观察者的一次性 flush 也从 `requestAnimationFrame` 挪到了微任务队列——这才是「还是没生效」的另一半原因:**排下去的那一帧在窗口不绘制时根本不会跑**(后台标签页、被遮住或最小化的桌面窗口、后台的 Electron 表面),于是标记永远落不下,面板就一直保持着卡片组给它的样子。在真实 `dsh web` 里量过:页面不绘制时,一个打开的头部菜单无限期保持未标记;改成微任务后,同一个菜单在下一个微任务就被标记上了。微任务同样能把一批变更合并成一次(`MutationObserver` 的回调本就按批投递),却完全不依赖合成器;而且它落在浮层挂载的那一帧里,打开面板时不再有一帧的「卡片样式」闪动。
104
- - **「标题栏」这一行的霜不再吞掉子层的模糊。** `backdrop-filter` 让标题行同时成为层叠上下文和 BACKDROP ROOT,于是挂在行内的就地浮层能采到的背景只剩这一行自己画的东西——一片平色——恰恰是这条从行上垂下来的面板,模糊滑块看起来完全失效。现在霜改走 `::before` 垫层(配 `isolation:isolate`:是层叠上下文,但**不是** backdrop root),也就是本插件给帧、输入位胶囊和各个部件列一直使用的配方;行继续保留补偿性的 `z-index:10`,而垫层需要的 `position:relative` 只在模糊非 0、且宿主把行留在 static 时才以内联方式写入。在真实 `dsh web` 里(配置档直接 link 本工作树,用界面页上真正的「顶栏选项」滑块驱动):就地的 `…` 菜单与 portal 出去的「更多打开方式」菜单,在透明度 100 / 模糊 4 时都是 `rgba(243,247,252,1)`,在透明度 8 / 模糊 44 时都是 `rgba(243,247,252,0.08)` + `blur(44px)`。再用「跑真实插件代码 + 宿主安装产物 CSS」的浏览器复测:行的 `backdrop-filter: none`、其 `::before` 报告滑块的模糊值,而所有头部浮层形状(portal 的 dense `MenuSurface`、就地的 dense、就地的非 dense、无 role 的后台任务表面、子代理血缘树)都取「顶栏选项」的底色与磨砂,不再落到卡片组。
105
- - **全新安装下界面透明度滑块不再是死的。** `readHostOpacityTokens()` 采的是 `getComputedStyle(:root)`,而 `design-platform.css` 把这一整套表面令牌声明在 `body { … }` 块里,自定义属性只会向下继承——于是没有选定主题色时每次读取都是空,`applyCustomTokensNow` 走 `surfaces === null` 那条出口,侧栏 / 卡片 / 输入框 / 顶栏选项的 alpha 从头到尾没被写出来。现在改从 `body` 采样,按宿主代次(它自己的深色标记 + 平台属性)缓存,并且读取时把插件自己那份 `body{}` 令牌表临时关掉——否则第二次采样拿回来的是插件自己刚写进去的字面量,表面颜色会冻结在第一次采到的那个值上,不再跟随宿主。
106
- - **菜单模糊滑块拉到 0,现在就是没有磨砂。** 0.3.7 加过一个 `<html>` 类门控(`dab-card-frost` / `dab-header-frost`),让停在 0 的滑块不去抹宿主自己的 `--dsw-menu-backdrop-filter: blur(40px) saturate(150%)`(元素自身声明恒胜继承,所以未门控的 `var(--x, none)` 确实会清掉它)。事后看这个取舍是错的:它把滑块的**下端变成了最糊的一端**(0 → 宿主的 40px,1 → 1px),于是「把模糊度关掉」根本够不到,用户恰恰在那个位置上觉得控件是死的。现在这个 frost 令牌在每个取值上都被声明,0 时由 `none` 兜底胜出,门控机制整体删除。不会有人因此丢掉宿主观感:每个部件的默认值都是 30px,全新安装依旧是磨砂的,只有明确拖到 0 才会清掉。
107
- - **版本表不动。** 上面这些形状都对着安装产物里的 `0.2.0-rc.1`、`0.2.1-alpha.3`、`0.2.1-rc.2` 三个构建复核过:两个 utilities 控件、`MenuSurface` 的 `data-menu-material` + `.material` 绘制层、菜单令牌集合三处一致,`0.2.1` 适配层的事实继续成立。
103
+ - **主题导出现在带上整个轮换池(v3)。** 旧导出只带配置与主壁纸:轮换池的元数据(文件名 + 缩略图)随配置走了,图片字节却留在原机器上,换机导入后每次切换都报 `file missing`。v3 导出把池内每张图的字节(从新路由 `/dsh-any-background/rotation/file` 取回,名称走与配置清洗相同的防穿越围栏)内嵌为 `pool`;导入走新的 `rotationRestore` RPC 整体重建——字节按导出时的原文件名落盘、清掉未被引用的旧文件、游标夹取进新范围。v2 旧文件里的死条目会在导入时清空,而不是留着每次激活都失败。
104
+ - **轮换新增「每小时」档,且页面开着也会推进。** 到期推进原来只发生在页面加载时的 `read` RPC 里,长驻窗口永远不换图;现在 30 秒一次的定时检查也会过一遍轮换(纯数学门控,没到期就是空操作)。日 / 周 / 随机的到期与选图数学同时抽成双端共享的 `src/shared/rotation.ts`(构建时内联进两个 bundle),两端不会再在日 / 周边界上各自为政。
105
+ - **配置档案支持重命名与复制。** 每条档案新增行内重命名(Enter 提交)与一键复制(新 id、`(2)` 名字后缀,沿用 20 条上限的最旧淘汰)。
106
+ - **持久化正确性修复——每条都对应一个已发生过的故障模式。** 首启或读盘瞬时失败时 `readConfig` 返回的浅拷贝会被轮换增删原地污染,模块级默认配置从此带上幽灵池项(改 `structuredClone`);配置写失败后按 mtime 的内存缓存与磁盘分叉(改为写前丢缓存);字体槽收养杂散文件时按文件名错标 MIME,woff2 字节被当 ttf 服务(改为嗅探真实魔数并回写配置);壁纸裸字节上传不验内容(补上与 URL 下载、字体上传相同的魔数围栏);轮换池文件名毫秒碰撞(加随机后缀);手动激活轮换后游标不落盘、刷新回跳旧图(服务端按「合并进最新配置」回写,与自动推进同一套语义)。
107
+ - **结构与性能。** 浏览器半侧四个 body 级 MutationObserver 合并为单一总线(`src/client/dom-watch.ts`,最后一个退订自动断开);parts 观察的合批从 rAF 挪到微任务——对齐 v0.3.9 的教训(不绘制的窗口里排队的帧根本不跑),后台挂载的视图与 takeover 卡现在立即补上样式;`applyWp` 扇出去重(产出物 / 顶栏浮层等 pass 从每次两遍降为一遍);配置写失败先重试一次(Windows 上杀软扫描文件造成的瞬时 EBUSY)再明确告警,不再静默吞掉;`read` 的三个槽位并行探测;首启只写一次配置;启动时清扫崩溃残留的 tmp 文件、超额的 `.corrupt-*` 归档(保留最近 3 份)与轮换池孤儿文件(10 分钟宽限期);轮换推进 / 激活改用 `copyFile`,多 MB 壁纸不再整块过内存。
108
+ - **i18n 清理。** 删除四个无引用死键,英文文案里内嵌的中文引号改为英文写法;中英 197/197 键位对齐。
108
109
 
109
110
  ## 安装
110
111