
Security News
PHP Foundation Says Chainguard Broke Security Embargo Ahead of Emergency Patches
The PHP Foundation says Chainguard broke a security embargo ahead of critical RCE patches, as leaks disrupted the coordinated release.
dsh-theme-mediascape
Advanced tools
媒体景观主题 —— 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、无蒙雾遮罩层;主题色只作用于侧边栏/对话框/按钮等组件)$DSH_HOME/theme-mediascape/logs/wallpaper.log(GET /theme-mediascape-assets/wallpaper/log?lines=50 可回读最近 N 行,自检/排查切换是否发生用)timeupdate 阈值)即预创建并预缓冲下一视频(隐藏元素、preload=auto、未挂 DOM);ended 切换时若预载目标命中且已就绪(readyState≥2)直接复用该元素(已缓冲数据,播放即出画面)——切换无加载间隙黑屏;未命中/未就绪照常新建不阻塞;关闭视频层/切换不匹配时自动清理预载(不占用连接)。$DSH_HOME/theme-mediascape/wallpaper/,刷新/重启/换浏览器都在)——上传视频(mp4/webm,浏览器原生可播)自动开视频层并加载它;上传图片更新图片层(视频开关状态不变)
os.freemem()),超限自动转写 .upload-<traceId>.tmp 保底(超大文件不 OOM);上传过程中素材目录不出现 .part 中间态,end 后正式文件落定(finalizeUpload 按「文件名+大小」去重);上传完成落定后自动清理同名孤儿 .part 快照(上次中断/暂停未续传的残留,不留盘).part + 继续从 .part 续传:点「⏸」abort 当前请求 → 服务端把已收内存缓冲落盘为 <正式名>.part 暂停快照(内存即刻释放);点「▶」带 ?offset=<已传字节> 续传 —— 服务端校验 .part 存在且大小一致(不匹配返回 409,前端从头重传,杜绝残缺落盘),对 .part 追加续写,收完 rename 落定,不重传已传部分;「✕」取消 → DELETE /upload/part?name=<文件> 清理快照isItemUsable 只认 file/name + size 数字)dock.__mediascapeDshCenter 定位靠 dock,点外自动收起)——每任务一行(文件名 + 进度条 + 叠加「已传/总 · 速度」),多任务动态多行且各列对齐,无任务自动隐藏;速度读数 EMA 平滑(progress 事件间隔不均不跳变)input.click() 打开系统选择器的 click 冒泡不再触发「点外关闭」(捕获阶段 stopPropagation)--dsw-* 设计令牌,随主题即时生效;正式基底 = 左侧「应用」写入真源 json 的当前值(以 lib/client-parts/foundation/tokens.js 与取色器为准,文档不写死)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→guideEntryvar(--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 同步)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)居中淡入,带绿色辉光边框#03070f(透不出下方界面);等待期呈现氛围粒子(中央呼吸光圈 + 24 粒星光缓缓上升,
均为纯装饰、不阻挡交互);并行取回 boot.json 配置与壁纸清单后建媒体元素,等首帧就绪(video readyState≥2 / img loaded)
后浮层加 .media-ready(星尘/光圈 0.5s 淡出——「星光聚成画面」)且媒体经 transition 0.6s 渐变淡入(opacity 0→1、scale 0.95→1);
无素材 / 配置失败 / 媒体超时 → 浮层保持深色背景展示 durationMs 后自动淡出(界面呈现,无黑框先行)。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/.png/.webp 用 <img>,.mp4 用 <video muted loop autoplay>(无声音);files 数组按每段 durationMs 顺序轮换(video 复用元素 load()+play() 保持流式),播完整个集合自动淡出,loop:true 则循环整个集合直到点画面跳过(2026-09-23 删「点击跳过」按钮,点画面即跳);默认 file:"auto" 联动当前启用的视频壁纸做开屏(同一份流式数据移交,不重复加载)lib/index.js + theme-studio/start-preview.mjs 的 serveStream,仅 media 且带 Range 才 206 分片、非法 Range 416、其余 200 全量),不再整段下载Cache-Control: public, max-age=86400 + ETag: "<mtime>-<size>"(文件校验指纹);重播同一 URL 时浏览器发 If-Range: <etag> 条件请求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 接管同一元素。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" 联动视频壁纸」条目。--dsh-content-font-size 变量,正文随档位缩放,localStorage 记忆、重启保持「乐」按钮:亮 = 播放中,暗 = 暂停;点击按状态机联动播放与二级面板:
| 当前状态 | 点「乐」按钮 | 点面板外 |
|---|---|---|
| 暗(暂停)+ 面板关 | 变亮播放 + 打开面板 | — |
| 亮(播放)+ 面板关 | 保持亮播放不变 + 打开面板 | — |
| 亮(播放)+ 面板开 | 变暗暂停,面板保持开不关 | 关面板,继续播放 |
| 暗(暂停)+ 面板开 | 恢复播放(亮),面板保持开 | 关面板,保持暂停 |
旋转唱片 + 封面(同名封面图或 music/music.json 指定;无封面自动读 MP3/FLAC 内嵌封面)+ 进度条拖动跳转
上一首 / 播放暂停 / 下一首 / 循环模式(单曲循环 → 列表循环 → 随机播放)
选择:弹出歌单,勾选后移除(内置隐藏、导入删除)或随机(以勾选歌曲为随机池)
+ 添加歌曲:dock「传」统一入口(按文件后缀识别音乐,一次多选)——服务器磁盘持久化($DSH_HOME/theme-mediascape/music/ + music.json 清单,刷新/重启/换浏览器都在),无封面自动读内嵌封面;封面可为当前歌曲指定,落盘同名封面图并写入 music.json(封面保留原设计:自动按音乐名重命名)
当前曲目与循环模式持久化
四个按钮收在可拖动的半透明竖条面板(右下角,圆角;无毛玻璃,纯深色底):
| 按钮 | 功能 |
|---|---|
| 景 | 壁纸设置(模式单按钮三合一:循环/顺序/随机点击切换 + 视频开关 + 随机间隔 + 氛围粒子四档 + 确定;选择器仅「移除」按钮) |
| 乐 | 背景音乐 开/关 + 播放面板 |
| 声 | 打字音效 开/关 + 壁纸音量 |
| 字 | 字号四档(小/标准/大/特大) |
| 传(下) | 统一上传入口:点击直接打开文件选择器(一次多选),按文件后缀天然识别壁纸/音乐(png/jpg/jpeg/webp/mp4 → 壁纸,mp3/ogg/m4a/wav/flac → 音乐);选完先按「文件名+大小」与现有列表去重预检,重复的在二级面板提示「已跳过」;上传开始前/结束后各刷新一次列表;上传进行时按钮高亮,进度显示在 dock 二级面板(自动弹出,无需手动开;⏸ 暂停落盘 .part / ▶ 继续从 .part 续传 / ✕ 取消清 .part) |
按钮顺序(上→下):景 乐 声 字 传(2026-09-23 定稿,apply.js 启动段统一重排)。
🖱️ 按住面板任意位置拖动即可移动,位置自动记忆(重启
dsh web后仍生效)。 面板/选择器弹层相对 dock 垂直对齐并自动保持在屏幕内。 按 ESC:关闭壁纸/歌单等浮层;音乐面板展开时收起。
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 列表)mediascape-dsh-dock-btn(dock 内统一圆形按钮样式),面板样式对齐 mediascape-dsh-bg-panel(position:absolute; z-index; display:none; flex-direction:column; .open{display:flex})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 统一定义壁纸添加两种方式:
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、不再被重定向, 所以「只有第一次慢」。
process.stdout.write,
在打印的瞬间抓取 token(只读取输出分片、原样透传,不改变既有日志行为);若插件加载晚于该打印,
回读 dsh-proxy.log 兜底(日志路径按 DSH_PROXY_LOG / DSH_HOME 推导,不硬编码)。GET /theme-mediascape-assets/media-token 返回当前 token(取不到返回空串)。withMediaToken,Range 请求即被代理直接放行,恢复 206 分段流式,首帧不再等待整文件下载。src(仅在确实需要时重设,避免无谓重新加载)。没有鉴权代理的环境(本地直连、预览服务)行为不变:取不到 token 时媒体 URL 原样使用,不影响加载。
运行时素材/上传 API 由插件挂载在 DSH 主实例
/theme-mediascape-assets(lib/index.js);预览服务(theme-studio/start-preview.mjs,默认端口见 start.sh)对同一前缀本地实现同源 API(不依赖主实例),配色盘 API 为预览服务专属。
| 方法与路径 | 说明 |
|---|---|
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 日志开关控制) |
| 方法与路径 | 说明 |
|---|---|
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 |
POST /api/theme-rename | 重命名预设:{ id, name } |
GET /api/capsules | 胶囊配方读取(运行态 → 仓库 → 内置三级兜底) |
POST /api/capsules/save | 保存胶囊:{ rules:[{key,selector,...}] }(每条必须含 key/selector 字符串) |
POST /api/restart | 重启预览服务(start.sh restart) |
| 路径 | 说明 |
|---|---|
GET / /index.html | 预览页(真实 client.js 垫片宿主渲染) |
GET /theme-swatch.html | 配色盘页面(配色区 + 胶囊区) |
GET /preset/* /lib/* /js/* /samples/* | 预设包 / 库脚本 / 页面脚本 / 元素分片(samples/.html)静态资源 |
完整文件树见 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.jsonpeerDependencies声明@deepseek-ai/dsh。
⚠️ 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 构建脚本,两条都需要):
本机要有 npm —— pnpm 构建 git 来源的包时会在临时目录执行 npm install;
缺 npm 时报 ERR_PNPM_PREPARE_PACKAGE + spawn ENOENT: npm: command not found。
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/<主题包目录>(换行内联写法见下文「配色盘/主题分开装」一节)。
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 项断言)
music.json 自动同步移除music.json 自动清洗孤儿 cover配置分两层(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/是开发预览用,跑的是真实前后端,不是简化模拟:加载真实的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:左元素控件手输 + 点击单项应用预设色、右侧预设包全量/混搭应用、左侧应用真正写入、导出新预设)dsh-theme-mediascape.pid背景:配色盘(
theme-studio/+ 取色器)随主题一起提供,曾调研「配色盘也可单独安装」的分发方式。本节记录设计前置考量——即设计时为何不把配色盘拆成独立分发,供将来若需独立分发时对照。
设计取向:配色盘与主题内置一体;若将来需要独立分发给其他作者,按本小节方案落地。
start-preview.mjs 的 buildTokensContent/buildIdentityBlock 即为此写)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/配色盘目录;装主题时经依赖自动带配色盘file:../配色盘目录(需实测 clone 后 pnpm 能否解析兄弟目录)→ B. workspace:* → C. GitHub Release tarball URL(https://github.com/EIGHTfs/<repo>/releases/download/<版本>/<配色盘包名>-<版本>.tgz,不依赖仓库内部路径解析,最稳)ctx.<服务名>,其他插件 inject: ['<服务名>'] 接入,典型方法 registerColorTarget(selector, options) / getPalette() / applyPalette(palette) / onPaletteChange(callback)按「架构设计 / 功能 / 问题」三类归纳全部历史提交消息(含早期 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,有差异才落盘)ALLOWED_UPLOAD_EXT)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 后刷新预览页即可验证,无需重启主 DSHtheme-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 令牌.tmp 保底)、暂停落盘 .part + 继续从 .part 续传(?offset= 追加)、上传前「文件名+大小」去重预检(重复走二级面板「已跳过」行)、进度二级面板(多行对齐/大小/速度/暂停/继续/取消)、上传/移除开始前与结束后各刷新一次列表(内置公共函数)、上传完释放连接(串行)、dock「传」统一入口(按文件后缀识别壁纸/音乐,壁纸/音乐上传移除共用同一公共函数)data-role="undefined",点击委托拿不到有效 role → 渲染循环改用显式 role 列表DSH_THEME_NO_RESTART=1 跳过重启hash_thinkBody(类名后无下划线),选择器 [class*='_thinkBody_'] 匹配不到 → 改 [class*='_thinkBody']LS_MUTED,拖过音量条即取消静音(LS_MUTED="0")→ 壁纸视频 video.muted=false(有声)→ 浏览器自动播放策略拒绝无交互的 play()(NotAllowedError)→ 视频卡在开头。与配置/服务端无关(videoAutoPlay 一直 true、Range/206 正常),重启也无用。修复:play() 被拒时静音重试(video.muted=true 再 play())——自动播放保底成立(无声),要声音点声音面板(用户交互后浏览器允许有声播放,applyVideoSound 把正在播的 video 改为有声)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)
FAQs
媒体景观主题 —— 动态媒体背景:立绘壁纸 + 霓虹配色 + 开屏动画 + 萤火粒子 + 背景音乐
We found that dsh-theme-mediascape demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
The PHP Foundation says Chainguard broke a security embargo ahead of critical RCE patches, as leaks disrupted the coordinated release.

Security News
PHP releases emergency patches for two critical RCE vulnerabilities as agencies urge organizations to take unpatched sites offline.

Threat Research
/Security News
A supply chain attack compromised LiteLLM 1.107.0 on PyPI and Docker, spreading malware that steals cloud credentials, developer secrets, and AI provider keys.