🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

动态背景 ZhaoAndy821/dsh-motion-background

DeepSeek Harness
ZhaoAndy82139 天前有提交已提供中文文档扫描:含敏感能力

给对话界面换上实时演算或视频动态底色

29.7/100
综合分
0
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/9/29 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add ZhaoAndy821/dsh-motion-background
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:待复验
是什么[目录]—
dsh 原生插件 · ui
暂无时间戳·判据 →
装得上吗[实装验证]—
曾在 dsh 0.2.0-rc.2 下真实安装成功,但当前 dsh 已更新至 0.2.1-rc.5,本站尚未在新版下复验
L4 · 真实安装等级说明 →
数据截至 2026/10/05·判据 →
安全吗[自动扫描]—
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
数据截至 2026/11/04·判据 →
还在维护吗[仓库]—
更新放缓:最近一次提交在 39 天前
暂无时间戳·判据 →
图例:✓ 通过! 需注意— 未判定 / 无数据· 「未判定」= 本站尚无数据,不代表插件有问题
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →

可信度证据

安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令读写/删除本地文件发起外部网络请求
查看扫描证据(共 96 处,展示前 30 条)
  • 读取本地文件
    lib/index.js:105
    if (!existsSync(MODS_DIR)) return { mods, errors, dir: MODS_DIR };
  • 读取本地文件
    lib/index.js:108
    try { names = readdirSync(MODS_DIR); } catch (e) {
  • 读取本地文件
    lib/index.js:118
    if (statSync(dir).isDirectory() !== true) continue;
  • 读取本地文件
    lib/index.js:122
    if (!existsSync(cfgPath)) { errors.push({ id: name, reason: '缺 mod.json' }); continue; }
  • 读取本地文件
    lib/index.js:124
    const cfg = JSON.parse(readFileSync(cfgPath, 'utf8'));
  • 读取本地文件
    lib/index.js:152
    if (!existsSync(join(dir, src))) {
  • 发起网络请求
    lib/client.js:943
    const res = await fetch(MODS_ENDPOINT, { headers: { accept: 'application/json' }, cache: 'no-store' });
  • 读取本地文件
    mutations.mjs:21
    const TMP_BASE = existsSync('D:/tmp') ? 'D:/tmp' : null;
  • 读取本地文件
    mutations.mjs:32
    const st = statSync(join(dir, OWNER_MARKER));
  • 读取本地文件
    mutations.mjs:34
    const o = JSON.parse(readFileSync(join(dir, OWNER_MARKER), 'utf8'));
  • 读取本地文件
    mutations.mjs:58
    for (const d of readdirSync(TMP_BASE)) {
  • 读取本地文件
    mutations.mjs:60
    let st; try { st = statSync(full); } catch { continue; }
  • 执行外部命令固定命令
    mutations.mjs:65
    execFileSync('powershell.exe',
  • 执行外部命令来自配置/变量
    mutations.mjs:118
    const p = spawn(process.execPath, args, { cwd: HERE });
  • 写入本地文件
    mutations.mjs:184
    writeFileSync(join(HERE, 'FALSIFICATION.md'), doc, 'utf8');
  • 读取本地文件
    verify.mjs:74
    const TMP_BASE = existsSync('D:/tmp') ? 'D:/tmp' : tmpdir();
  • 读取本地文件
    verify.mjs:88
    const REAL_MP4 = readFileSync(join(HERE, 'mods', 'aurora-video', 'bg.mp4'));
  • 读取本地文件
    verify.mjs:142
    if (!existsSync(p)) return true;
  • 执行外部命令固定命令
    verify.mjs:145
    execFileSync('powershell.exe',
  • 读取本地文件
    verify.mjs:148
    if (!existsSync(p)) return true;
  • 删除本地文件递归
    verify.mjs:151
    try { rmSync(p, { recursive: true, force: true }); } catch { }
  • 读取本地文件
    verify.mjs:152
    return !existsSync(p);
  • 写入本地文件
    verify.mjs:162
    function markOwned(dir, marker = OWNER_MARKER) { try { writeFileSync(join(dir, marker), OWNER, 'utf8'); } catch { } }
  • 读取本地文件
    verify.mjs:187
    const st = statSync(join(dir, marker));
  • 写入本地文件
    verify.mjs:400
    mkdirSync(probeDir, { recursive: true });
  • 写入本地文件
    verify.mjs:401
    mkdirSync(neighborDir, { recursive: true });
  • 写入本地文件
    verify.mjs:403
    writeFileSync(probeMp4, REAL_MP4);
  • 写入本地文件
    verify.mjs:405
    writeFileSync(join(probeDir, 'secret.json'), JSON.stringify({ secret: 'DO-NOT-SERVE' }));
  • 写入本地文件
    verify.mjs:408
    writeFileSync(join(probeDir, 'do..t.mp4'), REAL_MP4);
  • 发起网络请求
    verify.mjs:1382
    const url = route.request().url();
扫描于 2026/11/4(1 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

社区实装还没有人打卡 · 装过的第一个就是你
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

功能亮点

  • 着色器实时演算背景
  • 媒体型动态背景
  • 效果以文件夹管理
展开完整介绍(128 字)↓

DSH WebUI 的动态背景插件:着色器型(GLSL 实时演算)与媒体型(MP4/WebM/GIF/图片)两类效果,每个效果是 mods/ 下的一个文件夹。Dynamic backdrops for the DeepSeek Harness WebUI.

README

由 DeepSeek 最新模型翻译生成
dsh-motion-background 给 DSH WebUI 用的动态背景插件:把对话界面的底色换成实时演算的画面,或一段素材。 流星雨效果 当你的 DSH 启动失败时背景将播放流星雨 · 高清原片(MP4,无音轨) 两类背景,在设置页里切换: | 类型 | 怎么出画 | 需要什么 | | 着色器型 | WebGL2 + GLSL,逐帧实时演算 | mod.json + fragment.glsl(纯程序化,不用素材) | | 媒体型 | 浏览器原生 / 铺底 | mod.json 的 media 字段 + 素材文件(mp4 / webm / gif / webp / png / jpg) | 两类可以混放,效果下拉框、回落与错误上报一视同仁。媒体型完全不碰 WebGL,所以在没有 WebGL2 的环境里也能用。 效果不是写死在内核里的:每个效果是 mods/ 下的一个文件夹。 内核自身不含任何第三方着色器代码,全部 GLSL 由各效果自己的文件提供。 怎么加效果见下面的「加一个效果(扩展点)」。
展开完整文档(README 剩余 1 节)
dsh-motion-background

给 DSH WebUI 用的动态背景插件:把对话界面的底色换成实时演算的画面,或一段素材。

流星雨效果

当你的 DSH 启动失败时背景将播放流星雨 · 高清原片(MP4,无音轨)

两类背景,在设置页里切换:

| 类型 | 怎么出画 | 需要什么 |
| 着色器型 | WebGL2 + GLSL,逐帧实时演算 | mod.json + fragment.glsl(纯程序化,不用素材) |
| 媒体型 | 浏览器原生 / 铺底 | mod.json 的 media 字段 + 素材文件(mp4 / webm / gif / webp / png / jpg) |

两类可以混放,效果下拉框、回落与错误上报一视同仁。媒体型完全不碰 WebGL,所以在没有 WebGL2 的环境里也能用。

效果不是写死在内核里的:每个效果是 mods/ 下的一个文件夹。
内核自身不含任何第三方着色器代码,全部 GLSL 由各效果自己的文件提供。
怎么加效果见下面的「加一个效果(扩展点)」。

这句"不含第三方着色器"的证据边界要说清:它建立在 "对若干公开着色器库抽出的 86 个函数名 + 179 个 uniform 名 + 2186 条特征行做全词匹配,内核 0 命中;同款探测器在已知含第三方着色器代码的旧版皮肤上命中 14 个(阳性对照)"之上 ——换来源、改名、或改写到无法按名字识别的代码,不在这个结论范围内。

检测方法本身写在 docs/KERNEL-REVIEW.md §A,本文档与那份都不列出具体来源的名字。

开发环境:在 0.1.5-rc.2 的 DSH 上开发与验证;没有声明更宽的版本兼容范围。

三份文档,按你的角色读

| 你是谁 | 读哪份 |
| 要实现/重写内核 | docs/KERNEL-TASK.md —— 自包含的任务书(契约 + 需求 + 验收 + 已踩的坑) |
| 要独立审查本实现 | docs/KERNEL-REVIEW.md —— 复现步骤 + 必查清单 + 证伪方法 |
| 要写一个效果 | docs/MOD-FORMAT.md —— mod 契约 |

结构

lib/index.js 宿主半:扫 mods/ 目录,提供 GET /motion-background/mods 与 /motion-background/media// lib/client.js 客户端半:内核(渲染 / mod 加载 / 设置面板 / 配置) mods// 一个效果 = 一个文件夹 ├── mod.json 元数据 + 默认参数 + 面板映射(可选 range 量程 / order 排序 / media 媒体声明) ├── fragment.glsl 片元着色器(GLSL ES 3.00)——着色器型必需,媒体型不需要 └── vertex.glsl 可选 docs/ 上面三份 + 装配后的验收清单(POST-RESTART-CHECKLIST.md)与配置生命周期笔记 LICENSE MIT 正文(mods/ 的许可另见各自 mod.json,见文末「许可」) verify.mjs 自检(五组,可在无 DSH 的环境复跑) mutations.mjs 反证驱动器(注入坏实现,要求断言变红) FALSIFICATION.md 反证执行记录(由 mutations.mjs 生成,每次重跑覆盖)

两类 mod:

| 类型 | 出画方式 | 必需 | 例子 |
| 着色器型 | WebGL2 + GLSL,程序化实时演算 | mod.json + fragment.glsl | mods/meteor |
| 媒体型 | 浏览器原生 / 铺背景 | mod.json 里的 media 字段 + 素材文件 | mods/aurora-video |

媒体型的意义之一是在没有 WebGL2 的环境里照样能跑(完全不碰 WebGL)。
两类可混装,扫描/下拉框/回落一视同仁。细节见 docs/MOD-FORMAT.md §2.2。

两类效果在面板里怎么区分

两者共用同一套控制栏(不是两套),所以效果下拉的名字后面会带一个类型标注:程序化效果就叫「流星雨」,媒体型叫「极光(MP4 视频)」。标注由宿主半按素材扩展名派生(media.label),不是写在 mod.json 的名字里 —— 换素材(mp4 → webm)标注自动跟着变,也不会出现"标着 MP4、实际是 WebM"的谎报。

视频的播放模式(循环 / 往返)

选中视频型效果时,面板的「效果」下面会多出一个「播放」下拉框:

| 模式 | 行为 | 什么时候用 |
| 循环 | 播到结尾立刻跳回开头(原生 ) | 素材头尾本来就衔接得上 |
| 往返 | 正着放一遍,再倒着放回来 —— 两端在同一帧折返 | 头尾不衔接时,避免循环接缝处"跳一下" |

mods//mod.json 的 media.playMode 可以声明建议值(aurora-video 建议 pingpong),但用户的选择永远优先,且会存进 localStorage 跨效果生效。

⚠️ 「倒放」没有原生实现,这不是取舍,是被规范挡死的:el.playbackRate = -1 在 Chromium 上直接抛 NotSupportedError(HTML 规范里 playbackRate 只接受非负值)。
所以往返的倒放段是脚本逐帧驱动:暂停元素,每个动画帧把 currentTime 往回挪「距上一帧的墙上时间」那么多。(640×360 / 6s):速度约 0.99×、落点精确(误差 0)、画面真的逐帧更新。代价是倒放段比原生播放略耗 CPU(verify.mjs 的 E19b 用像素指纹钉住“画面真的在变”,而不是只看 currentTime 这个标量)。

⚠️⚠️ 还有一个不能想当然的地方:倒放不许“每帧都发 seek”。
每个新的 currentTime 赋值都会掐掉在途的那次 seek,解码器于是永远从头开始 ——越“努力”越卡。三种驱动写法的对照(各 1.2s 媒体时间):

| 驱动写法 | 发出 seek | 真的落位 | 落位率 | 可见画面 |
| 每帧无条件发(最初版本) | 156 | 6~14 | ~5% | 5~7/12(≈2~3 fps 幻灯片) |
| 在途不发新 seek(现在) | 23 | 23 | 100% | 12/12(满帧) |

⚠️ 上表各 1.2s 媒体时间;采样窗口拉长到固定 2.0s 后,同一个“拆掉闸门”的坏实现落位率回升到 0.64~0.72 —— 因此判据下限取 0.9(正常实现恒为 1.000,零浪费)。下限是校准出来的,不是取个看起来宽松的数:早先取 0.6 时与洪水版只差 0.04~0.12,这条反证于是 flaky(同一条变异一次被抓、一次放行)。

两种写法的速度都是 0.97~1.0×(不是“快了慢了”,是“流畅还是幻灯片”)。
现在的驱动有一个 seek 闸门:上一拍没落位时不发新的,但把墙上时间攒着留到下一拍补回(否则倒放整体变慢);另有 250ms 兜底超时,避免元素卡在 seeking 上导致永久停摆。
verify.mjs 的 E19b 直接量落位率(landed / assigns ≥ 0.9)来钉住这条契约 ——只数“不同像素指纹”是统计量,这种洪水写法也能蒙混过关(40/82)。

为什么往返模式下 loop 必须为 false:ended 事件只在非循环时触发,而它是“正放到头”的唯一可靠信号。loop 若为 true,浏览器会自己跳回开头 ⇒ ended 永不触发 ⇒ 倒放支路根本不会启动。

加一个效果(扩展点)

一个效果 = mods/ 下的一个文件夹。加就是把这个文件夹放进去,删就是删掉它 ——改完刷新页面即可,不需要重启 DSH(宿主半每次请求都重扫目录)。

原则

1. 内核不认识“效果” —— 它只认契约:加载、编译、驱动绘制、渲染面板。
画面本身(GLSL、素材、参数、面板映射)全部待在你的文件夹里。
2. 两类共用一个契约面 —— 着色器型与媒体型用同一份 mod.json,差别只在有没有 media 字段。
扫描、下拉框、回落、错误上报对两者一视同仁。
3. 一个效果坏掉不影响别人 —— 编译失败或契约体检不过的会被摘出下拉框、记进 errors,整页不会挂。
4. 不猜 —— 你没声明的东西内核不替你兜底:colors 非法直接判该效果不可用; panel 映射到不存在的 uniform 就把那个旋钮置灰(而不是给一个拖了没反应的滑块)。

要满足的条件

着色器型

- mod.json + fragment.glsl(+ 可选的 vertex.glsl;缺省用内核自带的极简全屏 quad)
- fragment.glsl 首行必须是 #version 300 es,必须声明并写入 out vec4 fragColor;
- 用 in vec2 v_uv; 取坐标(0..1,左下原点)
- 不得声明采样器(内核不绑定任何纹理;要随机就自己写 hash)
- 保留 uniform(u_time / u_resolution / u_pixelRatio / u_colorBack / u_colorFront / u_colors)的类型必须与契约一致,否则该效果判不可用
- colors 必须是 "front" 或 "array"(非法值直接判该效果不可用,不静默兜底)

媒体型

- mod.json 里含 media 字段(给了它就是媒体型,也不需要 colors)
- 素材扩展名限 mp4 / webm / gif / webp / png / jpg / jpeg —— 不在表里判不可用
- media.src 只能是本效果目录下的文件名:不许含路径分隔符、不许含 ..,否则整个效果不加载

两类共同

- 文件夹名与 id 只允许 [a-z0-9-];id 按 mod.json 里的值去重(重复时按文件夹名升序取第一个)
- 含第三方代码或素材的,必须在 mod.json 的 author / license 里声明来源与许可

扩展完之后能调什么

下面这些都由你在 mod.json 里声明:

| 你想控制 | 写在哪 |
| 面板上那四个旋钮分别接哪个 uniform | panel(映射到不存在的 uniform ⇒ 该旋钮置灰) |
| 每个旋钮的量程 / 步长 | range,如 [0, 2] 或 [0, 2, 0.1](写坏了会兜底成合法区间) |
| 旋钮初值 | spec(内核不提供自己的默认值,免得盖掉你的设计) |
| 哪个效果默认选中 | order(升序,id 字典序兜底) |
| 颜色从哪来 | colors:"front" 单色系 / "array" 多色团 —— 都取 DSH 主题令牌,深浅主题自动跟随 |
| 效果在面板里的名字与副标题 | name / description(name 里别写类型词,标注由宿主半按扩展名派生) |
| (媒体型)铺满方式 | media.fit:cover(铺满、可能裁切)/ contain(完整可见、可能留白) |
| (媒体型)整体不透明度 | media.opacity(超出 0..1 会被夹回) |
| (媒体型)叠在界面上的混合方式 | media.blend: true ⇒ CSS mix-blend-mode: screen |
| (视频型)播放模式的建议值 | media.playMode:loop / pingpong(用户在面板里选过之后以用户的选择为准) |

不在上面这张表里的,目前改不了 —— 如实说清:

- 绘制帧率与渲染分辨率是内核级常量,不是面板项,也不是某个效果能声明的。
要改就改 lib/client.js 里的三个常量并重启 DSH,见「资源占用」一节。
- 媒体型不能通过 mod.json 设定视频帧率或清晰度 —— 那是素材文件本身的属性。
内核只按 media.fit 决定 CSS 的铺法(object-fit),不重编码、不改素材。
- 用户在面板上能动的只有那几个控件(四个旋钮 + 面板浓度 + 播放模式 + 启用开关),以及各效果自己声明的量程与初值。

完整字段表与 GLSL 契约见 docs/MOD-FORMAT.md。

资源占用

背景是全屏绘制,开销大致与「帧率 × 像素量」成正比 —— 所以内核给两者都设了上限,并按窗口状态分档。下面是一个效果被选中之后,各状态下实际发生的事。

各状态下的档位

| 你的状态 | 着色器型 | 媒体型 |
| 选中它(dsh 窗口在前台) | 绘制上限 30 fps;画布像素 = CSS 尺寸 × min(devicePixelRatio, 1) | 浏览器原生播放,不占 WebGL |
| 失焦(你在用别的窗口 / 两栏并排) | 上限降到 15 fps | 继续播(见下方「管不到的情况」) |
| 标签页切走 / 窗口最小化 | 完全不画(0 帧) | 暂停 ,切回来自动续播 |
| prefers-reduced-motion 打开 | 完全不画(优先于上面所有档位) | 不动它(交给浏览器自身策略) |
| 关掉「启用」 | 画布被摘掉、WebGL 上下文回收 | 媒体元素被摘掉(真正释放解码内存) |

三个数字对应 lib/client.js 里的三个具名常量:

| # | 常量 | 取值 | 管什么 |
| 1 | FPS_ACTIVE | 30 fps | 窗口正常时的绘制上限 |
| 2 | FPS_BLURRED | 15 fps | 窗口失焦(用别的窗口 / 两栏并排)时的上限 |
| 3 | DPR_MAX | 1 | 渲染分辨率:画布像素 = CSS 尺寸 × min(devicePixelRatio, DPR_MAX) |
| + | 媒体型可见性门控 | document.hidden ⇒ pause() | 标签页切走 / 窗口最小化时停掉 ,可见时自动续播 |

为什么要设这些上限

症状:开着 dsh 的 WebUI,切到别的窗口后移动鼠标、点击都特别卡;把两个窗口并排时也一样。

根因:

1. 帧率不受控:requestAnimationFrame 会跟着显示器刷新率跑 —— 在 240 Hz 屏上是 240 次/秒;且窗口失焦时浏览器不会停 rAF(自动节流只管"标签页不可见"与 "窗口被完全遮挡","失焦但仍可见"这一档不管)⇒ 你用别的窗口时它还在后台全屏重绘。
2. 像素量太大:2560×1600 屏 + Windows 缩放 150% ⇒ devicePixelRatio = 1.5 ⇒ 全屏画布 3840×2400 = 920 万像素/帧。只限帧率不够 —— 30 fps 时 GPU 仍占 86.9%。
3. 盲区:媒体型(视频)不被帧率闸管到:它的 frame() 是空实现(媒体由浏览器驱动),而这里的视频是 muted ⇒ Chromium 对静音视频在后台标签页不会自动暂停 ⇒ 切走后它继续解码。这一档只能靠可见性门控单独管。

分辨率与帧率对照(数 gl.drawArrays 与系统 GPU 计数器)

| 场景 | 修复前 | 修复后 |
| 前台绘制帧率 | 240 fps | 30 fps |
| 失焦绘制帧率 | 240 fps(照跑不误) | 15 fps |
| Chrome GPU 进程占用(前台) | — | 86.9% → 17.4% |
| 本机 GPU 总负载 | 接近饱和(≈99%) | ≈56% |

观感(必读)

- 两栏并排时,你会看到流星雨"慢下来" —— 那不是卡,是 FPS_BLURRED 在生效(15 fps)。
它换来的正是"你在别的窗口里不卡"。
- 降分辨率看不出来:这是慢速模糊的氛围背景,DPR_MAX = 1 在 2560×1600 屏上 "背景没有糊"。想更锐可以调到 1.25 / 1.5,GPU 会同比例上升(1.25 ⇒ 像素 ×1.56 ⇒ 约 27%)。
- 切回来立刻恢复 30 fps —— 闸门是"跳过绘制但保留续帧",不需要等下次交互。

使用限制

- "失焦但仍可见"这一档不适用媒体型:并排看视频时它继续播。取舍理由:并排时视频是看得见的,暂停比继续播更突兀;而硬解全屏 MP4 的开销远小于全屏 shader 重绘。
(标签页切走/最小化仍然会停 —— 那一档是明确该停的。)
- 判据是 document.hasFocus():多显示器、虚拟机、远控(如 GameViewer)等环境下,焦点语义与常规不同 ⇒ 分档可能"看起来没生效"。此时它退化为"始终按 30 fps 跑",不会更差。
- 用户手动把窗口调小 ⇒ 像素量自然下降,上限档位不变。
- prefers-reduced-motion 打开时,着色器型完全不画(这一条优先于上面所有档位)。

要调的话改哪儿

三个常量都在 lib/client.js 的 createSurface 之前,改完必须重启 dsh(见下方"坑"):

const FPS_ACTIVE = 30; // 前台绘制上限 const FPS_BLURRED = 15; // 失焦上限("不和前台抢"的核心;调低更让路、调高更顺滑) const DPR_MAX = 1; // 渲染分辨率上限(GPU 占用与它近似成正比于 DPR²)

⚠️ 不要去掉任何一道上限。去掉帧率上限意味着它跟着显示器刷新率跑;去掉分辨率上限意味着 4 倍像素量 —— 两者都会把用户的桌面一起拖慢。

⚠️⚠️ 改了 lib/client.js 之后,必须重启 dsh,然后再刷新页面(踩过)。
dsh 给客户端 bundle 的响应头是 cache-control: public, max-age=31536000, immutable,
而 URL 里的 rev(内容哈希)只在 dsh 启动时算一次。
⇒ 只改文件、不重启:服务端内容已是新版(curl 能拿到新版),但 rev 没变 ⇒ 浏览器按 rev 命中 immutable 缓存 ⇒ 永远用旧代码,连普通刷新(F5)都绕不过。
判据:curl 模块表看 rev 有没有变;没变 = 没生效。
(症状表现:自己用无缓存的新实例测是好的,用户那边“依旧卡 / 依旧没变”。)

两条方法论

⚠️ 测这个量必须数 drawArrays,不能数页面 rAF 频率:节流的设计是“跳过绘制但保留续帧”(这样重新获得焦点能立刻恢复,不需要额外的 focus 监听),所以 rAF 频率本来就不变—— 拿它当判据会得到一条永远通过的断言。

⚠️ 必须做“前后台对照”才能定位:dsh 页面不在前台时它的 GPU 占用是 0(浏览器把 rAF 停了)—— 只测一个状态推不出结论。并排比“在前台 / 不在前台”两个数,才知道是谁在吃。

⚠️ 自动化断言未覆盖(如实记录):verify.mjs 里没有这些上限的断言。原因是那个夹具的动画时间轴不跑* —— 加了全套诊断后:hook 自检为真、live=true、mod 已挂上、
probeMods() 能真画一帧、hidden=false / hasFocus=true、prefersReduce() 为假,
但 2 秒窗口内 drawArrays 0 次(E 组其它用例也都靠 freeze 单帧或 paint() 显式重绘,从不依赖“动画自己跑”)。与其留一条在 0 帧时也通过的恒真断言(本项目最反对的假绿),这里如实不写。要把它变成断言,得先找到一个能驱动动画时间轴的夹具(未做)。

✅ 但媒体型的可见性门控有天然保护:现有的 E19 系列(~20 条,覆盖倒放 / 播放模式 / reduce-motion)会在门控写错时直接变红 —— 加这一层前后它们都是 233/0。

为什么需要宿主半

客户端插件是单文件 bundle、跑在浏览器里,没有文件系统 —— 而“读 mods/ 目录” 是可插拔的前提。所以由宿主读目录、经一个 HTTP 端点交给客户端。
(纯样式类插件可以不要宿主半;这个不行。)

媒体型还需要第二个端点把素材字节交给浏览器。它只服务白名单扩展名、路径必须是固定两段的 /media// 且文件名不许含 .. 或分隔符(不合格一律 404 且不泄漏目录内容),并必须支持 HTTP Range ——浏览器播 mp4 时先发 Range: bytes=0- 探测,不支持 Range 会导致视频不播或不能循环。

跑自检

前置:自检需要一个 headless 浏览器,本仓库不把它列为依赖(运行时零依赖)。
先任选一种方式备好:

npm i -D playwright-core # 方式一:装在仓库里
方式二:用环境变量指到你已有的那一份
PLAYWRIGHT_CORE=/path/to/playwright-core node verify.mjs

找不到时 verify.mjs 会直接失败并打印上面这两条出路,不会悄悄跳过 ——静默降级成“用假数据测通过”正是它要防的事。

node verify.mjs # 五组断言(宿主半 / 健壮性 / 客户端真跑 / 坏 mod 不崩 / 行为级)
node mutations.mjs # 反证:1 次基线 + 注入 20 种已知坏实现,要求断言变红,并写出 FALSIFICATION.md

verify.mjs 会:真 import 宿主半并用假 req/res 喂它、在临时副本里塞各类坏 mod 验证隔离性、在 headless Chromium 里真跑客户端并数像素、注入非法 GLSL 确认内核不崩;第五组(E)再真调用设置卡片组件树逐项检查四个旋钮(初值 / 量程 / 置灰)、真驱动 onChange 看运行时 spec 与 drawArrays 计数、读浅色与深色两种主题下的像素、并数 loseContext 确认卸载回收了 WebGL 上下文。
媒体功能另有 E18 组:真挂载 并核验 readyState / 尺寸 / muted / loop / currentTime (真在播)、加载失败要判不可用、媒体坏了要回落着色器、切走后 DOM 里不留 。
播放模式另有 E19 组:往返模式 loop 必须为 false、正放播完后方向真的翻转、倒放段 currentTime 单调递减、像素指纹真的在变(“倒放”与“只改了时间标量”的区分手段)、 seek 落位率 ≥ 0.9(“流畅倒放”与“每帧洪水式发 seek 退化成幻灯片”的区分手段 ——这个量直接拦 currentTime setter 数发出次数、数 seeked 事件数落位次数)、倒放速度 0.8~1.2× 与 可见帧率 ≥ 10 fps(后两条见下方“三个判据缺一不可”)、往返能闭环回到正放、「播放」下拉只在视频型出现且选择会落盘、类型标注出现在下拉的显示文案里。

三个判据缺一不可

只给 E19b 加「落位率 ≥ 0.6」是不够的 —— 下面这个坏实现能全绿:
把闸门换成纯时间限流(if ((now - lastIssue) ⚠️ 关于落位率 >1 的已知残余:“开窗前先 await waitSeeked() 再清零”
这一步没有修干净 —— waitSeeked 自带 200ms 超时,超时后仍会带着在途 seek 开窗, CPU 降速下可复现 rate=1.045。判据只要求 ≥ 0.9(不要求 ≤1),所以不影响结论;此处如实记录,不再假称已修掉。

报告器是 fail-closed 的:任何阶段抛错都会打印「验证未完成」并非零退出,不会打印成功标志。

⚠️ “现在是绿的”不构成验证。mutations.mjs 是验收的另一半:基线必须绿,而每条变异必须让对应的断言变红。两者都过,才有资格说这套断言在观察事实 —— 本仓库早期就吃过这个亏:注入 5 处坏实现,93 条断言仍然全绿。

写反证(变异)时必须注意的坑(都已踩过)

坑 1:变异“太弱” —— 删掉一处实现 ≠ 删掉这个能力。

el.play() 在 lib/client.js 里有两处(createMediaSurface().boot() 与 applyConfig())。
只干掉 boot 那处,mountSurface() 紧接着调的 applyConfig() 照样把视频播起来 ⇒ currentTime > 0 依旧成立 ⇒ 断言全绿、还打印成功标志。
记录里当时的原话是:media-noplay | exit=0 | 187/0 | ❌ 反证失败(exit=0 red=0 hit=0 且打印了成功标志)。

教训:做变异前先确认这个能力在源码里一共几处。现在 media-noplay 用循环替换一次干掉全部,并在命中数 1 时打印警告”的守卫。两道都验证过真的会触发。

坑 3:总闸自己也会假绿 —— “打了标记”不等于“能力被拆掉”。

第一版总闸只检查 from.includes('(变异:'),而 patch() 无论替换成什么都会追加这个标记。
于是把 el.muted = true 换成等价写法 el.muted = !!1,总闸照样放行、断言 187/0 全绿 ——它声称“能力被拆掉”,实际只断言了“我来过” —— 这正是最难发现的一类假绿。

教训:总闸改为按能力核对削减数 —— 每条变异要声明它该让哪个源码特征减少几处(removeAll / minRemoved / mustIntroduce),闸门拿变异前/后的计数差来判。
只数削减还不够:=> !!1 同样能让计数掉 1,所以凡“削弱点是一段字面量”的还必须
引入预期的弱实现。这套现在能抓住等价替换(改造前会放行)。
宿主半(hostSource())走同一套纪律。

坑 4:连跑整套会被“删除配额”打断 —— 症状像环境抖动,其实是机制性的。

现象:套件跑到中途起,之后每条都记成 ERROR(本轮没跑完),而单跑任一条却完全正常。

根因(查了垫片源码):调用方(Agent 运行时)经 NODE_OPTIONS 给每个 node 进程注入 safe-delete 垫片,它把 fs.rmSync / fs.rm / fs.unlink / fs.rmdir 换成“移入回收站”,并按对话回合累计删除项数(scope:'turn';totalCount = 已用 + 本次项数;达阈值即要求确认)。

⚠️ 这是调用方 Agent 施加的限制,不是这个插件本身的、也不是操作系统的通用行为 ——换个不带该垫片的运行时跑,裸 rmSync 完全正常。换言之,运行本插件的那个 DeepSeek Harness 本身不一定有删除配额(配额取决于“谁在调它”,不取决于本插件);这段是“遇到才需处理”,不是“必然发生”。

关键在越线之后每一笔删除都会被拒并抛错(不是只拦超标那一笔)⇒ 之后无论删多小的目录都炸。
多次 verify.mjs 共享同一回合的额度,于是前半程正常、后半程连续报“跑不完”。

⚠️ 阈值可配置(有一个默认值,本机被上调过若干次)—— 所以文档里不写任何具体阈值:数字会随环境变。可断定的是机制本身。

对照(同一回合内):裸 rmSync 删 1 个目录 ⇒ 被拒并抛 SAFE_DELETE_BULK_CONFIRM_REQUIRED;走 rmrf()(PowerShell Remove-Item)⇒ 删成功,且回合计数原值不变。

修法:verify.mjs 里清自建夹具/副本一律走 rmrf()。裸 rmSync 只允许出现在 rmrf() 内部作为 PowerShell 不可用时的兜底。

⚠️ 这个坑还有一个认知陷阱:旧注释把原因写成“钩子会拒绝超过某个项数的单次递归删除”。
方向错了 —— 按那个理解会去“缩小单次删除量”(无效),因为真正超限的是回合累计。
排查时不要停在“看起来合理的因果”上:报错里的 count 远小于一次 buildCopy() 的项数,这个数字本身就是“它数的是累计量”的线索。

写反证时的第三个陷阱:让无关组替被测代码背锅

反证跑不完(ERROR)与反证不成立(❌ 反证失败)都算失败,但成因完全不同 —— ERROR 说明脚本自己崩了,那时没有任何断言被观察过,容易把“没测”误当成“测过了”。

踩到的一次:为了让路径类变异真的打在副本宿主半上,把 buildCopy() 提到 A 组之前;于是 --mutate=no-isolation(见坏 mod 就中断整轮扫描)让 A 组的载荷塌成空,而 C 组开头有一句 payloadC.mods.length === 0 ⇒ throw ⇒ 整轮在 C 阶段抛错,被记成 ERROR 而不是“隔离性断言变红”。

根因是耦合:宿主半的故障归 A/B 组负责,却把只测客户端的 C/D/E 一起拖停了。
解法:C/D/E 的载荷改由 payloadForClient() 结算 —— 若 A 组载荷里没有 meteor,就改用真仓库宿主半另取一份,并显式打印回落原因(回落必须留痕,否则就成了“悄悄换掉被测对象”)。

写媒体安全断言时必须注意的坑:多道闸互相重叠

/motion-background/media 的路径校验是多道互相重叠的闸:① 段数必须为 2、 ② 文件名不许含 ../分隔符、③ 扩展名必须在白名单里。
随便一条非法路径通常被其中好几道同时拦住 —— 于是“拆掉某一道闸”时,那些断言照样全绿,看起来像“断言很稳”,其实是假绿:它们根本没在观察那道闸。

踩过两次:

1. 原来 18 条穿越向量全都够不着闸②(各被 ①/③ 兜住)⇒ 拆掉闸② 零红。
2. 修上面那条时,给“闸①”打的标签也是错的 —— 那些向量在闸③/id 就死了,拆掉段数校验后没有一条变成 200。

⇒ 现在的做法是:每道闸都配「只有它拦得住」的向量,且每条向量的落点文件必须真实存在(否则拆掉那道闸后 statSync 会兜底 404,反证又静默失效)。落点存在性不能靠推理,要在真目录里算一遍(有 2 条“看着合理”的向量落点其实不存在)。
verify.mjs 里有一条结构性断言按闸分别计数,谁删掉某道闸的证据向量就会立刻变红。

装进 DSH

从仓库直装(github: 走 pnpm 原生协议):

dsh plugin --profile web add github:ZhaoAndy821/dsh-motion-background

用本地克隆装(想改代码时):

1. 在 DSH profile 的 package.json 里加 link 依赖与 bundle 项
2. 在 profiles/web/node_modules/ 下建指向本目录的 junction
3. 重启宿主(因为改了 dsh.client.inject,只刷新页面不够)

装好后按 docs/POST-RESTART-CHECKLIST.md 逐条验收 ——六条 curl 自查 + 面板人眼确认 + 回滚步骤。

📌 两层,别混为一谈:profile 的 cordis.patch.yml 是热生效的 —— HMR 用 chokidar 监听
该文件,一变就重新组合(composeLive() 每次从磁盘重读),不必等重启。
而新增 / 卸载插件动的是 bundles 层,那是启动快照,必须重启。
详见 docs/CONFIG-LIFECYCLE.md。

📌 不要靠手改 dsh.profile.bundles 来停用某个插件 —— reconcilePlugins() 会在每次
dsh plugin 命令之后,把所有声明了 dsh.bundle 的 dependency 重新加回 bundles。
要停用就在 patch 层给它一条 disabled: true;那一条同时挡住宿主半与客户端半(客户端模块扫描器的判据里含 !entry.disabled,停用的条目根本不进模块表)。

许可

根目录 LICENSE 是 MIT 正文,适用于内核与仓库自有文件(lib/、docs/、verify.mjs、mutations.mjs、README.md)。

- mods/ 明确排除在上面那条总括之外:每个 mod 的许可看它自己的 mod.json (mods/meteor 是 MIT;mods/aurora-video 的素材是 ffmpeg 自产的测试视频,声明为 CC0-1.0)。
引入第三方代码/素材的 mod 必须在 mod.json 里声明来源与许可,且不得把第三方代码带进内核。
- ⚠️ “内核里没搜到某来源的着色器代码”不等于整个仓库具有再分发权:再分发还取决于仓库内其他资源与各随包 mod 的许可条件。上面这条分工是把“哪些文件适用哪个许可”写清,不是对第三方的授权声明。
预览分享卡片长什么样 ↓ZhaoAndy821/dsh-motion-background
分享 · DeepSeek Harness Hub
ZhaoAndy821/dsh-motion-background

给对话界面换上实时演算或视频动态底色

这个插件是你维护的吗?
作者本人可认领并核对本条目;认领通过后,详情页会显示「作者已认领」标记。
认领条目 →
回到顶部