3757 words
19 minutes
给 DeepSeek Harness Web 界面加一只 Live2D 桌宠

Static Badge Static Badge Static Badge Static Badge Static Badge Static Badge Static Badge

dsh-live2d-widget#

项目地址:dsh-live2d-widget

给 DeepSeek Harness Web 界面加一只 Live2D 桌宠:打开 DSH 就能看到可爱的二次元角色站在聊天界面角落。 内置 4 个角色可自由切换,点她会弹出聊天气泡说符合人设的台词(台词可自己改), 也支持随时导入自己的模型。零运行时依赖,纯插件挂载 —— 安装即启用,卸载不留痕。

功能亮点#

  • 可爱的内置角色 — 纱雾、雷姆、伊斯特、缇娅,覆盖 Cubism 2 与 Cubism 4 两代运行时。
  • 点击说话 — 点模型弹出一句符合角色人设的台词,气泡带角色名;每个角色的台词都能自己改(一行一句,存到本地)。
  • 拖动摆位 — 按住模型直接拖到屏幕任意位置,松手即记住,重启后还在。
  • 自由切换 — 面板点一下即换模型;每个模型各自记住自己的适配、位置与台词。
  • 导入自己的模型 — 支持 .zip、整个模型文件夹、或直接填本机目录路径;按内容识别入口文件,所以 model.json、rem.json、自定义文件名都能认出来。
  • 可控的大小 — 「大小」调的是画布,模型等比放大而永远不会被裁掉;「适配」单独控制模型在画布里的占比。
  • 精确输入 — 每个数值都同时有滑块和输入框:可以拖,也可以直接敲数字,还能按住数字左右拖动微调。
  • 不打扰办事 — 默认只占左下角,可隐藏(Ctrl+Shift+L),可穿透点击聊天界面。
  • 零运行时依赖 — Live2D 运行时随包发布(单个 JS 文件),不联网、不拉 CDN;解压也用自带的零依赖实现。
  • 不污染 DSH — 整只挂件活在 Shadow DOM 里,样式与 DSH 和其他插件互不影响。

快速开始#

前置条件:已安装 DeepSeek Harness(dsh 命令可用) 与 Node.js ^22.19 || >=24(与 DSH 自身要求一致)。

三种安装方式,任选一种:

Terminal window
# ① 从 GitHub 安装(推荐:装完即有 4 个内置角色)
dsh plugin --profile web add github:xieluyang912/dsh-live2d-widget
# 等价的显式 git 写法
dsh plugin --profile web add git+https://github.com/xieluyang912/dsh-live2d-widget.git
# ② 从 npm 安装
dsh plugin --profile web add dsh-live2d-widget
# ③ 本地目录安装(开发用,pnpm 会建 junction,改代码即生效)
dsh plugin --profile web add link:D:/Plugins/dsh-live2d-widget
# 卸载
dsh plugin --profile web remove dsh-live2d-widget

dsh plugin 是 pnpm 的透传(--profile web 之后的参数原样交给 pnpm), 所以 pnpm 支持的写法(github:、git+https:、npm 包名、link:、tarball)都能用。

安装命令会自动把插件写进 %USERPROFILE%\.dsh\profiles\web\package.json 的 dsh.profile.bundles,不需要手工改 profile patch。

从 git / npm 安装时不需要编译:本插件是纯 JavaScript,没有构建步骤, 也没有 install / prepare 生命周期脚本,所以不会被 pnpm 的 ERR_PNPM_IGNORED_BUILDS 拦住,装完即可用。

改完插件要重载才生效 —— 两半的加载方式不一样。

  • 浏览器那半(assets/live2d-widget.js、assets/vendor)每次请求都从磁盘重读,刷新页面即可。
  • Host 那半(lib/index.js、内置模型清单与模型文件)启动时读进进程内存,必须重启 dsh web。 DSH 在 agent 运行期间会拒绝重载插件(插件市场日志写的是 refused while agents are running), 所以要等当前会话结束或手动重启。

判断自己在看哪一版:面板里出现了「大小 / 适配 / 点击台词」但模型列表还是旧的, 就是浏览器半边已更新、host 半边还是旧的 —— 重启一下就好。

