本文件是任何人都能照做的验收流程:从「插件是否真的加载了」到「媒体通道安不安全」再到「怎么回滚」。
下面命令里出现两个占位符,请先按自己的环境替换:
占位符 含义 默认/示例 $REPO本仓库在你机器上的绝对路径 ~/dsh-motion-background(clone 出来的目录名)$DSH_PROFILEDSH profile 目录 ~/.dsh/profiles/web$PORTDSH WebUI 端口 3080(DSH 默认)装法与前置条件见
README.md。
装上之后,profile 里有三处会变:
| 位置 | 内容 |
|---|---|
dependencies["dsh-motion-background"] |
link:$REPO |
dsh.profile.bundles |
多一项 dsh-motion-background |
node_modules/dsh-motion-background |
指向 $REPO 的 junction / symlink |
reconcilePlugins()(见 @deepseek-ai/dsh/lib/plugin-*.js)会在每次 dsh plugin
命令之后,把所有声明了 dsh.bundle 的 dependency 重新追加回 dsh.profile.bundles。
⇒ 所以你不能靠手删 bundles 里的一行来停用某个插件 —— 它会被静默加回来。
要停用就把该插件的 patch 行设 disabled: true(见下)。
若你机器上还有本插件的上游/旧版(同为「更好看一点」那一系),两者不要同时启用:
它们占用同一个 DOM 作用域、覆盖同一条 --dsw-alias-bg-base、还注册同一个设置栏 id,
同时开启时画面与面板浓度会互相干扰,结果不可解释。
停法是在 profile 的 cordis.patch.yml 里加:
- id: <该插件自己 patch 里的 insert.id> # 注意:是行 id,不是包名
disabled: true一条 disabled 就够,因为它同时挡住两半:
- 宿主半:
assertEntriesLoaded明确把entry.disabled排除在「加载失败」之外, 所以停用不会让 boot 报错(packages/boot/app-boot/src/index.ts)。 - 客户端半:客户端模块扫描器(
packages/client/modules/src/index.ts)的判据是entry.options.name === entryName && entry.fiber !== undefined && !entry.disabled—— 停用的条目根本不进客户端模块表,它的client.js不会被浏览器加载。
离线自查(不用重启):
dsh --profile web --dump-config期望:目标条目下面多出 disabled: true,且行首注释标出它被哪个 patch 文件改过。
装配改动落在两层上,它们的热更新行为不一样:
| 层 | 是否热生效 | 依据 |
|---|---|---|
profile 的 cordis.patch.yml |
✅ 热生效 | composeLive() 每次从磁盘重读 loadOptionalPatches(profile.patchPath);watchUserPatches() 用 hmr.registerConfig(filename, …) 注册监听,回调里 entry.update({ config }) |
package.json 的 dsh.profile.bundles |
❌ 需重启 | composeLive() 里是 ...composed.bundlePatches —— 启动时的快照,从不重读 |
(web 模板的 patchReload 是 "live",且 cordis.patch.yml 与 package.json
都没显式覆盖 ⇒ 实际走 live。只有 acp / headless / sdk / sdk-minimal 是 "startup"。)
⇒ 结论:改 disabled 不用重启;新增/卸载插件(动 bundles 层)必须重启。
这条是怎么定下来的:曾观察到「一个长跑的宿主里,新旧插件都不在模块表」的怪现象, 一度被当成异常。后来用源码比对才确认它不是异常,而是两层生命周期叠加的必然结果 —— patch 层的那次改动当场就生效了(宿主没重启过),而 bundles 层的改动在等下次启动。 详见
docs/CONFIG-LIFECYCLE.md。
① 页面还活着
curl -s -o /dev/null -w '%{http_code}\n' "http://127.0.0.1:$PORT/"期望:200;若 DSH 开了认证,回 401 也说明服务在跑(只是没带令牌)。
② 本插件进了客户端模块表(最直接的「装上了」证据)
curl -s --max-time 8 -N "http://127.0.0.1:$PORT/plugins/events" \
| grep -o '{"id":"dsh-motion-background"[^}]*}'期望:能看到 {"id":"dsh-motion-background","url":"…/client.js&rev=…","inject":[…]}
③ 被停用的那个已不在模块表里
curl -s --max-time 8 -N "http://127.0.0.1:$PORT/plugins/events" \
| grep -c '{"id":"<被停用插件的包名>",'期望:0
若不是 0,说明
disabled: true没生效 —— 别继续往下测,先查$DSH_PROFILE/cordis.patch.yml里那条是否存在、id是否确实等于 该插件 patch 里的insert.id。两个皮肤同时加载时,设置页会同时出现两块同义面板。
④ mod 端点通了(宿主半的活儿,也是「可插拔」的命脉)
curl -s "http://127.0.0.1:$PORT/motion-background/mods" | head -c 400期望:{"mods":[{"id":"meteor",…},{"id":"aurora-video",…,"media":{…}}],"errors":[],"dir":"…"} ——
errors 必须是空数组。
⑤ 媒体端点通了(含 Range 支持)
curl -s -D- -o /dev/null "http://127.0.0.1:$PORT/motion-background/media/aurora-video/bg.mp4"期望:200 + content-type: video/mp4 + accept-ranges: bytes
curl -s -D- -o /dev/null -H "Range: bytes=0-99" \
"http://127.0.0.1:$PORT/motion-background/media/aurora-video/bg.mp4"期望:206 + content-range: bytes 0-99/<总长> + content-length: 100
(浏览器播放 mp4 就靠这个;若这里回 200 而不是 206,页面里视频会不播或不能循环。)
⑥ 媒体端点的路径校验拦得住(安全边界 —— 任何一条回 200 都是任意文件读取)
for u in "../package.json" "aurora-video/../meteor/mod.json" "aurora-video/bg.mp4.txt" "aurora-video/bg.mp4/extra"; do
printf '%-42s → ' "$u"
curl -s -o /dev/null -w '%{http_code}\n' "http://127.0.0.1:$PORT/motion-background/media/$u"
done期望:全部 404。
① 底纹 = 流星雨(order:-10,默认效果)
- 斜向划过的流星 + 收束拖尾 + 亮头,深色底上有细星点
- 颜色只从主色派生(与「统一色系」一致)
② 效果下拉里应有两个:流星雨、极光(MP4 视频)
-
⚠️ 「极光」后面的(MP4 视频)是面板自己加的(按素材扩展名派生),不是mod.json里写的 —— 它用来区分「程序化效果」与「播放一段素材」,两者共用同一套控制栏 -
选「极光(MP4 视频)」⇒ 背景换成一段蓝紫调极光视频
-
默认是「往返」(
mod.json声明了playMode: "pingpong"): 正放一遍(约 6 秒)到结尾后倒着放回来,两端同一帧折返、无跳变 -
倒放段要顺(这是往返的全部意义,也是最容易被将就过去的一条)—— 倒播时画面应连续、不卡顿、无明显跳帧,流畅度与正放段相当。 想量化确认,在控制台贴这段(数 seek 的落位率):
(() => { const v = document.querySelector('video[data-mb-surface]'); if (!v) return '没有视频'; const d = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'currentTime'); let a = 0, l = 0, on = false; Object.defineProperty(v, 'currentTime', { configurable: true, get() { return d.get.call(this); }, set(x) { if (on) a++; d.set.call(this, x); } }); v.addEventListener('seeked', () => { if (on) l++; }); console.log('等它进入倒放后再回车打印结果'); globalThis.__stop = () => { on = false; return { 发出: a, 落位: l, 落位率: a ? +(l / a).toFixed(3) : null }; }; on = true; return '已开始计数(等 ~2 秒后执行 __stop())'; })()
期望
落位率≈ 1.0。 若在 0.05 量级,说明每帧都在发新 seek、把在途解码掐掉了 —— 那是「幻灯片化」的典型特征, 应当回报(这种情况verify.mjs的 E19b 会直接变红)。 -
把「播放」下拉切到「循环」⇒ 变成播完立刻跳回开头(接缝处会有一跳,这正是往返要解决的)
-
同时确认画布数变 0(视频背景不占 WebGL):
document.querySelectorAll('canvas[data-mb-surface]').length -
选媒体型时,强度/柔度/颗粒/形态四个旋钮置灰,「播放」与「面板浓度」仍可调
-
切回「流星雨」⇒ 视频元素应被彻底摘掉(不只是隐藏):
document.querySelectorAll('video[data-mb-surface]').length应为0(摘掉 ≠ 隐藏:el.remove()让元素离开文档,pause()+ 清src+load()才回收它持有的缓冲与在途请求)
③ 设置页多了一栏「动态背景」
- 左侧导航里应当有「动态背景」
- 若你还装着旧版皮肤,它的那一栏应当消失(已被
disabled停用)
④ 面板里的控件
- 启用(勾选框)
- 效果(下拉,两个选项,媒体型带
(MP4 视频)标注) - 播放(下拉:
循环(播完跳回开头)/往返(正放到底再倒放回来)) ——⚠️ 只在选中视频型时出现;切回「流星雨」它应当消失(着色器型没有播放方向) - 强度 / 柔度 / 颗粒 / 形态(四个都渲染;mod 没映射的会置灰)
- 面板浓度
- 没有「发光巡游描边」那一组 —— 本插件还没有描边功能(那是旧版皮肤的东西)
⑤ 关掉底纹时输入框描边仍清晰
- 把「面板浓度」拉到 0 或关闭底纹 ⇒ 输入框的描边/效果不应跟着消失
__betterSkin.mods
// { loaded:['meteor','aurora-video'], errors:{}, dir:'…/mods', current:'meteor', uniforms:[…] }
__betterSkin.health
// { watchdogRunning:true, backdrop:{ idleMs, attached } }
__betterSkin.settings // 'ok' 才算设置卡片注册成功
__betterSkin.surfaceKind // 'shader' | 'media' —— 当前渲染面是哪种
await __betterSkin.probeMods() // { meteor: true, 'aurora-video': true } 才算两个 mod 都真能出画媒体型专属排查:若视频不出画,看 __betterSkin.mods.errors 里有没有 __mediaplay:<id>:
- 有 ⇒ 素材加载成功但播不动(多半是自动播放策略)—— 检查
muted是否为true - 没有、但画面空白 ⇒ 检查宿主半的媒体端点(上面第 ⑤ 条)与素材扩展名白名单
着色器型:
mkdir -p "$REPO/mods/my-test"
cp "$REPO/mods/meteor/fragment.glsl" "$REPO/mods/my-test/"
# 再写一个 mod.json,id 填 my-test(可照抄 meteor 的)刷新页面 ⇒ 效果下拉里应多出 my-test。
删掉文件夹 ⇒ 刷新后应消失。 两个方向都要试。
媒体型(顺手验一下媒体通道):
mkdir -p "$REPO/mods/my-video"
cp "$REPO/mods/aurora-video/bg.mp4" "$REPO/mods/my-video/"
# mod.json 里给 "media": "bg.mp4"(注意:不要写 colors,也不需要 fragment.glsl)刷新 ⇒ 下拉里应多出 my-video,且选中后能循环播放。
| 功能 | 旧版皮肤 | 本插件 |
|---|---|---|
| 底纹效果 | 6 个(含第三方着色器源码) | 2 个(自研流星雨 + 媒体型极光视频) |
| 媒体型 mod(MP4/GIF/图片) | ❌ 无 | ✅ 有(media 字段 + Range 支持) |
| 发光巡游描边(输入框) | ✅ 有 | ❌ 还没有 |
| 面板名 | 「更好看一点」 | 「动态背景」 |
| mod 机制 | ❌ 无 | ✅ 可拖文件夹扩展 |
| 可公开分发 | ❌ 含第三方源码 | ✅ 内核原创 |
这是刻意的取舍:本仓库为「能公开上线」而干净 —— 内核里零第三方着色器代码
(判据与边界见 README.md 顶部与 docs/KERNEL-REVIEW.md)。
旧版那 6 个效果可以逐个改造成 mod 搬过来(那是下一步);描边则是新功能,要重新做。
cd "$DSH_PROFILE"
# ① 恢复 patch 层(把停用的那个重新启用)
cp cordis.patch.yml.bak-<你备份时的时间戳> cordis.patch.yml
# ② 恢复 profile 清单(可选 —— 注意 bundles 会自动对齐,见 §0)
cp package.json.bak-<你备份时的时间戳> package.json
# 再重启宿主即可回到原状态动手前先备份这两个文件,文件名带上时间戳 —— 回滚全靠它。 (
node_modules下各插件目录若都还在,无需重建。)
只想临时禁用本插件、不碰别的:在本插件自己的 patch 行上加 disabled: true
(行 id 见仓库根目录的 cordis.patch.yml),而不是去动上游那一版。
- GPU 帧率:所有验证都跑在软件光栅(SwiftShader)上,没有真实 GPU 数据
- 真 React 渲染:自检用的是不含渲染的桩 ⇒ 面板的真实交互只有靠人眼看
- 真实
slots.inject语义:按官方源码契约与同类型插件推定 ctx.effect的 fiber 回收:卸载路径未在真机验证