鲸鱼娘桌宠 · dsh-whale-desktop
把 dsh-whale-musume 的桌宠本体 从 DSH 网页里解放出来,做成 Windows 桌面上的独立挂件:
- 透明:只有鲸鱼娘本身可见,没有窗口边框、没有底色
- 置顶:浮在其它窗口之上
- 点击穿透:指针不在她身上时,鼠标事件全部穿过,不挡你操作桌面
- 零改动复用上游:立绘、动作、互动、养成、成就、小游戏、齿轮设置面板全部原样可用
- DSH 工作状态联动:思考中 / 工作中 / 完成 / 出错 会切换对应立绘与状态签, 而且按工具类型换姿势(跑 shell → 摸鱼打电话,写文件 → 开会,搜索 → 灵光一现)
- 花费播报:每个任务结束时她说一句「这次任务花了 X tokens,约 ¥Y」 (读 DSH 会话里的真实用量,按官方峰谷价估算;低于阈值不打扰)
- 余额查询:气泡里点一下就念一整句 —— 「当前余额为 CNY 41.11,状态为充裕,今日共计消耗 35.2 万 tokens,消费 0.53 元」
- 打开 DSH:右键菜单里一条「打开DSH」—— 已经在跑就只开浏览器,没跑才把它拉起来(绝不重启,免得把你正在聊的会话带走)
- 托盘常驻:显示/隐藏、置顶、跟随 DSH 开关、开机自启、缩放、重置位置、退出
这不是 DSH 插件,而是一个独立的 Electron 应用。它不依赖 DSH 运行, 也不需要浏览器标签页。DSH 那边的插件可以照常装着,两者互不干扰。 工作状态联动是只读 DSH 的会话文件(
~/.dsh/sessions/**),不往 DSH 里装任何东西。
要接手维护它?先读
docs/HANDOVER.md:现状一页纸、铁律、 自检矩阵、环境事实、踩过的坑、常见维护动作(加菜单项 / 加设置项 / 发版 / 重启实例) 都在那里。**"为什么是这样设计"**看docs/DEVELOPMENT.md; **"整体长什么样"**看docs/ARCHITECTURE-DIAGRAM.md(带排版版本:ARCHITECTURE-DIAGRAM.html)。
快速开始
npm install # 若 Electron 二进制没下来,见下方"常见问题"
npm start # 启动桌宠
npm run dev # 带 DevTools
启动后:
| 操作 | 效果 |
|---|---|
| 左键点她 | 脸红 / 爱心 |
| 快速连点三次 | 星星眼庆祝 + 粒子特效 |
| 按住拖动 | 切换「被拎起来」立绘,松手后位置自动记住 |
| 右键她 | 互动 + 唤起 DSH:投喂 / 戳一下 / 夸夸 / 两个小游戏 / 回到原位 / 打开DSH |
| 齿轮 ⚙ | 头顶浮层:天气 / 余额 两行状态 + 并排的两个入口「日常养成」「余额查询」;点任一项浮层自动收起(点天气或余额那行去设置) |
| 托盘右键 | 显示桌宠 / 总是置顶 / 跟随 DSH 工作状态 / 开机自启 / 大小 / 重置到默认位置 / 重新加载页面 / 打开数据目录 / 打开日志 / 设置… / 退出 |
| 托盘 →「设置…」 | 独立设置窗口,二级菜单:左侧 6 个分类(看板娘 · 窗口与状态 · 大小与位置 · 维护 · 天气与余额 · 花费播报),点哪一类右侧就显示哪一屏 |
| 齿轮 ⚙ →「日常养成」 | 养成窗口,五个标签页;称号与成就也在这里(不再占用托盘菜单) |
Alt+Shift+W |
显示 / 隐藏 |
| 托盘图标单击 | 显示 / 隐藏 |
分工原则:和她互动的事在桌宠右键菜单;高频全局动作与快速开关在托盘; 需要解释的配置项集中在设置窗口(按分类分屏);养成/图鉴统一走气泡里的 「日常养成」入口 —— 托盘只留"随手就要用"的东西,避免菜单越堆越长。 托盘与设置窗口共用同一份配置,任何一处改动都会同步(所有 setter 末尾都会刷新托盘菜单)。
养成 / 图鉴
入口:气泡齿轮 ⚙ →「日常养成」(托盘菜单里不再单独放称号/成就 —— 它们本来就是 养成的一部分,单独占两行只会让托盘越堆越长)。窗口内五个标签页:
| 标签页 | 内容 |
|---|---|
| 今日任务 | 每天 3 个任务(签到 / 摸头 / 跑工具…),达成后一键领取,奖励好感与心情 |
| 本周签到 | 一周七格签到表 + 连续签到天数 + 1/3/7 天里程碑 |
| 称号 | 按等级解锁的称号,可佩戴(会显示在头顶浮层里) |
| 成长日记 | 她记录下来的成长事件(本地 80 条,不上传) |
| 成就 | 39 个成就;已解锁高亮显示名称与描述,未解锁灰显并隐藏名称 |
实现上直接复用上游的纯状态机(vendor/whale/whale-moe-core.js 是 UMD,
浏览器里挂 window.DshWhaleMoeCore、Node 里可 require)——
成就表、称号表、任务池、等级与签到规则全用它,没有重写任何养成算法。
存档(养成数据、成就、位置、偏好)全部在浏览器 localStorage 里,
键名前缀 whale-moe:,存放于 Electron 的 userData 目录,关掉应用不会丢。
花费播报
每个任务结束后她用一句台词报账:这次任务花了 154.7 万 tokens,约 ¥0.0518。
| 环节 | 实现 |
|---|---|
| 用量从哪来 | DSH 会话文件里每条 assistant/message 的 usage,按 turn/start…turn/end 累加 |
| 怎么算钱 | src/price-table.js 按当前模型从本地价目表取价 → src/pricing.js 乘用量 |
| 什么时候说 | 一个 turn 正常结束且金额 ≥ 阈值(默认 0.01 元) |
| 怎么开口 | 上游没有对外的"说话"接口,壳在返回桌宠脚本时注入一段 __dshWhaleMoeSay(vendor/whale/ 磁盘文件不改) |
| 在哪配置 | 设置窗口 →「花费播报」:开关、阈值、当前模型与价格(只读)、手动配置、测试播报 |
两个必须记住的计费细节(都踩过):
- 缓存命中与未命中分开计价,价差约 50 倍;混算会高估几十倍
reasoningTokens是outputTokens的子集,不能再加一遍
已知边界(都写进注释了):不处理中国法定节假日(那几天官方按空闲计价,我们会高估一倍); 统计范围是"一个 turn 内的全部记录",子代理(subagent)消耗的 token 也计入。
单价从哪来:本地价目表,只人工刷新
价格写在仓库根目录的 pricing.json 里 —— 桌宠运行时只读这个文件,不联网。
DeepSeek 没有价格 API,官网只有一张给人看的网页表格;爬页面一旦结构变动就会静默失效,
而计费算错是最难发现的一类 bug(她照样说话,只是数字不对)。所以刷新是人工动作:
npm run refresh:prices # 抓官方页面 → 解析 → 打印 diff → 写文件(不自动提交)
npm run refresh:prices -- --dry-run # 只看结果不写文件
npm run refresh:prices -- --from-file x.html # 用已保存的页面(没网时)
脚本解析不出完整表格时会报错退出且不写文件(宁可手动改,也不写半张表)。
价目表按模型分档,并带旧模型名别名:
| 模型 | 缓存命中(空闲/高峰) | 缓存未命中 | 输出 |
|---|---|---|---|
deepseek-flash |
0.02 / 0.04 | 1 / 2 | 4 / 8 |
deepseek-v4-pro |
0.15 / 0.30 | 4.5 / 9 | 13.5 / 27 |
(元 / 百万 tokens;高峰 = 北京时间周一至周五 9:00-12:00、14:00-18:00)
当前用哪个模型是自动识别的:会话记录 request/header 里的
data.header.config.model(没读到就用 ~/.dsh/settings.yaml 的默认模型)。
所以中途换模型不用重启,设置窗口里那行"当前模型 + 价格"每 2 秒自动跟上。
设置窗口 →「花费播报」分两块:
- 当前模型 / 价格(只读):显示模型名、它在价目表里的空闲与高峰价、当前用哪一档
- 手动配置(可改):只有当模型不在价目表里时才参与计算(高峰统一 ×2)
余额查询(气泡里那个按钮)
点一下,她把三件事念成一句话:
当前余额为 CNY 41.11,状态为充裕,今日共计消耗 35.2 万 tokens,消费 0.53 元
- 余额与状态:页面里上游自己按 60 秒刷新的余额(状态词沿用上游那套 已见底 / 告急 / 偏紧 / 正常 / 充裕 / 很充裕)
- 今日共计消耗:主进程的今日账本(
src/usage-today.js)—— 按 turn 记账、落盘、跨天归零,重启不丢,启动时还会从会话文件回放当天已有的 turn 来补账(所以中途才把她叫起来也不会少算)。按会话#turn去重,回放不会算两遍; 早于今天的 turn 直接丢弃,不会把账本弄脏。 - 没启用余额 / 接口没响应时不会念一串"不可用",而是换成一句提示(去设置里开、看看接口)
- 播报类台词打完字后停留 5 秒;看完不用等,任意点击立刻收起
已知边界:账本只统计当前正在跟的那个会话(换会话前的账在旧会话文件里,不会合计); 不处理中国法定节假日(与花费播报同一限制)。
打开 DSH(右键菜单里那条)
右键她 →「打开DSH」,她会看一眼 DSH 在不在,然后:
| 情况 | 她做什么 | 她说什么 |
|---|---|---|
| 已经在跑 | 只在默认浏览器里打开它(不重启、不再起一个服务) | 「DSH 已经开着啦,这就给你打开~」 |
| 没在跑 | 把它拉起来(见下),等到监听就绪再打开浏览器 | 「DSH 起来了,浏览器已经给你打开~」 |
| 起不来 | 提前退出就立刻报错,否则等到超时(默认 60 秒),不假装成功 | 「DSH 没起来…你看看运行日志吧」 |
没在跑时用什么把它拉起来(按顺序挑第一个能用的):
| 优先级 | 用什么 | 说明 |
|---|---|---|
| 1 | 你配置的命令/脚本 | 设置窗口 →「维护」→「DSH WebUI」那一行可以填 |
| 2 | 工作空间里的启动脚本 | 程序目录或它的上级目录里的 Start-DSH-Web-Background.bat / .cmd;地址被改成非默认端口时跳过(脚本不接受 --port) |
| 3 | dsh CLI |
直接 node <dsh>/lib/bin.js web --no-open --port <端口>;找不到 bin.js 就退回 cmd /c dsh.cmd,再不行 npx @deepseek-ai/dsh |
设置窗口那一行会写清这次走的是哪条路(鼠标悬停有完整命令行与日志路径)。
几个刻意的取舍:
- 绝不重启正在跑的 DSH。它每次启动都会换一个浏览器信任 token,重启等于把你 正在聊的会话连同页面一起打断;所以"在跑"就只开浏览器,真正的启停由你决定。
- 401 也算"在跑":DSH 的 browser-trust 会拒绝没有 cookie 的请求, 而那恰恰证明服务在监听(你的浏览器里本来就有那个 cookie)。把它当"没在跑" 就会平白多起一个服务,那更糟。
- 进程交给 shell 起,不挂在自己身上:桌宠是 GUI 进程,直接 spawn 出来的
子进程会跟它共用一个隐藏控制台 —— 那个控制台随时可能被一个 Ctrl+C / 关闭事件
顺手清掉,服务就死得没头没脑(0.12.0 就是这么栽的,日志里只有一个
^C)。 现在走Start-Process -WindowStyle Hidden,服务有自己独立的隐藏窗口,桌宠退出也不牵连它。 - 拉起前摘掉
DSH_SHELL/DSH_SESSION_ID/DSH_WEB_URL:它们描述的是 "我正处在哪个会话里",对一个新起的服务是错的(谁把桌宠从 DSH 会话里启动的,这些标记 就会一路继承下去)。DSH_HOME保留。 - 浏览器由我们开(
--no-open),因为这样才能把"起没起来"回报给她; 自己拉起来的这一次还能从输出里拿到带 token 的地址并优先用它打开 (旧 token 一律 401,只有本次启动的 token 有效)。 - 地址写在配置里(默认
http://127.0.0.1:3080),设置窗口 → 维护 →「DSH WebUI」 里可以直接改,也能从那儿点「打开」。 - 拉起过程的输出(含 token 地址)落在数据目录的
dsh-web.log;用启动脚本那条路时, 日志在脚本旁边的同名文件(例如D:\DSWorkspace\dsh-web.log)。
项目结构
dsh-whale-desktop/
├─ src/
│ ├─ main.js 主进程:窗口/托盘/IPC/自检探针
│ ├─ preload.js contextBridge 通道(页面拿不到 node)
│ ├─ server.js 本地静态服务器:/assets/* → vendor/whale/*(+ 说话钩子补丁)
│ ├─ dsh-state.js 读 DSH 会话文件 → 工作状态 + turn 用量 + 当前模型
│ ├─ pricing.js 怎么乘(纯函数)
│ ├─ price-table.js 单价从哪来(读 pricing.json,按模型 + 峰谷取价)
│ ├─ usage-today.js 今日消耗账本(落盘、跨天归零、按 会话#turn 去重)
│ ├─ open-dsh.js 打开 DSH:探活 / token 地址 / 启动计划 / 交接起进程(纯 node)
│ ├─ balance-proxy.js 内置余额代理(自动读 DSH 里的 DeepSeek Key)
│ └─ pet/
│ ├─ index.html 只负责按顺序引入上游三个文件
│ ├─ shell.css 透明画布 + 选中/滚动等宿主适配
│ ├─ shell.js 点击穿透判定 + 头顶浮层 + 日志回传
│ ├─ settings.* 独立设置窗口
│ └─ growth.* 养成 / 图鉴窗口
├─ pricing.json 本地价目表(运行时只读,人工刷新)
├─ vendor/whale/ 上游桌宠素材(MIT,见 THIRD-PARTY.md)
├─ scripts/
│ ├─ sync-upstream.mjs 从上游 tag 同步素材
│ ├─ check-vendor.mjs 素材完整性校验
│ ├─ verify-shell.ps1 点击穿透自动化验证(真鼠标)
│ ├─ open-dsh-e2e.ps1 「打开DSH」端到端(真鼠标点菜单项,隔离 DSH_HOME)
│ ├─ cold-start-check.mjs 真机冷启动 dsh web(独立 DSH_HOME + 空闲端口)
│ ├─ check-desktop-input.ps1 这套环境能不能驱动鼠标(真鼠标类脚本的前置检查)
│ ├─ test-cost.mjs 计价 / 账本 / 价目表单测(npm run test:cost)
│ ├─ refresh-prices.mjs 刷新价目表(npm run refresh:prices)
│ └─ push.ps1 走代理推送
└─ docs/
├─ HANDOVER.md **接手维护先看这份**(现状/铁律/自检/环境/坑)
├─ DEVELOPMENT.md 架构细节与调试方法
├─ ARCHITECTURE-DIAGRAM.md 一页看完的**项目框架图**(拓扑/数据流/职责/自检)
└─ ARCHITECTURE-DIAGRAM.html 同一份框架图的**带排版版本**(浏览器打开)
上游素材一个字都没改——这是刻意的,见 docs/DEVELOPMENT.md 里"为什么不需要改上游"。
常见问题
npm install 之后启动报找不到 Electron
npm 11+ 默认拦截依赖的生命周期脚本,Electron 的二进制就下不来。手动补一次:
node node_modules/electron/install.js
鲸鱼娘没出现 / 位置跑到屏幕外 托盘右键 →「重置到默认位置」。
天气不显示
设置面板里城市留空时完全不联网;填了城市会请求 api.open-meteo.com。
余额那里报错
默认走内置余额代理(127.0.0.1:3020,随桌宠启停,Key 自动从
~/.dsh/.credentials.yaml 的 DEEPSEEK_API_KEY 读取),不需要手工配置。
只有在你把余额接口改成别的地址、而那个服务又没起来时才会报错。
设置窗口 →「天气与余额」→「测试」会直接把接口返回摊开给你看。
点了「打开DSH」没反应 / 浏览器里是 401 先看服务自己的日志(其中 token 地址就在里面):
- 没在跑时需要它自己起来:日志在数据目录的
dsh-web.log(用工作空间启动脚本那条路时, 日志在脚本旁边的同名文件,例如D:\DSWorkspace\dsh-web.log), 失败原因也会写进shell.log(托盘 →「打开日志」)。设置窗口 →「维护」→「DSH WebUI」 那一行会告诉你走的是哪条路,想手工试就跑一遍同样的命令。 - 已经在跑却打不开:多半是浏览器丢了 DSH 的信任 cookie(有效期 30 天)。 让 DSH 重启一次会打印带 token 的地址,用它打开一次就能重新拿到 cookie; 桌宠不会替你重启(那会打断你正在聊的会话)。
真鼠标的验证脚本(verify:shell / e2e:open-dsh)不是"过时不过"
它们会真的移动系统指针;锁屏 / RDP 断开 / 快速用户切换时 SetCursorPos 会被拒,
表现就是"鼠标移不到她身上"。先跑前置检查看退出码:
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/check-desktop-input.ps1
退出码 0 = 能驱动(可以跑那两条),3 = 环境不可驱动(只跑无头探针,并在结论里写明)。
无头探针(test:cost / open-dsh:probe / settings:probe / menu:probe 等)不受影响。
git push 报 Recv failure: Connection was reset
这台机器上 GitHub 的 API 与 codeload 都直连正常,但 git 的 push 端点
(git-receive-pack)会被重置。用仓库自带的推送助手:
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/push.ps1 # 检测到本机 Clash 代理就自动走代理
它只对本次命令生效,不写进 git 全局配置。也可以手动:
git -c http.proxy=http://127.0.0.1:7890 push origin main
路线图
- 多显示器支持(当前铺满主显示器工作区)
-
electron-builder打包成免安装 exe - 全局快捷键自定义
- 单实例之外的"每屏一只"
- 花费播报:节假日按空闲价、按会话区分主/子代理用量
许可
- 本项目代码:MIT
- 上游桌宠素材:MIT,版权归 Sutera-Diffusus 及其贡献者,详见
THIRD-PARTY.md与vendor/whale/LICENSE
No comments yet. Be the first to write one.