# dsh-agent-whale-girl > 本文是中文开发文档。功能概览与安装见 [README.md](README.md):本包已声明 > `dsh.bundle`,可直接 `dsh plugin --profile desktop add github:Turanbuturan/dsh-agent-whale-girl` > 安装,不必手工改 profile 补丁。下面「安装 / 卸载」一节写的是手工 drop-in 方式,仍然有效。 社区二创的「鲸鱼娘」(Whale-chan)形象,作为 DSH Web GUI 的品牌标记,外加一个可 **拖动**、可 **收起**、会 **看你**、会 **说话**(实时用真实数字播报)的看板娘。 ## 两个插槽 | 位置 | 插槽 | 效果 | | --- | --- | --- | | 侧边栏品牌行(展开态 + 收起轨道) | `sidebar.brand.mark` | 官方小鱼 logo 换成鲸鱼娘**脸部特写**(`avatar-face.png`)。单值插槽不允许同优先级重复注册,因此以 `priority: -1` 遮蔽官方品牌插件(优先级最低者渲染);品牌名文字(`DSH 本地构建`)不在此插槽内 | | 整帧浮层 | `shell.overlay` | 220×298 的看板娘 + 上方 token 卡片 + 台词气泡;见下 | ## 交互(全部四类 + 心情) | 操作 | 行为 | | --- | --- | | **鼠标靠近她** | **视线跟随**:头朝光标转(±7° 上限)、尾巴反向摆(±4.8°),离开后 0.22s 内回到正中。**基准是她脸上实测的眼睛/睫毛质心**(art 坐标 `220.9, 163.8`),所以「正对着她的脸」= 0° | | **按住 ≥ 350ms** | **摸头**:一次性「挤压回弹」动画(WAAPI,320ms)+ 专属台词;**不会**收起她 | | **快速单击(< 350ms)** | 收起 / 展开(4px 阈值内算点击,不会和拖动混淆) | | **连点 3 / 4 次** | 连击播报:「已经点我 N 次了哦」 | | **连点 5 次** | **彩蛋**:一次性蹦跳 + ✨ + 「喷水给你看!」;同一次连击内只触发一次 | | **拖动** | 按住拖到帧内任意位置;位置存 `localStorage['whale-girl:position']`,刷新后保持 | | **双击** | 回到出厂的右下角,并清除位置记忆 | | **窗口变小** | 自动夹回可视区,不会被甩出屏幕 | | 鼠标 / 触屏 / 手写笔 | 统一 Pointer Events + `touch-action:none`;非起始指针的抬起事件被 id 守卫丢弃 | | 收起态 | 64px 圆泡(同一张脸部特写),卡片与台词气泡仍在 | **心情刻度**:`data-mood` 由上下文占用率决定 —— `<50%` = `calm`、`≥50%` = `tense`(偏冷)、`≥85%` = `crit`(泛红)。每次跨档会掉一颗 💦 在她右上角,2.6s 后消失。CSS 只对**头部图层和尾巴**做滤镜,身体不动,避免接缝色差。 ### 关于动效的边界 她只在**被交互时**动:`@keyframes` 只有三条一次性动画(`wg-pat` / `wg-egg` / `wg-emote`),全部 `1` 次、无 `infinite`、无 `alternate`、无 `will-change`。空闲时 `getAnimations()` 为 0(真实浏览器实测)。骨架振幅仍留在 `layers.json`(`head_amp` / `tail_amp`),注视幅度由它推导(×5 / ×1.4 / ×1.6)。 ## 台词系统(她说什么) 台词表就是组件里的一个对象,**改文案 = 改这个对象**: ```js const LINES = { hello: ['我在这儿呢~', '今天也一起加油吧', '需要我帮忙吗?'], busy: ['在写了…', '马上就好', '让我想想…'], done: ['写完啦!', '搞定~', '要不要看看结果?'], click: ['诶?', '在的!', '怎么啦'], progress: ['已经写了 {out} 了…', '还在写,已经 {out}', '快了快了,{out} 了'], warn: ['上下文用到 {pct}% 了', '上下文快到顶了({pct}%)'], pat: ['呜…好痒', '别摸头啦', '诶嘿…', '头发会乱的'], combo: ['已经点我 {n} 次了哦', '诶…还要点?({n})'], combo5: ['好啦好啦,喷水给你看!', '呜哇!别戳啦~', '被你点晕了…(转圈)'], }; ``` | 触发 | 时机 | 台词 | | --- | --- | --- | | `hello` | 挂载后 1.5s,说 5s | 打招呼 | | `click` | 点她(收起/展开) | 应答 | | `progress` | **实时播报**:每 1s 检查,Token 在最近 4s 内有增长就算「在干活」,每 3.5s 换一句 | 带真实数字,`{out}` = 当前输出量 | | `warn` | 上下文 ≥ 80% 时提醒一次(每会话一次,且等这轮说完再插话) | `{pct}` = 当前上下文占用 | | `done` | 停止增长 2.5s 后 | 收工 | - 占位符由 `wgFill()` 在**说话时**用真实数字填充:数字来自已经在订阅的会话投影 `tokenUsage` / `contextPressure`。台词是模板 + 本地数字拼接,**她说话本身不联网、不调模型、不额外请求**;真正消耗 token 的是你我在对话里的模型推理,与本插件无关。 - 呈现:`.wg-say` 气泡在她**上方**(悬停标签在左侧,互不打架),3–6s 自动消失,带 `aria-live="polite"`。 - 节奏常量:`TICK_MS=1000`、`WORKING_MS=4000`、`BUSY_EVERY_MS=3500`、`WARN_PCT=80`、`LINE_MS=4000`。嫌吵就调大 `BUSY_EVERY_MS` / `WORKING_MS`。 - `error` 类台词没做:当前没有可靠的失败信号,硬凑会变成假报错。 ## 三层皮影骨架 三层(身体 / 头 / 尾)仍然保留:静止时与原立绘**逐像素一致**(alpha 差 0、颜色均值 ≈2.1/255),所以它们是"隐形"的,只在交互时显形。头与尾各有**实测**旋转支点(不是猜的): - 头:`transform-origin: 53.5% 46.62%` —— 由头部图层的左右镜像轴算出(153/286 px) - 尾:`transform-origin: 17.23% 116.6%` —— 藏在裙摆后的鳍根 支点写在 `layers.json` 的 `pivots` 里,自检会断言 CSS 用的是这两个值。 **注视基准**(她看的方向,与转动支点是两件事)写在 `layers.json` 的 `gaze_center`:art 坐标 `220.9, 163.8`,是 `avatar-head.png` 里眼睛/睫毛像素的质心(由 `anim/layers.py` 的测量脚本得出)。用包围盒中心会偏 21px/44px —— 那个盒子连头发和肩膀都算进去了。两个值都在**美术坐标系(414×560)**,换算到客户端像素时用的是舞台的缩放比。 ## token 卡片 只显示当前会话的用量,数字来自 DSH 自己的会话投影,因此与输入框右下角的官方指示器完全一致: - `contextPressure` → `projectedTokens`(回退 `pressureTokens`)与 `contextWindow` → `上下文 xx%` + 进度条、`占用 tokens / 窗口` - `tokenUsage` → `输出`、`输入 / 缓存读` 没有可用会话时卡片隐藏(不显示假零值);全新会话报 0 时仍显示(窗口大小仍有意义);窗口窄于 900px 时卡片让位。 > 账户余额功能**已删除**:`remote.account` 在部署里没被投放(客户端看不到这个面),而 API Key 钱包那条路(宿主半边查 `api.deepseek.com/user/balance`)被实测证明该 Key 已被上游拒绝(401 invalid),所以整块拆掉了。 ## 文件 | 文件 | 作用 | | --- | --- | | `package.json` | `dsh.client.platform = "web"`,让 DSH 把 `./client` 作为浏览器 bundle 投放 | | `index.js` | 宿主半边:**空实现**(只负责能被 Loader 载入) | | `lib/client.js` | 构建产物:`window.__ModuleLoader__.load({...})` 注册 + 两个插槽注册 | | `lib/client.css` | 构建产物:样式表原文;自检断言「bundle 真正注入的文本 == 这个文件」 | | `build.mjs` | 由四张 PNG + `layers.json` 生成上面两个产物(内联图片、把骨架几何写进 CSS) | | `layers.json` | 皮影骨架:每层裁切框、旋转原点(原始 414×560 坐标系)、动作幅度、`pivots`(实测支点) | | `verify-bundle.cjs` | 结构 + 行为自检(见下) | | `avatar-body.png` / `avatar-head.png` / `avatar-tail.png` | 皮影三层 56 KB / 31 KB / 3 KB | | `avatar-face.png` | 脸部特写 96×96,用于 24px 标记与收起态圆泡 | | `avatar.png` | 拆骨前的整身立绘(渲染回归的参照图) | | `assets/` | 原始素材(抠好背景的 720×974 全身图、上一版 Q 版头像等) | ## 自检覆盖什么 分两层,因为「DOM 替身」无法诚实地回答「浏览器到底画成什么样」。 **结构 + 单元(`verify-bundle.cjs`,纯 Node)** - 注册 id / `require` 白名单 / `inject` / 两个插槽 / 样式只插一次 - 四张内联图与源文件字节一致 - **注入的 CSS 与 `lib/client.css` 逐字节相同,且不是被引号包住的字符串**(这条断言曾经救过一次线上事故:整页样式全丢) - 每层的 CSS 定位、图层顺序、`wg-l` 定位类与 `layers.json` 一致;支点用 `pivots` 的实测值 - **动效边界**:`@keyframes` 恰好三条、名字必须是那三条、不许 `infinite` / `alternate` / `will-change` - **注视数学**:直接调用 bundle 的 `wgGaze`,断言「指针在她的**脸上** → 0°」「向右看 → 正角度」「尾巴反相」「无指针 → 全零」「不会超过幅度上限」「基准不是包围盒中心」;探测点由脸中心的 art 坐标推导,而不是硬编码客户端像素(硬编码会在基准移动时静默变成另一个方向) - **注视写入**:断言数字真的落到头部图层的 `--sg` / `--st` - 行为:token 卡片(有用量 / 全新会话零值 / 无会话)、单击出话、问候定时器、心跳带真实数字、**连击阶梯(第 3 次有计数、第 5 次出彩蛋、同一轮不重复)**、**按住 400ms 出摸头动画且不收起她**、心情三档取值 - 时间语义:替身的 `setTimeout` 是**排队 + 手动推进**,不是立即执行 —— 否则「900ms 后结束连击」会在同一次点击里触发,连击永远到不了 3(这个坑真出现过) **真实浏览器(`cdp-interact-live.mjs`,Chrome DevTools Protocol)** 在真实页面上用真鼠标事件驱动,并**在线标定**她头部的实际转动中心(不靠手算),断言:三层在屏、**9 点扫描全程单调**(`-3.74° → +3.23°`,0 次反向)、尾巴在每个采样点都反相、**扫描零点与 bundle 自报的脸中心重合**(实测差 0.1px)、按压产生一条真实的 320ms 一次性动画、**空闲时 `getAnimations()` 为 0**、`data-mood` 合法。当前状态:`LIVE PASS`。 > 探针的取点必须落在**按钮自己的盒子内**:超出范围的坐标根本不会送达,读到的还是上一次的值,看起来就像"功能坏了"。同理,`getComputedStyle(el).getPropertyValue('--sg')` 对未设置的自定义属性返回**继承来的空值**,要读内联样式(`el.style.getPropertyValue`)。两个坑都踩过。 ## 安装 / 卸载 装载条目写在 profile 的用户补丁层 `~/.dsh/profiles/desktop/cordis.patch.yml`: ```yaml - insert: - id: whale-girl name: "./plugins/dsh-agent-whale-girl/index.js" ``` - 路径相对 profile 目录解析(Loader 的 `baseUrl` 锚在 profile 目录上)。 - 卸载:删掉这个 `insert` 条目,保存后刷新页面。 - **客户端改动**:刷新页面即可生效;**宿主半边改动**:必须重启应用 —— Node 的 ESM 缓存会钉住同一个文件路径,profile 热重载不会重新 import 它。 ## 重新构建 ```powershell $py = "$env:USERPROFILE\.dsh\dsh-runtimes\dsh-primary-runtime\dependencies\python\python.exe" $node = "$env:USERPROFILE\.dsh\dsh-runtimes\dsh-primary-runtime\dependencies\node\bin\node.exe" # 1) 换图或调骨架参数后,重切三层并导出到本目录(会打印 REST exactness,应为 0) & $py "$env:USERPROFILE\Documents\deepseek-harness\default-workspace\whale-girl\anim\layers.py" --export # 2) 生成产物并自检 & $node build.mjs & $node verify-bundle.cjs lib/client.js # 3) 真实浏览器验证交互(需先签 cookie;退出码 0 = LIVE PASS) & $node "$env:USERPROFILE\Documents\deepseek-harness\default-workspace\whale-girl\mint-cookie.mjs" & $node "$env:USERPROFILE\Documents\deepseek-harness\default-workspace\whale-girl\cdp-interact-live.mjs" 9341 "$env:TEMP\wg-live" (Get-Content "$env:TEMP\dsh-cookie.txt" -Raw).Trim() 'http://127.0.0.1:19387/' "$env:TEMP\live.png" ``` `layers.py --dump` 会逐行打印鲸尾蒙版的 x 范围,方便校形;`sim-*.png` 让穿帮在浏览器之前就暴露。 ### 工作室(工作区 `whale-girl/`) | 文件 | 作用 | | --- | --- | | `anim/layers.py` | 切层 / 抠洞 / 补洞 / 合成模拟;`--export` 写进插件 | | `anim/shots.py` | 探针截图拼成对照表 + 与静止立绘做像素比对 | | `harness.mjs` | 用 `lib/client.css` + 四张图 + 等价 DOM 生成 `anim/harness.html`(绕开登录态测量/截图) | | `cdp-anim.mjs` | 探针:样式表是否真生效、图层定位、静态性、悬停、reduced-motion、静止帧与原图对比 | | `cdp-drag.mjs` | 拖动 / 位置记忆 / 单击收起 / 双击复位 | | `cdp-interact-live.mjs` | **真实浏览器交互验证**:在线标定转动中心、视线跟随单调性、尾巴反相、按压动画、空闲零动画、心情 | | `cdp-eval.mjs` | 在真实页面里跑任意表达式(配合 `anim/probe-*.js`) | | `mint-cookie.mjs` / `probe-auth.mjs` | 用凭据库里宿主自己的 `client-connection/browser-session` 秘钥现签会话 cookie(宿主的 cookie 有效期很短:**1 小时有效、6 小时会被拒**) | | `anim-hunt/FINDINGS.md` | 动画素材调研:三个候选仓库都无动画帧;另有 Live2D 模型(CC BY-NC-SA,非商用)与精灵图(无 LICENSE) | | `PLAN-lines.md` | 台词系统的设计稿(已实现,留作后续扩展参考) | ### 踩过的坑(都留了断言) 1. **注入 CSS 多了一层引号** —— `const CSS = '"…"'`,浏览器解析出 0 条规则,页面变成裸 DOM。现在自检逐字节比对注入文本与 `lib/client.css`。 2. **`