- Accueil
- Plugins
- Juste pour le plaisir
- softpet-plugin
softpet-plugin
zhanjing617/softpet-plugin
Installer
dsh plugin --profile web add github:zhanjing617/softpet-pluginREADME
SoftPet · 治愈桌宠
一只不打扰、会呼吸、零压力的小家伙。她住在 DSH 的界面浮层里,默认完全离线:不配任何 API Key,也能待机、互动、说气泡、陪你工作。
- 待机:呼吸起伏、随机眨眼、视线漂移、呆毛和尾巴的次级摆动
- 互动:摸头会蹭手心、戳身体会 Q 弹、拖拽会甩尾;位置和大小都记得住
- 陪伴:只感知 DSH 的粗粒度状态(思考 / 工具 / 等待审批 / 成功 / 出错),工作时不插嘴
- 隐身:页面全屏或标签页隐藏时自动退场,回来时安静回到原位
- 零遥测:不联网、不上报、不读提示词 / 文件路径 / 代码内容
一、调研结论(为什么是现在这个形态)
| 问题 | 结论 |
|---|---|
| DSH 有没有 OS 级透明窗口 / 桌面 API | 没有。Host Service 目录中没有 window / desktop / native / tray 一类服务,插件无法创建系统级无边框置顶窗口。 |
| 现有桌宠插件能否复用 | 没有可复用的。插件市场查询“桌宠 / pet / live2d / 鲸鱼 / 陪伴”均返回 0 条;已装的 dsh-pocket 是手机扫码访问,dsh-dream-skin 是换肤。 |
| 插件挂在哪里 | shell.overlay:全框架浮层,位于所有列之上、滚动容器之外;容器本身点击穿透,条目自行恢复 pointer-events。这是当前 SDK 里最接近“桌宠”的位置。 |
| 怎么知道 DSH 在干什么 | Client 的 useSessionStatus + useSessions 找主会话,再用 ctx.sessions.binding(id).eventSource 订阅事件;只读 event.type 和 turn/end 的 reason.kind,不读消息内容。 |
| 外部程序全屏能否感知 | 不能。浏览器只能看到页面内 document.fullscreenElement 和 document.hidden;游戏 / 视频播放器的独占全屏属于 OS 层,Web 插件无权访问。见“已知限制”。 |
| Live2D | 当前 DSH 未提供 Live2D 运行时,也没有可注入的模型渲染器。MVP 用内联 SVG + CSS 微运动;SVG 分层 class 已按可替换素材设计。 |
| 打包/安装 | 标准 bundle:package.json 的 dsh.bundle.patch + dsh.client,cordis.patch.yml 里 insert 一条记录,用 plugin_manager install_bundle 安装。 |
二、安装与启用
已安装(当前机器)
本插件已通过 plugin_manager install_bundle 安装到 desktop profile,application: applied,无 warning。更新代码后需要刷新 DSH 页面(Ctrl+R);如果只改 lib/client.js 仍看到旧界面,说明浏览器拿到了不可变缓存的旧 bundle,请在插件管理里重新应用一次,或重启 DSH。client bundle 带 immutable 缓存头,重新安装 / 重启是让新字节生效的可靠方式。
从源码安装
# 在 DSH 会话里执行(需要审批)
plugin_manager action:install_bundle target:"C:\Users\詹靖\Documents\deepseek-harness\default-workspace\softpet"
安装完成后刷新页面。打开 设置 → 桌宠 即可看到开关与选项;桌宠默认出现在右下角。
停用 / 卸载
- 临时隐藏:点击桌宠旁的小
–,或在设置里关掉“显示桌宠”。 - 卸载:
plugin_manager action:remove_bundle target:softpet,然后刷新页面。
三、怎么玩
| 操作 | 反应 |
|---|---|
| 点头部 | 蹭手心、腮红、闭眼;羁绊 +3 |
| 点身体 | Q 弹回弹;羁绊 +2 |
| 点尾巴 | 尾巴轻甩;羁绊 +2 |
| 拖拽 | 跟手移动,松手后按松手速度惯性滑动;撞到屏幕边缘轻轻回弹并停住 |
| 悬停 | 右上角出现三个小按钮:✿ 成长日记、✦ 来访图鉴、– 隐藏 |
| 键盘 | Tab 聚焦桌宠;方向键移动(Shift 加速);Enter / Space 摸头;Esc 关日记 / 图鉴 |
| 气泡 | 出现几秒后自动淡出;不挡点击(pointer-events: none) |
位置、尺寸、主题、羁绊和日记都会自动保存;重启 DSH 后回到原位。
四、工作状态映射
只使用粗粒度类别,不读取任何会话内容:
| DSH 状态 | 桌宠表现 | 会不会说话 |
|---|---|---|
| 待机 | 呼吸、眨眼、视线漂移、呆毛摇 | 仅问候 / 主动关怀(可关)/ 用户点击时 |
| 思考 | 歪头、抱膝、眼神上飘 | 不主动说 |
| 工具调用 | 掏出小本子、低头记 | 不主动说 |
| 等待审批 | 睁大眼睛、安静看着你 | 不主动说 |
| 成功 | 轻撒花 + 递一杯虚拟饮品 | 不弹文字(避免打断) |
| 出错 | 眉眼安静垂下,陪着你 | 不说“失败了”,不弹任何字 |
气泡只在三种时机出现:页面刚打开的一次问候、用户主动互动、用户开启的“主动关怀”。工作期间一律静默。
五、形象轮换与来访
叙事设定:同一个陪伴者,换了形态
SoftPet 采用 A + B 的融合,并写在代码注释里:
- A|同一个灵魂的不同形态:羁绊、成长日记、主动关怀阈值、对 DSH 状态的反应,全部共享。换个形态,不是换一只宠物。
- B|轮流来访的小客人:每个形态有自己的语气、色调、静止体态和时段偏好;偶尔来串门,也偶尔离开。没有“收集度”、没有图鉴百分比、没有“今天必须来”的任务。
一句话:你养的不是七个宠物,是一个会换样子的小朋友,和几位偶尔来坐坐的朋友。
首批七位
| id | 名字 | 形态 | 性格 | 更容易出现的时段 |
|---|---|---|---|---|
mochi | 糯糯 | 奶油团子(默认) | 慵懒 | 全天 |
lumi | 露米 | 软萌外星人,触角 + 轻漂浮 | 好奇 | 傍晚 |
boo | 小雾 | 半透明小幽灵,害羞 | 害羞 | 深夜 |
nimbus | 云团 | 云朵精灵,蓬松 | 温柔 | 清晨(出错时下小雨) |
mush | 菇菇 | 蘑菇小灵,扎根轻摆 | 安静 | 清晨 |
stella | 星屑 | 小星星,自转 + 轨道光点 | 活泼 | 深夜 |
juno | 水母灯 | 水母,四条触须慢慢漂 | 慢悠悠 | 夜晚 / 傍晚 |
七位都具备完整的呼吸、眨眼、视线漂移、次级摆动(呆毛 / 触角 / 下摆 / 触须)、三热区互动、六相状态映射和情绪粒子,没有静态贴图。
切换规则(默认很安静)
| 方式 | 默认 | 规则 |
|---|---|---|
| 随机来访 | 开启 | 默认每 6 小时一次(可选 3 / 6 / 12 / 24 小时,实际间隔在 ±20% 内浮动)。必须同时满足:待机相、页面可见、没在拖拽、没打开日记 / 图鉴、本次挂载满 3 分钟、键鼠已停顿 45 秒–20 分钟。 |
| 手动切换 | 开启 | 点桌宠旁的 ✦ 打开图鉴,或设置 → 来访 → 点任意形象。只换绘制层,位置 / 尺寸 / 主题 / 点击穿透都不动。 |
| 摸头召唤 | 关闭 | 2.8 秒内连续摸头 6 次,会有一位朋友“被叫来”。短促、有仪式感,不打断工作。 |
| 状态触发 | 未实现 | 不做“完成任务才出现”的奖励机制,避免制造焦虑。 |
绝不切换的时机:思考 / 工具调用 / 等待审批、全屏隐身、页面隐藏、拖拽中、日记或图鉴打开时。从全屏或隐藏恢复后,还会再等 3 分钟才可能自动切换。
过渡与登场
- 切换只有约 0.7 秒:淡出 280ms → 写入本地状态 → 淡入 360ms,伴随一圈很轻的光点;不闪白、不弹窗。
- 新形象出现时,
aria-live播报一句“一位新朋友出现了:露米。”;气泡最多一句(默认开,可关)。 prefers-reduced-motion或低电量时,过渡退化为纯淡入淡出,不再有光点和漂浮。
与原有系统的关系
- 共享同一个
localStorage状态、同一套羁绊与日记;每个形象只额外记录“第一次见面时间 / 来访次数”。 - 工作状态、关怀阈值、气泡规则、无障碍播报、零遥测全部沿用,不因换形象而改变。
- 素材是内联 SVG 字符串,没有图片请求;隐藏时组件整体卸载,切换本身不加载任何外部资源。
六、设置项(全部热生效)
| 设置 | 说明 |
|---|---|
| 显示桌宠 | 关掉后浮层直接卸载,零渲染开销 |
| 尺寸 | 96–200 px 滑块,拖动即时生效,无需重启 |
| 主题 | cream(奶油) / mint(薄荷),只换配色,不换动作 |
| 主动关怀 | 默认关闭。开启后:连续活跃 ≥90 分钟、键鼠停顿 ≥60 秒且不超过 10 分钟、距上次关怀 ≥120 分钟,才轻轻问一句;23:00–06:00 语气更轻、气泡更短 |
| 重置位置 | 回到右下角默认位置 |
| 成长日记 | 查看已解锁的句子与羁绊值 |
| 来访模式 | 关闭 / 随机来访 / 仅手动,默认随机;切到关闭后当前形象保持不变 |
| 来访间隔 | 3 / 6 / 12 / 24 小时,默认 6 小时;实际触发在 ±20% 内浮动 |
| 登场气泡 | 默认开启;关掉后切换只保留 aria-live 播报,不再说话 |
| 摸头召唤 | 默认关闭;开启后 2.8 秒内摸头 6 次会召唤一位朋友 |
| 来访图鉴 | 七宫格浏览,显示第一次见面时间与来访次数,点一下即可切换 |
| 视觉提醒 | 默认开启。等待审批时头顶出现信封徽标,桌宠进入轻提醒姿态,不阻塞操作 |
| 声音提醒 | 默认关闭。开启时需要一次用户手势解锁音频;解锁失败只在气泡里轻声提示,不报错 |
| 提示音 | chime(微信式“叮咚”,默认) / wood(木叩) / drop(水滴)三种,可试听;全部内置在插件里 |
| 音量 | 0–100%,默认 55% |
| 重复间隔 | 1 / 3 / 5 / 10 分钟,默认 3 分钟 |
| 最多重复 | 0–5 次,默认 3 次;达到上限后只保留视觉提醒 |
| 勿扰时段 | 默认关闭;支持跨午夜(如 23:00–08:00),静默期不响铃、只留视觉 |
| 全屏恢复后补声 | 默认关闭;避免刚从全屏回来就被声音吓到 |
| 完成时轻声说一句 | 默认开启,与审批声音相互独立;用系统自带的中文女声说一句「做好啦」,可试听,可关闭 |
七、审批等待提醒(本次增量)
它做什么
任意会话——包括没打开的后台会话——只要出现一个等待处理的审批,桌宠就会:
- 在头顶挂一个很小的信封徽标,并进入轻提醒姿态(眉毛微抬、光晕轻呼吸),不弹窗、不抢焦点;
- 如果声音提醒已开启且已解锁,播放一次约 1.3 秒的“叮咚”;
- 3 分钟没处理再响一次,最多重复 3 次(可配);达到上限后彻底安静,只留视觉;
- 全部处理或关闭后,徽标消失,轻声说一句「处理好啦。」
多个会话同时等待时只算一次提醒:以最早进入等待的审批为计时基准,不会按会话数倍增响铃。切换会话、关闭其中一个等待中的会话,都不会重置计时;只有全部清空才归零。
跨会话状态从哪里来
- 订阅 client 服务
uiSession.sessionStatus(全局聚合,不是当前会话)。它把sessions.list里的所有会话与各 pending-interaction domain 合并成Map<sessionId, { running, pendingInteraction, completionUnread }>。 - 只读
pendingInteraction.kind === 'approval'以及它的key,去重键是${sessionId}:${key};不读审批内容、工具名、reason、displayReason、工具参数或会话标题。 - 启动时直接读一次
getSnapshot(),所以插件加载前就已存在的审批也能被看见;之后靠订阅增量,不轮询。 - 重载页面后
key会重建,这是正常的「新提醒」,不会影响计时逻辑。
声音与解锁
浏览器要求音频必须由用户手势解锁。首次开启「声音提醒」时,设置页会顺手调用一次解锁;如果仍然失败,桌宠只会在气泡里说:
有审批在等你,点我开启声音提醒。
点一下桌宠即可重试;也可以点设置页的「试听」。解锁失败不会抛错、不会弹系统对话框。声音和视觉是两个独立开关:关掉声音,视觉提醒照常工作。
什么时候不响
| 情况 | 行为 |
|---|---|
| 页面隐藏 / 浏览器全屏 | 暂停计时与播放;恢复后按「全屏恢复后补声」决定是否补一次 |
桌宠被隐藏(visible=false) | 暂停;重新显示后继续剩余时间 |
| 勿扰时段 | 只留视觉,每 30 秒安静地看一眼是否出时段 |
| 声音开关关闭 | 只留视觉,计时逻辑不受影响 |
| 音频未解锁 | 只留视觉,并给一次可点击的解锁提示 |
| 审批被处理 / 会话关闭 | 从集合移除;集合空时立即停声、清计时器 |
替换提示音
内置三枚 16 kHz / 16-bit / 单声道 WAV,原始文件在 softpet/assets/:
softpet/assets/approval-chime.wav # 默认,微信式“叮咚”(1.3 秒)
softpet/assets/approval-wood.wav # 木质感,更闷
softpet/assets/approval-drop.wav # 水滴感,更轻
替换步骤:
- 用同名文件覆盖
softpet/assets/里的 WAV(建议 ≤ 2 秒、单声道、8–16 kHz,体积越小越好); - 在仓库根目录执行
node softpet-dev/make-sounds.cjs,它会把三枚音频重新编码成 base64,并写回client.js里/* softpet:sounds:start */ … /* softpet:sounds:end */之间的SOUND_B64; node --check softpet/lib/client.js,然后重新应用一次 bundle(设置 → 插件 → SoftPet 关再开,或刷新页面)。
make-sounds.cjs 只做本地文件读写与 base64,不联网。也可以直接改那三行 base64,但要注意保持合法 WAV。
没有音频文件时不会崩:
playApprovalSound会退回到 WebAudio 振荡器合成一个极短的替代音;再失败就静默,只留视觉。
已知限制
- DSH 没有暴露「点击提醒跳转到对应会话」的公开 API,所以点击桌宠只做解锁/互动,不会跳转;README 不臆造这个能力。
- 后台会话能否出现审批,取决于 DSH 是否把该会话的审批发布到全局
uiSession。当前版本按「会发布」实现;若某个版本不发布,只会表现为该会话不提醒,不影响其它会话。 - 浏览器标签页层面的全屏可以感知;外部程序(游戏/视频)的全屏无法从 Web UI 内感知,这是宿主限制,与本插件无关。
- 勿扰时段使用本机时区;跨时区旅行后需要手动调整时间。
任务完成时的一句话
一个任务成功结束时,桌宠会用系统自带的中文女声轻轻说一句「做好啦。」(随机三句之一),不弹窗、不重复。它走的是浏览器 speechSynthesis,不需要音频文件,也不联网。
- 直接监听主会话的
turn/end,且只在原因为completed时触发;手动停止、被阻止、超 token 都不会说话。这样即使启用了自动审查授权(审批瞬时出现又消失),完成提示也不会被吞掉。 - 与审批提示音相互独立:审批音关着,完成提示仍然可以说话;反之亦然。
- 页面隐藏、全屏、勿扰时段、或仍有审批在等待时,保持安静,避免两句话叠在一起。
- 系统没有可用语音、或语音被系统策略禁用时,静默跳过,不报错、不弹提示。
- 设置页「完成时轻声说一句」里可以随时关掉,也可以点「试听」先听一句。
- 不读取任务内容、会话标题或任何工具参数,只看
turn/end的原因类别。
八、数据与隐私
-
全部状态存在浏览器
localStorage,键名softpet.state.v1,结构:{ "size": 132, "theme": "cream", "visible": true, "care": false, "bond": 0, "diary": 0, "x": null, "y": null, "lastSeen": 0, "lastCare": 0, "interactions": 0, "charId": "mochi", "lastSwitch": 0, "met": {}, "switchMode": "random", "switchEvery": 6, "introBubble": true, "touchVisit": false, "approvalVisual": true, "soundOn": false, "soundId": "chime", "volume": 55, "repeatMin": 3, "repeatMax": 3, "dndOn": false, "dndStart": "23:00", "dndEnd": "08:00", "resumeSound": false, "doneSound": true } -
不发送任何网络请求:没有
fetch/XHR/WebSocket/sendBeacon(单测会静态断言)。 -
不读取提示词、文件路径、代码内容、会话标题;状态判断只 switch 事件
type。 -
localStorage 不可用时(隐私模式等)自动退回内存态,功能不崩。
-
清空数据:浏览器 DevTools → Application → Local Storage → 删除
softpet.state.v1;或“重置位置”只重置坐标。
注意:
localStorage按 origin 隔离。如果 DSH Web 端口变化(例如从:2692换到别的端口),旧配置不会自动迁移,会回到默认值。
九、素材替换与新增形象
形象库是 softpet/lib/client.js 里的一个纯数据清单 CHARACTERS。每条记录就是一只完整的桌宠:
{
id: 'mochi', // 稳定 id,写进 localStorage,发布后不要改
name: { zh: '糯糯', en: 'Mochi' }, // 显示名
intro: { zh: '我在这儿。', en: '…' },// 第一次来访时的一句话
trait: { zh: '慵懒', en: 'sleepy' },
palette: { body1: '#…', ink: '#…' },// 可选,会映射成 --sp-* 变量
motion: { breath: 8.4, head: 6.2, sway: 5.4, hover: 7 },
svg: `…`, // 自包含 SVG 字符串,viewBox 200×220
lines: { head: ['…'], body: ['…'], tail: ['…'], care: ['…'] },
}
替换现有形象
- 只改那条记录的
svg字符串,保持viewBox="0 0 200 220";或同步调整.sp-pet的height: calc(var(--sp-size) * 1.1)。 - 保留这些 class,动画与状态才会生效:
- 结构:
sp-svg、sp-shadow、sp-breathe(外层容器的sp-stage / sp-visit / sp-art由代码负责) - 次级运动:
sp-tail、sp-ahoge、sp-headtilt、sp-head;漂浮角色可加sp-hover,软体/触须用sp-wisp - 眼睛:
sp-eyes、sp-eye、sp-glint;眉毛:sp-brow-l/sp-brow-r;嘴:sp-m-smile/sp-m-small/sp-m-flat/sp-m-o - 情绪:
sp-blush;道具:sp-notebook、sp-drink;手:sp-arm-l/sp-arm-r - 彩蛋:云朵的
sp-rain、蘑菇的sp-cap/sp-cap-spot
- 结构:
- 颜色尽量用变量:
--sp-body-1、--sp-body-2、--sp-belly、--sp-ear、--sp-ear-in、--sp-accent、--sp-ink、--sp-blush、--sp-scarf。 - 改完执行
node --check softpet/lib/client.js+node softpet-dev/test.cjs(会校验每只形象的 viewBox、class、成对标签)。
新增一位形象
- 在
CHARACTERS末尾追加一条上面的记录。 - 在文件顶部的
CHARACTER_IDS数组里加入同一个 id(两处必须一致,单测会检查)。 - 如果希望它在特定时段更常出现,在
visitorWeight(id, hour)里加一行;不加就是全天均等。 - 跑
node softpet-dev/test.cjs与node softpet-dev/smoke.cjs。 - 不需要改状态机、设置页或 CSS——图鉴、设置网格、随机来访都会自动读到新记录。
模板文件:
softpet/docs/character-template.md(含空白 manifest 片段与 SVG 骨架)。
文案同样可替换:client.js 顶部的 ZH / EN 字典(通过 ctx.locale.register 注册,不是硬编码在 JSX 里)。共享台词是数组,用 trList(key) 取;每只形象自己的台词放在 lines 里,用 loc() 取中文。
十、目录结构
softpet/
├─ package.json # bundle 清单:dsh.bundle / dsh.client
├─ cordis.patch.yml # insert: softpet
├─ icon.svg # 设置页与插件市场图标
├─ locale/{zh,en}.json # meta.title / meta.description
├─ README.md
└─ lib/
├─ index.js # Host 半:空 apply(本插件不需要 Host 服务)
└─ client.js # 全部逻辑:状态机 / 渲染 / 交互 / 设置页
softpet-dev/ 是开发期工具,不属于插件包:test.cjs(纯函数单测)、smoke.cjs(Node vm + 迷你 React/DOM shim 的渲染冒烟)、asar.cjs(只读解包调研工具)。
十一、自测结果
在本机执行:
node softpet-dev/test.cjs → 28/28 passed
node softpet-dev/smoke.cjs → 80/80 passed
node --check softpet/lib/client.js → ok
plugin_manager install_bundle → applied, warnings: []
smoke.cjs 用真实 client.js 在 vm 里跑了一个迷你 React/DOM 环境,覆盖:注册契约、挂载、问候、摸头羁绊、思考 / 工具 / 出错 / 审批相位、拖拽惯性 + 持久化 + 边界、设置滑杆热改尺寸、document.hidden 卸载与恢复、图鉴打开 / 切换过渡 / aria-live 播报 / 设置页即时切换、跨会话审批合并 / 存量识别 / 3 分钟重复 / 上限停声 / 解锁提示 / 徽标渲染、无网络调用。test.cjs 另外校验七只形象的 manifest 完整性、随机来访门槛、来访计数、持久化字段、勿扰时段边界、提示音 WAV 头与 CSS / SVG 契约。
验收清单
| # | 验收项 | 结果 |
|---|---|---|
| 1 | 无 API Key 可启动、待机、互动、气泡 | ✅ 全部离线;单测 + 冒烟覆盖 |
| 2 | 拖拽有惯性、自然停住、重启保留位置 | ✅ 冒烟覆盖惯性、边界、localStorage 写读 |
| 3 | 呼吸 / 眨眼 / 视线漂移,不闪烁 | ✅ CSS 动画 + JS 随机眨眼;周期见“性能” |
| 4 | 分区点击不同反馈、摸头治愈反馈 | ✅ 头 / 身 / 尾三热区,摸头加腮红与羁绊 |
| 5 | 状态联动、工作时不主动弹气泡 | ✅ 6 相映射;仅成功粒子、出错静默 |
| 6 | 全屏自动隐藏、退出回原位 | ⚠️ 页面内全屏与标签隐藏已实现;外部程序全屏无法感知(平台限制) |
| 7 | 改尺寸即时生效 | ✅ 滑杆改 CSS 变量,无需重挂载 |
| 8 | 隐藏 / 标签页停止渲染;低电量降级 | ✅ 隐藏即卸载组件;低电量或 reduced-motion 关闭动画 |
| 9 | 断网 / API 失败静默回退 | ✅ 本来就没有网络路径,不存在失败弹窗 |
| 10 | 无遥测、无隐私读取;气泡不挡操作 | ✅ 静态断言无网络 API;气泡 pointer-events:none |
| 11 | 键盘可互动和移动 | ✅ Tab / 方向键 / Enter / Space / Esc |
| 12 | 安装、配置、素材替换说明 | ✅ 即本文档 |
形象轮换验收(本次增量)
| # | 验收项 | 结果 |
|---|---|---|
| 1 | 原有 P0 功能在切换后仍正常 | ✅ 旧 16 + 35 项断言全过;位置 / 尺寸 / 主题不随切换改变 |
| 2 | 6+ 形象均可加载、待机、互动、响应状态 | ✅ 7 只,每只独立 SVG + 台词 + 微运动参数 + 状态体态 |
| 3 | 随机来访低频、工作 / 全屏不切换 | ✅ shouldVisit 门槛单测覆盖;组件在隐身时整体卸载 |
| 4 | 手动切换立即生效、窗口属性不变 | ✅ 图鉴与设置页均写同一 store;只换 art 层 |
| 5 | 过渡柔和、无闪烁、无大弹窗;登场一句可关 | ✅ 280ms 出 / 360ms 入 + 光圈;introBubble 可关 |
| 6 | 重启恢复当前形象、上次时间、已遇见列表 | ✅ charId / lastSwitch / met 持久化,冒烟验证写入 |
| 7 | 素材按需 / 缓存、隐藏停止、无泄漏 | ✅ 内联 SVG 无请求;隐藏即卸载;切换定时器在结算时清空 |
| 8 | 断网可用 | ✅ 零网络路径;形象系统纯本地 |
| 9 | 切换无障碍播报 | ✅ aria-live 播“一位新朋友出现了:{name}” |
| 10 | 新增形象模板与文档,只加 manifest + 素材 | ✅ 见第九节与 softpet/docs/character-template.md |
审批提醒验收(本次增量)
| # | 验收项 | 结果 |
|---|---|---|
| 1 | 后台会话进入等待审批也能触发 | ✅ 订阅全局 uiSession.sessionStatus,冒烟用两个后台会话验证 |
| 2 | 启动时存量审批立即识别 | ✅ connect() 里先 getSnapshot();冒烟用预置快照验证 |
| 3 | 多会话同时等待只响一次,3 分钟后也只再响一次 | ✅ 合并为单计时器;冒烟断言 4 次后静音,不随会话数倍增 |
| 4 | 会话间切换不重置计时与次数 | ✅ 计时挂在 key 集合上;切换只改变快照内容 |
| 5 | 任一审批处理 / 全部处理后的归零 | ✅ 冒烟覆盖“移除一个”与“全部清空” |
| 6 | 关闭某个等待中会话不影响其它 | ✅ 冒烟覆盖 |
| 7 | 3 分钟重复;达上限停声、只留视觉 | ✅ 冒烟推进假时钟验证 1 + 3 次 |
| 8 | 声音未开启 / 未解锁不报错,有可点击提示 | ✅ needsUnlock + 气泡“点我开启声音提醒”;单测校验 WAV 与静默回退 |
| 9 | 全屏 / 勿扰 / 声音开关规则 | ✅ 隐藏与全屏暂停、恢复续计;勿扰时段跨午夜单测 |
| 10 | 订阅创建 / 销毁正确,无泄漏 | ✅ start() 返回 cleanup;重试上限 40 次;冒烟停止后无残留计时 |
| 11 | 断网完全可用 | ✅ 音频内嵌 base64,零网络请求 |
完成提示音验收(本次增量)
| # | 验收项 | 结果 |
|---|---|---|
| 1 | 任务成功时温柔女声说一句 | ✅ 系统中文女声,随机三句短句;冒烟验证 success 转场触发一次 |
| 2 | 与审批音独立、可关闭 | ✅ doneSound 独立开关;设置页可试听,关掉后不再说话 |
| 3 | 不打扰 | ✅ 隐藏 / 全屏 / 勿扰 / 审批等待中保持安静;同一成功状态不重复 |
| 4 | 无语音时静默不报错 | ✅ speakDone 捕获异常并返回 false,无弹窗 |
| 5 | 离线 | ✅ 走系统 speechSynthesis,无音频文件、无网络请求 |
未能在本机完成的部分:真实浏览器端到端测试。当前环境的文件沙箱禁止启动 Edge / Chromium(spawn EPERM、crashpad / mojo 报“拒绝访问”),当前 Web 端口也没有可用的 DevTools 端口。因此页面内的最终视觉与点击效果需要你刷新页面后确认;代码侧已用 shim 渲染测试 + 静态断言尽量兜底。
十二、已知限制与假设
- 不是系统级桌宠:只存在于 DSH Web UI 内;最小化或关闭 DSH 就看不到她。这是当前 SDK 的硬边界,不是实现偷懒。已在
docs/dsh-desktop-window-proposal.md里向宿主提出桌面浮层窗口 API 提案。 - 外部全屏无法感知:只能感知页面内 fullscreen 和标签页隐藏。若后续 DSH 暴露桌面窗口 / 全屏状态 API,可在
usePageHidden里接一条订阅即可。 - 主会话判定:优先取
retainedBy.mainView > 0的会话;取不到时回退到s.list.current,再回退到第一个running会话。多窗口并排时只跟其中一个,属于有意的克制。 - 冷启动状态:事件窗口尚未回放历史时,只能拿到
running这类粗状态;等事件流到达后自动补齐,不阻塞渲染。 - localStorage 按 origin 隔离:端口变化会丢配置与位置(回到默认),不会报错。
- React 由宿主提供:组件若在真实环境抛错会清空整个 slot。已做防御式取值(未就绪快照、缺失 binding、
props.t回退),但无法在本机做真实 React 挂载验证。 - 提示音需要一次用户手势:浏览器自动播放策略决定第一次必须由点击解锁;未解锁时只有视觉提醒,不会报错。音频用
AudioContext解码内联 WAV,解码失败会退到振荡器合成,再失败就静默。 - 在线模型增强未启用:按“默认不联网”的约束,P2 的在线台词生成没有接;当前版本零网络请求。
- 自动来访依赖浮层挂载:全屏 / 页面隐藏 / 手动隐藏期间组件整体卸载,所以不会“在幕后偷偷换人”;恢复后重新计 3 分钟安静期。
- 完成提示音依赖系统语音:走浏览器
speechSynthesis,音色由操作系统决定(Windows 上通常是 Huihui / Yaoyao)。系统没有可用中文语音,或语音被策略禁用时,静默跳过,不影响其它功能。 - 设置页切换不做淡出:设置页里点形象会立即写入并淡入,因为桌宠此时在设置面板后面;图鉴里切换才有完整的 280ms 淡出。
- 首次升级会重置一次节奏:旧版本没有
lastSwitch,升级后会以当前时间作为起点,避免一升级就立刻换形象;之后按配置间隔走。 - 形象数量与体积:七只形象都是内联 SVG(client 单文件约 195KB,含三枚 WAV 的 base64),不产生图片请求;继续增加形象会线性增大 client bundle,建议单只 SVG 控制在 8KB 内。
- 不能点击跳转会话:DSH 未暴露“跳到某个等待审批的会话”的公开 API;点击桌宠只做解锁与互动,不臆造跳转。
- 后台审批依赖宿主发布:若某一版 DSH 不把后台会话的审批合并进
uiSession.sessionStatus,那个会话就不会触发提醒;其它会话不受影响。
十三、P1 / P2 覆盖情况
- P1 羁绊与成长日记:已实现。羁绊里程碑
5 / 12 / 22 / 36 / 54 / 78,解锁 6 页日记;没有饿死、没有连续登录惩罚,离开再久回来也只是“你回来啦”。 - P1 低频主动关怀:已实现,默认关闭;门槛见设置表。
- P1 轻记账彩蛋:未实现。它需要读取已配置的 API 余额(Host 的
deepseekAccount),属于可选增强;当前版本不碰账户数据。 - P2 分部位触摸:已实现(头 / 身 / 尾)。
- P2 低电量降级:已实现(
navigator.getBattery≤15% 且未充电,或prefers-reduced-motion,关闭动画与粒子)。 - P2 键盘无障碍:已实现(焦点环、方向键、Enter / Space、
aria-live气泡、role="group")。 - P2 在线模型增强:未实现,保持“默认离线、失败静默”的约束;后续可在
speak()前加一个可选的异步台词 provider,超时 1.5s 直接回落本地台词。
形象轮换的 P1 / P2
- P1 来访图鉴:已实现。记录“第一次见面 / 来访次数”,没有收集度百分比、没有奖励、没有“未遇见”红点;离开很久回来不会补一句催收。
- P1 性格微差异:已实现,但只影响台词池、色调与静止体态,不影响功能;不做需要用户管理的好感度。
- P1 时段彩蛋:已实现。深夜偏幽灵 / 星星,清晨偏云朵 / 蘑菇,傍晚偏外星人 / 水母;纯本地
new Date().getHours()。 - P2 切换播报:已实现(
aria-live短句),并与气泡共用播报区。 - P2 低电量 / 低性能降级:已实现。低电量或 reduced-motion 时关闭漂浮、光点与粒子,切换只做淡入淡出。
- P2 形象创作模板:已实现,见
softpet/docs/character-template.md。 - P2 Live2D:当前 SDK 没有暴露 Live2D 运行时,维持 SVG + CSS 方案;如后续宿主提供,可把
svg字段换成 canvas 渲染器,manifest 与状态机不变。
审批提醒的 P1 / P2
- 视觉与声音独立:两个开关互不影响;关声只停音频,徽标与姿态照常。
- 无障碍:
aria-live="polite"播报“有一份审批在等你”/“处理好啦”;徽标本身aria-hidden,避免重复。 - 勿扰时段:默认关闭,支持跨午夜;静默期不补声、不累计次数。
- 轻量试听:设置页可单独试听三种音色,不触发全局提醒。
十四、性能约定
- 隐藏(
document.hidden/ 全屏 / 用户关闭)时不挂载组件:没有 rAF、没有定时器、没有 DOM。 - 待机动画全是 CSS
transform/opacity,不触发布局;呼吸 5.6s、眨眼 2.6–6.2s、视线 3.2–7s,均为低频。 - 状态轮询 2s 一次(只读内存快照),关怀检查 30s 一次;随机来访 60s 一次,且只在待机相做几次数值比较;空闲 CPU 目标仍为 1%–2% 以下。
- 审批提醒只在“有等待项”时挂一个计时器(3 分钟量级),集合清空立即
clearTimeout;页面隐藏 / 全屏时暂停,不做后台轮询。 - 形象素材是内联字符串,没有图片请求、没有预加载队列;切换只替换一层
innerHTML,不动布局与窗口。 - 位置写入 localStorage 去抖 180ms;拖拽结束才落盘。
Plugins associés
dsh-web-ui (dsh-pet)
zhu1090093659/dsh-web-ui
dsh-pet (dsh-pet)
pc2005-cloud/dsh-pet
dsh-tavern (dsh-tavern)
flizzywine/dsh-tavern
dsh-tavern (dsh-profile-bridge)
flizzywine/dsh-tavern