用法#

打开 DSH 后左下角出现角色,右上角有个圆形 L2D 按钮,点它展开控制面板。

区块能做什么
模型点任意一项立即切换;「导入」标记的是你自己导入的模型,右侧 × 可删除
大小整体大小(0.4× ~ 2.2×)。调的是画布,模型跟着等比放大,永远不会被裁掉
适配模型在画布里的占比(0.1 ~ 3.0)。主要给导入的模型微调用;内置角色已标定好,保持 1 即可
左右 / 上下模型在画布内的位置微调(−2 ~ 2)
透明整体透明度(15 ~ 100)
左右翻转 / 换边镜像、在左下角和右下角之间切换
随机动作 / 随机表情手动逗一下模型(模型没有动作或表情时按钮自动置灰)
恢复默认把当前模型的大小、适配、位置和外观重置回内置默认值
隐藏模型收起来,Ctrl+Shift+L 恢复
点击台词折叠区块:气泡开关、停留时长、台词编辑
导入自己的模型折叠区块:zip / 文件夹 / 本机路径三种导入方式

数值怎么调#

每个数值都有三种调法,三者永远同步:

  1. 拖滑块 —— 最直观。
  2. 直接输入 —— 在右边的输入框里敲数字,回车或点开别处生效。超出范围会自动收到边界,敲了非法字符会退回原值。
  3. 按住数字左右拖 —— 在输入框上按住横向拖动即可微调(拖满约 320px 走完整个范围),松手时如果没拖动就进入输入模式。

点击台词#

  • 展开「点击台词」,勾选 点击模型时说话,点模型就会弹气泡说一句随机台词。
  • 文本框里 一行一句,点「保存台词」生效;「恢复默认台词」回到该角色的内置人设台词;「试说一句」立即预览。
  • 「停留」控制气泡显示多久(2 ~ 20 秒)。
  • 台词 按模型分别保存,可以给每个角色写符合自己口味的话。
  • 切换模型时也会自动打个招呼。

内置台词都是按角色写的,例如雷姆会说「雷姆,会一直相信你的。」、 纱雾会说「哥哥,你回来啦……」,伊斯特会说「知识,就是力量哦。」

拖动与快捷键#

按住模型直接拖到想放的地方,松手自动记住。拖动不会误触发点击台词。 放到屏幕上方也没问题,面板会自动收缩以免超出屏幕顶部。

操作效果
拖动模型移动位置(自动记住)
单击模型说一句台词
Ctrl+Shift+L显示 / 隐藏模型
点 L2D 按钮展开 / 收起控制面板

导入自己的模型#

「导入自己的模型」区块提供三种方式,都会自动去掉压缩包 / 文件夹的最外层目录:

方式适合
选择 zip 压缩包最常见;模型包通常就是 zip
选择模型文件夹模型已经解压在磁盘上
填写本机模型目录大模型:由插件直接从磁盘读取,不经过浏览器上传

入口文件是 按内容识别 的:Cubism 3/4/5 认 FileReferences.Moc,Cubism 2 认顶层的 model + textures。所以入口叫 model.json、rem.json、chara-def.json 还是别的名字都能认出来, 不必符合命名规范。同一个目录里有多个入口时(例如同一角色的多套衣服), 内置模型用显式 entry 指定,导入的模型可以先只放要用的那一套。

模型版权归原作者所有。请自行确认你有权使用所导入的模型(很多模型禁止商用或二次分发)。 本插件只提供加载能力,不附带任何你导入的模型。

内置角色#

角色出处Cubism
纱雾 · 埃罗芒阿老师埃罗芒阿老师2
雷姆 · Re:从零开始Re:从零开始的异世界生活2
伊斯特 · 海王星海王星2
缇娅 · Live2D 官方示例Live2D 官方示例模型2

模型资源取自公开合集 Eikanya/Live2d-model, 著作权归各自权利人,不适用本插件的 MIT 许可(详见 LICENSE)。

工作原理#

插件分 host 与 browser 两半:

