SOH4C4759/dsh-pv-home ↗★ 1

dsh-pv-home

把一支 PV 当作 DeepSeek Harness 的主页:启动与刷新都落在默认窗口、影片全屏循环、Esc 返回、PV 风格的音量按钮、播放期间标题栏跟随影片配色。不改 DSH 本体源码。 适合想要个性化主页背景的用户,支持视频全屏循环播放、音量控制及标题栏配色跟随。

パッケージ
dsh-pv-home
互換性
未検証
バージョン
0.1.1
ライセンス
MIT
最終更新
2026/10/11

インストール

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:SOH4C4759/dsh-pv-home

ドキュメント

README 全文を読む ↗

出处:Bilibili BV1xCai6aE9g(上传者 西西弗斯的风车)。请按上方 NOTICE 的条款使用。

yt-dlp -f "bv*+ba/b" -o "assets/video/film.mp4" "https://www.bilibili.com/video/BV1xCai6aE9g/"


放好之后**不用改代码、不用重新构建**。清单与 URL 里的版本号按 `mtime-字节数` 生成,同名覆盖会得到新 URL,不会被旧缓存糊弄。影片缺失时插件有兜底:主页照常挂载、注入照常发生,只是没有影片可放,控制台与清单里会说明原因(`verify-injection.mjs` 有一条断言盯着这条兜底路径)。

换成你自己拍的片子也一样。真片子请守这几条:

| 项 | 建议 | 原因 |
|---|---|---|
| 容器 | **`moov` 必须在文件最前** | 否则浏览器要整段下载完才出画面,主页会先黑一会儿。原片已实测 moov 在偏移 32;`ffmpeg -movflags +faststart` 可以补齐 |
| 轨道 | 带音轨 | 静音起播,点按钮出声 |
| 画幅 | 16:9 | 默认 `object-fit: contain` 会完整呈现,比例不符时留黑边而不是裁画面;想铺满裁切在设置里切 |
| 时长 | 任意(会循环) | 主页常驻,按 Esc 随时走 |

设置卡片会自己探「浏览器播不了 / 未优化」并给你原因。

## 装法

**路一:免重启(当前已用)** —— 建一个 `node_modules` 目录链接 + 往 profile 的 `cordis.patch.yml` 追加一行 insert。

