跳过主要内容
Z

softpet-plugin

zhanjing617/softpet-plugin

安装

dsh plugin --profile web add github:zhanjing617/softpet-plugin

README

SoftPet · 治愈桌宠

一只不打扰、会呼吸、零压力的小家伙。她住在 DSH 的界面浮层里,默认完全离线:不配任何 API Key,也能待机、互动、说气泡、陪你工作。

  • 待机:呼吸起伏、随机眨眼、视线漂移、呆毛和尾巴的次级摆动
  • 互动:摸头会蹭手心、戳身体会 Q 弹、拖拽会甩尾;位置和大小都记得住
  • 陪伴:只感知 DSH 的粗粒度状态(思考 / 工具 / 等待审批 / 成功 / 出错),工作时不插嘴
  • 隐身:页面全屏或标签页隐藏时自动退场,回来时安静回到原位
  • 零遥测:不联网、不上报、不读提示词 / 文件路径 / 代码内容

SoftPet 头像

一、调研结论(为什么是现在这个形态)

问题结论
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. 在头顶挂一个很小的信封徽标,并进入轻提醒姿态(眉毛微抬、光晕轻呼吸),不弹窗、不抢焦点;
  2. 如果声音提醒已开启且已解锁,播放一次约 1.3 秒的“叮咚”;
  3. 3 分钟没处理再响一次,最多重复 3 次(可配);达到上限后彻底安静,只留视觉;
  4. 全部处理或关闭后,徽标消失,轻声说一句「处理好啦。」

多个会话同时等待时只算一次提醒:以最早进入等待的审批为计时基准,不会按会话数倍增响铃。切换会话、关闭其中一个等待中的会话,都不会重置计时;只有全部清空才归零。

跨会话状态从哪里来

  • 订阅 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    # 水滴感,更轻

替换步骤:

  1. 用同名文件覆盖 softpet/assets/ 里的 WAV(建议 ≤ 2 秒、单声道、8–16 kHz,体积越小越好);
  2. 在仓库根目录执行 node softpet-dev/make-sounds.cjs,它会把三枚音频重新编码成 base64,并写回 client.js 里 /* softpet:sounds:start */ … /* softpet:sounds:end */ 之间的 SOUND_B64;
  3. 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: ['…'] },
}

替换现有形象

  1. 只改那条记录的 svg 字符串,保持 viewBox="0 0 200 220";或同步调整 .sp-pet 的 height: calc(var(--sp-size) * 1.1)。
  2. 保留这些 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
  3. 颜色尽量用变量:--sp-body-1、--sp-body-2、--sp-belly、--sp-ear、--sp-ear-in、--sp-accent、--sp-ink、--sp-blush、--sp-scarf。
  4. 改完执行 node --check softpet/lib/client.js + node softpet-dev/test.cjs(会校验每只形象的 viewBox、class、成对标签)。

新增一位形象

  1. 在 CHARACTERS 末尾追加一条上面的记录。
  2. 在文件顶部的 CHARACTER_IDS 数组里加入同一个 id(两处必须一致,单测会检查)。
  3. 如果希望它在特定时段更常出现,在 visitorWeight(id, hour) 里加一行;不加就是全天均等。
  4. 跑 node softpet-dev/test.cjs 与 node softpet-dev/smoke.cjs。
  5. 不需要改状态机、设置页或 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 项断言全过;位置 / 尺寸 / 主题不随切换改变
26+ 形象均可加载、待机、互动、响应状态✅ 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关闭某个等待中会话不影响其它✅ 冒烟覆盖
73 分钟重复;达上限停声、只留视觉✅ 冒烟推进假时钟验证 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 渲染测试 + 静态断言尽量兜底。

十二、已知限制与假设

  1. 不是系统级桌宠:只存在于 DSH Web UI 内;最小化或关闭 DSH 就看不到她。这是当前 SDK 的硬边界,不是实现偷懒。已在 docs/dsh-desktop-window-proposal.md 里向宿主提出桌面浮层窗口 API 提案。
  2. 外部全屏无法感知:只能感知页面内 fullscreen 和标签页隐藏。若后续 DSH 暴露桌面窗口 / 全屏状态 API,可在 usePageHidden 里接一条订阅即可。
  3. 主会话判定:优先取 retainedBy.mainView > 0 的会话;取不到时回退到 s.list.current,再回退到第一个 running 会话。多窗口并排时只跟其中一个,属于有意的克制。
  4. 冷启动状态:事件窗口尚未回放历史时,只能拿到 running 这类粗状态;等事件流到达后自动补齐,不阻塞渲染。
  5. localStorage 按 origin 隔离:端口变化会丢配置与位置(回到默认),不会报错。
  6. React 由宿主提供:组件若在真实环境抛错会清空整个 slot。已做防御式取值(未就绪快照、缺失 binding、props.t 回退),但无法在本机做真实 React 挂载验证。
  7. 提示音需要一次用户手势:浏览器自动播放策略决定第一次必须由点击解锁;未解锁时只有视觉提醒,不会报错。音频用 AudioContext 解码内联 WAV,解码失败会退到振荡器合成,再失败就静默。
  8. 在线模型增强未启用:按“默认不联网”的约束,P2 的在线台词生成没有接;当前版本零网络请求。
  9. 自动来访依赖浮层挂载:全屏 / 页面隐藏 / 手动隐藏期间组件整体卸载,所以不会“在幕后偷偷换人”;恢复后重新计 3 分钟安静期。
  10. 完成提示音依赖系统语音:走浏览器 speechSynthesis,音色由操作系统决定(Windows 上通常是 Huihui / Yaoyao)。系统没有可用中文语音,或语音被策略禁用时,静默跳过,不影响其它功能。
  11. 设置页切换不做淡出:设置页里点形象会立即写入并淡入,因为桌宠此时在设置面板后面;图鉴里切换才有完整的 280ms 淡出。
  12. 首次升级会重置一次节奏:旧版本没有 lastSwitch,升级后会以当前时间作为起点,避免一升级就立刻换形象;之后按配置间隔走。
  13. 形象数量与体积:七只形象都是内联 SVG(client 单文件约 195KB,含三枚 WAV 的 base64),不产生图片请求;继续增加形象会线性增大 client bundle,建议单只 SVG 控制在 8KB 内。
  14. 不能点击跳转会话:DSH 未暴露“跳到某个等待审批的会话”的公开 API;点击桌宠只做解锁与互动,不臆造跳转。
  15. 后台审批依赖宿主发布:若某一版 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;拖拽结束才落盘。

相关插件