DSH web app
│ tapIndex 注入 <script defer src="/dsh-live2d/widget.js">
▼
browser 半(Shadow DOM:画布 + 气泡 + 控制面板)
├─ GET /dsh-live2d/runtime.js Live2D 运行时(随包发布,单文件自足)
├─ GET /dsh-live2d/api/state 模型清单 + 设置 + 被跳过的模型
├─ POST /dsh-live2d/api/settings 持久化偏好
├─ POST /dsh-live2d/api/import-zip 原始 zip 上传
├─ POST /dsh-live2d/api/import-files 文件夹上传(base64)
├─ POST /dsh-live2d/api/import-path 直接读本机目录
├─ POST /dsh-live2d/api/delete 删除导入的模型
└─ GET /dsh-live2d/model/<id>/<相对路径> 模型文件(prefix 路由 + ETag)
▲
│ 每一条路由都先过 ctx.connection.requestRejection()
host 半(lib/index.js,跑在 DSH 进程里)
  • Host 半 用 ctx.webServer.register 注册路由、用 ctx.webServer.tapIndex 注入脚本; 模型文件走一条 prefix 路由,路径经过 .. 过滤 + realpath 校验,越权访问直接 403。 大贴图用 ETag + Cache-Control: immutable,浏览器只回源一次。 入口文件为 0 字节(文件损坏)的模型会被显式跳过,并在 /api/state 的 skipped 里说明原因, 而不是让运行时抛出难以理解的 Unexpected end of JSON input。
  • Browser 半 整只挂在 Shadow DOM 里;只在主聊天界面初始化(见下);层级取 z-index: 90, 在聊天内容之上、DSH 的浮层与弹窗之下。

四个踩过的坑(都写在代码注释里)#

  1. 运行时只 init() 一次,切模型靠重复 load()。 l2d 的 destroy() 不释放 WebGL 上下文, 每切一次就 init() 一次会一路泄漏上下文,直到浏览器把最早的丢掉、模型变白。
  2. 画布的 CSS 尺寸必须由插件自己写。 运行时的引导函数会在「计算尺寸 == 属性尺寸」时把 style.width/height 冻结到画布上;一旦冻结,画布就再也不跟随外层容器, 表现为「改大小没反应 / 模型被裁」。所以 applyStyles() 每次都显式写 canvas.style.width/height。
  3. 尺寸不能靠放大模型。 运行时先把模型适配到画布,scale 是相对这个适配的倍率, 超过标定值的 scale 只会把角色裁掉,不会让他变大。因此「大小」调画布,「适配」调占比。
  4. 挂载时机必须观察,不能假设。 DSH 启动会先往 #root 画一层「Loading plugins…」, 所有插件加载完才挂 composer。所以用 MutationObserver 持续观察(最长 5 分钟) 而不是等几秒就放弃 —— 否则冷启动慢一点就静默没有模型。

默认值怎么来的#

Live2D 模型没有统一尺寸:每个作者在自己的画布里按任意比例作画, 所以一个全局默认值不可能让所有模型都好看。tools/calibrate.html 会依次加载每个模型、 读回画面像素、迭代调整 scale/position,直到轮廓居中且填满画布高度 —— 内置 4 个角色的默认值就是这样量出来的(目标 88% 高度, 留出 12% 余量吸收待机动作带来的轮廓抖动)。

数据与设置#

都在 %USERPROFILE%\.dsh 下,重装插件不丢:

路径内容
.dsh-live2d.json当前模型、大小、外观、每个模型的适配与位置、每个模型的自定义台词、已导入模型清单
live2d-models/<id>/你导入的模型文件

权限与安全边界#

  • 每条自定义路由都先过 ctx.connection.requestRejection():Host/Origin 被伪造(DNS 重绑定) 或未认证的请求一律拒绝,和官方插件同一套信任栅栏。
  • 模型文件路由做 .. 过滤 + realpath 校验,符号链接也无法越出模型目录。
  • 导入同时限制文件数量、单文件体积与解压后总体积,防止 zip 炸弹。
  • 插件不自带沙箱:文件、Shell、sandbox 与 approval 策略完全沿用当前 DSH profile。
  • 插件不联网:运行时随包发布,模型从本地磁盘读取。

