npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dsh-theme-mediascape

v1.1.2

Published

媒体景观主题 —— 动态媒体背景:立绘壁纸 + 霓虹配色 + 开屏动画 + 萤火粒子 + 背景音乐

Readme

dsh-theme-mediascape

媒体景观主题 —— DeepSeek Harness(DSH)Web UI 主题插件:立绘/视频壁纸 × 基底配色 × 开屏动画 × 氛围粒子 × 背景音乐 × 打字音效。

界面预览:theme-studio/ 独立预览环境(真实前后端,见「theme-studio/ 预览环境」)

功能总览

| 功能块 | 做什么 | 入口 | |---|---|---|

| mht 快照还原预览 | preview.html 直接解析 DSH 界面 .mht 快照还原为静态预览(聊天框+侧边栏+壁纸背景+dock),叠加假数据(操作日志→模拟用户消息、任务列表「预览页加载」自动完成、工作区一个文件夹一个会话)、侧边栏收起按 DSH 真实行为(会话列表 DOM 移除)、操作日志/顶部提示默认隐藏+「日志」开关 | theme-studio/preview.html | | 宿主元素配色 | 胶囊配方外置 JSON(选择器/背景源/透明度/圆角/padding)+ 层级注入(按 parent 拓扑排序:父先子后、子级自然覆盖父级)+ 品牌区 sidebar-brand 独立键(lPcGpa_brand 背景+文字一体),取色器胶囊 tab 可编辑保存并自动 build | theme-studio/capsules.json | | 启动播放策略 | 视频壁纸/背景音乐启动时是否自动播放独立配置 | theme-studio/playback.json | | 开屏动画 | 启动页 gif/图片/视频多画面轮换 + 渐显式无黑屏窗口(浮层立即盖界面,等待期氛围粒子+呼吸光圈、媒体首帧就绪后渐变淡入)+ 可配置标题副标题 + 视频 Range 流式 + HTTP 缓存(ETag+If-Range 重播省 80% 流量);file:"auto" 时开屏即当前视频壁纸(与壁纸层同一份流,播完移交、不重复加载) | boot/boot.json 配置 |

| 字号调节 | 会话区与状态文字四档缩放(小/标准/大/特大),点击「字」循环切换、按钮不显示大小(大小自行感知) | dock「字」按钮 | | 壁纸系统 | 图片/视频壁纸铺底、上传持久化、本地在线共存。背景氛围粒子 | dock「景」按钮 | | 上传体系 | 中途不落盘(上传数据先攒内存缓冲,上限=可用内存 20% 动态,超限转 .tmp 保底)、暂停落盘 .part + 继续从 .part 续传(?offset= 追加续写,取消 DELETE /upload/part 清理快照)、上传前「文件名+大小」去重预检(重复的文件在二级面板提示「已跳过」,不发送请求)、dock「传」统一上传入口(按文件后缀天然识别壁纸/音乐,一次多选统一分发)、上传进度二级面板(多行对齐/进度条/大小/速度 EMA/暂停/继续/取消)、上传开始前与结束后各刷新一次列表(内置在公共函数)、上传完释放连接(串行上传不占满浏览器同源 6 连接) | dock「传」按钮 | | 打字音效 | 输入框打字按键音 + 壁纸音量调节 | dock「声」按钮 | | 背景音乐 | 播放器(旋转唱片/封面/进度条可拖动)、歌单选择/随机、本机添加 | dock「乐」按钮 |

