Modern-eve/taozi-pet

TypeScript

0

95 commits

updated Oct 12, 2026

See the code

README

套子桌宠

相关文档

基于 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)

项目几乎所有行为都由 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)不参与清理。

字段默认含义
cacheSweepOnStartuptrue启动时是否清理
diskCacheLimitMb10Chromium 磁盘缓存上限(MB)
keepCorruptFiles3损坏隔离文件保留个数
logMaxKb512日志体积上限(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 体系

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待机-张望77待机轮播1.75sapp:start / ambient:idle—
blink待机-眨眼66待机轮播1.5sambient:blink—
belly-ok待机-拍拍肚66待机轮播1.5sambient:belly-ok—
kick待机-踢腿77待机轮播1.75sambient:kick—
standby-gap待机-间歇11待机间歇按挡位轮播调度(无外部触发)—
feed喂食55单次1.0sfile:drop(拖入桌宠)—
happy点击-开心1224单次6.0spointer:tap(单击)½周期
notify提醒1212循环3.0sreminder:due—
peek贴边窥视1212单次3.0swindow:edge-snap(吸附)½周期
pet-face交互-捏捏脸612单次3.0sinteraction:pet-face½周期
pet-head点击-摸摸头612单次3.0sstate:pet-head(单击头部区域)½周期
pumpkin-bag交互-掏南瓜包1224单次6.0sinteraction:pumpkin-bag½周期
petal-spin交互-花瓣转圈1224单次6.0sinteraction:petal-spin½周期
starfish-wave交互-海星招手1224单次6.0sinteraction:starfish-wave½周期
walk走路1212循环3.0sstate:walk—
sleep睡觉11定格末帧0.6sstate:sleep(3 分钟无互动)—
sad沮丧88定格末帧5.0sstate:sad(心情↓25)—
clap拍手66单次·分段0.12styping: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 名单)

优先级通过 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)预判渲染层是否采纳。

数值规则

数值范围默认规则
好感度 affection0–1000点击-摸摸头 +3、交互-捏捏脸 +3、交互-掏南瓜包 +2、交互-花瓣转圈 +2、交互-海星招手 +2、点击桌宠 +2、贴边窥视 +2、喂食 +3;每个来源每天仅首次加好感(同日重复不再加,互动仍计今日互动,防刷)。封顶 100
心情 mood0–10040每 2 分钟 -1(每分钟检测一次,单次结算封顶 6 点);<25 进入「沮丧 sad」(播一遍并定格末帧),≥25 回到待机轮播。每次互动(含同日重复)、点击、窥视、喂食都加心情,增量与同来源的好感增益同值、不限次数,封顶 100
今日互动 todayInteractions≥00每次互动 +1;跨到新的一天(日期变化)时从 0 起重新累计
陪伴时长 companionMinutes≥00按实际运行毫秒实时折算为分钟累计
提醒 reminders—空到点进入 notify 并气泡播报该条文本;用户触发互动或喂食即视为完成(消费当前待处理提醒),每条只提醒一次后自动移除

数据以 JSON 原子写入 userData,损坏自动回退默认值;4 个互动动作 durationMs=6000ms(动画播完即回待机,不留静止展示期),触发互动或喂食会顺带消费挂起的提醒。

随机行走(walk 移动规则)

随机行走有 5 个挡位(小屋面板「随机行走」下拉):木头人(彻底不走)→ 散步 → 正常(默认)→ 活泼 → 多动症。挡位越高,走得越勤、越远;同一挡位同时决定待机间歇的呼吸次数(挡位越高越活跃、间歇越短)。开启后桌宠在以拖动中心为圆心的范围内随机游走,播放 walk 动画并匀速移动:

挡位移动范围游走间隔单次位移待机间歇(呼吸次数)说明
0 木头人———3–4 次(7.8–10.4s)完全不移动(等于关闭),待机轮播照常
1 散步220px14–30s60–140px2–4 次(5.2–10.4s)慵懒,偶尔挪两步
2 正常(默认)320px10–20s120–260px2–3 次(5.2–7.8s)适中
3 活泼400px6–14s200–320px1–3 次(2.6–7.8s)走得勤、迈得远
4 多动症480px4–12s300–400px1–2 次(2.6–5.2s)高频大步快走
  • 移动速度固定 2 px/帧(约 60fps 匀速)。

这张表是 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关
好感度 affection0
心情 mood40
今日互动 todayInteractions0
陪伴时长 companionMinutes0
语录 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、流水线脚本与文档。