已知限制#

  • 内置 4 个角色都是 Cubism 2;Cubism 3/4/5(*.model3.json)由运行时支持,可通过导入使用。
  • 「适配」超过 1 会让模型超出画布而被裁切 —— 那是这个控件的本意(给画得很小的导入模型放大用), 想要整体变大请用「大小」。
  • 面板要挤在模型上方那块空间里;空间不够时(模型很大或窗口很矮)会自动改成覆盖在模型上显示。
  • 隐藏模型只是视觉隐藏,运行时的渲染循环仍在跑;想彻底停掉请用「隐藏模型」+ 关掉页面。
  • 音频静音:模型的待机动作若自带音效,播放音量为 0。

常见问题#

模型不见了 / 想找回来 —— 按 Ctrl+Shift+L;或删掉 .dsh-live2d.json 回到默认状态。

点了模型不说话 —— 检查「点击台词」里的开关;台词清空后会显示占位台词「……」。

导入报「没有找到 Live2D 模型文件」 —— 包里既没有 *.model3.json / *.model.json, 也没有任何含 model + textures(或 FileReferences.Moc)的 json。 有些包只放了贴图和 .moc3,缺模型描述文件,这种包本身不完整、任何加载器都用不了。

导入报「不支持压缩算法」/「该 zip 已加密」 —— 换一个未加密的普通 zip; 自带解压器支持 store 与 deflate,不支持加密和分卷包。

模型显示成一片空白 / 提示入口文件为空 —— 文件在复制过程中被截断成了 0 字节。 重新复制一次模型文件即可;插件会把这类模型列进 skipped 并说明原因。

面板里少了几项 —— 「点击台词」和「导入自己的模型」默认折叠,点标题栏展开。

开发#

Terminal window
# 独立调试服务:把插件挂在桩 ctx 上,用普通 HTTP 暴露(无 DSH 鉴权,方便调试)
node tools/harness.mjs 8899

harness 会把 DSH_HOME 指到自己的临时目录($env:TEMP\dsh-live2d-harness-home), 不会读写你在用的 DSH 的设置;需要时可用 DSH_HARNESS_HOME 覆盖。

URL用途
http://127.0.0.1:8899/模拟聊天页,看挂件
?panel=1自动展开控制面板
?speak=1持续显示气泡(方便截图)
?ce=1用 contenteditable 当输入框(DSH 真实形态)
?boot=1先显示 9 秒启动遮罩,再出现输入框(验证挂载门限)
?diag=1量化自检:画布尺寸、模型实际绘制尺寸、拖动、气泡、数值控件
/tools/calibrate.html为新模型标定默认 scale / position

改 assets/live2d-widget.js 后直接刷新页面即可;改 lib/index.js 需要重载插件。

目录结构#

dsh-live2d-widget/
├── cordis.patch.yml # bundle 挂载声明
├── package.json # dsh.bundle.patch 指向 cordis.patch.yml
├── lib/
│ ├── index.js # Host 半:路由、模型注册表、导入、设置持久化
│ └── zip.mjs # 零依赖 zip 解压(store + deflate + Zip64 + GBK 文件名)
├── assets/
│ ├── live2d-widget.js # Browser 半:渲染 + 气泡 + 控制面板 + 导入 UI
│ ├── vendor/l2d.min.js # Live2D 运行时(Cubism 2 & 6 内置,单文件自足)
│ └── models/<id>/ # 内置模型
├── docs/ # 截图与 README 素材
└── tools/ # 开发用,不随包发布
├── harness.mjs
└── calibrate.html

致谢#

Cubism SDK 的使用须遵守 Live2D Proprietary Software License。 商业用途请自行确认是否需要向 Live2D Inc. 申请许可。

License#

MIT(插件代码)。内置模型与 Cubism SDK 的授权见上文「致谢」与 LICENSE。

给 DeepSeek Harness Web 界面加一只 Live2D 桌宠
https://007912.xyz/posts/chajain/
Author
Xieluyang
Published at
2026-09-29
License
CC BY-NC-SA 4.0
Comments