相关文档
taozi-pet/README.md —— 素材流水线 / 预处理脚本
taozi-pet/tools/README.md —— QA 与校验体系指南
基于 Electron 的跨平台桌面宠物。「套子」是一只活泼俏皮的元气小精灵(蓝紫渐变长发、紫色眼睛、海星羽毛发饰、南瓜包),会驻留在桌面一角,与你互动、说话、提醒你,甚至帮你存文件。
状态机驱动的拟真动作:待机轮播(张望 / 眨眼 / 拍拍肚 / 踢腿,动作之间夹一段静态母版呼吸的间歇)/ 睡觉 / 开心 / 伤心(定格末帧)/ 走路 / 贴边窥视 / 提醒等多个状态自由切换,支持呼吸、挤压回弹等程序化动效。
4 个角色专属互动:交互-捏捏脸、交互-掏南瓜包、交互-花瓣转圈、交互-海星招手,触发后播放动画并回复语录、增长好感度。
单击分流:点头部播放「点击-摸摸头」、点别处播放「开心」,两种都回复语录、增长好感度。头部区域由 experience.clickZone 声明,坐标为按参照状态首帧归一化的比例框,缩放窗口不影响命中;开发者模式下可在调试叠加层里核对区域位置。
自由尺寸:气泡区与精灵解耦,桌宠可在 50%–150% 间滑块自由缩放,气泡尺寸不随桌宠变化。窗口宽度按人物可见宽度收窄(下限为气泡区最小宽度 240px),精灵帧两侧的透明留白由窗口裁掉,不白占桌面点击区域。
语录小屋:状态语录与互动语录均可在线编辑,单一数据源,编辑即生效、重启保留。
提醒:到点后桌宠连续提醒,点击桌宠、触发互动或喂食即视为完成。
系统集成:托盘图标、置顶显示、鼠标穿透、贴边吸附、随机行走、开机自启、右键菜单、全局键盘监听。
文件口袋:拖拽文件到桌宠身上即**收纳(移动)**到指定文件夹;拖入时桌宠播放「喂食」动画(播完定住),放手即收纳并倒序收回动画。默认收纳到 文档/taozi-pet,状态页可开关功能、更换目录、打开口袋。
键盘监听:敲字母键时桌宠拍手应和——按下字母键播放 clap 前 3 帧并定格,抬起播放后 3 帧并定格,抬起后 3 秒内没有再次按下就回待机。状态页可开关,默认开;睡觉 / 沮丧时不响应。钩子走原生全局键盘库,取不到时该功能自行退回「不可用」,不影响其余功能。
一键重置:语录、状态、提醒、设置一键恢复到最初默认值。
主进程 src/main.ts + src/main/:入口装配与生命周期(main.ts)+ 按职责拆分的模块(窗口几何、状态回报、随机行走、提醒、托盘、IPC 等)。模块之间不互相 import,共享的可变状态集中在 src/main/context.ts。
渲染进程 src/renderer/:
pet/:桌宠窗口,状态机 + 动画 + 气泡 + 拖拽 + 交互。
dashboard/:小屋面板(状态 / 语录 / 提醒 / 设置)。
共享层 src/shared/contracts.ts:类型契约、IPC 参数校验。
数据:语录、设置、状态、提醒以 JSON 原子写入 userData;损坏时隔离为 *.corrupt 并回退默认值,同时写一条 data-corrupt-fallback 日志(不静默丢弃)。
原生依赖:仅 uiohook-napi(全局键盘钩子)一个,随包分发 N-API 预编译产物、跨 Node/Electron 版本 ABI 稳定,因此在 forge.config.js 的 rebuildConfig.ignoreModules 中排除@electron/rebuild(无需本机 node-gyp 工具链)。主进程按 Node 规则在运行时解析并 require,取不到只让键盘监听退回「不可用」,不影响其余功能。
配置源 pet-spec.json:角色、状态机、语录、主题、素材管线等唯一权威配置。
src/
main.ts 主进程入口:装配共享上下文、生命周期、单实例与退出落盘
preload.ts 预加载(安全暴露 petAPI,CSP 严格)
shared/contracts.ts 类型契约与数据校验
shared/quotes.ts 语录组枚举与语录选取(主进程与渲染层共用,纯函数,有单测)
main/
context.ts 主进程共享运行时状态(模块只依赖它,彼此不互相 import)
runtime.ts 缓存治理、运行时证据文件、致命退出、IPC 来源校验
windows.ts 窗口几何/创建、气泡区常量、广播、小屋面板与文件口袋、渲染层错误恢复
stats.ts 好感/心情/陪伴时长的对外口径与落盘、好感与心情增益
mood.ts 心情衰减与 sad 判定
sleep.ts 入睡计时(无互动触发 sleep)
random-walk.ts 随机行走挡位、位移与镜像调度
activity.ts 状态接管判定、渲染层状态回报镜像、互动结算
reminders.ts 提醒定时器与到点队列
settings.ts 设置落盘生效、打字反应监听
tray.ts 托盘图标与菜单(桌宠右键 / 托盘)
quotes.ts 语录种子生成与落盘
broadcast.ts 向两个窗口广播的通用出口
ipc.ts 全部 IPC 处理器
data-validation.ts settings/quotes/stats/reminders 数据解析与阈值
persistence.ts 原子化 JSON 读写
drag.ts 拖拽阈值、吸附判定带与贴边位置
overlay-layout.ts 调试叠加层的绘制数据换算(纯函数,有单测;含点击热点区按精灵方框展开为屏幕矩形)
debug-overlay.ts 调试叠加层(吸附判定带 + 行走范围 + 尺寸标注 + 点击热点区)
logger.ts JSON 结构化日志
typing-listener.ts 全局键盘监听(原生钩子,装载失败即退回「不可用」)
renderer/
pet/ 桌宠窗口(状态机、动画、气泡、拖拽、互动)
state-machine.ts 状态机与轮播/间歇调度
hit-area.ts 气泡区点击判定(纯函数,有单测)
dashboard/ 小屋面板(状态、语录、提醒、设置、重置)
debug/ 调试叠加层(开发者模式:判定带 / 行走范围 / 桌宠与气泡尺寸 / 点击热点区)
assets/
pet/ 运行时精灵 PNG(512×512 逐帧,256 色索引)
tray/ 托盘图标
pet-spec.json 角色 / 状态机 / 语录 / 主题 / 素材管线的唯一配置源
tools/ QA、校验、素材管线、启动/打包脚本(见 tools/README.md)
tests/ 单元 + e2e + 冒烟测试
# 安装依赖(锁定工具链,务必用 npm ci,勿直接 npm install)
npm ci
# 开发运行(含快速 QA、实时热重载)
npm run dev
# Windows 也可以直接双击 启动桌宠.bat
npm run dev启动前会先跑依赖预检:核对node_modules/electron的运行时就位情况、版本与架构是否同package.json和当前 Node 一致,缺失或版本、架构不符(例如装成 32 位 Electron)时按校验和重新下载——架构不符会让原生模块静默退回「不可用」。随后自动跑一次check:quick(亚秒级),任一环节失败都会中止启动。 想要无命令行黑窗的正式打包版,请用下面的打包命令。
npm run dev / start # 开发运行(自动跑 check:quick)
npm run check:quick # 快速 QA(dev-contract + spec + asset-links + ui + experience,亚秒级)
npm run check # 全量 QA(快速 QA + 素材像素级质检)
npm run test # 单元测试(含 tsc 类型检查)
npm run test:e2e # 端到端测试(应用 + 提醒到点触发,自动打包 host)
npm run test:dev-smoke # 开发冒烟测试(隔离 userData)
npm run qa:* # 单项 QA(qa:ui / qa:experience / qa:assets)
npm run process:assets # 素材处理(incoming-assets/ -> src/assets/pet/)
npm run inspect:assets # 处理素材 + 跑像素级 QA(process-assets → qa-assets)
npm run doctor # 环境诊断
npm run package:win / make:win / portable:win # 打包 / 安装包 / 绿色版
打包产物自带瘦身:
package后自动清理 Electron 冗余运行时文件(除en-US.pak外的语言包、SwiftShader 软件渲染、Chromium LICENSE),比未瘦身小约 15MB。运行时 143 帧为索引色 PNG,合计 5.9 MiB(PNG-32 为 18.2 MiB)。
项目几乎所有行为都由 pet-spec.json 驱动,改配置通常不需要改代码:
character:显示名、个性、核心素材帧。
states:18 个状态,各自 frames、triggers、canInterrupt(可打断名单)、interrupt、cooldownMs、anchor、segments(分段播放区间)、breathing(自带的呼吸参数)等。
idleRotation:待机轮播池(动作 id + 权重)与待机间歇 gap。待机时按权重随机挑一个动作单轮播放,播完进入 gap.stateId 指定的静态帧停顿整数个呼吸周期,再续接下一个动作;新增待机动作只需在 states 里加状态、再到此处登记权重。gap.levels 按随机行走挡位(0–4)给出呼吸次数区间,间歇时长 = 次数 × motion.breathing.periodMs,挡位越高次数越少。时长恒为呼吸周期的整数倍,因此间歇结束时呼吸正好走完整数个周期、回到缩放 1 的相位,摘下呼吸动画时不会出现半相位回落。间歇期间叠一层缓慢呼吸(参数取 motion.breathing 的默认值);该呼吸同时用于定格末帧的常驻状态(sad,以及单帧的 sleep——它自带一套更慢更深的参数),两者都是静止画面。
experience.quotes:全部语录的默认文本,键为状态 id,每项给出 label / emoji / quotes。状态语录与互动语录同表——互动动作的 id 与其状态 id 相同,其语录即该状态的语录组。运行时以 userData/quotes.json 为唯一数据源。
experience.interactions:4 个角色专属互动的行为定义(id / stateId / durationMs / affectionGain);显示名、emoji 与回复语录取自 experience.quotes[id],菜单与语录页共用同一处。
experience.clickZone:单击热点区,键名即区域名,每项给出命中后改播的 stateId、坐标参照的 referenceStateId,以及相对该状态首帧归一化的 x / y / width / height(0–1,框内为命中)。单击时先判热点区,未命中则回落 pointer:tap 对应状态。
experience.theme:主题色(小屋面板)。
motion.walk:随机行走 5 档的默认参数(游走半径、间隔区间、单次位移区间、每帧位移与帧间隔);运行时可在开发者面板覆盖,覆盖值存 userData/tuning.json,不写回 spec。motion.breathing(呼吸总开关与默认周期/幅度;状态可在自己的 breathing 里按字段覆盖,如 sleep 用更慢更深的睡息)、motion.squashStretch 同段。
assetPipeline:素材处理阈值(背景抠除、占用率、画布、交付编码颜色数等唯一权威配置)。
全部语录的默认文本来自
experience.quotes这一处,首次启动据此生成userData/quotes.json,之后语录页与桌宠都读写这份运行时文件。
运行时数据以 JSON 原子写入 Electron 的 userData 目录(写临时文件后 rename,防写坏):
| 文件 | 内容 |
|---|---|
quotes.json | 语录(编辑后的唯一数据源) |
settings.json | 设置(含autoStartInit 自启标记) |
pet-stats.json | 状态(好感度 / 心情 / 今日互动 / 陪伴时长) |
reminders.json | 提醒列表 |
tuning.json | 运行时调参覆盖(目前只有随机行走参数,见开发者面板) |
logs/app.jsonl | 结构化日志 |
文件口袋目录:默认 <文档目录>/taozi-pet,可在状态页「文件口袋」改用自定义目录(存 settings.json 的 filePocketDir)。
userData 下除业务数据外还有三类可重建内容,启动时清理一次(src/main/cache-maintenance.ts,阈值读 pet-spec.json 的 maintenance):
| 对象 | 说明 |
|---|---|
| Chromium 派生缓存 | Cache / Code Cache / GPUCache / DawnGraphiteCache / DawnWebGPUCache / Shared Dictionary / blob_storage,删除后按需重建 |
| 写入残留 | 原子写入中断留下的 *.tmp 全部清除;校验失败隔离出的 *.corrupt 按修改时间保留最近 keepCorruptFiles 个 |
| 结构化日志 | 超过 logMaxKb 时按行截断,保留最近记录 |
清理在 app.whenReady() 之前发起(此时 Chromium 尚未打开缓存目录),删除失败只记日志、不影响启动;diskCacheLimitMb 作为 --disk-cache-size 限制磁盘缓存上限。业务数据(quotes.json / settings.json / pet-stats.json / reminders.json / tuning.json、Local State / Preferences、Local Storage / Session Storage / Network)不参与清理。
| 字段 | 默认 | 含义 |
|---|---|---|
cacheSweepOnStartup | true | 启动时是否清理 |
diskCacheLimitMb | 10 | Chromium 磁盘缓存上限(MB) |
keepCorruptFiles | 3 | 损坏隔离文件保留个数 |
logMaxKb | 512 | 日志体积上限(KB) |
状态页「🔄 数据管理」下的「🧹 清理缓存」可随时手动触发(IPC data:clear-cache),完成后弹出各类释放量。
分「致命」与「可恢复」两档,判据集中在主进程(src/main/runtime.ts / windows.ts / ipc.ts):
| 情形 | 处理 |
|---|---|
| 进程级未捕获异常 / 未处理拒绝、窗口加载失败、CSP 等安全违规 | 致命:写 .build/runtime-failed.json + 结构化日志后退出 |
渲染进程 crashed / oom / integrity-failure | 致命同上;clean-exit / killed 只记日志 |
渲染层脚本异常(Uncaught / Unhandled、runtime:renderer-failed) | 可恢复:记日志并重载对应窗口,每个窗口最多 RENDERER_RECOVERY_LIMIT(3)次,超过才升为致命 |
| 渲染层可恢复问题(IPC 调用失败、素材帧缺失) | 经 runtime:renderer-warning 上报,只记结构化日志、不阻断播放;缺帧保留上一帧降级 |
| 数据文件损坏 | 隔离为 *.corrupt 回退默认值,并记 data-corrupt-fallback 日志 |
日志自身写失败退到 console(logger.ts),保证失败本身可观测。
QA 与校验脚本统一在 tools/(详见 taozi-pet/tools/README.md):
快速 QA check:quick:validate-dev-contract / validate-spec / validate-asset-links + qa-ui + qa-experience,开发启动时自动运行,亚秒级。类型校验由 tsc(npm test 前置)与打包时的 fork-ts-checker 硬闸承担。
全量 QA check:快速 QA + qa-assets 逐帧像素质检(尺寸/透明/贴边/锚点/占用率/地面残留/跨帧稳定/重复帧/交付编码/回归基线)。
责任边界:结构契约归 validate-spec,素材引用归 validate-asset-links,像素质检归 qa-assets,运行时体验语义(打断矩阵、动画、语录、连通性)归 qa-experience。每条规则只在一个文件维护,防止漂移。
帧率:默认 250ms/帧(4 FPS),feed 用 200ms/帧(5 FPS)、sad 用 625ms(1.6 FPS)、clap 用 20ms/帧(50 FPS)。待机轮播动作(look / blink / belly-ok / kick)单轮播放、每帧只出现一次,sad 同为单轮播放、播完定格末帧,sleep 是单帧常驻、不推进帧、起伏全由它自带的呼吸参数提供(周期 4.2s、纵向 +8%,比待机更慢更深),其余非循环动作把独立帧按序播两遍构成完整单次动画(如 happy 12 张 → 播放帧 24);clap 是分段播放的例外,按下 / 抬起两段区间由 segments 给出、播放进度完全由输入驱动。素材共 143 张 base 帧(分布见下表)、无 -r2 副本。
| 状态 | 动作 | 独立帧 | 播放帧 | 类型 | 单周期 | 触发器 | 冷却 |
|---|---|---|---|---|---|---|---|
| look | 待机-张望 | 7 | 7 | 待机轮播 | 1.75s | app:start / ambient:idle | — |
| blink | 待机-眨眼 | 6 | 6 | 待机轮播 | 1.5s | ambient:blink | — |
| belly-ok | 待机-拍拍肚 | 6 | 6 | 待机轮播 | 1.5s | ambient:belly-ok | — |
| kick | 待机-踢腿 | 7 | 7 | 待机轮播 | 1.75s | ambient:kick | — |
| standby-gap | 待机-间歇 | 1 | 1 | 待机间歇 | 按挡位 | 轮播调度(无外部触发) | — |
| feed | 喂食 | 5 | 5 | 单次 | 1.0s | file:drop(拖入桌宠) | — |
| happy | 点击-开心 | 12 | 24 | 单次 | 6.0s | pointer:tap(单击) | ½周期 |
| notify | 提醒 | 12 | 12 | 循环 | 3.0s | reminder:due | — |
| peek | 贴边窥视 | 12 | 12 | 单次 | 3.0s | window:edge-snap(吸附) | ½周期 |
| pet-face | 交互-捏捏脸 | 6 | 12 | 单次 | 3.0s | interaction:pet-face | ½周期 |
| pet-head | 点击-摸摸头 | 6 | 12 | 单次 | 3.0s | state:pet-head(单击头部区域) | ½周期 |
| pumpkin-bag | 交互-掏南瓜包 | 12 | 24 | 单次 | 6.0s | interaction:pumpkin-bag | ½周期 |
| petal-spin | 交互-花瓣转圈 | 12 | 24 | 单次 | 6.0s | interaction:petal-spin | ½周期 |
| starfish-wave | 交互-海星招手 | 12 | 24 | 单次 | 6.0s | interaction:starfish-wave | ½周期 |
| walk | 走路 | 12 | 12 | 循环 | 3.0s | state:walk | — |
| sleep | 睡觉 | 1 | 1 | 定格末帧 | 0.6s | state:sleep(3 分钟无互动) | — |
| sad | 沮丧 | 8 | 8 | 定格末帧 | 5.0s | state:sad(心情↓25) | — |
| clap | 拍手 | 6 | 6 | 单次·分段 | 0.12s | typing:activity(敲字母键) | — |
待机不是单一状态,而是
idleRotation池中四个动作的随机轮播 + 每轮之后的间歇:动作播完先进入间歇(单帧静态母版core-ip,按挡位停顿整数个呼吸周期 = 呼吸次数 ×motion.breathing.periodMs,期间叠一层缓慢呼吸),再按权重随机续接下一个动作(允许连续两次是同一动作)。池成员与间歇都可被任意其它状态打断,池成员之间互不打断、切换由状态机自行推进。 周期 = 播放帧数 × 对应frameDurationMs(各状态帧率见表上方说明);sad是定格末帧状态:心情跌破阈值时播一遍沮丧动画、播完停在末帧(末帧叠呼吸),期间单击桌宠重播一遍,心情回升才回待机;sleep同为定格末帧但只有一帧:进入即静止睡姿,起伏全交给呼吸动效(与待机间歇同一套机制,但自带更慢更深的breathing参数),故 spec 里它只引用一张sleep.png;feed与clap是定向状态:feed由文件拖入正向播放并定格末帧,放手收纳 / 拖离后倒序播放一轮、播完回到拖入前的状态(idle/ sad / sleep / notify;正放未完则先播完再倒放,倒放中途又拖入则从当前帧转回正向);clap由敲键驱动按下 / 抬起两段,各按segments给出的区间播到holdFrame定格,前段未完就抬起则直接跳到后段起点、不补播中间帧。各状态的anchor/mirrorSafe/interrupt等配置全部收敛在pet-spec.json。
优先级通过 canInterrupt("可打断名单")实现:名单列出自己可以打断(压过)谁,* 表示可打断一切;当前处于 idle 或名单包含当前状态时即可切换。同一状态再次触发时另判 interrupt(见「打断自己」列)。下表按可压制范围从宽到窄排列:
| 状态 | 能打断(压过) | 会被谁打断 | 打断自己 |
|---|---|---|---|
| 喂食 | * | 提醒 | 否 |
| 提醒 | * | 喂食、开心、interactions | 否 |
interactions | 除喂食外的全部 | 喂食、提醒、interactions | 是 |
| 开心 | 除喂食、interactions、沮丧外的全部 | 喂食、提醒、interactions、沮丧 | 是 |
| 睡觉 | idle、贴边窥视、走路、沮丧、拍手 | 喂食、开心、提醒、interactions | 否 |
| 沮丧 | idle、开心、贴边窥视、走路、拍手 | 喂食、提醒、interactions、睡觉 | 是 |
| 拍手 | idle、贴边窥视、走路 | 除idle、贴边窥视、走路外的全部 | 是 |
| 贴边窥视 | idle、走路 | 除idle、走路外的全部 | 否 |
| 走路 | idle | 除idle外的全部 | 否 |
idle | 无 | 除idle外的全部 | 否 |
①
idle= 待机表现,即状态表中类型为「待机轮播」「待机间歇」的 5 个状态:待机-张望 / 待机-眨眼 / 待机-拍拍肚 / 待机-踢腿 / 待机-间歇(与idleRotation同源);interactions=experience.interactions的 4 个角色互动:交互-捏捏脸 / 交互-掏南瓜包 / 交互-花瓣转圈 / 交互-海星招手(点击-摸摸头属点击族群,不在interactions名单内)。 ② 「能打断(压过)」列的*是canInterrupt的字面值,代表可打断一切(含idle);聚合行取成员并集,「除…外的全部」指表中其余状态(不含自身)。 ③idle是抢占基底:处于idle之一时任意状态都能接管,故各状态名单里无需也不能写出它们;idle的 4 个动作单轮播完自行进入待机-间歇、走完再续接下一个。两个状态互相写进对方名单时可互相接管,实际由先后顺序决定(后到者接管),整体不存在严格全序。 ④ 「打断自己」列取自interrupt(restart= 是 /resume= 否),只在同一状态被再次下发时生效;idle的进入与续接、喂食的拖入 / 拖离、拍手的分段播放走各自内部路径,不经该判定。主进程在下发活动前用同一判据(stateCanInterrupt)预判渲染层是否采纳。
| 数值 | 范围 | 默认 | 规则 |
|---|---|---|---|
| 好感度 affection | 0–100 | 0 | 点击-摸摸头 +3、交互-捏捏脸 +3、交互-掏南瓜包 +2、交互-花瓣转圈 +2、交互-海星招手 +2、点击桌宠 +2、贴边窥视 +2、喂食 +3;每个来源每天仅首次加好感(同日重复不再加,互动仍计今日互动,防刷)。封顶 100 |
| 心情 mood | 0–100 | 40 | 每 2 分钟 -1(每分钟检测一次,单次结算封顶 6 点);<25 进入「沮丧 sad」(播一遍并定格末帧),≥25 回到待机轮播。每次互动(含同日重复)、点击、窥视、喂食都加心情,增量与同来源的好感增益同值、不限次数,封顶 100 |
| 今日互动 todayInteractions | ≥0 | 0 | 每次互动 +1;跨到新的一天(日期变化)时从 0 起重新累计 |
| 陪伴时长 companionMinutes | ≥0 | 0 | 按实际运行毫秒实时折算为分钟累计 |
| 提醒 reminders | — | 空 | 到点进入 notify 并气泡播报该条文本;用户触发互动或喂食即视为完成(消费当前待处理提醒),每条只提醒一次后自动移除 |
数据以 JSON 原子写入
userData,损坏自动回退默认值;4 个互动动作durationMs=6000ms(动画播完即回待机,不留静止展示期),触发互动或喂食会顺带消费挂起的提醒。
随机行走有 5 个挡位(小屋面板「随机行走」下拉):木头人(彻底不走)→ 散步 → 正常(默认)→ 活泼 → 多动症。挡位越高,走得越勤、越远;同一挡位同时决定待机间歇的呼吸次数(挡位越高越活跃、间歇越短)。开启后桌宠在以拖动中心为圆心的范围内随机游走,播放 walk 动画并匀速移动:
| 挡位 | 移动范围 | 游走间隔 | 单次位移 | 待机间歇(呼吸次数) | 说明 |
|---|---|---|---|---|---|
| 0 木头人 | — | — | — | 3–4 次(7.8–10.4s) | 完全不移动(等于关闭),待机轮播照常 |
| 1 散步 | 220px | 14–30s | 60–140px | 2–4 次(5.2–10.4s) | 慵懒,偶尔挪两步 |
| 2 正常(默认) | 320px | 10–20s | 120–260px | 2–3 次(5.2–7.8s) | 适中 |
| 3 活泼 | 400px | 6–14s | 200–320px | 1–3 次(2.6–7.8s) | 走得勤、迈得远 |
| 4 多动症 | 480px | 4–12s | 300–400px | 1–2 次(2.6–5.2s) | 高频大步快走 |
这张表是
pet-spec.json的motion.walk默认值。开发者模式下的「🚶 随机行走参数」面板可以运行时改这些数值:改动作为整体覆盖写入userData/tuning.json(不动机库里的 spec),主进程立即重建游走调度生效;「恢复默认」或重置数据即清掉覆盖回到 spec 默认值。参数在写入前校验范围与自洽性(间隔/位移区间不倒置、单次位移不超过游走半径),非法值会被拒绝并在面板上提示;qa-experience的walk-levels-shape/walk-levels-monotonic则守住 spec 默认值本身的形状与"挡位越高越勤越远"的排序语义。
待机间歇随挡位联动:间歇长度以呼吸次数计,时长 = 次数 × motion.breathing.periodMs(表内秒数按 2.6s/次折算),在当前挡位区间内随机取整数次;调挡位后桌宠窗口实时生效(无需重启)。木头人挡不走动,但待机轮播与间歇照常运行。待机语录在间歇走完、进入下一个待机动作时播报,节奏同样随挡位走。
动画循环至移动结束:walk 动画在物理移动期间持续循环播放(durationMs:0 触发渲染层无限循环),移动结束才回到待机轮播,不会只播一轮就静止。
移动目标同时受屏幕工作区约束,不会超出屏幕。
镜像:移动含向右分量时播放左右镜像的 walk 动画(素材默认朝左);纯上下移动与贴屏边被钳成零位移时不镜像。
拖动打断:用户拖动桌宠期间暂停游走,位移结束回到待机轮播。
睡觉期间不走:进入 sleep 常驻状态(连续 3 分钟无互动)时立即停掉游走调度,避免定时器在常驻状态下空转;被互动 / 提醒唤醒后自动恢复游走。睡觉计时只由用户主动动作重置(互动 / 拖动 / 开发者面板点击),随机行走等自动行为不会推迟入睡。
互动期间停下脚步:触发任意互动(交互-捏捏脸 / 交互-掏南瓜包 / 交互-花瓣转圈 / 交互-海星招手)时立即停止正在进行的移动,避免桌宠一边播互动动画一边滑行;互动为一次性状态,播完后自动恢复游走调度。
移动时锁定尺寸:每帧移动都按 spec 基准 × 桌宠缩放 的权威尺寸一并下发。frameless 透明窗口在 Windows 上被程序化移动时尺寸会漂移且逐次累积(表现为「越走越大」),钉死尺寸可杜绝累积放大。
气泡显示逻辑统一在桌宠渲染层:带反馈文本的活动直接显示;无文本的普通状态自动从对应语录组随机取一句;notify 为常驻气泡。普通气泡 5 秒后自动消失(peek 贴边窥视与待机轮播动作为 3 秒),同时最多一个(新气泡顶掉旧的)。
| 动作 | 自动触发间隔 | 气泡内容 | 气泡方式 |
|---|---|---|---|
| 待机轮播 look / blink / belly-ok / kick | 一个动作播完 → 间歇(按挡位 0.8–12s)→ 随机续接下一个 | 进入待机动作时取该动作的一句语录(间歇期不发声) | 普通 3s |
| 开心 happy | 用户单击 | 「开心」语录(happy 组) | 普通 5s |
| 提醒 notify | 定时提醒到点 | 该条提醒文本 | 常驻,直到单击/互动/喂食才消失 |
| 贴边窥视 peek | 吸附到屏幕边缘 | 「贴边窥视」语录 | 普通 3s |
| 交互-捏捏脸 pet-face | 右键 → 捏捏脸 | 「捏捏脸」语录 | 普通 5s |
| 点击-摸摸头 pet-head | 单击头部区域 | 「点击-摸摸头」语录(pet-head 组) | 普通 5s |
| 交互-掏南瓜包 pumpkin-bag | 右键 → 掏南瓜包 | 「掏南瓜包」语录 | 普通 5s |
| 交互-花瓣转圈 petal-spin | 右键 → 花瓣转圈 | 「花瓣转圈」语录 | 普通 5s |
| 交互-海星招手 starfish-wave | 右键 → 海星招手 | 「海星招手」语录 | 普通 5s |
| 走路 walk | 按随机行走挡位的游走间隔 | 「走路」语录 | 普通 5s |
| 睡觉 sleep | 连续 3 分钟无互动,进入时播 | 「睡觉」语录 | 普通 5s |
| 沮丧 sad | 心情 < 25 进入时播一遍后停在末帧,其间单击重播 | 「沮丧」语录 | 普通 5s |
| 喂食 feed | 文件拖入桌宠时 | 无 | — |
语录一律来自
experience.quotes这一张表(运行时在userData/quotes.json,可编辑);互动语录同表,键即该互动的状态 id。
单击小屋「状态」页底部 一键重置 后,各数据恢复为以下默认值:
| 项 | 默认值 |
|---|---|
| 桌宠大小 | 0.8(80%,可 0.5–1.5 自由调) |
| 随机行走 | 2(正常) |
| 贴边吸附 edgeSnap | 开 |
| 置顶显示 alwaysOnTop | 开 |
| 打字反应 typingReaction | 开 |
| 鼠标穿透 clickThrough | 关 |
| 开机自启 autoStart / autoStartInit | 关 |
| 好感度 affection | 0 |
| 心情 mood | 40 |
| 今日互动 todayInteractions | 0 |
| 陪伴时长 companionMinutes | 0 |
| 语录 quotes | 恢复pet-spec.json 默认文本 |
| 提醒 reminders | 清空 |
| 开发者模式 devMode | 关(关闭开发者页并回到状态页) |
| 文件口袋 filePocket / filePocketDir | 开 / 默认(文档/taozi-pet) |
拖动:按住桌宠任意拖动,松手自动吸附屏幕边缘(可在面板调整贴边吸附/随机行走挡位)。判定以贴边基准框为基准,不拿窗口缘判——窗口宽度有气泡区最小宽度兜底,随人物缩放变化的成分很小。基准框宽度取人物可见宽度与「贴边将要播报的那条语录」气泡宽度的宽者:贴上边会弹 peek 语录,渲染层在拖拽开始时就把那条语录定下来、实测它的气泡宽度报给主进程,主进程据此把气泡让出来——吸附后气泡正好贴住屏幕边缘、完整留在屏内,人物停在框内(内缩 (语录气泡实宽 − 人物可见宽) / 2,随语录长短变化),贴边后播报的就是同一条语录;语录气泡不比人物宽、或本次贴边不播 peek(睡觉 / 沮丧等状态会拒掉它)时,基准框就是人物本身,人物直接压在屏边。两条判定带固定:基准框缘进入 20px 即判定贴边,松手光标进入 50px 即视为有贴边意图(抓取点在角色中心时光标先顶到屏边,用光标距离补齐意图)。拖拽中、随机行走、桌宠尺寸变化都会解除贴边。吸附恒生效;「贴边窥视」动画只在当前状态允许被它接管时才播——睡觉 / 沮丧 / 提醒等名单不含 peek 的状态下只吸附、不播动画;播了窥视动画即计入好感与心情各 +2。
单击:命中头部区域时播放「点击-摸摸头」,否则播放「点击-开心」;两者都随机讲一句对应语录,都计入好感与心情各 +2(好感每天仅首次计入)。沮丧期间单击只重播一遍沮丧动画、补一句沮丧语录(不切开心,也不判头部区域),播完仍停在末帧。头部区域取自 experience.clickZone.head——按参照状态(待机间歇 standby-gap)那一帧的像素位置归一化存储,故点哪一帧都按同一块区域判定,人物摆别的姿势时那块区域未必落在头上;区域用 stateId / x / y / width / height / interactionId 描述,调整只改这一处。
敲字母键:按住字母键桌宠拍手(播 clap 前 3 帧后定格在抬手前一张),松开播后 3 帧并定格,松开后 3 秒内没有再按就回待机;期间按下新键会立刻从头再走一遍。可在状态页关闭「键盘监听」;睡觉 / 沮丧时常驻状态优先,不响应键盘。
文件口袋(拖放收纳):把文件拖到桌宠身上,桌宠播放「喂食」动画并保持张口;放手即把文件移动进文件口袋目录并倒序收回动画(重名按 Windows 规则加 (2)、 (3) 后缀);拖离(没放手)则不收纳、同样倒序收回。倒放中途再次拖入即恢复张口等待。收纳计入好感与心情各 +3,并消费队首待处理提醒。收纳后在状态页点「打开口袋」直达目录。
右键桌宠:弹出菜单——4 个互动动作(🤏 捏捏脸 / 🎃 掏南瓜包 / 🌸 花瓣转圈 / 👋 海星招手)、打开状态 / 语录 / 提醒面板、开关鼠标穿透、隐藏桌宠。
托盘图标:显示桌宠、打开状态 / 语录 / 提醒、鼠标穿透开关、设置 / 退出。
小屋面板:
状态页:查看好感度 / 心情 / 今日互动 / 陪伴时长;开关置顶显示、鼠标穿透、开机自启、键盘监听、文件口袋(拖放收纳开关 + 更换目录 + 打开口袋);随机行走 5 挡(木头人/散步/正常/活泼/多动症);滑块自由调整桌宠大小(50%–150%);底部一键重置(语录 / 状态 / 提醒 / 设置 / 行走参数恢复正常)。
开发者模式(隐藏调试入口):在状态页末尾的版本号上 2 秒内连点 6 次进入(第 4、5 下会弹出倒计时气泡,第 6 下弹出「已进入开发者模式」并解锁「🧪 开发者」页签)。开发者页提供:关闭开发者模式开关、💊 喂安眠药(立即触发睡觉、遵守打断逻辑)、🪢 抽鞭子(走路开关「抽一次/一直抽」:抽一次=按随机行走挡位立即走一趟,一直抽=不产生位移、循环播放走路动画与语录)、🍃 飞叶子(心情置 100)、👊 轻抚脸颊(心情置 0)、🚶 随机行走参数(调 5 档的游走半径/间隔/位移与速度;改动写入 userData/tuning.json 覆盖 spec 默认值,可「恢复默认」)、📐 调试叠加层(在桌宠所在屏幕上画出左右吸附判定带、当前挡位的随机行走范围、点击热点区及其参照的精灵方框,并标注桌宠大小与气泡实际大小;全屏透明、点击穿透,退出开发者模式自动撤下)。开启状态持久化到 settings.json(devMode),重启仍保持;关闭后可再次连点版本号进入。
语录页:状态语录与互动语录均可在线编辑,编辑即生效、重启保留(唯一数据源 userData/quotes.json)。
提醒页:新增 / 删除定时提醒,到点由桌宠气泡提醒。
数据位置:运行时数据在 Electron userData(quotes.json / settings.json / pet-stats.json / reminders.json / logs/app.jsonl)。
不纳入 Git 追踪、勿提交:taozi-pet/incoming-assets/(抠图中间产物)、taozi-pet/src/assets/(精灵帧与头像,均由流水线/make-graphics.py 重建)、taozi-pet/qa/(QA 报告与接触表)、.build/、.webpack/、out/、release/。
纳入版本库:白底源图 assets-raw/(不可再生的 AI 源图)、配置 taozi-pet/pet-spec.json、流水线脚本与文档。
相关文档
taozi-pet/README.md —— 素材流水线 / 预处理脚本
taozi-pet/tools/README.md —— QA 与校验体系指南
基于 Electron 的跨平台桌面宠物。「套子」是一只活泼俏皮的元气小精灵(蓝紫渐变长发、紫色眼睛、海星羽毛发饰、南瓜包),会驻留在桌面一角,与你互动、说话、提醒你,甚至帮你存文件。
状态机驱动的拟真动作:待机轮播(张望 / 眨眼 / 拍拍肚 / 踢腿,动作之间夹一段静态母版呼吸的间歇)/ 睡觉 / 开心 / 伤心(定格末帧)/ 走路 / 贴边窥视 / 提醒等多个状态自由切换,支持呼吸、挤压回弹等程序化动效。
4 个角色专属互动:交互-捏捏脸、交互-掏南瓜包、交互-花瓣转圈、交互-海星招手,触发后播放动画并回复语录、增长好感度。
单击分流:点头部播放「点击-摸摸头」、点别处播放「开心」,两种都回复语录、增长好感度。头部区域由 experience.clickZone 声明,坐标为按参照状态首帧归一化的比例框,缩放窗口不影响命中;开发者模式下可在调试叠加层里核对区域位置。
自由尺寸:气泡区与精灵解耦,桌宠可在 50%–150% 间滑块自由缩放,气泡尺寸不随桌宠变化。窗口宽度按人物可见宽度收窄(下限为气泡区最小宽度 240px),精灵帧两侧的透明留白由窗口裁掉,不白占桌面点击区域。
语录小屋:状态语录与互动语录均可在线编辑,单一数据源,编辑即生效、重启保留。
提醒:到点后桌宠连续提醒,点击桌宠、触发互动或喂食即视为完成。
系统集成:托盘图标、置顶显示、鼠标穿透、贴边吸附、随机行走、开机自启、右键菜单、全局键盘监听。
文件口袋:拖拽文件到桌宠身上即**收纳(移动)**到指定文件夹;拖入时桌宠播放「喂食」动画(播完定住),放手即收纳并倒序收回动画。默认收纳到 文档/taozi-pet,状态页可开关功能、更换目录、打开口袋。
键盘监听:敲字母键时桌宠拍手应和——按下字母键播放 clap 前 3 帧并定格,抬起播放后 3 帧并定格,抬起后 3 秒内没有再次按下就回待机。状态页可开关,默认开;睡觉 / 沮丧时不响应。钩子走原生全局键盘库,取不到时该功能自行退回「不可用」,不影响其余功能。
一键重置:语录、状态、提醒、设置一键恢复到最初默认值。
主进程 src/main.ts + src/main/:入口装配与生命周期(main.ts)+ 按职责拆分的模块(窗口几何、状态回报、随机行走、提醒、托盘、IPC 等)。模块之间不互相 import,共享的可变状态集中在 src/main/context.ts。
渲染进程 src/renderer/:
pet/:桌宠窗口,状态机 + 动画 + 气泡 + 拖拽 + 交互。
dashboard/:小屋面板(状态 / 语录 / 提醒 / 设置)。
共享层 src/shared/contracts.ts:类型契约、IPC 参数校验。
数据:语录、设置、状态、提醒以 JSON 原子写入 userData;损坏时隔离为 *.corrupt 并回退默认值,同时写一条 data-corrupt-fallback 日志(不静默丢弃)。
原生依赖:仅 uiohook-napi(全局键盘钩子)一个,随包分发 N-API 预编译产物、跨 Node/Electron 版本 ABI 稳定,因此在 forge.config.js 的 rebuildConfig.ignoreModules 中排除@electron/rebuild(无需本机 node-gyp 工具链)。主进程按 Node 规则在运行时解析并 require,取不到只让键盘监听退回「不可用」,不影响其余功能。
配置源 pet-spec.json:角色、状态机、语录、主题、素材管线等唯一权威配置。
src/
main.ts 主进程入口:装配共享上下文、生命周期、单实例与退出落盘
preload.ts 预加载(安全暴露 petAPI,CSP 严格)
shared/contracts.ts 类型契约与数据校验
shared/quotes.ts 语录组枚举与语录选取(主进程与渲染层共用,纯函数,有单测)
main/
context.ts 主进程共享运行时状态(模块只依赖它,彼此不互相 import)
runtime.ts 缓存治理、运行时证据文件、致命退出、IPC 来源校验
windows.ts 窗口几何/创建、气泡区常量、广播、小屋面板与文件口袋、渲染层错误恢复
stats.ts 好感/心情/陪伴时长的对外口径与落盘、好感与心情增益
mood.ts 心情衰减与 sad 判定
sleep.ts 入睡计时(无互动触发 sleep)
random-walk.ts 随机行走挡位、位移与镜像调度
activity.ts 状态接管判定、渲染层状态回报镜像、互动结算
reminders.ts 提醒定时器与到点队列
settings.ts 设置落盘生效、打字反应监听
tray.ts 托盘图标与菜单(桌宠右键 / 托盘)
quotes.ts 语录种子生成与落盘
broadcast.ts 向两个窗口广播的通用出口
ipc.ts 全部 IPC 处理器
data-validation.ts settings/quotes/stats/reminders 数据解析与阈值
persistence.ts 原子化 JSON 读写
drag.ts 拖拽阈值、吸附判定带与贴边位置
overlay-layout.ts 调试叠加层的绘制数据换算(纯函数,有单测;含点击热点区按精灵方框展开为屏幕矩形)
debug-overlay.ts 调试叠加层(吸附判定带 + 行走范围 + 尺寸标注 + 点击热点区)
logger.ts JSON 结构化日志
typing-listener.ts 全局键盘监听(原生钩子,装载失败即退回「不可用」)
renderer/
pet/ 桌宠窗口(状态机、动画、气泡、拖拽、互动)
state-machine.ts 状态机与轮播/间歇调度
hit-area.ts 气泡区点击判定(纯函数,有单测)
dashboard/ 小屋面板(状态、语录、提醒、设置、重置)
debug/ 调试叠加层(开发者模式:判定带 / 行走范围 / 桌宠与气泡尺寸 / 点击热点区)
assets/
pet/ 运行时精灵 PNG(512×512 逐帧,256 色索引)
tray/ 托盘图标
pet-spec.json 角色 / 状态机 / 语录 / 主题 / 素材管线的唯一配置源
tools/ QA、校验、素材管线、启动/打包脚本(见 tools/README.md)
tests/ 单元 + e2e + 冒烟测试
# 安装依赖(锁定工具链,务必用 npm ci,勿直接 npm install)
npm ci
# 开发运行(含快速 QA、实时热重载)
npm run dev
# Windows 也可以直接双击 启动桌宠.bat
npm run dev启动前会先跑依赖预检:核对node_modules/electron的运行时就位情况、版本与架构是否同package.json和当前 Node 一致,缺失或版本、架构不符(例如装成 32 位 Electron)时按校验和重新下载——架构不符会让原生模块静默退回「不可用」。随后自动跑一次check:quick(亚秒级),任一环节失败都会中止启动。 想要无命令行黑窗的正式打包版,请用下面的打包命令。
npm run dev / start # 开发运行(自动跑 check:quick)
npm run check:quick # 快速 QA(dev-contract + spec + asset-links + ui + experience,亚秒级)
npm run check # 全量 QA(快速 QA + 素材像素级质检)
npm run test # 单元测试(含 tsc 类型检查)
npm run test:e2e # 端到端测试(应用 + 提醒到点触发,自动打包 host)
npm run test:dev-smoke # 开发冒烟测试(隔离 userData)
npm run qa:* # 单项 QA(qa:ui / qa:experience / qa:assets)
npm run process:assets # 素材处理(incoming-assets/ -> src/assets/pet/)
npm run inspect:assets # 处理素材 + 跑像素级 QA(process-assets → qa-assets)
npm run doctor # 环境诊断
npm run package:win / make:win / portable:win # 打包 / 安装包 / 绿色版
打包产物自带瘦身:
package后自动清理 Electron 冗余运行时文件(除en-US.pak外的语言包、SwiftShader 软件渲染、Chromium LICENSE),比未瘦身小约 15MB。运行时 143 帧为索引色 PNG,合计 5.9 MiB(PNG-32 为 18.2 MiB)。
项目几乎所有行为都由 pet-spec.json 驱动,改配置通常不需要改代码:
character:显示名、个性、核心素材帧。
states:18 个状态,各自 frames、triggers、canInterrupt(可打断名单)、interrupt、cooldownMs、anchor、segments(分段播放区间)、breathing(自带的呼吸参数)等。
idleRotation:待机轮播池(动作 id + 权重)与待机间歇 gap。待机时按权重随机挑一个动作单轮播放,播完进入 gap.stateId 指定的静态帧停顿整数个呼吸周期,再续接下一个动作;新增待机动作只需在 states 里加状态、再到此处登记权重。gap.levels 按随机行走挡位(0–4)给出呼吸次数区间,间歇时长 = 次数 × motion.breathing.periodMs,挡位越高次数越少。时长恒为呼吸周期的整数倍,因此间歇结束时呼吸正好走完整数个周期、回到缩放 1 的相位,摘下呼吸动画时不会出现半相位回落。间歇期间叠一层缓慢呼吸(参数取 motion.breathing 的默认值);该呼吸同时用于定格末帧的常驻状态(sad,以及单帧的 sleep——它自带一套更慢更深的参数),两者都是静止画面。
experience.quotes:全部语录的默认文本,键为状态 id,每项给出 label / emoji / quotes。状态语录与互动语录同表——互动动作的 id 与其状态 id 相同,其语录即该状态的语录组。运行时以 userData/quotes.json 为唯一数据源。
experience.interactions:4 个角色专属互动的行为定义(id / stateId / durationMs / affectionGain);显示名、emoji 与回复语录取自 experience.quotes[id],菜单与语录页共用同一处。
experience.clickZone:单击热点区,键名即区域名,每项给出命中后改播的 stateId、坐标参照的 referenceStateId,以及相对该状态首帧归一化的 x / y / width / height(0–1,框内为命中)。单击时先判热点区,未命中则回落 pointer:tap 对应状态。
experience.theme:主题色(小屋面板)。
motion.walk:随机行走 5 档的默认参数(游走半径、间隔区间、单次位移区间、每帧位移与帧间隔);运行时可在开发者面板覆盖,覆盖值存 userData/tuning.json,不写回 spec。motion.breathing(呼吸总开关与默认周期/幅度;状态可在自己的 breathing 里按字段覆盖,如 sleep 用更慢更深的睡息)、motion.squashStretch 同段。
assetPipeline:素材处理阈值(背景抠除、占用率、画布、交付编码颜色数等唯一权威配置)。
全部语录的默认文本来自
experience.quotes这一处,首次启动据此生成userData/quotes.json,之后语录页与桌宠都读写这份运行时文件。
运行时数据以 JSON 原子写入 Electron 的 userData 目录(写临时文件后 rename,防写坏):
| 文件 | 内容 |
|---|---|
quotes.json | 语录(编辑后的唯一数据源) |
settings.json | 设置(含autoStartInit 自启标记) |
pet-stats.json | 状态(好感度 / 心情 / 今日互动 / 陪伴时长) |
reminders.json | 提醒列表 |
tuning.json | 运行时调参覆盖(目前只有随机行走参数,见开发者面板) |
logs/app.jsonl | 结构化日志 |
文件口袋目录:默认 <文档目录>/taozi-pet,可在状态页「文件口袋」改用自定义目录(存 settings.json 的 filePocketDir)。
userData 下除业务数据外还有三类可重建内容,启动时清理一次(src/main/cache-maintenance.ts,阈值读 pet-spec.json 的 maintenance):
| 对象 | 说明 |
|---|---|
| Chromium 派生缓存 | Cache / Code Cache / GPUCache / DawnGraphiteCache / DawnWebGPUCache / Shared Dictionary / blob_storage,删除后按需重建 |
| 写入残留 | 原子写入中断留下的 *.tmp 全部清除;校验失败隔离出的 *.corrupt 按修改时间保留最近 keepCorruptFiles 个 |
| 结构化日志 | 超过 logMaxKb 时按行截断,保留最近记录 |
清理在 app.whenReady() 之前发起(此时 Chromium 尚未打开缓存目录),删除失败只记日志、不影响启动;diskCacheLimitMb 作为 --disk-cache-size 限制磁盘缓存上限。业务数据(quotes.json / settings.json / pet-stats.json / reminders.json / tuning.json、Local State / Preferences、Local Storage / Session Storage / Network)不参与清理。
| 字段 | 默认 | 含义 |
|---|---|---|
cacheSweepOnStartup | true | 启动时是否清理 |
diskCacheLimitMb | 10 | Chromium 磁盘缓存上限(MB) |
keepCorruptFiles | 3 | 损坏隔离文件保留个数 |
logMaxKb | 512 | 日志体积上限(KB) |
状态页「🔄 数据管理」下的「🧹 清理缓存」可随时手动触发(IPC data:clear-cache),完成后弹出各类释放量。
分「致命」与「可恢复」两档,判据集中在主进程(src/main/runtime.ts / windows.ts / ipc.ts):
| 情形 | 处理 |
|---|---|
| 进程级未捕获异常 / 未处理拒绝、窗口加载失败、CSP 等安全违规 | 致命:写 .build/runtime-failed.json + 结构化日志后退出 |
渲染进程 crashed / oom / integrity-failure | 致命同上;clean-exit / killed 只记日志 |
渲染层脚本异常(Uncaught / Unhandled、runtime:renderer-failed) | 可恢复:记日志并重载对应窗口,每个窗口最多 RENDERER_RECOVERY_LIMIT(3)次,超过才升为致命 |
| 渲染层可恢复问题(IPC 调用失败、素材帧缺失) | 经 runtime:renderer-warning 上报,只记结构化日志、不阻断播放;缺帧保留上一帧降级 |
| 数据文件损坏 | 隔离为 *.corrupt 回退默认值,并记 data-corrupt-fallback 日志 |
日志自身写失败退到 console(logger.ts),保证失败本身可观测。
QA 与校验脚本统一在 tools/(详见 taozi-pet/tools/README.md):
快速 QA check:quick:validate-dev-contract / validate-spec / validate-asset-links + qa-ui + qa-experience,开发启动时自动运行,亚秒级。类型校验由 tsc(npm test 前置)与打包时的 fork-ts-checker 硬闸承担。
全量 QA check:快速 QA + qa-assets 逐帧像素质检(尺寸/透明/贴边/锚点/占用率/地面残留/跨帧稳定/重复帧/交付编码/回归基线)。
责任边界:结构契约归 validate-spec,素材引用归 validate-asset-links,像素质检归 qa-assets,运行时体验语义(打断矩阵、动画、语录、连通性)归 qa-experience。每条规则只在一个文件维护,防止漂移。
帧率:默认 250ms/帧(4 FPS),feed 用 200ms/帧(5 FPS)、sad 用 625ms(1.6 FPS)、clap 用 20ms/帧(50 FPS)。待机轮播动作(look / blink / belly-ok / kick)单轮播放、每帧只出现一次,sad 同为单轮播放、播完定格末帧,sleep 是单帧常驻、不推进帧、起伏全由它自带的呼吸参数提供(周期 4.2s、纵向 +8%,比待机更慢更深),其余非循环动作把独立帧按序播两遍构成完整单次动画(如 happy 12 张 → 播放帧 24);clap 是分段播放的例外,按下 / 抬起两段区间由 segments 给出、播放进度完全由输入驱动。素材共 143 张 base 帧(分布见下表)、无 -r2 副本。
| 状态 | 动作 | 独立帧 | 播放帧 | 类型 | 单周期 | 触发器 | 冷却 |
|---|---|---|---|---|---|---|---|
| look | 待机-张望 | 7 | 7 | 待机轮播 | 1.75s | app:start / ambient:idle | — |
| blink | 待机-眨眼 | 6 | 6 | 待机轮播 | 1.5s | ambient:blink | — |
| belly-ok | 待机-拍拍肚 | 6 | 6 | 待机轮播 | 1.5s | ambient:belly-ok | — |
| kick | 待机-踢腿 | 7 | 7 | 待机轮播 | 1.75s | ambient:kick | — |
| standby-gap | 待机-间歇 | 1 | 1 | 待机间歇 | 按挡位 | 轮播调度(无外部触发) | — |
| feed | 喂食 | 5 | 5 | 单次 | 1.0s | file:drop(拖入桌宠) | — |
| happy | 点击-开心 | 12 | 24 | 单次 | 6.0s | pointer:tap(单击) | ½周期 |
| notify | 提醒 | 12 | 12 | 循环 | 3.0s | reminder:due | — |
| peek | 贴边窥视 | 12 | 12 | 单次 | 3.0s | window:edge-snap(吸附) | ½周期 |
| pet-face | 交互-捏捏脸 | 6 | 12 | 单次 | 3.0s | interaction:pet-face | ½周期 |
| pet-head | 点击-摸摸头 | 6 | 12 | 单次 | 3.0s | state:pet-head(单击头部区域) | ½周期 |
| pumpkin-bag | 交互-掏南瓜包 | 12 | 24 | 单次 | 6.0s | interaction:pumpkin-bag | ½周期 |
| petal-spin | 交互-花瓣转圈 | 12 | 24 | 单次 | 6.0s | interaction:petal-spin | ½周期 |
| starfish-wave | 交互-海星招手 | 12 | 24 | 单次 | 6.0s | interaction:starfish-wave | ½周期 |
| walk | 走路 | 12 | 12 | 循环 | 3.0s | state:walk | — |
| sleep | 睡觉 | 1 | 1 | 定格末帧 | 0.6s | state:sleep(3 分钟无互动) | — |
| sad | 沮丧 | 8 | 8 | 定格末帧 | 5.0s | state:sad(心情↓25) | — |
| clap | 拍手 | 6 | 6 | 单次·分段 | 0.12s | typing:activity(敲字母键) | — |
待机不是单一状态,而是
idleRotation池中四个动作的随机轮播 + 每轮之后的间歇:动作播完先进入间歇(单帧静态母版core-ip,按挡位停顿整数个呼吸周期 = 呼吸次数 ×motion.breathing.periodMs,期间叠一层缓慢呼吸),再按权重随机续接下一个动作(允许连续两次是同一动作)。池成员与间歇都可被任意其它状态打断,池成员之间互不打断、切换由状态机自行推进。 周期 = 播放帧数 × 对应frameDurationMs(各状态帧率见表上方说明);sad是定格末帧状态:心情跌破阈值时播一遍沮丧动画、播完停在末帧(末帧叠呼吸),期间单击桌宠重播一遍,心情回升才回待机;sleep同为定格末帧但只有一帧:进入即静止睡姿,起伏全交给呼吸动效(与待机间歇同一套机制,但自带更慢更深的breathing参数),故 spec 里它只引用一张sleep.png;feed与clap是定向状态:feed由文件拖入正向播放并定格末帧,放手收纳 / 拖离后倒序播放一轮、播完回到拖入前的状态(idle/ sad / sleep / notify;正放未完则先播完再倒放,倒放中途又拖入则从当前帧转回正向);clap由敲键驱动按下 / 抬起两段,各按segments给出的区间播到holdFrame定格,前段未完就抬起则直接跳到后段起点、不补播中间帧。各状态的anchor/mirrorSafe/interrupt等配置全部收敛在pet-spec.json。
优先级通过 canInterrupt("可打断名单")实现:名单列出自己可以打断(压过)谁,* 表示可打断一切;当前处于 idle 或名单包含当前状态时即可切换。同一状态再次触发时另判 interrupt(见「打断自己」列)。下表按可压制范围从宽到窄排列:
| 状态 | 能打断(压过) | 会被谁打断 | 打断自己 |
|---|---|---|---|
| 喂食 | * | 提醒 | 否 |
| 提醒 | * | 喂食、开心、interactions | 否 |
interactions | 除喂食外的全部 | 喂食、提醒、interactions | 是 |
| 开心 | 除喂食、interactions、沮丧外的全部 | 喂食、提醒、interactions、沮丧 | 是 |
| 睡觉 | idle、贴边窥视、走路、沮丧、拍手 | 喂食、开心、提醒、interactions | 否 |
| 沮丧 | idle、开心、贴边窥视、走路、拍手 | 喂食、提醒、interactions、睡觉 | 是 |
| 拍手 | idle、贴边窥视、走路 | 除idle、贴边窥视、走路外的全部 | 是 |
| 贴边窥视 | idle、走路 | 除idle、走路外的全部 | 否 |
| 走路 | idle | 除idle外的全部 | 否 |
idle | 无 | 除idle外的全部 | 否 |
①
idle= 待机表现,即状态表中类型为「待机轮播」「待机间歇」的 5 个状态:待机-张望 / 待机-眨眼 / 待机-拍拍肚 / 待机-踢腿 / 待机-间歇(与idleRotation同源);interactions=experience.interactions的 4 个角色互动:交互-捏捏脸 / 交互-掏南瓜包 / 交互-花瓣转圈 / 交互-海星招手(点击-摸摸头属点击族群,不在interactions名单内)。 ② 「能打断(压过)」列的*是canInterrupt的字面值,代表可打断一切(含idle);聚合行取成员并集,「除…外的全部」指表中其余状态(不含自身)。 ③idle是抢占基底:处于idle之一时任意状态都能接管,故各状态名单里无需也不能写出它们;idle的 4 个动作单轮播完自行进入待机-间歇、走完再续接下一个。两个状态互相写进对方名单时可互相接管,实际由先后顺序决定(后到者接管),整体不存在严格全序。 ④ 「打断自己」列取自interrupt(restart= 是 /resume= 否),只在同一状态被再次下发时生效;idle的进入与续接、喂食的拖入 / 拖离、拍手的分段播放走各自内部路径,不经该判定。主进程在下发活动前用同一判据(stateCanInterrupt)预判渲染层是否采纳。
| 数值 | 范围 | 默认 | 规则 |
|---|---|---|---|
| 好感度 affection | 0–100 | 0 | 点击-摸摸头 +3、交互-捏捏脸 +3、交互-掏南瓜包 +2、交互-花瓣转圈 +2、交互-海星招手 +2、点击桌宠 +2、贴边窥视 +2、喂食 +3;每个来源每天仅首次加好感(同日重复不再加,互动仍计今日互动,防刷)。封顶 100 |
| 心情 mood | 0–100 | 40 | 每 2 分钟 -1(每分钟检测一次,单次结算封顶 6 点);<25 进入「沮丧 sad」(播一遍并定格末帧),≥25 回到待机轮播。每次互动(含同日重复)、点击、窥视、喂食都加心情,增量与同来源的好感增益同值、不限次数,封顶 100 |
| 今日互动 todayInteractions | ≥0 | 0 | 每次互动 +1;跨到新的一天(日期变化)时从 0 起重新累计 |
| 陪伴时长 companionMinutes | ≥0 | 0 | 按实际运行毫秒实时折算为分钟累计 |
| 提醒 reminders | — | 空 | 到点进入 notify 并气泡播报该条文本;用户触发互动或喂食即视为完成(消费当前待处理提醒),每条只提醒一次后自动移除 |
数据以 JSON 原子写入
userData,损坏自动回退默认值;4 个互动动作durationMs=6000ms(动画播完即回待机,不留静止展示期),触发互动或喂食会顺带消费挂起的提醒。
随机行走有 5 个挡位(小屋面板「随机行走」下拉):木头人(彻底不走)→ 散步 → 正常(默认)→ 活泼 → 多动症。挡位越高,走得越勤、越远;同一挡位同时决定待机间歇的呼吸次数(挡位越高越活跃、间歇越短)。开启后桌宠在以拖动中心为圆心的范围内随机游走,播放 walk 动画并匀速移动:
| 挡位 | 移动范围 | 游走间隔 | 单次位移 | 待机间歇(呼吸次数) | 说明 |
|---|---|---|---|---|---|
| 0 木头人 | — | — | — | 3–4 次(7.8–10.4s) | 完全不移动(等于关闭),待机轮播照常 |
| 1 散步 | 220px | 14–30s | 60–140px | 2–4 次(5.2–10.4s) | 慵懒,偶尔挪两步 |
| 2 正常(默认) | 320px | 10–20s | 120–260px | 2–3 次(5.2–7.8s) | 适中 |
| 3 活泼 | 400px | 6–14s | 200–320px | 1–3 次(2.6–7.8s) | 走得勤、迈得远 |
| 4 多动症 | 480px | 4–12s | 300–400px | 1–2 次(2.6–5.2s) | 高频大步快走 |
这张表是
pet-spec.json的motion.walk默认值。开发者模式下的「🚶 随机行走参数」面板可以运行时改这些数值:改动作为整体覆盖写入userData/tuning.json(不动机库里的 spec),主进程立即重建游走调度生效;「恢复默认」或重置数据即清掉覆盖回到 spec 默认值。参数在写入前校验范围与自洽性(间隔/位移区间不倒置、单次位移不超过游走半径),非法值会被拒绝并在面板上提示;qa-experience的walk-levels-shape/walk-levels-monotonic则守住 spec 默认值本身的形状与"挡位越高越勤越远"的排序语义。
待机间歇随挡位联动:间歇长度以呼吸次数计,时长 = 次数 × motion.breathing.periodMs(表内秒数按 2.6s/次折算),在当前挡位区间内随机取整数次;调挡位后桌宠窗口实时生效(无需重启)。木头人挡不走动,但待机轮播与间歇照常运行。待机语录在间歇走完、进入下一个待机动作时播报,节奏同样随挡位走。
动画循环至移动结束:walk 动画在物理移动期间持续循环播放(durationMs:0 触发渲染层无限循环),移动结束才回到待机轮播,不会只播一轮就静止。
移动目标同时受屏幕工作区约束,不会超出屏幕。
镜像:移动含向右分量时播放左右镜像的 walk 动画(素材默认朝左);纯上下移动与贴屏边被钳成零位移时不镜像。
拖动打断:用户拖动桌宠期间暂停游走,位移结束回到待机轮播。
睡觉期间不走:进入 sleep 常驻状态(连续 3 分钟无互动)时立即停掉游走调度,避免定时器在常驻状态下空转;被互动 / 提醒唤醒后自动恢复游走。睡觉计时只由用户主动动作重置(互动 / 拖动 / 开发者面板点击),随机行走等自动行为不会推迟入睡。
互动期间停下脚步:触发任意互动(交互-捏捏脸 / 交互-掏南瓜包 / 交互-花瓣转圈 / 交互-海星招手)时立即停止正在进行的移动,避免桌宠一边播互动动画一边滑行;互动为一次性状态,播完后自动恢复游走调度。
移动时锁定尺寸:每帧移动都按 spec 基准 × 桌宠缩放 的权威尺寸一并下发。frameless 透明窗口在 Windows 上被程序化移动时尺寸会漂移且逐次累积(表现为「越走越大」),钉死尺寸可杜绝累积放大。
气泡显示逻辑统一在桌宠渲染层:带反馈文本的活动直接显示;无文本的普通状态自动从对应语录组随机取一句;notify 为常驻气泡。普通气泡 5 秒后自动消失(peek 贴边窥视与待机轮播动作为 3 秒),同时最多一个(新气泡顶掉旧的)。
| 动作 | 自动触发间隔 | 气泡内容 | 气泡方式 |
|---|---|---|---|
| 待机轮播 look / blink / belly-ok / kick | 一个动作播完 → 间歇(按挡位 0.8–12s)→ 随机续接下一个 | 进入待机动作时取该动作的一句语录(间歇期不发声) | 普通 3s |
| 开心 happy | 用户单击 | 「开心」语录(happy 组) | 普通 5s |
| 提醒 notify | 定时提醒到点 | 该条提醒文本 | 常驻,直到单击/互动/喂食才消失 |
| 贴边窥视 peek | 吸附到屏幕边缘 | 「贴边窥视」语录 | 普通 3s |
| 交互-捏捏脸 pet-face | 右键 → 捏捏脸 | 「捏捏脸」语录 | 普通 5s |
| 点击-摸摸头 pet-head | 单击头部区域 | 「点击-摸摸头」语录(pet-head 组) | 普通 5s |
| 交互-掏南瓜包 pumpkin-bag | 右键 → 掏南瓜包 | 「掏南瓜包」语录 | 普通 5s |
| 交互-花瓣转圈 petal-spin | 右键 → 花瓣转圈 | 「花瓣转圈」语录 | 普通 5s |
| 交互-海星招手 starfish-wave | 右键 → 海星招手 | 「海星招手」语录 | 普通 5s |
| 走路 walk | 按随机行走挡位的游走间隔 | 「走路」语录 | 普通 5s |
| 睡觉 sleep | 连续 3 分钟无互动,进入时播 | 「睡觉」语录 | 普通 5s |
| 沮丧 sad | 心情 < 25 进入时播一遍后停在末帧,其间单击重播 | 「沮丧」语录 | 普通 5s |
| 喂食 feed | 文件拖入桌宠时 | 无 | — |
语录一律来自
experience.quotes这一张表(运行时在userData/quotes.json,可编辑);互动语录同表,键即该互动的状态 id。
单击小屋「状态」页底部 一键重置 后,各数据恢复为以下默认值:
| 项 | 默认值 |
|---|---|
| 桌宠大小 | 0.8(80%,可 0.5–1.5 自由调) |
| 随机行走 | 2(正常) |
| 贴边吸附 edgeSnap | 开 |
| 置顶显示 alwaysOnTop | 开 |
| 打字反应 typingReaction | 开 |
| 鼠标穿透 clickThrough | 关 |
| 开机自启 autoStart / autoStartInit | 关 |
| 好感度 affection | 0 |
| 心情 mood | 40 |
| 今日互动 todayInteractions | 0 |
| 陪伴时长 companionMinutes | 0 |
| 语录 quotes | 恢复pet-spec.json 默认文本 |
| 提醒 reminders | 清空 |
| 开发者模式 devMode | 关(关闭开发者页并回到状态页) |
| 文件口袋 filePocket / filePocketDir | 开 / 默认(文档/taozi-pet) |
拖动:按住桌宠任意拖动,松手自动吸附屏幕边缘(可在面板调整贴边吸附/随机行走挡位)。判定以贴边基准框为基准,不拿窗口缘判——窗口宽度有气泡区最小宽度兜底,随人物缩放变化的成分很小。基准框宽度取人物可见宽度与「贴边将要播报的那条语录」气泡宽度的宽者:贴上边会弹 peek 语录,渲染层在拖拽开始时就把那条语录定下来、实测它的气泡宽度报给主进程,主进程据此把气泡让出来——吸附后气泡正好贴住屏幕边缘、完整留在屏内,人物停在框内(内缩 (语录气泡实宽 − 人物可见宽) / 2,随语录长短变化),贴边后播报的就是同一条语录;语录气泡不比人物宽、或本次贴边不播 peek(睡觉 / 沮丧等状态会拒掉它)时,基准框就是人物本身,人物直接压在屏边。两条判定带固定:基准框缘进入 20px 即判定贴边,松手光标进入 50px 即视为有贴边意图(抓取点在角色中心时光标先顶到屏边,用光标距离补齐意图)。拖拽中、随机行走、桌宠尺寸变化都会解除贴边。吸附恒生效;「贴边窥视」动画只在当前状态允许被它接管时才播——睡觉 / 沮丧 / 提醒等名单不含 peek 的状态下只吸附、不播动画;播了窥视动画即计入好感与心情各 +2。
单击:命中头部区域时播放「点击-摸摸头」,否则播放「点击-开心」;两者都随机讲一句对应语录,都计入好感与心情各 +2(好感每天仅首次计入)。沮丧期间单击只重播一遍沮丧动画、补一句沮丧语录(不切开心,也不判头部区域),播完仍停在末帧。头部区域取自 experience.clickZone.head——按参照状态(待机间歇 standby-gap)那一帧的像素位置归一化存储,故点哪一帧都按同一块区域判定,人物摆别的姿势时那块区域未必落在头上;区域用 stateId / x / y / width / height / interactionId 描述,调整只改这一处。
敲字母键:按住字母键桌宠拍手(播 clap 前 3 帧后定格在抬手前一张),松开播后 3 帧并定格,松开后 3 秒内没有再按就回待机;期间按下新键会立刻从头再走一遍。可在状态页关闭「键盘监听」;睡觉 / 沮丧时常驻状态优先,不响应键盘。
文件口袋(拖放收纳):把文件拖到桌宠身上,桌宠播放「喂食」动画并保持张口;放手即把文件移动进文件口袋目录并倒序收回动画(重名按 Windows 规则加 (2)、 (3) 后缀);拖离(没放手)则不收纳、同样倒序收回。倒放中途再次拖入即恢复张口等待。收纳计入好感与心情各 +3,并消费队首待处理提醒。收纳后在状态页点「打开口袋」直达目录。
右键桌宠:弹出菜单——4 个互动动作(🤏 捏捏脸 / 🎃 掏南瓜包 / 🌸 花瓣转圈 / 👋 海星招手)、打开状态 / 语录 / 提醒面板、开关鼠标穿透、隐藏桌宠。
托盘图标:显示桌宠、打开状态 / 语录 / 提醒、鼠标穿透开关、设置 / 退出。
小屋面板:
状态页:查看好感度 / 心情 / 今日互动 / 陪伴时长;开关置顶显示、鼠标穿透、开机自启、键盘监听、文件口袋(拖放收纳开关 + 更换目录 + 打开口袋);随机行走 5 挡(木头人/散步/正常/活泼/多动症);滑块自由调整桌宠大小(50%–150%);底部一键重置(语录 / 状态 / 提醒 / 设置 / 行走参数恢复正常)。
开发者模式(隐藏调试入口):在状态页末尾的版本号上 2 秒内连点 6 次进入(第 4、5 下会弹出倒计时气泡,第 6 下弹出「已进入开发者模式」并解锁「🧪 开发者」页签)。开发者页提供:关闭开发者模式开关、💊 喂安眠药(立即触发睡觉、遵守打断逻辑)、🪢 抽鞭子(走路开关「抽一次/一直抽」:抽一次=按随机行走挡位立即走一趟,一直抽=不产生位移、循环播放走路动画与语录)、🍃 飞叶子(心情置 100)、👊 轻抚脸颊(心情置 0)、🚶 随机行走参数(调 5 档的游走半径/间隔/位移与速度;改动写入 userData/tuning.json 覆盖 spec 默认值,可「恢复默认」)、📐 调试叠加层(在桌宠所在屏幕上画出左右吸附判定带、当前挡位的随机行走范围、点击热点区及其参照的精灵方框,并标注桌宠大小与气泡实际大小;全屏透明、点击穿透,退出开发者模式自动撤下)。开启状态持久化到 settings.json(devMode),重启仍保持;关闭后可再次连点版本号进入。
语录页:状态语录与互动语录均可在线编辑,编辑即生效、重启保留(唯一数据源 userData/quotes.json)。
提醒页:新增 / 删除定时提醒,到点由桌宠气泡提醒。
数据位置:运行时数据在 Electron userData(quotes.json / settings.json / pet-stats.json / reminders.json / logs/app.jsonl)。
不纳入 Git 追踪、勿提交:taozi-pet/incoming-assets/(抠图中间产物)、taozi-pet/src/assets/(精灵帧与头像,均由流水线/make-graphics.py 重建)、taozi-pet/qa/(QA 报告与接触表)、.build/、.webpack/、out/、release/。
纳入版本库:白底源图 assets-raw/(不可再生的 AI 源图)、配置 taozi-pet/pet-spec.json、流水线脚本与文档。