packages/dsh-task-board
zhu1090093659/dsh-web侧边栏多列任务看板:卡片交给真实 DSH 智能体会话执行,支持 cron 定时(Host 侧到点执行,关浏览器也生效)。
打开 DSH 时、打开新对话时,或每次打开你钉住的那个会话时,播放一段全屏片头动画。内置素材是《流浪地球》风格的太空站与量子 AI MOSS(550W):红色单眼点亮,地球弧线从视窗外掠过。四段片子直接内嵌在代码里,不需要单独的视频文件;片库可勾选任意几段随机轮换(不会连续两次同一段)、预览轮换,也能换成你自己的视频。声音无需点击即可播放;钉住按钮就在设置旁边。
本版本是修改版。 基于 NativeDog1/dsh-boot-animation (BSD-3-Clause)修改:新增「打开 DSH 即播」、默认带声音、去掉点击全屏、片库多选轮换 (勾 1 段=固定 / 勾多段=每次随机且不连续重复),并把随机决策移到服务端、修掉了会导致 「永远只播一段」的缓存问题。完整改动见 CHANGELOG.md。
给 DSH 加一段开机动画:打开 DSH、打开一个新对话、或打开你指定的那个会话时,视频铺满整个窗口播放。
内置画面的主题是《流浪地球》风格的太空站与量子智能体 MOSS(550W)—— 红色独眼亮起,地球弧线从舷窗外掠过。四段片头都已内嵌在代码里,装完即用, 也随时能换成你自己拍的视频。
English: README.en.md
一个开机片头插件。打开 DSH、打开新对话、或每次打开你钉住的那个会话时, 铺满窗口播放一段动画,放完自动关闭。
内置画面的主题是《流浪地球》风格的太空站与量子智能体 MOSS(550W)—— 红色独眼亮起,地球弧线从舷窗外掠过。四段片头都已内嵌在代码里,装完即用, 也随时可以换成你自己拍的视频。
内置画面是以该风格为主题的自制/自录片段,与《流浪地球》版权方无关。
打开 设置 → 插件 → 添加插件,把下面这一行粘进输入框,点「安装」:
https://github.com/jjt98118-beep/dsh-moss-boot/releases/latest/download/dsh-moss-boot.tgz
那个输入框接受四种写法:npm 包名、GitHub 仓库地址、tarball 链接、本地目录路径。 本插件没有发布到 npm,所以请用上面这条 tarball 链接;写成包名是找不到的。 也不需要 git —— 它直接下载成品包。
dsh plugin add https://github.com/jjt98118-beep/dsh-moss-boot/releases/latest/download/dsh-moss-boot.tgz
DSH 目前不支持插件自动更新(安装对话框里也有这句提示):把上面的安装步骤再做一遍即可。
链接用的是 latest,永远指向最新发布,不需要换地址。
如果提示"已安装"而没有更新,是因为同名插件已存在——先在插件列表里卸载旧版,再重新安装。 更新前想看改了什么,翻 CHANGELOG.md。
片库文件夹里你自己添加的视频会组成一个轮换池,每次开播随机挑一段,不用你指定:
videos/ 里那些);你还没加过视频时,就用插件内置的四段videos/ 里再丢视频,会自动加入轮换,不用改配置在片库面板里点条目勾选,想让它轮换的每一段都勾上:
也可以直接改 ~/.dsh/boot-animation/selection.json:
{ "picked": [] } // 你自己加的全部
{ "picked": ["clip-1a2b3c4d"] } // 只播这一段
{ "picked": ["clip-1a2b3c4d", "clip-2b.."] } // 这两段随机
旧的 {"id": "..."} / {"random": true} 写法仍然读得懂。
做法:浏览器每次开播给视频地址挂一个新的播放 id(
boot.mp4?p=…), 由 host 把这个 id 解析成一段随机视频并记住它。这样同一次播放的多个 Range 请求一定落在同一段上(不然拼出来的文件不会报错,只会永远不出画面), 而且不会连着两次播同一段。
排查用日志:~/.dsh/boot-animation/access.log 每次播放记录一行
(请求路径、播放 id、实际播了哪一段)。
打开一个还没说过话的新对话时会自动播一次。
DSH 一打开(页面加载完成)就先播一遍,不需要进入任何对话。这是这个插件新增的 第三个触发点,原来那两条(新对话播一次、被钉住的会话每次打开都播)没有任何改动。
不想每次打开都播的话,在浏览器控制台关掉它:
localStorage.setItem('dsh-moss-boot:boot', 'off')
再打开:localStorage.removeItem('dsh-moss-boot:boot')。
这个开关存在浏览器本地,和「钉住」一样是每个浏览器各存各的。
之后每次进入这个会话都会播一遍 —— 切走再切回来、刷新页面,都会重播。
再点一下图标取消。
注意:如果刚启动时你的活动主面板不是「对话」(比如停在某个插件的面板上), 当前会话还不存在,图钉是禁用状态。先打开一个对话即可。
插件现在是一个片库,不是单个槽位:它会把所有能找到的视频都列出来,你勾选要轮换的 片子(一段或多段),选择会被记住。
装完不用加任何东西,片库里就已经有四段可选:
| 片库里的名字 | 来源 | 大小 |
|---|---|---|
片头 1 | 内嵌 lib/clips.data.js | 443 KB |
片头 2 | 内嵌 lib/clips.data.js | 1.11 MB |
片头 3 | 内嵌 lib/clips.data.js | 1.11 MB |
片头 4 | 内嵌 lib/clips.data.js | 5.08 MB |
这些片段没有落盘的 mp4 文件 —— 它们以 base64 存在 lib/clips.data.js 里,host 在
第一次被请求时才 import(约 10.3 MB 的模块,如果在启动时解析,每次开 DSH 都要白付这个代价)。
这样做的意义是:不会再有 files 字段漏写、安装副本过期、或者随包发出一个坏容器这些事。
media/*.mp4 只是 npm run embed-clips 的输入,不随包发布。
faststart 状态(四段各不相同):其中三段把索引表放在文件头,可以边下边播; 第一段的索引表在文件末尾。生成脚本原本会直接拒绝这种输入,这里改成了 警告后照常内嵌——因为内嵌片段是从内存经回环地址返回的,整段瞬间到达,尾部索引 在这种情况下无害。真要把同一个文件当普通下载分发时,先重排:
ffmpeg -i in.mp4 -c copy -movflags +faststart out.mp4
体积:tarball 7.6 MB / 解包 约 11 MB。四段里只重编码了第 4 段——它原本是 6625 kbps、16 秒、12.65 MB,压到 CRF 23 后 5.08 MB,实测 SSIM 0.9916 / PSNR 47.5 dB (判据:SSIM ≥ 0.99 且 PSNR ≥ 40 dB 即实际观看分辨不出)。前 3 段保持原样:它们已是 322–830 kbps,再压会真的变糊,而合计只有 2.65 MB。原始片段不在本仓库内。
想换成自己的片子:把 mp4 放进 media/,改 scripts/embed-clips.mjs 里的清单,
跑 npm run embed-clips。(临时试片不用这么麻烦 —— 见下面的「最省事的方式」。)
~/.dsh/boot-animation/videos/勾选的片子会在下一次播放片头时登场:打开 DSH、新对话、你钉住的会话,或点「▶ 预览当前」。
Windows 上就是
C:\Users\<你>\.dsh\boot-animation\videos\具体路径以片库面板底部显示的那一行为准。你不需要为了"干净"删任何东西:同一个视频存在多份副本时,片库只列一条 (身份是文件内容的 sha256,不是大小+mtime;同一个文件的哈希结果按 size+mtime 缓存), 并标注「合并 N 份重复」。你的文件一直留在 原处,只是不重复显示。这解决的是历史上一个真实的困惑:用户自己也放了一份
intro.mp4,于是同一段片子在面板上出现两次、挂两个不同徽章,看起来像 "插件里没有这一段"。
| 元素 | 作用 |
|---|---|
| ✓ 标记 | 这一条在不在轮换里(勾上就参与轮换) |
| 来源徽章 | 你自己加的 / 插件内置 / 环境变量 |
| 文件大小 | 帮你确认换对了没有 |
| ▶ 预览当前 | 立刻按当前轮换播一次,不用等下一次触发;勾了多段时它也会随机挑一段(见下) |
| 刷新 | 刚往文件夹里丢完文件,点它重新扫描 |
原片源 徽章 | 历史上那个 intro.mp4 落点,仍然优先 |
| ⚠ 未优化 徽章 | 该文件的索引表 moov 在末尾,建议重排(见下) |
铺满屏幕 / 完整显示 | 播放时怎么贴合窗口,见下 |
片头动画的触发故意很窄:
dsh-moss-boot:boot 关掉,见上)所以在同一个新对话里刷新页面,"新对话"这条触发不会重播;但刷新本身等于重新打开 DSH,所以那一遍还是会播(除非把开机那条关掉)。这很容易被误认为 "我换了片子但另一个视频不出现"。点片库里的 ▶ 预览当前 可以立刻按当前轮换播一次, 确认换对了没有。
随机模式下每次开播都重新挑一段:所以下一次触发时看到的是池子里的另一段, 不是"没生效"。想核对服务端到底播了哪一段,看
~/.dsh/boot-animation/access.log。
覆盖层铺满整个窗口,但窗口的长宽比几乎不会是视频的长宽比——浏览器有标题栏和 工具栏,可视区通常比 16:9 更宽。这时:
| 模式 | CSS | 效果 |
|---|---|---|
| 铺满屏幕(默认) | object-fit: cover | 填满窗口,没有黑边,超出部分被裁掉 |
| 完整显示 | object-fit: contain | 整帧都在,长宽比不匹配时留黑边 |
在片库面板里切换,下次播放生效。选「完整显示」的情况:片子里有贴着边缘的字幕、 logo 或水印,不想被裁掉。
如果黑边来自视频本身烧进去的边框(导出时带上的),改 CSS 没用,要用 ffmpeg 裁掉:
ffmpeg -i in.mp4 -vf "crop=W:H:X:Y" -c:a copy out.mp4。 判断方法:ffmpeg -v info -i in.mp4 -vf cropdetect=24:16:0 -f null -, 若crop=值全程稳定,就是烧进去的;若随画面变化,那只是深色背景,别裁。
.mp4 .m4v .webm .mov .mkv —— 但能不能播取决于浏览器解码。
H.264 + AAC 的 mp4 最稳;HEVC(H.265)、ProRes、部分 mkv 大概率只有声或黑屏。
host 半侧按这个顺序解析,每次请求都重新解析(换片子不用重启):
| 顺序 | 位置 |
|---|---|
| 1 | ~/.dsh/boot-animation/selection.json 里勾选的 id(勾 1 段 = 就播它;勾多段时先由上面的轮换挑好一段) |
| 2 | 环境变量 DSH_BOOT_ANIMATION 指向的文件 |
| 3 | ~/.dsh/boot-animation/intro.mp4(历史落点,仍优先于片库里的其他文件) |
| 4 | ~/.dsh/boot-animation/videos/ 里最新修改的那个 |
| 5 | 内嵌的四段(按 scripts/embed-clips.mjs 里的顺序)—— 永远兜得住,因为它在代码里 |
所以最保险的手动换法依然是:
mkdir -p ~/.dsh/boot-animation
cp 我的片子.mp4 ~/.dsh/boot-animation/intro.mp4
想确认当前用的是哪一个,直接访问状态端点:
curl http://127.0.0.1:3080/dsh-moss-boot/status.json
curl http://127.0.0.1:3080/dsh-moss-boot/videos.json
多半是容器没做 faststart。 如果 mp4 的索引表 moov 在文件末尾,浏览器必须
整段下完才能解码,中间一直黑屏;而客户端有 25 秒看门狗(STALL_TIMEOUT_MS),
超时就自己把覆盖层关掉 —— 症状就是「点开什么都没有」。
用 ffmpeg 重排一下容器(无损,不重新编码):
ffmpeg -i 原片.mp4 -c copy -movflags +faststart 修好的.mp4
验证 moov 是否前置:
ffprobe -v trace 修好的.mp4 2>&1 | grep -m1 moov # 偏移应该很小
声音是自动的,正常情况下不需要点任何东西。 片头以带声音开始:
不再调用 Fullscreen API。 覆盖层本来就铺满整个窗口(视觉上就是全屏),真实全屏同样需要用户手势, 所以去掉了 —— 原来那个「点一下进真全屏」的行为也随之消失。
万一连静音自动播放都被拒(极少见),会显示「点击播放」而不是黑屏。
| 现象 | 原因 / 处理 |
|---|---|
| 完全没出现 | 十有八九是缓存:Ctrl+Shift+R。或重启一次 DSH 服务 |
| 打开 DSH 时没播 | 先确认没被关掉:控制台执行 localStorage.getItem('dsh-moss-boot:boot'),返回 off 就是关过了 |
| 新对话不播 | 这个会话已经播过了(每个会话只播一次)。钉住它可变成每次都播 |
| 钉住了也不播 | 确认图钉是绿色;确认打开的就是被钉的那个会话 |
| 黑屏无画面 | 先看 moov 是否前置(见上「排错:视频是黑的」);再访问 /dsh-moss-boot/status.json 看片源;最后看浏览器控制台有没有解码错误 |
| 换了片没生效 | 片库里勾上它才有 ✓(✓ = 在轮换里);确认文件在 videos/ 里并点了「刷新」。勾了多段时每次开播都会重新随机,下次看到另一段是正常的 —— 拿不准就看 ~/.dsh/boot-animation/access.log |
| 播到一半自己没了 | 25 秒看门狗(STALL_TIMEOUT_MS)超时 —— 通常还是 faststart 或解码太慢 |
| 想看到插件在干什么 | 把 src/client/index.ts 顶部的 DEBUG 改成 true 重新构建,控制台会打印每次决策 |
shell.overlay(帧级浮动层,kind: list,新增一格不顶替官方 UI)+
sidebar.footer.action(页脚那个图钉和 🎛 片库入口)ctx.uiSession.adapter.current 这个 React 友好的 store。
它的快照不是会话记录,而是解析后的描述符产物
{ key, hooks, keyedHooks, props } —— 会话 id 在 props.sessionId,会话快照在 hooks.sessionblankBit(hooks.session.blankBit)。
session.blank 属于别的包的投影对象,不在这个快照上bootPlayed 是模块级标记而不是 ref:
片库对话框打开时会把 overlay 整棵卸载,用 ref 会导致每关一次片库就重播一遍boot.mp4 的响应是 cache-control: no-store:它的字节每次开播都可能换,
任何缓存都意味着浏览器不再问服务端,轮换看起来就是「永远只播一段」。逐段的
/media/<id>?v=<hash> 是真正的内容寻址,可以 immutable 永久缓存;不带 ?v= 的裸地址
则用 no-cache + ETag(没换片子 → 304 秒开;换了片子 → ETag 不同 → 重新下发)。
verify-routes.mjs 里的跨片重校验检查断言"不同内容的 ETag 必须不同,且带旧 ETag 请求另一段必须 200"activeVersion 指纹(现在恒为 null):浏览器曾把它拼成
boot.mp4?v=<hash> 并按一年不过期永久缓存,地址在页面加载后就固定了,服务端再也收不到
请求,随机逻辑等于没跑过。没有这个指纹,浏览器每次都必须问服务端apply() 是插件加载器调的,不是 React 调的,所以状态
全部住在 AppRoot 组件内。片库入口在图钉那个 slot、对话框在 overlay 那个 slot,
是两个独立的 React 根,用模块级 libraryOpeners 订阅集合桥接videos.json(列)、media/<id>(按 id 流)、select(POST 写选择)、
boot.mp4(老路由,同时是随机轮换的入口)boot.mp4?p=…):浏览器每次开播只挂一个新的 id,
挑哪一段由 host 决定并记住,所以一次播放的所有 Range 请求必然落在同一段上。不带播放
id 的旧前端由 host 在 20 秒窗口内复用同一段,窗口过期后才重新随机;每次挑都会避开
上一次播过的那段builtin:<name>,与路径派生的 id 不会撞;它们的 ETag 用自身的
内容哈希("embedded-<sha256前16位>"),所以重校验是精确的、不依赖 statpathname !== prefix && !pathname.startsWith(prefix + '/') 匹配,注册
.../media/ 会被当成 .../media//,永远匹配不上(曾导致 /media/ 全 404)| 命令 | 作用 |
|---|---|
npm run verify:routes | 用服务器自己的匹配规则驱动真实 handler,断言每条路由 |
npm run verify:letterbox | 用 CDP 驱动本机 Edge,量出所选贴合方式实际留多少黑边 |
npm run check | CSS 模板反引号检查 + verify:routes(不含 verify:letterbox) |
npm run build:client | 先跑 CSS 检查再构建(防带病构建) |
两个脚本都是被真实 bug 逼出来的,各自都有过一次"用自己的规则测自己"的教训: 它们的断言刻意复刻被测方的规则,并且在提交前会做反向验证(故意改坏 → 必须报错)。
客户端构建有一个坑:整个 CSS 是一段模板字符串,注释里写一个反引号就会提前把它 结束掉,而报错是 TypeScript 的 parse error 指向某行 CSS,同时
lib/client.js保持不变 —— 看起来像改成功了其实没生效。scripts/check-css-template.mjs专门 拦这个,已接进build:client。
BSD-3-Clause,见 LICENSE。包内分发的媒体 —— 内嵌在 lib/clips.data.js 里的
四段片头 —— 以相同条款分发;media/*.mp4 只是 embed-clips 的输入,不随包发布。