```powershell
sh tools/install-live.sh         # macOS / Linux
powershell -File tools\install-live.ps1   # Windows

跑完刷新浏览器即可。

路二:正式依赖(推荐最终状态) —— 路一不写依赖声明,profile 里以后任何一次 pnpm install(例如用 dsh plugin add 装别的插件)都可能把这个未声明的链接剪掉,而 patch 行还在,那时 DSH 会因为「行解析不到包」起不来。

dsh plugin --profile desktop add link:F:/CodeProj/dsh-pv-home

装完完全退出并重开 DSH。撤销用 remove,或把 patch 里那行 insert 删掉。

⚠️ link: 安装时,包内需要一个 node_modules/@deepseek-ai/schemastery。 entry.js 是静态导入它(Config 必须是模块导出对象的属性),而 ESM 解析不认 NODE_PATH,只从导入文件所在目录逐级往上找 node_modules。link: 装法下本包的真实路径在 profile 之外,上一级没有 node_modules —— 少了包内那份,整个模块求值就失败,路由和主页一起没了。

两种装法的区别,按你的情况挑:

  • link: 装法(本机当前用法):自己放一份,或者从内核那支复制过来。必须是内核在用的 3.18.4+(.volatile() 存在于该版本)。仓库没有提交这份副本(.gitignore 里排除了 node_modules/),因为提交包管理器目录是仓库腐烂的开始。
  • 装进 profile 的 node_modules(普通的 npm install):profile 自己那层就有 @deepseek-ai/schemastery,解析得到,包内不需要副本。

tools/verify-asset.mjs 有一条断言盯着这件事;tools/verify-all.mjs 在解析不到它时会跳过需要它的两套并打印原因,而不是假装通过。

连带的一个后果:如果把本包打包成 tarball 分发,package.json 的 files 不含 node_modules,那份副本会丢。link: 安装不受影响(整目录链接);真要打包分发,得先解决这个依赖怎么随包走。

右上角那三个窗口按钮

它们不是这个页面画的:桌面壳用的是 titleBarStyle: 'hidden' + titleBarOverlay,颜色属于 Electron 主进程。但页面握着一条官方接缝 —— 壳的 preload 一直在测量页面自己的 --dsw-specific-sidebar-fill 与 --dsw-alias-label-primary,经 dsh-desktop:windows-appearance 把结果送给主进程,主进程校验后调 setTitleBarOverlay。所以主页播放期间,把这两个变量写在 document.body 上(内联样式在那个子树里赢过任何样式表规则),右上角就变成影片的深色;返回时删掉,应用配色立刻回来。顺带还免费触发了那个 MutationObserver —— 因为这两次写入本身就是 body 的 style。

它做不到的:改字形。那三个符号由系统绘制,只有颜色和高度是可寻址的;换形状必须动 Electron 主进程,那不是插件该做的事。所以这个功能是「让它们融进画面」,不是「换一套按钮」。

「与 dsh-boot-animation 的关系」见下一节;主页以外的地方它们本来就跟主题一致,不需要动。

画面适配:为什么默认不铺满

原来写的是 object-fit: cover(铺满),这是个错的默认值。cover 会按比例放大到填满窗口,超出部分直接裁掉,而裁掉多少取决于窗口比例差多少 —— 不是一点点。

实测:一个 1280×820 的窗口(比例 1.561)放 16:9 的片子,要填满高度就得横向放大到 1458px,于是左右各裁掉 89px。片子里左边那个 dsh 聊天窗口、右边一半的 TUI 世界,就这么没了。窗口越扁,裁得越狠。

现在默认 object-fit: contain:整幅画面完整呈现,代价是上下各 50px 黑边 —— 而这片子本身的底色就接近纯黑,黑边基本看不出来。想铺满裁切的话,设置里能切回去。

("把视频本身裁成窗口的比例"是另一条路,但那等于永久丢掉画面内容,和"完整呈现"正好相反,所以没做。)

锁屏时也播放它

先说清楚 Windows 的限制:锁屏界面只接受静态图(或图片幻灯片),不支持视频,第三方程序也没有任何办法在安全桌面上画东西。真正会「播放」的只有屏保 —— 所以这一半做成了屏保:空闲触发、全屏循环播放这支 PV,配合 ScreenSaverIsSecure=1(恢复时要求登录)实际效果就是「回来一看,刚才在放它」。

从侧栏那颗锁屏按钮点进来(dsh-lock-button 插件):那边默认就是「先播放 PV」,做法是带 /lock 启动这个屏保 —— 放完片、一动鼠标,屏保退出时自己调 LockWorkStation() 落到登录界面。所以那条链路是:按钮 → 屏保放 PV → 退出时锁屏,而不是「按钮 → 立刻锁屏」。

screensaver/src/PvScreensaver.cs   WPF 屏保源码(MediaElement + 每显示器一个窗口)
screensaver/build.ps1              用系统自带的 csc.exe 编译,并跑两项探针
screensaver/install.ps1            复制到 %LOCALAPPDATA%\dsh-pv-home + 设置注册表 + 立即生效
screensaver/uninstall.ps1          还原安装前的四项注册表值并删文件
powershell -ExecutionPolicy Bypass -File screensaver\build.ps1      # 编译 + 自检
powershell -ExecutionPolicy Bypass -File screensaver\install.ps1    # 安装(默认 10 分钟、恢复需登录)
cmd /c "%LOCALAPPDATA%\dsh-pv-home\PvScreensaver.scr" /s            # 立刻看一次

install.ps1 的参数:-TimeoutMinutes 5、-Secure 0(恢复不要密码)、-Muted 0(开声音)、-Volume 0.4、-Screens primary(只在一台显示器上放)、-Video 。它只写 HKCU\Control Panel\Desktop 下的四项(SCRNSAVE.EXE / ScreenSaveActive / ScreenSaveTimeOut / ScreenSaverIsSecure),先把旧值备份成 JSON,再用 SystemParametersInfo 推进当前会话,所以不用注销。

默认静音,以及那个「双重播放」

默认静音(muted=1)。屏保是在没人在场时触发的,第一版会自己唱起来 —— 那是我做错了,不是你的设置问题。

更要紧的是它背后那个结构性 bug,也是「双重播放」的来源:这台机器有两块显示器,而 /s 给每块屏幕各建一个窗口、各有一个 MediaElement。同一条音轨被解码两遍,相差几毫秒叠加在一起 —— 听感上不是回声,是一条发闷、加倍的音轨。所以现在加了音频归属规则:

无论开几个窗口,只有一个窗口能出声(主显示器那个)。其余的一律 IsMuted,且永远不会被取消静音。

「双重播放」如果是说两块屏都看到画面,那是 screens=all 的预期行为(声音仍然只有一路)。想只在一块屏上放:

powershell -ExecutionPolicy Bypass -File screensaver\install.ps1 -Screens primary

多窗口之间还会各自漂移(每个窗口独立解码),所以主窗口每 20 秒会被当作时钟对一次,偏差超过 400 ms 才 seek —— 阈值不是容差:seek 一次有肉眼可见的卡顿,比这点偏差更难看。

三个实测细节留在这里,因为它们都是「不踩一次不会知道」:

  • .scr 在注册表里的关联是 "%1" /S:用 ShellExecute 启动(Start-Process、双击)会强行追加 /S,你传的参数会被丢掉。所以构建脚本用 cmd /c(CreateProcess)。反过来,这也意味着双击这个 .scr 就是运行屏保,正是想要的行为。
  • 没有屏保能配的参数:/c 只弹一个说明框,告诉你改哪个 ini。片名、静音、音量、放哪几块屏都在 pv-screensaver.ini 里。
  • 多显示器:每台显示器一个窗口、各自循环,但只有一路声音。/geom 探针会报告窗口落点和音频归属数(本机实测 windows=2 scale=1 audioWindows=0(静音)/ audioWindows=1(有声),两块 2560×1440,DIP 落点 0,0 与 2560,0)。
  • DSH 主页如果还开着,屏保启动时它不会自己停 —— 屏保已静音,所以你只会听到主页那一路。想彻底安静就先按 Esc 离开主页。
  • /lock 参数:给屏保加这个参数,它退出时会调 LockWorkStation()。空闲触发的路径不需要它(ScreenSaverIsSecure=1 已经保证恢复时要登录),只有"手动启动"才需要 —— 否则退出屏保就等于把桌面还给你,那不叫锁屏。参数位置无关,/geom /lock 这种调用也认。
  • 抢焦点:被别的进程启动的屏保不会自动成为前台窗口,所以它 Loaded 时会调 SetForegroundWindow —— 否则第一次按键会打到它背后的窗口上,什么都不发生(鼠标移动仍然有效,因为鼠标消息跟着指针走 —— 正是那种"半能用"最值得提前修掉)。

与 dsh-boot-animation 的关系

本机 desktop profile 里原本装着 dsh-boot-animation(启动动画:播一段片子再溶解进界面)。它和「主页」是两回事:启动动画会先播 8–15 秒再交接,如果两个都开着,你会先看一段启动动画、再看到这个主页。

因此安装时把它那一行改成了 enabled: false(它自己的开关,不是禁用插件)。想恢复:把 profiles/desktop/cordis.patch.yml 里

- id: boot-animation
  name: dsh-boot-animation
  config:
    fadeMs: 2000

加上 enabled: true 即可。两者共存是能跑的,只是会连播两段影片。

已经验证到哪一步(2026-10-11,desktop profile)

在真实运行进程里验证过的(http://127.0.0.1:19387,插件路由不需要令牌):

事实怎么验的
patch 行解析到了包、模块求值成功、Config 被投影cordis_inspect_query 的 Config 条目:patchId: pv-home、status: schema、packageDir 指向 profile 的 node_modules,9 个字段都带 x-cordis.volatile: true
路由注册进了真实的 web serverGET /plugins/dsh-pv-home/film.json → 200,cache-control: no-store,faststart: true,字节数与磁盘一致
配置读取走的是真实的 volatile 引用同一份清单里的 settings 就是 readField() 读出来的当前值
Range 语义正确bytes=0-63 → 206 / content-range: bytes 0-63/40111552;bytes=-64 → 206 后缀;越界 → 416 带 bytes */40111552
HEAD 与不可缓存200 + content-length,no-store
本包 client.js 没被自己的路由截胡该请求回的是框架的 404(无 content-type),不是本插件的 JSON 404
字体路由200,98232 字节,font/ttf
影片在本机真的能解码屏保的 /selftest 走 Media Foundation 打开同一个文件:result=ok width=1920 height=1080 seconds=211 audio=True
屏保的多显示器几何与音频归属/geom:windows=2 scale=1,两块 2560×1440,DIP 落点 0,0 / 2560,0;muted=1 → audioWindows=0、muted=0 → audioWindows=1(两个方向都断言过,这就是「双重播放」的回归测试)
/lock 参数真的被解析/geom 探针:不带参数 lockOnExit=False,带 /lock(放在第三个位置)lockOnExit=True
画面不再被裁桩 DOM:默认样式表里是 object-fit:contain,显式给 cover 时才变成 cover;实机量到 1280×820 窗口下 cover 会左右各裁 89px
屏保已装成当前用户的屏保注册表四项:SCRNSAVE.EXE 指向安装副本、ScreenSaveActive=1、ScreenSaveTimeOut=600、ScreenSaverIsSecure=1;旧值已备份成 JSON

没有端到端验证的那一步,说清楚:把两行 head 注入真的拼进服务端返回的那个 index HTML,我没抓到——启动令牌是进程内生成的(BrowserAuth 的 launchToken 不落盘),拿不到就没有令牌去换 cookie,首页只会回 401。这一步的依据是读源码(renderIndexInjections 把 placement:'head' 的行拼在 head 最前)+ 桌面宿主确实调用了 collectIndexInjections() + 同机制的 dsh-boot-animation 在本机有既成事实。

另外,「在真浏览器里看起来怎么样」不在任何自动化覆盖里:桩 DOM 能证明「Esc 会移除节点」,证明不了「面板在真主题下好不好看」;/selftest 能证明 Media Foundation 解得开这个文件,证明不了窗口铺满屏幕好不好看。视觉与听感只能靠刷新页面看一眼,屏保只能靠跑一次 /s。

目录

entry.js                  Host 半侧:影片/字体路由 + head 注入 + Config
src/home-screen.js        注入进  的主页脚本(无框架,必须在 shell 之前跑)
src/client.js             浏览器半侧:「设置 → 插件」里的卡片
lib/client.js             构建产物(window.__ModuleLoader__.load 闭包工厂)
build-client.mjs          产物构建:仓库外的包拿不到共享 preset,自己复现那五行外壳
cordis.patch.yml          bundle 层:一条 `- insert: id: pv-home`
assets/video/film.mp4     影片 —— **不随仓库发布**,自己放一份(见「换片」)
assets/video/film.source.json  影片出处与抓取记录
assets/fonts/SpaceMono-Bold.ttf + OFL_spacemono.txt   面板标题用的字体与其许可
node_modules/@deepseek-ai/schemastery   内核那支 3.18.4 的副本(静态导入必需)
tools/stub-dom.mjs        桩 DOM:让注入脚本真跑起来
tools/verify-*.mjs        六套离线自检
tools/install-live.{ps1,sh} / uninstall-live.{ps1,sh}   免重启接线与撤销
screensaver/              屏保:C# 源码 + 编译/安装/卸载脚本(见上一节)

离线自检

node build-client.mjs      # 改了 src/client.js 之后重新出产物
node tools/verify-all.mjs  # 跑全部六套
检查覆盖的契约
verify-docs.mjs文档与磁盘不许漂移:files[] 每项在盘上、且含文档让人去跑的 tools/screensaver、没有 private: true、exports 每项存在;README 目录清单里每个路径都存在(node_modules/ 与影片条目改断言「被 .gitignore 排除而非提交」,而不是断言它存在);verify-all 声明的套件集合与盘上一致;AGENTS.md 的规则编号从 1 连续无洞;README/MANUAL/NOTICE/AGENTS/package.json 里不许出现本机绝对路径、用户名或凭据
verify-injection.mjs恰好两行 head 注入且配置行在脚本行之前、总开关关掉时一行都不注入、影片缺失时不注入但留告警、路由形态(清单/字体是 exact,影片是 /clip 前缀,没有任何前缀压在 ROUTE 上)、volatile 引用与普通值两种配置形态都能读
verify-routes.mjs挂真 HTTP 服务器打真请求:清单、整片逐字节、Range 206(首段/中段/后缀)、416、HEAD、404、目录穿越、伪造扩展名、本包 client.js 不被自己的路由截胡、字体逐字节
verify-home-screen.mjs把注入脚本加载进桩 DOM 推真事件:挂在 documentElement 而非 body、重复执行不会挂出第二个覆盖层、清理恢复点且只动那一个键、存储不可用时不炸、Esc 从第一帧起有效(含播放被拒、解码失败两种情形)、音量按钮的点击/滚轮/键盘三条路径与记住的选择、电平表按音量取整
verify-client-bundle.mjs产物外壳契约、只向加载器要 react、不许出现 ctx.get((必须先剥注释)、summary 视图回字符串、卡片真渲染并驱动开关断言写出的 op 与 revision、被拒时走远端复核并显示原因、四种残缺上下文都不抛
verify-asset.mjs影片盒子链 ftyp→moov→…→mdat、moov 在文件前 1 MiB、H.264+AAC 双轨、从 mvhd 读出的片长、字体是 TrueType 且随附 OFL、出处记录与署名链完整、包内 schemastery 在位

它证明不了的那一步必须说清楚:这些都不包含「在真浏览器里看起来怎么样」。桩 DOM 能证明「Esc 会移除节点」,证明不了「面板在真主题下好不好看」,也证明不了影片在 Chromium 里真的解码出画面。视觉与听感只能靠你看一眼 —— 这是本插件唯一没有自动化覆盖的边界。

诊断:地址加 ?dshpvdiag=1,左上角会显示 session=cleared dur=211.9 muted=true vol=1 state=showing 这样的实时读数,控制台另有一份配置与清理结果。