开源 BeautiCode:给 DeepSeek Harness 加动态皮肤,我是如何实现视频背景、插件接入和安全回滚的
最近在使用 DeepSeek Harness 和 Codex 做 AI Coding 时,我一直有一个很简单的问题:
为什么 AI Coding 工具的界面一定只能是统一的灰黑色?
现在 Agent 已经可以帮我们读代码、修改文件、执行命令、跑测试。
与此同时,我们待在 AI Coding 界面里的时间也越来越长。
于是我做了一个开源项目:
BeautiCode
GitHub:
https://github.com/starsstreaming/beautiCode
它可以给 DeepSeek Harness 添加图片、视频和动态背景,同时也支持 Codex Desktop。
你可以把电脑里的:
- 动漫 / 番剧
- 电影
- MV
- 动态壁纸
直接放到 AI Coding 界面后面。


但这篇文章不只是介绍「怎么给 DSH 放个视频」。
我更想讲讲 BeautiCode 背后的几个设计问题:
如何在不破坏宿主应用的情况下加入动态背景?
视频如何加载,才能避免黑屏、闪屏和旧视频残留?
如果背景写入成功,但实际页面没显示,应该算成功吗?
本地视频如何提供给浏览器,又不把整个文件系统暴露出去?
DeepSeek Harness 和 Codex 两个完全不同的宿主,又该怎么共用一套核心逻辑?
这些才是 BeautiCode 真正比较有意思的部分。
一、首先,它不是「在窗口上盖一个播放器」
最简单的实现其实非常暴力:
直接创建一个永远置顶的视频窗口,然后调个透明度。
但这样很快就会遇到问题:
- 鼠标点击被播放器截走
- 输入框无法正常操作
- 窗口尺寸和宿主不同步
- Alt + Tab 多出一个窗口
- 宿主滚动、弹窗、侧栏层级全部变得麻烦
- 看起来也不像真正属于 Coding 界面的一部分
所以 BeautiCode 的设计方向从一开始就是:
背景属于宿主页面,而不是另一个播放器。
整体结构可以简单理解为:
┌───────────────────────────────┐
│ DeepSeek Harness │
│ │
│ ┌───────────────────────┐ │
│ │ 原本的 UI / 对话区 │ │
│ │ 输入框 / 按钮 / 代码 │ │
│ └───────────────────────┘ │
│ │
│ ───────────────────────── │
│ BeautiCode Background │
│ Image / Video │
└───────────────────────────────┘
动态背景处在内容层后方。
真正的 UI 依然属于 DeepSeek Harness。
这带来了 BeautiCode 的第一个设计原则:
Background First,Host UI First
BeautiCode 只负责「背景」。
不应该为了让背景更好看,就把宿主自己的 UI 大改一遍。
所以注入的背景层会:
pointer-events: none;
也就是说:
背景永远不能抢鼠标。
无论后面是一张图片还是正在播放的视频,都不会挡住:
- 输入框
- 按钮
- Sidebar
- 对话
- 代码区域
这件事情看起来很小,但其实决定了 BeautiCode 到底是「皮肤」,还是一个盖在软件上面的播放器。
二、为什么首页很亮,真正工作时却会自动变暗?
如果只是为了展示效果,背景当然越清晰越好。
但真正 Coding 几分钟之后就会发现:
背景太亮是灾难。
尤其是视频。
人物、字幕、灯光不停变化,会持续干扰文字阅读。
所以 BeautiCode 没有简单地做一个固定的:
opacity: 0.3;
而是区分了两种状态。
首页 / 空闲状态
刚打开 DeepSeek Harness,还没有真正进入任务时:
Background
↓
保持原本亮度
这时候背景本身就是视觉主体。
可以完整看到壁纸、动漫或者视频。
项目 / 对话工作状态
真正进入任务后:
Background
↓
自动降低视觉权重
↓
Foreground UI
↓
重新成为视觉主体
也就是说,BeautiCode 的目标不是:
“让视频永远最显眼。”
而是:
需要工作时退到背景,需要休息时再回来。
我觉得这是整个项目里一个很重要的产品设计选择。
动态背景如果影响 Coding,那这个功能本身就失去了意义。
三、「摸鱼模式」其实不是重新创建一个播放器
BeautiCode 里还有一个很不正经的功能:
摸鱼模式
快捷键:
Ctrl + Shift + Space
Agent 正在执行一个比较长的任务时:
AI:正在修改 17 个文件……
我:Ctrl + Shift + Space
工作区域会暂时隐藏,只留下完整背景视频。
于是:
Coding Mode
┌─────────────────────┐
│ UI UI UI UI UI │
│ │
│ dimmed video │
└─────────────────────┘
↓ hotkey
Fish Mode
┌─────────────────────┐
│ │
│ │
│ full video │
│ │
└─────────────────────┘
再按一次:
直接回到 Coding。
这里有一个实现上的细节:
BeautiCode 不会因为进入摸鱼模式就销毁并重建视频。
它做的只是切换页面状态。
类似:
data-bc-fish="true"
然后让宿主内容区域:
opacity → 0
visibility → hidden
背景舞台本身继续播放。
所以切换摸鱼模式时:
- 视频不会重新加载
- 不会重新解码
- 不会从头播放
- 不需要重新生成背景
- 不会产生一次新的背景事务
这也是为什么它可以做到按一下立即隐藏,再按一下立即回来。
从实现上看,它更接近一个状态切换,而不是重新构造 UI。
四、真正麻烦的是视频,而不是图片
图片背景其实比较简单。
把图片验证、复制、加载、显示即可。
视频就完全不同了。
第一次做动态背景时,很容易碰到:
视频节点创建了
↓
浏览器还没解码第一帧
↓
背景突然黑一下
↓
视频出现
或者切换视频:
旧视频消失
↓
新视频加载中
↓
白屏 / 黑屏
↓
新视频出现
视觉效果会非常糟糕。
BeautiCode 因此采用了一个:
Poster → Video 的两阶段模型
每个视频背景并不只有视频本身。
而是:
Video Theme
├── poster
└── background.mp4
加载流程是:
显示 poster
↓
创建 video
↓
加载视频
↓
等待浏览器真正解码出第一帧
↓
video ready
↓
显示 video
↓
poster 退出
也就是说:
视频没有真正 ready 之前,用户看到的一直是 Poster,而不是一个空白的视频元素。
同时,新视频加载时,旧背景也不会立刻被干掉。
大致是:
旧背景正常显示
↓
新视频后台加载
↓
新视频 first frame ready
↓
完成切换
如果新视频失败:
new video
↓
decode failed
↓
保持 / 恢复旧背景
用户不会看到一个已经损坏的半成品状态。
五、Generation:解决异步视频最讨厌的竞态问题
视频加载是异步的。
这会产生一个非常经典的问题。
假设用户快速切换:
A.mp4
↓
B.mp4
理论上 B 是最新背景。
但 A 的某一个异步事件可能晚一点才回来:
A load()
B load()
B ready
A error ← 现在才回来
如果不做任何保护:
A 的 error handler 可能会把已经正常显示的 B 错误地标记成失败。
所以 BeautiCode 给每次 Apply 都分配一个单调递增的:
generation
例如:
A → generation 31
B → generation 32
异步回调执行前先判断:
callbackGeneration === currentGeneration
如果:
31 !== 32
那么这个事件已经属于旧世界线。
直接忽略。
这套机制不仅用于视频。
它也是 BeautiCode 整个背景状态的一部分。
Manifest 大致可以理解为:
{
"schema": "beauticode.background/v1",
"generation": 32,
"background": {
"type": "video",
"image": "poster.jpg",
"video": "background.mp4"
}
}
这样可以避免大量:
- stale callback
- 快速切换
- 延迟 error
- 页面重新注入
- 旧媒体覆盖新媒体
导致的竞态问题。
六、写进磁盘,不等于应用成功
这是后来我越来越在意的一点。
假设用户点击:
设置 video.mp4 为背景
程序:
复制文件成功
写 JSON 成功
很多程序到这里可能就:
success!
但实际上页面可能:
- 根本没有加载
- video decode 失败
- 插件连接断了
- DOM 结构变化
- 页面没有成功插入背景
- 当前宿主已经退出
所以 BeautiCode 做了一个完整的:
Apply Transaction
核心原则是:
Disk success ≠ User-visible success
一次背景应用大致经历:
idle
↓
snapshot
↓
stage
↓
commit disk
↓
publish media
↓
apply host
↓
live verify
↓
┌──────────────┐
│ │
pass fail
│ │
finalize rollback
1. Snapshot
应用新背景之前,先记录当前状态。
包括:
- 当前 Manifest
- 当前 Poster
- 当前 Video
- 当前 Generation
2. Stage
新媒体不会直接覆盖当前文件。
而是先创建:
staging/
在 staging 中完成:
- 文件复制
- 类型验证
- Manifest 构建
- 完整性检查
3. Commit
只有 staging 完整后,才会切换成新的 active 状态。
因此不会出现:
manifest 是新的
但 video 还是旧的
这种一半新、一半旧的状态。
4. Apply Host
然后通知真正运行中的 DeepSeek Harness / Codex:
generation 42 已经准备好了。
宿主开始加载背景。
5. Live Verify
接下来不是直接返回成功。
而是检查:
背景舞台真的存在吗?
图片真的显示了吗?
视频真的 ready 了吗?
当前 generation 对吗?
背景层有没有造成页面横向 overflow?
浏览器有没有报告 videoFailed?
只有 Live Verify 通过:
transaction → finalize
否则:
transaction → rollback
恢复旧背景。
这套东西对于一个「动态壁纸工具」看起来可能有点重。
但我后来越来越觉得:
只要一个工具开始修改另一个应用的运行状态,就应该区分“我执行了操作”和“操作真的生效了”。
七、本地视频为什么还需要 Media Server?
浏览器里的视频不能简单理解成:
<video src="D:\Anime\xxx.mp4">
尤其是当你进入:
- CSP
- Chromium
- Electron
- 本地文件权限
- Range Request
之后,会遇到非常多边界问题。
所以 BeautiCode 抽象了一层:
Media Server
结构类似:
Local MP4
↓
Validation
↓
beautiCode Data Root
↓
Local Media Server
↓
127.0.0.1:random_port
↓
DeepSeek Harness
这里我给自己定了一个很明确的安全边界:
永远只监听 Loopback
只允许:
127.0.0.1
localhost
::1
不允许:
0.0.0.0
LAN IP
因为 BeautiCode 根本没有理由把你正在看的本地视频暴露给局域网。
八、不是选到一个 MP4 就直接提供给浏览器
导入媒体时也会进行检查。
例如图片目前限定:
.jpg
.jpeg
.png
.webp
除了扩展名,还会检查:
- regular file
- 实际路径
- 文件大小
- Magic Bytes
- Symlink / Reparse Point
视频目前主要支持:
.mp4
同样不仅仅检查:
filename.endsWith(".mp4")
还需要确认基本的 MP4 / ISO-BMFF ftyp 结构。
也就是说:
evil.exe
↓
改名
↓
evil.mp4
不会因为扩展名正确就直接被当成媒体。
导入的文件也不会继续直接引用用户原始路径。
而是复制到 BeautiCode 自己的数据目录。
这样 Core 只管理自己拥有的:
active/
staging/
saved/
snapshots/
而不是拿着任意用户路径到处传递。
九、本地服务也需要鉴权
有人可能会觉得:
都已经 127.0.0.1 了,还做什么鉴权?
但 localhost 并不意味着:
只有 BeautiCode 可以访问。
浏览器里的其他页面、本机其他程序也可能尝试访问本地端口。
所以 BeautiCode 的控制接口和媒体接口都增加了随机 Token。
可以理解为:
http://127.0.0.1:xxxxx/media/<random-token>
同时还会检查:
- token
- origin
- 文件身份
- 路径范围
也就是说,它不是:
GET /?file=C:\Users\xxx\anything
这种任意文件服务器。
Media Server 只知道:
我当前被允许提供的这一份媒体。
从设计上尽量把攻击面限制在一个非常小的范围内。
十、为什么不直接修改 DeepSeek Harness 源码?
早期做类似功能,一个非常自然的想法是:
找到前端文件
↓
修改 CSS
↓
Patch
↓
启动
但这种方案的问题也非常明显:
宿主升级
↓
文件变化
↓
Patch 失效
甚至更糟:
- 修改官方安装目录
- 修改签名
- 修改打包后的文件
- 每次更新重新 Patch
所以 BeautiCode 给自己定了一条安全边界:
不修改官方宿主安装。
对于 DeepSeek Harness,现在采用的是:
Cordis Plugin
大致结构:
DeepSeek Harness
│
│ Cordis Plugin
▼
@beauticode/dsh-plugin
│
│ local authenticated bridge
▼
BeautiCode Core
│
├── Background Store
├── Apply Transaction
├── Media Server
├── Validation
└── Theme
这样 BeautiCode 不需要 Fork DeepSeek Harness。
也不需要修改 DSH 源码。
更重要的是:
宿主负责宿主,插件负责插件。
这是我现在比较倾向的一种 Agent 工具扩展方式。
十一、甚至可以直接从 DSH 对话里换背景
既然已经是一个 DSH Plugin,就不一定非得通过托盘操作。
BeautiCode 目前已经可以向 DSH 注册对应的工具和命令。
例如:
/bg D:\Videos\rain.mp4
或者切换主题:
/bg-theme 雨夜写代码
清除:
/bg-clear
进一步还可以把操作暴露成 Tool。
于是理论上可以直接告诉 Agent:
把 D:\Videos\city.mp4 设置成背景。
插件负责:
Tool Call
↓
验证路径
↓
导入媒体
↓
Apply Transaction
↓
Host Apply
↓
Live Verify
这一点我其实觉得比「动态壁纸」本身更有意思。
因为它意味着 BeautiCode 正在从:
一个外部修改工具
逐渐变成:
AI Coding 宿主内部的一项能力。
十二、为什么 Codex 和 DSH 不共用同一套注入代码?
目前 BeautiCode 同时面对两个宿主:
DeepSeek Harness
Codex Desktop
它们的扩展能力不同。
因此项目没有强行写成一个巨型:
if (dsh) ...
else if (codex) ...
目前整体大致分成:
packages/
├── core
├── adapter-dsh
└── adapter-codex
Core
只负责与宿主无关的能力:
Background Store
Media Validation
Media Server
Apply Transaction
File Lock
Paths
Types
Adapter DSH
负责:
DSH Bridge
Cordis Plugin
Host Verify
DSH Page Integration
Adapter Codex
负责:
Codex Host Discovery
CDP Connection
Renderer Injection
Host Verify
于是:
┌── adapter-dsh ── DeepSeek Harness
│
Core ────────┤
│
└── adapter-codex ─ Codex Desktop
媒体管理、安全规则、事务逻辑没有因为宿主变化而复制两份。
变化的只是:
怎么把最终状态送进 Host。
这也是这次重构之后我比较满意的地方。
十三、Codex 为什么会用到 CDP?
DeepSeek Harness 有插件接口。
Codex Desktop 并没有完全相同的扩展入口。
所以 Codex 适配走的是另一条路线:
Chrome DevTools Protocol
但 BeautiCode 并不会修改:
app.asar
官方 exe
签名
MSIX / AppX
而是尽量通过宿主已经暴露出来的调试能力连接 Renderer。
然后在运行时添加自己的 Background Stage。
例如:
#beauticode-bg-stage
├── img
└── video
整个 Stage:
position: fixed
pointer-events: none
再根据当前状态设置:
data-bc-active
data-bc-media
data-bc-video-ready
data-bc-generation
data-bc-working
data-bc-fish
然后由 CSS 和 Runtime 根据这些明确状态决定:
该显示谁
该隐藏谁
该降低多少视觉权重
而不是不停往页面上散落各种临时 Class。
这让运行时状态相对容易验证和回滚。
十四、保存主题不仅仅是保存一个文件路径
BeautiCode 还支持:
雨夜写代码
赛博城市
海边下午
番剧模式
这样的 Saved Theme。
视频主题还会记录:
videoPositionSec
也就是当前播放位置。
比如今天:
01:13:42
关掉。
明天重新选择这个主题:
seek → 01:13:42
继续播放。
这意味着 Theme 保存的不只是:
videoPath
而是一个小型的:
工作环境状态。
后续其实还可以继续扩展:
背景
+
亮度
+
音量
+
布局偏好
+
不同项目绑定
让不同项目真正拥有不同的 Coding Environment。
十五、BeautiCode 真正想做的,并不是「上班偷偷看番」
摸鱼模式当然很好玩。
但如果只把 BeautiCode 理解成:
AI 干活,我看番
其实少了一半。
我真正比较感兴趣的是另一个问题:
AI Coding 工具会不会逐渐变成开发者的新桌面?
过去我们主要待在:
IDE
Terminal
Browser
现在越来越多时间开始待在:
Claude Code
Codex
DeepSeek Harness
各种 Coding Agent
当 Agent 逐渐接管:
- 搜代码
- 改代码
- 执行命令
- 跑测试
- Debug
- 重构
开发者真正长时间注视的,可能不再只是编辑器。
而是:
Agent 的工作空间。
既然这个空间每天可能陪我们几个小时,那它是否也应该允许:
个性
氛围
审美
习惯
状态
进入其中?
有人喜欢纯黑。
有人喜欢 Cyberpunk。
有人喜欢雨夜。
有人喜欢海边。
也有人可能真的想让一部番剧慢慢在身后播放。
所以我最后给 BeautiCode 定下来的方向是:
把你喜欢的画面,放进 vibe coding 的每一分钟。
它首先必须是一个不打扰工作的工具。
然后才是一张动态皮肤。
十六、目前支持
目前 BeautiCode 主要支持:
Windows
DeepSeek Harness
Codex Desktop
JPG
JPEG
PNG
WebP
MP4
项目采用 Node.js / TypeScript 多包结构,核心逻辑与宿主 Adapter 分离。
同时提供 Windows 安装方式。
项目完全开源:
MIT License
GitHub:
https://github.com/starsstreaming/beautiCode
最后
BeautiCode 现在仍然是一个比较新的项目。
所以比起单纯增加更多背景格式,我目前更希望先把这些基础东西做好:
宿主适配稳定性
媒体生命周期
失败回滚
安全边界
DSH 插件化
安装体验
不同版本兼容
如果你愿意帮忙测试,欢迎直接下载使用。
遇到:
- Bug
- DeepSeek Harness 兼容问题
- Codex 兼容问题
- 视频播放异常
- 安装问题
- 页面显示问题
都可以直接提交 Issue。
有什么想要的功能,也欢迎提 Feature Request。
如果觉得这个项目有点意思,也欢迎顺手点一个:
⭐ Star
GitHub:
starsstreaming/beautiCode
AI Coding 工具正在变得越来越强。
我也希望它们在变强的同时,能够稍微变得更像一个属于自己的地方。
更多推荐

所有评论(0)