一、壁纸系统

  • 全屏 cover 铺底;中间画面全透明(壁纸区域不铺底色:--dsw-alias-bg-base: transparent、html 深色兜底 #0a0c12、无蒙雾遮罩层;主题色只作用于侧边栏/对话框/按钮等组件)
  • 双层结构(图片常驻 + 视频可选覆盖):图片壁纸一直存在(无类型切换按钮);「视频:开/关」开关决定是否加载视频,开则视频覆盖在图片之上显示(图片仍在,仅被完全挡住),关则只显示图片。图片层与视频层各自独立记忆当前壁纸
  • 选择:弹出缩略图网格(正方形预览、固定三行、可滚动),点卡片直接应用——按视频开关只显示一层:关=图片列表,开=视频列表
  • 模式切换(面板「模式」一行):按钮标题跟随状态——亮=「视频」/ 暗=「图片」(标题实时切换,不再固定显示「视频壁纸」);dock「景」按钮悬停标题也只显示当前层(视频模式→视频名 / 图片模式→图片名),不再并列两个标题
  • 移除:勾选后移除 = 真实删除服务器文件(壁纸/音乐统一同一份删除逻辑:开始前/结束后各刷新一次列表,音乐封面随歌曲一并删除)
  • 随机:随机模式走面板模式单按钮(循环/顺序/随机单按钮切换;选择器已去掉随机按钮)——进入随机后按当前层轮换,随机间隔自定义分钟(默认 5 分钟);随机/顺序作用于最上层(视频开→视频层内切换,视频播完自动切下一张;关→图片层分钟定时切换);随机池跨层兼容——池在图片模式勾选而当前是视频层(或反之)时自动回退当前层全部,视频播完仍会切换、不会停住
  • 切换日志:每次壁纸切换(视频 ended 切下一张 / 图片定时随机 / 手动切壁纸 / 模式切换)自动上报一行到运行态 $DSH_HOME/theme-mediascape/logs/wallpaper.log(GET /theme-mediascape-assets/wallpaper/log?lines=50 可回读最近 N 行,自检/排查切换是否发生用)
  • 视频无缝切换预加载(2026-09-29):顺序/随机模式多视频轮换时,当前视频播放剩余 3s(timeupdate 阈值)即预创建并预缓冲下一视频(隐藏元素、preload=auto、未挂 DOM);ended 切换时若预载目标命中且已就绪(readyState≥2)直接复用该元素(已缓冲数据,播放即出画面)——切换无加载间隙黑屏;未命中/未就绪照常新建不阻塞;关闭视频层/切换不匹配时自动清理预载(不占用连接)。
  • 添加壁纸:dock「传」按钮统一入口(按文件后缀天然识别壁纸/音乐,一次多选统一分发)——上传立即生效并服务器磁盘持久化($DSH_HOME/theme-mediascape/wallpaper/,刷新/重启/换浏览器都在)——上传视频(mp4/webm,浏览器原生可播)自动开视频层并加载它;上传图片更新图片层(视频开关状态不变)
    • 落盘 = 原始文件名(不做 SHA 重命名;同名同大小视为重复复用,同名不同大小自动加 (1)(2)… 后缀);前端直接显示文件名(去扩展名)
    • 中途不落盘:上传数据先攒内存缓冲(上限=可用内存 20% 动态,os.freemem()),超限自动转写 .upload-<traceId>.tmp 保底(超大文件不 OOM);上传过程中素材目录不出现 .part 中间态,end 后正式文件落定(finalizeUpload 按「文件名+大小」去重);上传完成落定后自动清理同名孤儿 .part 快照(上次中断/暂停未续传的残留,不留盘)
    • 暂停落盘 .part + 继续从 .part 续传:点「⏸」abort 当前请求 → 服务端把已收内存缓冲落盘为 <正式名>.part 暂停快照(内存即刻释放);点「▶」带 ?offset=<已传字节> 续传 —— 服务端校验 .part 存在且大小一致(不匹配返回 409,前端从头重传,杜绝残缺落盘),对 .part 追加续写,收完 rename 落定,不重传已传部分;「✕」取消 → DELETE /upload/part?name=<文件> 清理快照
    • 上传前「文件名+大小」去重预检:重复的文件在二级面板提示「已跳过」(不请求);去重基准/移除目标只认「真实可用」项(列表 stat 失败的文件已从服务端列表剔除,前端 isItemUsable 只认 file/name + size 数字)
    • 上传进度:上传时 dock 二级面板自动弹出(dock.__mediascapeDshCenter 定位靠 dock,点外自动收起)——每任务一行(文件名 + 进度条 + 叠加「已传/总 · 速度」),多任务动态多行且各列对齐,无任务自动隐藏;速度读数 EMA 平滑(progress 事件间隔不均不跳变)
    • 开始前/结束后各刷新一次列表(内置在公共上传/移除函数内):上传前刷新保证去重基准最新,结束后刷新保证新文件即时入列表;移除同理(删除前刷新、删除后刷新)
    • 上传完释放连接:串行上传(一次一个请求,不占满浏览器同源 6 连接),每个 XHR 完成即释放引用
    • 上传不关闭二级面板:input.click() 打开系统选择器的 click 冒泡不再触发「点外关闭」(捕获阶段 stopPropagation)
    • 跨设备即时可见:打开壁纸面板自动刷新列表,其他设备上传的壁纸无需手动刷新网页
  • 上传上限:按服务器剩余磁盘空间动态计算(默认单文件最多占剩余空间 80%,底线 512MB),快满自动收紧
  • 默认壁纸:首次安装(无历史记录)时展示真实数据目录第一张壁纸(本地/在线均可)
  • 面板点右上角「—」或再点「选择」收起;「确定」收起设置面板,设置实时生效并持久化

二、基底配色

  • 覆盖 100+ 个 --dsw-* 设计令牌,随主题即时生效;正式基底 = 左侧「应用」写入真源 json 的当前值(以 lib/client-parts/foundation/tokens.js 与取色器为准,文档不写死)
  • 单 json 真源 + 预设包(V2 架构,json 化):应用配色只读一个 json——真源 theme-studio/json/theme-colors.json(label/badge + bg 系层级 bg/bgSoft/bgLayer + colors 全部元素色,每个颜色带不透明度 alpha {hex, alpha},是唯一写盘目标);预设包是 theme-studio/json/preset/*.json(只承载颜色集合 + 显示名,不含 bg 系),用于把真源快速改成预设配色;没有预设也能纯手改(左侧输入颜色值)。apply 自始至终只跟真源 json 相关
  • 取色器 theme-studio/theme-swatch.html(双 tab:配色 / 胶囊):配色 tab 顶部 = 背景层级区块(bg→bgSoft→bgLayer 缩进展示,改 bg 系 → dock/面板/胶囊 layer 跟随层联动),左侧 = 当前主题元素 class 控件列表(json 数据驱动,每行 6 列:元素名/类键/色号手输/色块/真实元素样子分片/不透明度滑块,滑块先预览第 5 列、点「应用」才写盘),「重置」还原真源,「应用」真正写入真源 json 并自动 build(POST /api/theme-apply { items:[{key,hex,alpha}] },原子写真源 → identity.js/tokens.js → build);右侧 = 预设包(下拉选择 + 逐行显示该预设颜色,「套到左侧全部」全量套 / 每行「应用」= 单项套该预设该键色到左侧,键缺失→无色透明行且不可应用;「存为新预设」POST /api/theme-export;「改名」写回预设 json 的 label,改/导出只作用预设包)。键 = 元素 class:15 键各自唯一(btn-primary/text-primary/gold-glow/status-capsule/sidebar-left/sidebar-brand…),无独立语义键,应用/写回按同名键直取;不透明度滑块读取真源 json 的 alpha 现值(非默认 1),拖滑先预览第 5 列真实元素样子、点「应用」才写盘
  • 品牌区单独控制:sidebar-brand 键驱动左侧边栏品牌区(lPcGpa_brand,CSS Modules 尾缀 _brand/_brandName 稳定不随 hash 变)——背景 + 品牌名文字一体(--dsw-specific-sidebar-brand / --dsw-specific-sidebar-brand-text 令牌);层级注入:sidebar-left(整体,--dsw-specific-sidebar-fill)先注入 → 品牌区后注入,同特异性自然覆盖整体底
  • 胶囊层级注入:capsuleCSS() 渲染时按 parent 关系拓扑排序(深度优先、父先子后)——外层胶囊先注入、内层后注入,同特异性选择器后写覆盖先写(子级自然覆盖父级);同层保持原数组顺序(稳定)。实测注入顺序:chatColumn→flowItem/older、tabStrip→guide→guideEntry
  • 胶囊背景「构建期展开字面 rgb」:layer 型胶囊背景不再引用运行期 CSS 变量 var(--mediascape-dsh-theme-bg-layer),改为 build 注入 CAPSULE_BG_LAYER_RGB 常量(值 = theme-colors.json 的 bgLayer.hex 转 rgb,与身份层 --mediascape-dsh-theme-bg-layer 同源),capsuleCSS() 直接拼 rgba(字面, α)。原因:平板息屏过段时间后会话区胶囊全消失、刷新恢复——运行期变量在渲染层重建时未就绪 → rgba(var(...), α) 声明整体 invalid → 背景透明。展开字面后渲染层重建也能画出胶囊,不依赖变量存活(改 bgLayer 后需 build 生效,配色盘 apply 自动 build 同步)
  • 宿主元素配色(胶囊 tab):胶囊配方外置 theme-studio/capsules.json(markdown / thinkBody / summary / todoPanel / queueDock / status / chatColumn 七类宿主元素的选择器、背景源、透明度、圆角、padding、额外声明),build.cjs 构建时注入 CAPSULES_DATA、identity.js capsuleCSS() 渲染(与旧写死值逐字一致);取色器「🧩 宿主元素配色」区每类一行解释卡片(数据源 theme-studio/json/theme-capsules.json,含宿主组件与避坑说明 + 该胶囊不透明度 alpha)+ 控件(底色源下拉 layer/颜色集合/solid、RGB 输入、透明度滑杆、圆角/padding 文本、额外声明;颜色集合 = 真源 + 全预设色聚合去重),「保存并 build 生效」→ POST /api/capsules/save(只写固定文件防路径穿越、原子写、自动 build);胶囊参数改后刷新页面生效
  • chatColumn 胶囊:给聊天消息流主列容器加整列胶囊面板——选择器 [data-conversation-scroll] [class$='_column'](锚点 data-conversation-scroll 为全局属性不随 hash 变、类名尾缀 _column 稳定;hash_column 中的 hash 前缀随构建变化不影响匹配),半透明主题底(layer 源 alpha 0.32)+ 圆角 16px + 内边距 10px 14px,消息流整体呈一块胶囊面板
  • 启动播放策略:theme-studio/playback.json(构建时注入 PLAYBACK_CONFIG)——videoAutoPlay(默认 true)控制视频壁纸挂载后是否自动播放(false 时仅显示首帧,点「视频」按钮才播);musicAutoPlay(默认 false)控制背景音乐启动时是否自动播第一首(默认不自动播,点「乐」/选曲才播;上传/选曲等用户操作后仍即时播放)

设计理念:配色盘可独立于主题单独存在

「键 = 元素 class」重构后,取色器本质是**「元素 → 色值」的键集合**(15 个 --swatch-<元素class> + 元数据键),加上 theme-studio/comps.json(元素清单)、theme-studio/capsules.json(胶囊配方)、 theme-studio/json/preset/(预设包)——这套东西与主题本体仅剩两个接触点:

| 接触点 | 现状 | 独立时 | |---|---|---| | 令牌生成 | buildTokensContent/buildIdentityBlock 在 start-preview.mjs 里、直接写回主题的 identity.js/tokens.js | 职责迁回主题侧:配色盘只输出「元素键 css」,主题侧适配器消费后自生成令牌 | | 构建接线 | apply 后调主题的 build.cjs | 配色盘不再管 build,只负责原子写真源 json |

其余能力(theme-current / theme-presets / theme-export / theme-rename / 胶囊 / 预设 / UI)本就自洽, 只是物理上住在主题仓库的 theme-studio/。若将来要把配色盘独立成单独项目,契约只有一份: --swatch-<元素class> 键名清单 + 元数据键规则(以 theme-studio/comps.json 为准),两边按此对接即可, 无需改动取色器任何配色逻辑。当前不拆,保留在主题仓库内(单仓库便于一体化 build/自测/预览)。

三、开屏动画

  • 启动页动画(运行态 $DSH_HOME/theme-mediascape/boot/boot.json 配置指定文件,读运行态目录——仓库根 boot/ 是分发模板,改配置/换素材直接放运行态目录即生效,无需重新 build)居中淡入,带绿色辉光边框
  • 无黑框直接出现(渐显式设计):开屏浮层在页面启动时同步立即插入盖住 DSH 界面(防界面先出、无黑屏窗口)—— 浮层背景为完全不透明深色 #03070f(透不出下方界面);等待期呈现氛围粒子(中央呼吸光圈 + 24 粒星光缓缓上升, 均为纯装饰、不阻挡交互);并行取回 boot.json 配置与壁纸清单后建媒体元素,等首帧就绪(video readyState≥2 / img loaded) 后浮层加 .media-ready(星尘/光圈 0.5s 淡出——「星光聚成画面」)且媒体经 transition 0.6s 渐变淡入(opacity 0→1、scale 0.95→1); 无素材 / 配置失败 / 媒体超时 → 浮层保持深色背景展示 durationMs 后自动淡出(界面呈现,无黑框先行)。
  • 媒体首帧就绪判定(修复 2026-09-26):视频就绪判定(waitBootMediaReady)三处加固——①就绪超时后最后复查真实 readyState(反代/主实例慢路径下视频首帧约 2s、可能晚于原 2s 超时,视频实际已就绪(readyState 4、4K 播放)不再误判为超时黑屏);②补 loadeddata 监听(防 canplay 事件漏捕竞态);③就绪超时放宽至 5s(BOOT_MEDIA_READY_MS)。修复前症状:实机路径视频流正常读取播放但画面黑(ready 类未加、opacity 0)后淡出,预览直连路径不受影响。 点画面跳过 / 自动结束前:媒体的结束态尺寸 = 壁纸背景同规格(absolute 全屏 + object-fit: cover,与 .mediascape-dsh-bg video 一致), 配合全屏渐变闪光(accent 光 → 深色)再整体淡出——开屏与壁纸交界视觉无缝。 不再 build 注入预置首图(GIF_DATA 恒 null),开屏内容完全按运行时 boot.json 的 file/files 决定(改配置刷新即生效,无需 build)
  • 支持 gif/图片/视频与多画面:.gif/.png/.webp 用 <img>,.mp4 用 <video muted loop autoplay>(无声音);files 数组按每段 durationMs 顺序轮换(video 复用元素 load()+play() 保持流式),播完整个集合自动淡出,loop:true 则循环整个集合直到点画面跳过(2026-09-23 删「点击跳过」按钮,点画面即跳);默认 file:"auto" 联动当前启用的视频壁纸做开屏(同一份流式数据移交,不重复加载)
  • 视频流式加载(Range/206):开屏视频经静态服务 Range 分片流式播放(lib/index.js + theme-studio/start-preview.mjs 的 serveStream,仅 media 且带 Range 才 206 分片、非法 Range 416、其余 200 全量),不再整段下载
  • 视频 HTTP 缓存(ETag + If-Range,已实现):媒体响应带 HTTP 缓存头,播放过的视频分片由浏览器缓存、重播零网络请求——
    • 206 分片响应返回 Cache-Control: public, max-age=86400 + ETag: "<mtime>-<size>"(文件校验指纹);重播同一 URL 时浏览器发 If-Range: <etag> 条件请求
    • 服务端校验:ETag 匹配 → 继续 206 分片(浏览器直接用缓存分片,不重新传输);ETag 不匹配(文件被替换/上传新版本)→ 回 200 全量,缓存自动失效、绝不给旧内容
    • 生效点:切回看过的壁纸、开屏重播、多画面轮换回到已播片段——均不再重新下载(file:"auto" 开屏=壁纸同一份流本就一次请求,缓存进一步覆盖「跨会话重播」)
    • 实现位置:lib/index.js 与 theme-studio/start-preview.mjs 的 serveStream 同步(两处保持一致,与 Range 分片同一函数);非媒体(js/css/json)保持 no-cache 即时生效不受影响
    • 自动验证:node test/e2e/video-cache-compare.mjs(零依赖,临时 2MB 视频 + 无缓存/有缓存双服务对比)——实测 5 轮播放:无缓存 10MB、有缓存 2MB(省 80%);重播 4 次零请求零传输;文件被替换后 If-Range 不匹配回 200 全量拿新内容(ETag 自动失效,绝不给旧缓存)
  • file: "auto" 联动视频壁纸(开屏=当前壁纸,同一份流):boot.json 的 file 填 auto 时,开屏自动取当前启用的视频壁纸做开屏动画—— 开屏素材与壁纸素材合一,全程只发一次 Range 请求(开屏与壁纸层共用同一 video 元素、同一个流式数据); 开屏播完淡出时把该 video 元素移交给壁纸层继续播放(按 id 匹配接管、保留 src 不重载),无缝过渡、无第二次加载。 机制:boot 解析阶段挂 loading 占位 → 壁纸层在 auto 解析完成前不自建视频 → 解析出目标 id 后同 id 则维持等待、不同 id 则恢复正常自建 → 开屏 finish 时挂出 window.__mediascapeDshBootVideo 移交对象 → 壁纸层 renderLayers() 按 id 接管同一元素。
  • 壁纸视频关闭时关闭守卫(2026-09-29):视频开关(LS_BG_VIDEO)非 "1"(视频壁纸未开/已关)时,file:"auto" 不再兜底首个视频——auto 解析返回空、开屏守卫关闭、无视频按 durationMs 正常淡出(不卡等待),避免「开屏播视频但壁纸层不消费移交」的错配。
  • 配置(运行态 boot/boot.json):
{
  "files": ["demo-6s.mp4"],
  "file": "auto",
  "title": "",
  "sub": "",
  "durationMs": 3000,
  "loop": false
}
  • 标题/副标题可配置:boot.json 的 title/sub(空字符串=不显示,默认只显示媒体本身);files 优先于旧 file 字段,file:"auto" 再优先于 files 与 file(联动视频壁纸时不参与集合轮换,单画面=当前壁纸)
  • 每次刷新播放,单画面/每段时长取配置 durationMs(默认 3000;2026-09-23 起代码读取钳制最小 3000——配置可写更低但不生效,防闪屏);点击任意处或点画面可跳过(auto 视频首帧未就绪前不可跳,避免壁纸丢失)
  • 尊重系统 prefers-reduced-motion,自动跳过

已实现

  • 开屏 file: "auto" 联动视频壁纸:boot.json 的 file 填 auto 时,开屏自动取当前启用的视频壁纸做开屏动画(走既有 Range 流式通道),开屏播完把同一 video 元素移交给壁纸层继续消费(同一份流式数据、同一元素,不重复加载)——开屏素材与壁纸素材合一,全程仅一次 Range 请求;详情见本章「file: "auto" 联动视频壁纸」条目。

四、氛围粒子与字号调节

  • 氛围粒子(嵌在「景」面板内一行):单循环按钮——点击循环 关→一档→二档→三档,按钮显示当前档位、关档暗其余档亮;数量切换带 0.9s 淡入淡出过渡;每粒星光独立尺寸、亮度、漂浮轨迹与呼吸式闪烁
  • 字号调节(dock「字」按钮):点击循环切换四档——小 0.85 / 标准 / 大 1.15 / 特大 1.3(按钮文字保持「字」,不显示大小,高亮暗=标准档、亮=非标准档;大小由会话区文字自行感知);直接写宿主 --dsh-content-font-size 变量,正文随档位缩放,localStorage 记忆、重启保持

五、背景音乐

  • 「乐」按钮:亮 = 播放中,暗 = 暂停;点击按状态机联动播放与二级面板:

    | 当前状态 | 点「乐」按钮 | 点面板外 | |---|---|---| | 暗(暂停)+ 面板关 | 变亮播放 + 打开面板 | — | | 亮(播放)+ 面板关 | 保持亮播放不变 + 打开面板 | — | | 亮(播放)+ 面板开 | 变暗暂停,面板保持开不关 | 关面板,继续播放 | | 暗(暂停)+ 面板开 | 恢复播放(亮),面板保持开 | 关面板,保持暂停 |

  • 旋转唱片 + 封面(同名封面图或 music/music.json 指定;无封面自动读 MP3/FLAC 内嵌封面)+ 进度条拖动跳转

  • 上一首 / 播放暂停 / 下一首 / 循环模式(单曲循环 → 列表循环 → 随机播放)

  • 选择:弹出歌单,勾选后移除(内置隐藏、导入删除)或随机(以勾选歌曲为随机池)

  • + 添加歌曲:dock「传」统一入口(按文件后缀识别音乐,一次多选)——服务器磁盘持久化($DSH_HOME/theme-mediascape/music/ + music.json 清单,刷新/重启/换浏览器都在),无封面自动读内嵌封面;封面可为当前歌曲指定,落盘同名封面图并写入 music.json(封面保留原设计:自动按音乐名重命名)

  • 当前曲目与循环模式持久化

六、打字音效

  • 聊天输入框打字时播放「咔哒 + 低频咚」的星光质感按键音
  • Web Audio 实时合成,零音频文件;空格/回车音色更低;「声」按钮开关;另有壁纸音量调节

七、界面操作(dock 工具条)

四个按钮收在可拖动的半透明竖条面板(右下角,圆角;无毛玻璃,纯深色底):

| 按钮 | 功能 | |---|---| | 景 | 壁纸设置(模式单按钮三合一:循环/顺序/随机点击切换 + 视频开关 + 随机间隔 + 氛围粒子四档 + 确定;选择器仅「移除」按钮) | | 乐 | 背景音乐 开/关 + 播放面板 | | 声 | 打字音效 开/关 + 壁纸音量 | | 字 | 字号四档(小/标准/大/特大) | | 传(下) | 统一上传入口:点击直接打开文件选择器(一次多选),按文件后缀天然识别壁纸/音乐(png/jpg/jpeg/webp/mp4 → 壁纸,mp3/ogg/m4a/wav/flac → 音乐);选完先按「文件名+大小」与现有列表去重预检,重复的在二级面板提示「已跳过」;上传开始前/结束后各刷新一次列表;上传进行时按钮高亮,进度显示在 dock 二级面板(自动弹出,无需手动开;⏸ 暂停落盘 .part / ▶ 继续从 .part 续传 / ✕ 取消清 .part) |

按钮顺序(上→下):景 乐 声 字 传(2026-09-23 定稿,apply.js 启动段统一重排)。

🖱️ 按住面板任意位置拖动即可移动,位置自动记忆(重启 dsh web 后仍生效)。 面板/选择器弹层相对 dock 垂直对齐并自动保持在屏幕内。 按 ESC:关闭壁纸/歌单等浮层;音乐面板展开时收起。

如何新增 dock 按钮 / 二级面板

dock 是 toolbar/dock.js 创建的固定容器(startDock()),各按钮是独立 start* 函数在 lib/client.js 启动段按序挂载(const stopX = startX(dock);),返回 dispose 供卸载。新增一个按钮 + 二级面板照此模式:

// lib/client-parts/<模块>/<文件>.js
function startMyBtn(dock) {
  const btn = document.createElement("button");
  btn.type = "button";
  btn.className = "mediascape-dsh-mybtn mediascape-dsh-dock-btn";
  btn.textContent = "钮";                       // 单字按钮文字
  btn.title = "我的功能";
  dock.appendChild(btn);                        // dock 参数传入,直接 append

  const panel = document.createElement("div");
  panel.className = "mediascape-dsh-my-panel";  // CSS 对齐 bg-panel(absolute + .open 显示)
  dock.appendChild(panel);

  // 点击 toggle 面板 + 用 dock 的定位函数让面板贴 dock 侧边弹出(左右自动判断、视口内钳制)
  btn.addEventListener("click", (e) => {
    e.stopPropagation();                        // 防冒泡触发外层「点外关闭」
    panel.classList.toggle("open");
    if (panel.classList.contains("open")) dock.__mediascapeDshCenter(panel);
  });

  // 点外自动收起(对齐 onBgDocClick 模式;排除自身按钮)
  const onDocClick = (e) => {
    if (panel.contains(e.target) || e.target === btn) return;
    panel.classList.remove("open");
  };
  document.addEventListener("click", onDocClick);

  return () => {                                // dispose:清理监听 + 移除元素
    document.removeEventListener("click", onDocClick);
    btn.remove();
    panel.remove();
  };
}

要点:

  • 挂载:在 lib/client.js 启动段调用 const stopMyBtn = startMyBtn(dock);,并把 stopMyBtn() 加进整体清理函数(return 的 dispose 列表)
  • CSS:按钮用 mediascape-dsh-dock-btn(dock 内统一圆形按钮样式),面板样式对齐 mediascape-dsh-bg-panel(position:absolute; z-index; display:none; flex-direction:column; .open{display:flex})
  • 定位:面板位置不要自己写 fixed/left/top——调用 dock.__mediascapeDshCenter(panel) 由 dock 统一处理(dock 偏左弹右侧、偏右弹左侧、贴底上移钳制),窗口 resize 时 dock 会 repositionPopups() 自动重摆
  • 构建:lib/client-parts/ 下新建文件后,把文件加入 build.cjs 的 PART_ORDER(按依赖顺序);跑 node build.cjs 拼回 lib/client.js
  • 按钮高亮:.on 类由各按钮自己维护(如上传按钮上传中 btn.classList.toggle("on", 有活动行)),CSS 在 scenes/identity.js 统一定义

八、在线资源与自定义素材

  • 壁纸添加两种方式:

    1. 运行时添加(推荐):点 dock「传」按钮,一次多选图片/视频/音乐,立即生效并持久化
    2. 在线资源(随主题分发,别人安装自动下载):lib/sources.json 的 sources 登记
  • 在线资源(壁纸图片/视频 + 音乐统一机制,启动后台静默下载缺失项):

{
  "sources": {
    "<sha1-40位hex>": {
      "name": "示例视频.mp4",
      "url": "https://example.com/video.mp4",
      "kind": "wallpaper"
    },
    "<sha1-40位hex>": {
      "name": "示例歌曲.mp3",
      "url": "https://example.com/song.mp3",
      "kind": "music"
    }
  }
}
  • hash:下载内容 SHA-1(40 位 hex),仅作完整性校验(下载后校验一致才落定),不做磁盘文件名

  • kind 分流:wallpaper → wallpaper/online/;music → music/

  • 落盘 = 原始文件名(name 字段即落盘名,与上传/内置素材一致);已存在同名文件即跳过(幂等)

  • 断点续传:中断保留 <name>.part,下次启动从断点继续(Range 请求),校验通过才重命名落定

  • 本地/在线共存:同一列表展示,同内容以本地上传优先

  • 以后加资源只改配置、不改代码

  • 目录复制(插件启动把仓库自带素材目录复制一份到真实数据目录,仓库根保留不删源):

{ "dirs": { "music": "music", "wallpaper": "wallpaper", "boot": "boot" } }

整目录递归复制、已存在文件跳过、只复制一次(目标已存在非空即跳过);以后加目录只改 json 的 dirs

  • 开屏素材:boot/boot.json 配置 files(多画面数组,优先)/ file(单素材,兼容)/ title / sub / durationMs(每段时长)/ loop(循环整个集合),改配置刷新即生效(运行时 fetch,无需 build)

媒体加载与鉴权代理(视频/开屏加载不出来时看这里)

现象

首次打开时,视频壁纸或开屏动画长时间黑屏、甚至一直不出画面;刷新或第二次打开又恢复正常。

为什么视频加载不了

当 DSH 前面部署了鉴权代理(要求 ?token= 的访问链接)时,代理会把没有携带 token 的媒体请求重定向(302)。

浏览器的媒体元素(<video>)本来用 Range 请求分段拉流(206),但跟随 302 重定向会丢失 Range 语义—— 请求退化成「整文件下载」:上百 MB 的壁纸视频要全部下完才有首帧,表现就是卡住 / 加载不出来。

之所以只在首次出现:第一次访问会完成一次 token 认证并建立会话 cookie,之后的请求都带 cookie、不再被重定向, 所以「只有第一次慢」。

解决

  1. 服务端捕获 token:DSH 启动时会打印带 token 的访问地址;插件在启动阶段包装 process.stdout.write, 在打印的瞬间抓取 token(只读取输出分片、原样透传,不改变既有日志行为);若插件加载晚于该打印, 回读 dsh-proxy.log 兜底(日志路径按 DSH_PROXY_LOG / DSH_HOME 推导,不硬编码)。
  2. 下发给前端:GET /theme-mediascape-assets/media-token 返回当前 token(取不到返回空串)。
  3. 媒体 URL 拼 token:开屏图片/视频、壁纸视频、壁纸图片背景、预载元素、音乐封面—— 所有媒体赋值点统一经 withMediaToken,Range 请求即被代理直接放行,恢复 206 分段流式,首帧不再等待整文件下载。
  4. 晚到修正:token 就绪晚于媒体元素创建时,修正已挂载元素的 src(仅在确实需要时重设,避免无谓重新加载)。

边界

没有鉴权代理的环境(本地直连、预览服务)行为不变:取不到 token 时媒体 URL 原样使用,不影响加载。

九、HTTP API 一览

运行时素材/上传 API 由插件挂载在 DSH 主实例 /theme-mediascape-assets(lib/index.js);预览服务(theme-studio/start-preview.mjs,默认端口见 start.sh)对同一前缀本地实现同源 API(不依赖主实例),配色盘 API 为预览服务专属。

9.1 素材与上传(运行时 + 预览同源)

| 方法与路径 | 说明 | |---|---| | GET /theme-mediascape-assets/config | 上传配置(accept 扩展名 / 上限,前端 file input 用) | | GET /theme-mediascape-assets/media-token | 当前鉴权 token(供前端拼媒体 URL,使 Range 请求不被鉴权代理 302 重定向;取不到返回空串) | | GET /theme-mediascape-assets/wallpaper/list | 壁纸列表(id/kind/label/file/size/url;stat 失败的失效文件剔除) | | GET /theme-mediascape-assets/music/list | 音乐列表(id/name/file/size/cover/url;music.json 自动同步登记/删除/封面) | | GET /theme-mediascape-assets/{boot,music,wallpaper}/<file> | 素材流式下载(图片/视频/音频):Range 206 分片 + ETag(If-Range 条件请求命中缓存省流量);1MB 大块流 + 错误关连接 + 客户端中断释放源流 | | POST /theme-mediascape-assets/upload?name=<文件名>[&offset=<已传字节>] | 壁纸上传(按扩展名自动识别图片/视频 mp4/webm):中途不落盘内存缓冲(可用内存 20% 上限)、offset>0 续传 <正式名>.part(偏移不匹配返 409)、落定清理孤儿 .part、同名同大小复用、同名不同大小加 (1)(2) 后缀 | | POST /theme-mediascape-assets/music/upload?name=<文件名> | 音乐上传(mp3/ogg/m4a/wav/flac;自动登记 music.json) | | POST /theme-mediascape-assets/music/cover | 音乐封面上传(自动按音乐名重命名) | | DELETE /theme-mediascape-assets/wallpaper/<file> | 真实删除壁纸文件(列表刷新后消失) | | DELETE /theme-mediascape-assets/music/<file> | 真实删除音乐(按 music.json 一并删除封面 + 清登记;预览服务本地实现) | | DELETE /theme-mediascape-assets/upload/part?name=<文件> | 取消上传:清理暂停快照 <正式名>.part | | GET/POST /theme-mediascape-assets/wallpaper/log | 壁纸切换日志(POST 落盘 / GET 读回,受 debug 日志开关控制) |

9.2 配色盘(theme-swatch.html,预览服务专属)

| 方法与路径 | 说明 | |---|---| | GET /api/theme-current | 当前配色真源(label/badge/bg 系 / colors key:{hex,alpha} / comps 元素行——元素行=colors 键全量动态生成,comps.json 仅覆盖中文名/模板) | | POST /api/theme-apply | 应用配色:{ items:[{key,hex,alpha?}] }(含 bg 系)写真源 json → build(tokens/identity 生成器三处同步) | | GET /api/theme-presets | 预设包清单([{id,label,badge,swatches}]) | | POST /api/theme-export | 当前配色存为新预设:{ name, css|json } | | POST /api/theme-rename | 重命名预设:{ id, name } | | GET /api/capsules | 胶囊配方读取(运行态 → 仓库 → 内置三级兜底) | | POST /api/capsules/save | 保存胶囊:{ rules:[{key,selector,...}] }(每条必须含 key/selector 字符串) | | POST /api/restart | 重启预览服务(start.sh restart) |

9.3 页面与静态资源(预览服务)

| 路径 | 说明 | |---|---| | GET / /index.html | 预览页(真实 client.js 垫片宿主渲染) | | GET /theme-swatch.html | 配色盘页面(配色区 + 胶囊区) | | GET /preset/* /lib/* /js/* /samples/* | 预设包 / 库脚本 / 页面脚本 / 元素分片(samples/.html)静态资源 |

待办(TODO)

  • 配色盘功能基本完善,页面颜色区需要重新设计层级排布(2026-09-23 记录):颜色区当前为平铺元素行列表;后续按宿主元素层级重新排布(右侧边栏→品牌区/文件树/指南页,会话区→正文/胶囊/状态提示,等),层级关系与胶囊区 parent 树对齐。

目录结构

完整文件树见 docs/目录结构.md(git-push doc-tree 自动维护)

真实数据目录(插件启动时由 dirs 配置驱动复制,仓库根保留;运行态文件统一放运行态目录——素材、配置、日志、pid 等): $DSH_HOME/theme-mediascape/:wallpaper/(上传落盘原始文件名 + online/ 在线子目录)、music/(音乐文件原始文件名 + 封面 + music.json 清单)、boot/(开屏动画:boot.json 配置 + 素材文件,改配置/换素材直接放这里即生效)、logs/(运行日志:壁纸切换日志 wallpaper.log)

安装与要求

要求:DeepSeek Harness(dsh)+ Node.js;纯客户端主题,无额外依赖。

支持 DSH 版本:>=0.1.6-0 <0.3.0-0(0.1.6 开发验证 + 0.2.0-rc.2 实测可用;防 0.3.0 破坏性变更)——package.json peerDependencies 声明 @deepseek-ai/dsh。

方式一:从 GitHub 源安装(公开仓库,匿名可装)

⚠️ dsh plugin --profile <名> add … 当前实测会挂死且零输出(两次都以 timeout 结束、日志为空)⇒ 安装改在该 profile 目录里直跑 pnpm(dsh plugin 的设计就是把参数转发给 profile 的 pnpm,两者本应等价):

cd <DSH_HOME>/profiles/<profile>
pnpm add "github:EIGHTfs/dsh-theme-mediascape#<commit>"     # 用 commit 固定版本(远端暂无 tag)

两条前置条件(缺一即失败;本仓库有 prepack 构建脚本,两条都需要):

  1. 本机要有 npm —— pnpm 构建 git 来源的包时会在临时目录执行 npm install; 缺 npm 时报 ERR_PNPM_PREPARE_PACKAGE + spawn ENOENT: npm: command not found。

  2. pnpm 11 默认禁止 git 源依赖执行构建脚本(供应链策略)⇒ 先在 profile 的 pnpm-workspace.yaml 里放行,否则报 ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED:

    allowBuilds:
      dsh-theme-mediascape@https://codeload.github.com/EIGHTfs/dsh-theme-mediascape/tar.gz/<commit>: true

    ⚠️ key 里带 commit 哈希 ⇒ 换版本/换提交要同步改这一行。

装完重启 dsh web 生效。本仓库是 monorepo(主题在 packages/<主题包目录>),需要指定子目录时用 github:EIGHTfs/dsh-theme-mediascape#path:/packages/<主题包目录>(换行内联写法见下文「配色盘/主题分开装」一节)。

方式二:打包安装(本地 tgz)

cd <DSH_HOME>/profiles/<profile>
pnpm add ./dsh-theme-mediascape{tag}.tgz
# 装完重启 dsh web 生效

卸载

cd <DSH_HOME>/profiles/<profile>
pnpm remove dsh-theme-mediascape
# 重启 dsh web 生效

⚠️ 与其它主题互斥:多个主题都会调用 ctx.theme.setTheme 并注入 !important 令牌样式,同时只能启用一个主题(多主题时 patch 行注释掉即可,可随时恢复)。

健壮性自测

针对「手动移动/删除素材文件」场景的自动化回归测试:验证服务端在文件与清单不一致时 不崩溃、列表正确、清单自动同步、孤儿记录被清洗。测试使用独立数据目录(/tmp/ms-*-test-home), 不污染真实 $DSH_HOME/theme-mediascape/。

node theme-studio/tests/ms-split-behavior-check.mjs   # 路由/上传/删除/白名单基础链路
node theme-studio/tests/ms-file-move-robustness.mjs   # 移动壁纸/音乐/封面文件健壮性(12 项断言)
  • 移动壁纸文件(json 未同步)→ 列表不崩、不含被移项
  • json 孤儿记录 + 磁盘缺失 → 再次上传同内容重新落盘,不误判「重复复用不存在文件」
  • 移动音乐文件 → 列表不崩、music.json 自动同步移除
  • 静态 GET 已移动文件 → 404 不崩
  • 封面判定与「填空」一致:json 记录的 cover 文件不存在 → 自动找同名图片 → 再无则无封面; music.json 自动清洗孤儿 cover
  • 移回文件 → 列表恢复

debug 模式(配置分层:仓库默认 + 运行态覆盖;多开关:配色服务 / 预览页 / 日志)

配置分两层(2026-10-09 起):默认值随仓库下发(插件根 debug.default.json,入库、随版本升级), 运行态只需在数据目录写想覆盖的键($DSH_HOME/theme-mediascape/debug.json,$DSH_HOME 缺省 ~/.dsh):

// $DSH_HOME/theme-mediascape/debug.json —— 只写要覆盖的键即可,其余回落 debug.default.json
{
  "log": { "wallpaper": true, "api": false },  // 按类别开关(也可写 true=全开 / false=全关)
  "logging": {
    "mediaDetail": true,   // 切换级全字段:预载就绪 / 是否复用预载 / 出画间隔 / 是否带 token 等(false=只留事件主干)
    "maxBytes": 1048576,   // 单文件轮转阈值(字节)
    "keep": 3              // 轮转保留份数(.1/.2/.3)
  },
  "theme-swatch": true,    // 配色服务(仓库默认 false,开发用)
  "preview": true          // 预览页(仓库默认 false,开发用)
}
  • 任意新键自动透传:debug.json 里加任意未知键(如 "performance": true)无需改代码,readDebugConfig() 自动带出、isDebug('performance') 可直接判断——以后加 debug 开关只改配置文件

  • 日志统一入口:所有运行态日志(含 build 期胶囊诊断 build-capsules.log)都经 lib/debug.js 的 writeLog(name, entry) 单点写入,受 log 类别开关控制——关闭时任何日志零落盘,无绕过开关的散落直写(lib/log.js 为 re-export 兼容层)

  • 轮转可配:logging.maxBytes / logging.keep 控制单文件阈值与保留份数(缺失/非法回落 1MB / 3 份,与旧行为一致)

  • 任一开关开 → 每次 node build.cjs 自动执行 theme-studio/tests/ 下的自测脚本(任一失败即标红)

  • log 开启 → 运行态 $DSH_HOME/theme-mediascape/logs/ 按类生成日志文件,每条带日期时间(YYYY-MM-DD HH:mm:ss.SSS)+ 自动轮转:

    • wallpaper.log:壁纸切换链路(前端上报)。除 switch / video-ended 外,2026-10-09 起增加三个排查用事件: mount(本次挂载用的是哪来的元素:source = takeover(开屏移交的同一元素/同一份流)|preload(复用预载元素)|new(新建元素从头加载); 并记 readyState / bufferedS / tokenized)、preload(只在真正发起一次预载时记,含 remainS 距结尾剩余秒)、 first-frame(gapMs = ended→新视频出画面的间隔;受 timeupdate 约 250ms 节流,仅用于粗判,精确口径见测试脚本)
    • api.log:API 请求耗时。媒体请求额外记 kind(video/image)、tokened(URL 是否带反代 token)、range、ifRange ——用来直接判定「是否 Range 直通」与「是否因缺 token 被反代 302 ⇒ 丢 Range 退化整文件」
    • theme-swatch.log:配色操作(theme-save / theme-apply / 页面访问)
    • preview.log:预览页访问
    • upload.log:上传行为(上传/音乐上传/封面上传)
    • startup.log:服务启动(主实例 / 预览服务器)
    • api.log:API 请求耗时(method/path/status/ms,唯一入口统一记录,不打散到各函数)
  • 只认新键:旧 {"enabled": true} 不再生效;三键都缺/非 true = 默认关闭,构建只输出提示,不产生额外动作

  • 关闭:删除该文件,或把对应键置 false

theme-studio/ 预览环境

theme-studio/ 是开发预览用,跑的是真实前后端,不是简化模拟:加载真实的 lib/client.js(构建产物)、接真实的 lib/*.js 服务端逻辑(上传/删除/壁纸列表/音乐列表/在线下载/目录复制全部走真实代码)、读写真实的 $DSH_HOME/theme-mediascape/ 数据目录——预览所见即插件实际行为,改完代码 build 后刷新预览页即可验证,无需重启主 DSH。

./theme-studio/start.sh start     # 启动预览服务器(默认端口 30999)
./theme-studio/start.sh stop      # 停止
./theme-studio/start.sh restart   # 重启(默认命令)
./theme-studio/start.sh status    # 查看状态
  • 预览页:http://<本机IP>:30999/(preview.html 加载真实 client.js)
  • 预览模式变量:?mode=full → 完全真实数据模拟(关空清单兜底、隐藏提示条、开屏/壁纸/音乐/胶囊全部走真实后端刷新即见;缺省 = 独立预览态,fetch 失败兜底空清单仍可演示交互)
  • 取色器:/theme-swatch.html(配色/胶囊双 tab:左元素控件手输 + 点击单项应用预设色、右侧预设包全量/混搭应用、左侧应用真正写入、导出新预设)
  • 写操作(upload/delete)转发真实后端;素材与列表本地直供,不依赖主实例重启
  • PID 文件在项目根 dsh-theme-mediascape.pid

配色盘独立分发调研(设计前置考量:保持内置的理由)

背景:配色盘(theme-studio/ + 取色器)随主题一起提供,曾调研「配色盘也可单独安装」的分发方式。本节记录设计前置考量——即设计时为何不把配色盘拆成独立分发,供将来若需独立分发时对照。

设计取向:配色盘与主题内置一体;若将来需要独立分发给其他作者,按本小节方案落地。

为何不设计成独立分发(双向耦合事实)

  • 配色盘写盘 = 写真源 json → 生成主题的 identity.js/tokens.js → 调主题 build.cjs(start-preview.mjs 的 buildTokensContent/buildIdentityBlock 即为此写)
  • 主题 build.cjs 又读配色盘的 json / capsules.json(build 注入 CAPSULES_DATA)
  • samples 单片分片、胶囊配方、comps 键 Schema 均为两端配套——拆双仓会撞上「配色盘没主题构建能力只是改 json 的编辑器、主题没配色盘是无法配色的主题」的双向依赖

将来若要独立分发(monorepo 方案,调研过的路径)

dsh-theme-mediascape(一个 GitHub 仓库,monorepo)
├── pnpm-workspace.yaml          # packages/*
└── packages/
    ├── 主题包/                    # dsh 字段;依赖 配色盘包
    └── 配色盘包/                  # dsh 字段;独立可安装(theme-studio/ + start-preview.mjs 迁入)
  • 安装:只装主题 dsh plugin --profile web add github:EIGHTfs/dsh-theme-mediascape#path:/packages/主题包目录;只装配色盘 ...#path:/packages/配色盘目录;装主题时经依赖自动带配色盘
  • 依赖方案(按优先级):A. file:../配色盘目录(需实测 clone 后 pnpm 能否解析兄弟目录)→ B. workspace:* → C. GitHub Release tarball URL(https://github.com/EIGHTfs/<repo>/releases/download/<版本>/<配色盘包名>-<版本>.tgz,不依赖仓库内部路径解析,最稳)
  • 扩展(服务提供者模式,参考 dsh-better-sidebar 的 ctx.betterSidebar):配色盘注册服务 ctx.<服务名>,其他插件 inject: ['<服务名>'] 接入,典型方法 registerColorTarget(selector, options) / getPalette() / applyPalette(palette) / onPaletteChange(callback)
  • 不发布 npm:仅 GitHub 安装

单仓 vs 双仓权衡(调研记录)

  • 单仓 monorepo:schema/生成/样例同仓同步、原子提交、开发体验好;两包共用 git 历史需约定版本策略
  • 双仓:独立版本/tag/Release、仓库聚焦;但本项目配色盘依赖主题 build 能力 → 跨仓同步地狱(改键要两仓提交两仓发版)
  • 结论:若拆,走单仓 monorepo(双向耦合无法物理分离)

演进总结(提交消息归纳)

按「架构设计 / 功能 / 问题」三类归纳全部历史提交消息(含早期 backup 分支),便于理解项目为什么长成这样。非版本列表。

架构设计

  • 服务端模块化(lib/):按职责拆为 10 个模块——
    • index.js:DSH 插件应用入口(注册服务与路由)
    • config.js:上传扩展名单一权威源(ALLOWED_UPLOAD_EXT + 派生 accept,build 时注入客户端)+ 静态服务目录白名单
    • paths.js:数据目录统一($DSH_HOME/theme-mediascape/ 下的 wallpaper/、music/ 等路径)
    • handlers.js:HTTP 处理器(配置/删除/壁纸列表/音乐列表/封面判定/重复判断)
    • labels.js:壁纸显示名存取(wallpaper/wallpaper.json,读盘缓存 + 原子写)
    • online.js:在线资源下载(断点续传 + SHA-1 校验)
    • bootstrap.js:插件启动时目录迁移(仓库根素材整目录复制到数据目录,已存在跳过)
    • debug.js:debug 配置读取(多开关 + 新键自动透传 + isDebug 判断)+ 运行态日志统一写入 writeLog(受 log 总开关控制)
    • log.js:re-export 兼容层(writeLog/logTs 转发自 debug.js)
    • client.js:构建产物(仅配置与逻辑,约 188KB,资源全走运行时 API)
  • 前端模块化(lib/client-parts/):按职责拆为 4 个子目录 + 顶层——
    • foundation/:基础层(utils 工具 / constants 常量 / assets 素材注册 / loader 加载器 / theme 主题变量 / tokens 设计令牌)
    • scenes/:场景层(boot 开屏 / wallpaper 壁纸 / ambience 氛围 / font 字号 / upload 上传 / identity 界面样式覆盖)
    • sound/:声音层(typesound 打字音效 / music-player 音乐播放 / music-extract 音乐提取)
    • toolbar/:dock 工具条(dock.js)
    • apply.js:顶层聚合入口
  • 全资源在线化:壁纸/音乐统一 lib/sources.json(hash 主键 + dirs 目录复制映射);插件启动按 dirs 把仓库根素材整目录复制到数据目录(已存在跳过,仓库根永远保留分发素材,数据目录获得运行副本,跨设备天然兼容);/music/list 每次调用自动把目录实际内容写回 music.json(新增自动登记、删除自动移除、同名封面自动补 cover、保留用户已存 name/cover,有差异才落盘)
  • 上传落盘规则:原始文件名落盘到数据目录(跨设备即时可见),全流程 traceId 日志;accept 单一权威化(复用配置 ALLOWED_UPLOAD_EXT)
  • 配色机制(V2 键=元素 class):只读一个真源 json(theme-studio/json/theme-colors.json,label/badge + bg 系层级 + 全部元素色,每色带 alpha {hex, alpha}、唯一写盘目标),其它 json 只是预设包(theme-studio/json/preset/,只承载颜色集合 + 显示名,不含 bg 系);key 即元素 class(15 个元素各一个唯一键,无独立语义键,键名与 comps.json 一一对应);取色器左侧 = 真源元素 class 控件(comps.json 15 角色),右侧 = 预设包(应用预设全量套左侧 / 点左侧元素单项应用预设对应键色,混搭——键缺失只跳过该元素,绝不波及其他元素);左侧「应用」收完整真源 json(POST /api/theme-apply)→ 原子写真源 json → 令牌零映射直接按元素键名取值(text←text-primary、accent←btn-primary、border←gold-glow、invert←status-capsule 等)→ identity.js 主题变量段 + tokens.js 令牌 → build → 主实例生效;改名/导出只作用预设包(写回预设 json、POST /api/theme-export 存新预设)
  • 预览环境:theme-studio/ 跑真实前后端(加载真实 client.js + 真实服务端逻辑 + 真实数据目录),改完 build 后刷新预览页即可验证,无需重启主 DSH

功能

  • 壁纸系统:动态/静态两种壁纸、在线壁纸下载(后台静默 + 断点续传 part)、壁纸主题自动配色(图片主色驱动 UI 换肤)、壁纸面板自适应宽度
  • 基底配色(V2 单真源 + 预设,键=元素 class):15 个元素各一个唯一键(btn-primary/text-primary/gold-glow/status-capsule…,键名=元素 class,无独立语义键)、真源 json 唯一(theme-studio/json/theme-colors.json)、预设包多套可选(theme-studio/json/preset/)+ 导出增补与改名、配色双 tab 页(左侧元素 class 控件手输/点击单项应用、右侧预设全量套用混搭——键缺失只跳过该元素不波及其他、左侧应用真正写入、导出新预设);令牌零映射:宿主令牌直接按元素键名取值(text←text-primary、accent←btn-primary、border←gold-glow、invert←status-capsule),应用时写 identity.js 主题变量段 + tokens.js 令牌
  • 开屏动画:gif/图片/视频开屏(视频为流复制截取的 h264-in-mp4,浏览器原生支持)、boot.json 可配置标题/副标题/时长
  • debug 多开关:debug.json 三独立开关(log 总日志 / theme-swatch 配色服务 / preview 预览页,任一开跑自测);log 开启时运行态 logs/ 按类生成日志(壁纸切换/配色操作/预览访问/上传/启动/API 耗时,带时间戳 + 1MB 轮转)
  • 氛围与字号:氛围粒子 + toggle 交互、四档字号调节(会话区 + 状态文字,写宿主字号变量)
  • 音乐与音效:背景音乐播放器 + 打字音效开关;「乐」按钮状态机(亮=播放中/暗=暂停,与二级面板状态联动)
  • 上传体系:中途不落盘(内存缓冲 + 可用内存 20% 动态上限 + .tmp 保底)、暂停落盘 .part + 继续从 .part 续传(?offset= 追加)、上传前「文件名+大小」去重预检(重复走二级面板「已跳过」行)、进度二级面板(多行对齐/大小/速度/暂停/继续/取消)、上传/移除开始前与结束后各刷新一次列表(内置公共函数)、上传完释放连接(串行)、dock「传」统一入口(按文件后缀识别壁纸/音乐,壁纸/音乐上传移除共用同一公共函数)
  • 界面操作:dock 竖排工具条(字/景/声/乐,萤并入景面板)、二级面板改侧边弹出(左右判断)、模态遮罩透明化(设置打开壁纸仍透出)、中间画面全透明(壁纸区域不铺底色,主题色只作用于组件)、景模式三按钮合一(循环/顺序/随机单按钮循环切换)、选择壁纸去随机按钮
  • 可读性:全局文字轻描边 + 深色半透明衬底(任意壁纸下文字可读;无毛玻璃)、「深度求索中…」状态胶囊定稿

问题手册

  • 配色对照页点击换色失效:renderCol 构建 comps 时只存 {style,text} 丢了 role 字段,渲染出 data-role="undefined",点击委托拿不到有效 role → 渲染循环改用显式 role 列表
  • 选择器崩溃:大面积 backdrop-filter 在 SAF 快照瞬间 OOM → 禁毛玻璃 + 文件选择 input 挂 DOM 触发(安卓静默拦截修复;毛玻璃已移除后该修复为空操作,守卫保留防回归)
  • 配色应用后服务被杀:theme-apply 内 execSync 调 build 触发 debug 模式重启,自杀宿主进程 → DSH_THEME_NO_RESTART=1 跳过重启
  • 思考展开无胶囊:CSS Modules 编译类名是 hash_thinkBody(类名后无下划线),选择器 [class*='_thinkBody_'] 匹配不到 → 改 [class*='_thinkBody']
  • 封面/重复判定:json 记录与磁盘不一致 → 封面校验文件存在 + 同名回退 + 孤儿清洗;重复上传先查磁盘 existsSync
  • 状态文字模糊/描边反复:白描边改四方向硬描边 → 去描边 → 胶囊化定稿(不透明深底+纯白字)
  • dock 悬浮条叠层:重复 apply/热重载不再叠层(幂等)
  • 壁纸视频不自动播放(停在开头):声音面板「壁纸音量」条/喇叭写 LS_MUTED,拖过音量条即取消静音(LS_MUTED="0")→ 壁纸视频 video.muted=false(有声)→ 浏览器自动播放策略拒绝无交互的 play()(NotAllowedError)→ 视频卡在开头。与配置/服务端无关(videoAutoPlay 一直 true、Range/206 正常),重启也无用。修复:play() 被拒时静音重试(video.muted=true 再 play())——自动播放保底成立(无声),要声音点声音面板(用户交互后浏览器允许有声播放,applyVideoSound 把正在播的 video 改为有声)
  • 权限漂移(已删映射文件):曾用 .names.json 记录映射,saveBaseChoice rename 覆盖导致权限漂移(755/777)→ 已删除映射机制与文件(映射不参与生效,选基底只影响对照页显示与应用来源),改名直接写 css
  • 上传文件时二级面板被关闭(修):input.click() 打开系统文件选择器时 click 事件冒泡到 document,被「点面板外 → 关闭」逻辑(onBgDocClick / onDocClick)误判 → 面板/选择器随上传被关。修复:三处上传用 file input 捕获阶段 stopPropagation()(选择器的 click 不再冒泡)
  • 乐面板进度条不可拖动(修):repositionPopup 垂直钳制写反——Math.max(0, vh-8-dr.top-r.height) 先把上移量钳成 0,再 Math.min(0, …) 恒得 0,dock 贴视口底部时浮层(音乐卡片)底部超出视口 → seek 条在屏幕外不可见不可拖。修复:直接用负上移量 Math.min(0, vh - 8 - dr.top - r.height)

版本记录

完整发布历史(含 1.1.0 命名空间清理与路径可移植化 + localForage 媒体缓存、1.0.2 mht 快照还原预览、1.0.1 审计优化与 1.0.0 首发说明)见 CHANGELOG.md。

注意事项

  • 主题互斥:同一时间只启用一个主题(见安装与要求)
  • 素材不内嵌:lib/client.js 仅含配置与逻辑(约 188KB),壁纸/音乐/开屏全部由服务端静态路由与运行时 API 提供
  • 可读性设计:中间画面全透明 + 全局文字描边 + 深色半透明衬底——任意壁纸下文字可读(无毛玻璃)
  • 隐私:本主题纯客户端,无任何遥测/外部请求(在线资源仅按 sources.json 显式登记下载)

函数列表

全部 JS 函数索引见 docs/函数列表.md(git-push doc-func 自动维护)

版本记录

版本发布历史表见 docs/版本记录.md(git-push doc-version 自动维护;完整发布记录见 CHANGELOG.md)