Modern-eve/taozi-pet

TypeScript

0

95 commits

updated Oct 12, 2026

See the code

README

套子桌宠

相关文档

基于 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)

项目几乎所有行为都由 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)不参与清理。

字段默认含义
cacheSweepOnStartuptrue启动时是否清理
diskCacheLimitMb10Chromium 磁盘缓存上限(MB)
keepCorruptFiles3损坏隔离文件保留个数
logMaxKb512日志体积上限(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 体系

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待机-张望77待机轮播1.75sapp:start / ambient:idle—
blink待机-眨眼66待机轮播1.5sambient:blink—
belly-ok待机-拍拍肚66待机轮播1.5sambient:belly-ok—
kick待机-踢腿77待机轮播1.75sambient:kick—
standby-gap待机-间歇11待机间歇按挡位轮播调度(无外部触发)—
feed喂食55单次1.0sfile:drop(拖入桌宠)—
happy点击-开心1224单次6.0spointer:tap(单击)½周期
notify提醒1212循环3.0sreminder:due—
peek贴边窥视1212单次3.0swindow:edge-snap(吸附)½周期
pet-face交互-捏捏脸612单次3.0sinteraction:pet-face½周期
pet-head点击-摸摸头612单次3.0sstate:pet-head(单击头部区域)½周期
pumpkin-bag交互-掏南瓜包1224单次6.0sinteraction:pumpkin-bag½周期
petal-spin交互-花瓣转圈1224单次6.0sinteraction:petal-spin½周期
starfish-wave交互-海星招手1224单次6.0sinteraction:starfish-wave½周期
walk走路1212循环3.0sstate:walk—
sleep睡觉11定格末帧0.6sstate:sleep(3 分钟无互动)—
sad沮丧88定格末帧5.0sstate:sad(心情↓25)—
clap拍手66单次·分段0.12styping: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 名单)

优先级通过 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)预判渲染层是否采纳。

数值规则

数值范围默认规则
好感度 affection0–1000点击-摸摸头 +3、交互-捏捏脸 +3、交互-掏南瓜包 +2、交互-花瓣转圈 +2、交互-海星招手 +2、点击桌宠 +2、贴边窥视 +2、喂食 +3;每个来源每天仅首次加好感(同日重复不再加,互动仍计今日互动,防刷)。封顶 100
心情 mood0–10040每 2 分钟 -1(每分钟检测一次,单次结算封顶 6 点);<25 进入「沮丧 sad」(播一遍并定格末帧),≥25 回到待机轮播。每次互动(含同日重复)、点击、窥视、喂食都加心情,增量与同来源的好感增益同值、不限次数,封顶 100
今日互动 todayInteractions≥00每次互动 +1;跨到新的一天(日期变化)时从 0 起重新累计
陪伴时长 companionMinutes≥00按实际运行毫秒实时折算为分钟累计
提醒 reminders—空到点进入 notify 并气泡播报该条文本;用户触发互动或喂食即视为完成(消费当前待处理提醒),每条只提醒一次后自动移除

数据以 JSON 原子写入 userData,损坏自动回退默认值;4 个互动动作 durationMs=6000ms(动画播完即回待机,不留静止展示期),触发互动或喂食会顺带消费挂起的提醒。

随机行走(walk 移动规则)

随机行走有 5 个挡位(小屋面板「随机行走」下拉):木头人(彻底不走)→ 散步 → 正常(默认)→ 活泼 → 多动症。挡位越高,走得越勤、越远;同一挡位同时决定待机间歇的呼吸次数(挡位越高越活跃、间歇越短)。开启后桌宠在以拖动中心为圆心的范围内随机游走,播放 walk 动画并匀速移动:

挡位移动范围游走间隔单次位移待机间歇(呼吸次数)说明
0 木头人———3–4 次(7.8–10.4s)完全不移动(等于关闭),待机轮播照常
1 散步220px14–30s60–140px2–4 次(5.2–10.4s)慵懒,偶尔挪两步
2 正常(默认)320px10–20s120–260px2–3 次(5.2–7.8s)适中
3 活泼400px6–14s200–320px1–3 次(2.6–7.8s)走得勤、迈得远
4 多动症480px4–12s300–400px1–2 次(2.6–5.2s)高频大步快走
  • 移动速度固定 2 px/帧(约 60fps 匀速)。

这张表是 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关
好感度 affection0
心情 mood40
今日互动 todayInteractions0
陪伴时长 companionMinutes0
语录 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、流水线脚本与文档。