Files
dsh-pet-floating/NOTES.md
T
mtfyun 7d2e8fed8e 小鲸娘 v1.0:悬浮宠物完整工程
- 106 动画 + 2366 表情包 + 语义标签
- 三层记忆 / 心情系统 / 9 可配置插件 / 10 技能
- DeepSeek 对话直连 + MCP/HTTP 接入 + 接入文档
- 动画随机链 + 物理引擎 + 贴边碰撞校准
- 大资源(动画webp/表情图)由 scripts/ 再生成,不入库
2026-11-08 08:07:09 +00:00

9.2 KiB
Raw Blame History

悬浮宠物(com.dsh.petfloat)工程备忘

用户偏好(重要)

  • 默认一律构建并交付「原画质」包(640×360,-Phq),不要再默认出小体积包。
  • 原因:pilot 的 app.install 有 128MB 硬上限(见 /opt/pilot/USAGE.md), 原画质包 ~427MB 无法通过 pilot 安装 → 由用户手动安装。
  • 小体积包(320×180,~118MB)仅作为「需要我远程直装调试」时的临时手段,不作为交付物。

素材目录(已拆分)

目录 内容
app/src/main/assets_common config.json、candy.ttf、pet.html、m01..m27.webp(表情包)
app/src/main/assets_small a0001..a0106.webp(320×180, q68)
app/src/main/assets_hq a0001..a0106.webp(640×360, q80,原画质)

app/build.gradle.kts 按 -Phq 选择 small / hq:

# 原画质(交付用)
JAVA_HOME=/opt/jdk17 /opt/gradle/gradle-8.5/bin/gradle assembleDebug -Phq
# 小体积(仅用于 pilot 直装调试)
JAVA_HOME=/opt/jdk17 /opt/gradle/gradle-8.5/bin/gradle assembleDebug

产物统一复制为 petfloat-hq.apk / petfloat-small.apk,HQ 通过 present 交给用户手动安装。

动画素材来源

/root/projects/.dsh/profiles/web/node_modules/dsh-pet/assets/webm/*.webm(106 个,VP9-alpha,10.042s; 「工作状态-垂头叹气冒汗」为 4.917s)。

转码(本机 ffmpeg 的 webp 解码器不支持动画 webp,只能编码,校验用 RIFF 解析):

ffmpeg -y -c:v libvpx-vp9 -i in.webm \
  -vf scale=640:360 -c:v libwebp_anim -q:v 80 -loop 0 -pix_fmt yuva420p -an out.webp

已知限制

  • app.install 单文件 ≤128MB,且每次都要用户在手机确认。
  • pilot 审批卡只在 60 秒窗口内有效;安装前需用户在看手机。
  • 动画池/权重/物理参数全部来自 assets_common/config.json(由 dsh config.jsonc 生成)。

对外接入端口(供 workbuddy / DeepSeek 等调用)

本机 HTTP 服务监听 127.0.0.1:7350(仅回环,设备内任意 App 可访问)。 控制面板「接入」页可查看地址并复制。

方法 路径 说明
GET /api/health 返回应用信息与端点清单
POST /api/notify {state|level, text, durationMs, meme} 推通知
POST /api/task {state: start|working|result|waiting|done|error, text}
POST /api/anim {name, holdMs?} 点播任意动画(中文名)
POST /api/broadcast {text, duration, meme} 仅气泡
POST /api/clear 清空气泡
GET /api/state 当前动画与气泡状态

状态 → 动画档位:start/thinking→思考冒泡,working→忙碌点按,result→清点归档, waiting→原地踱步张望,done/success→雀跃庆祝,error→垂头叹气冒汗。 level 取值 info/success/warn/error/working/result。

通知会设置 holdUntil,随机动画链在展示时长内不会覆盖它。

示例:

curl -X POST http://127.0.0.1:7350/api/notify \
  -H 'Content-Type: application/json' \
  -d '{"state":"done","text":"任务完成:报告已生成"}'

调用方需允许明文 HTTP(usesCleartextTraffic 或 network-security-config 放行 127.0.0.1)。

气泡(独立悬浮窗)

  • 气泡是独立 overlay 窗(FLAG_NOT_TOUCHABLE),浮在宠物窗口上方, 位置 = 宠物窗口顶边 - 气泡高度 + 6dp,水平居中,自动夹在安全区内;宠物移动时跟随。
  • 因此气泡不会被宠物窗口裁切(此前 240×135dp 窗口内的 HTML 气泡会被截断)。
  • 样式存 pet_prefs/bubble_style,可选 default|dark|outline|note, 由 BubbleDrawable(圆角矩形 + 底部三角尾巴)绘制;面板「外观 → 气泡样式」切换。
  • 宠物窗口 HTML(pet.html)现在只画宠物本体。

自助接入文档(端口自带)

  • GET /docs —— 人看的 HTML 文档页:地址/鉴权/在线测试表单/接口表/状态映射/错误码/代码示例
  • GET /api/docs —— 机器可读 JSON 规范:endpoints[] / states / levels / errors / auth
  • 已支持 CORS 预检(OPTIONS → 204,允许 Content-Type, X-Pet-Token), 浏览器/WKWebView 类应用可直接 fetch 调用。

互动事件流(SSE)

GET /api/events?token=<授权码> 返回 text/event-stream,每 15s 心跳 : ping。

事件类型(event: pet,data 为 JSON):

type 触发 额外字段
tap 用户点按宠物 anim
drag_start 开始拖拽 x, y
drag_end 松手 fling(bool), x, y / vx, vy
long_press 长按打开面板 -
play 点播动画(面板或 /api/anim) name
notify 推送通知 state, level, text
bubble 只发气泡 text
bubble_clear 清空气泡 -
anim 随机链换动作 name, kind

EventSource 不能设请求头,所以 SSE 用 ?token= 传授权码。

应用主界面(MainActivity)

  • 以前 MainActivity 启动服务后立刻 finish(),点图标「像没反应」。现在改成真正的首页: 状态卡(运行中/未运行 + 当前动作 + 订阅数 + 是否需授权码)、显示/收起宠物、 控制面板入口、复制接入地址、打开接入文档、最近通知(4 条)、版本信息。
  • 首次进入自动拉起宠物(需已授予悬浮窗权限,否则显示授权入口)。
  • 主题改为 Theme.Material.Light.NoActionBar。
  • 状态通过轮询 /api/health + /api/state 获取(2s 一次,页面不可见时停止)。

交付方式:手机共享目录(推荐)

/root/share/DSHBox 是手机内部存储 DSHBox/ 的 bind 挂载(/storage/emulated/0/DSHBox), 构建好的 APK 直接放进去,手机上用文件管理器点开即可安装,不受 pilot 128MB 限制。

./build.sh          # 原画质 → petfloat-hq.apk,并自动复制到 /root/share/DSHBox/
./build.sh small    # 小体积 → petfloat-small.apk

手机端路径:内部存储/DSHBox/petfloat-hq.apk(首次安装需允许「安装未知应用」)。

界面合并(v1.0 后期)

  • SettingsActivity 已删除,所有设置并入 MainActivity 的 4 个页签: 状态 / 外观 / 动作 / 接入。
  • 长按宠物 → 直接打开 MainActivity(默认落在「外观」页,EXTRA_TAB=TAB_LOOK)。
  • 点通知 → 打开 MainActivity(「状态」页)。
  • MainActivity 为 launchMode=singleTop,重复打开走 onNewIntent 切换页签。
  • 授权码仍在「接入 → 推送授权」列表的二级弹窗里查看/复制/改名/删除。

UI 方向(最终)

  • 参考 DeepSeek App:白底分组列表(Inset Grouped)+ 细分割线 + 右侧 chevron + 品牌蓝 #4D6BFE。
  • 不再做液态玻璃(试过 A/B/C 三版 + 奶白玻璃,用户否决)。
  • 三个页签:状态 / 动作 / 设置(底部浮动胶囊,纯文字,选中为浅蓝底 + 品牌蓝加粗)。
  • 跟随系统浅色/深色(Configuration.UI_MODE_NIGHT_MASK,浅 #F7F8FA/白卡,深 #121316/#1B1D21)。
  • 字体统一用系统字体,只有品牌大标题可用软糖体。
  • 长按宠物 → 主界面「设置」页;点通知 → 「状态」页。

碰撞边界(重要)

  1. getRealMetrics 取真实屏幕(1440×3200),且系统对悬浮窗有 offsetY=169px(状态栏)偏移, 用 getLocationOnScreen 自校准得到 offsetX/offsetY,碰撞边界据此补偿。
  2. 素材本身带大量透明留白(实测 l≈33.8%、r≈34.1%、t≈17.8%、b≈11.7%), 所以窗口贴边时角色仍离边很远。解法:pet.html 用 canvas 扫码 alpha 通道量出边距, POST /sprite 上报(免鉴权,服务端取各动画的最大值),Service 允许窗口超出屏幕这些距离, 使角色真正贴边。
  3. 旋转/折叠由 onConfigurationChanged 重算边界并夹回。

物理引擎(v2)

  • 真实帧间隔积分(System.nanoTime,dt 上限 0.05s 防卡顿跳跃)
  • 半隐式欧拉 + 空气阻力(0.55/s)
  • 重力按密度换算:gravity × density × 0.85(跨机型手感一致)
  • 分面弹性:地面 restitution,墙面 0.82×restitution
  • 地面摩擦衰减(groundFriction 指数衰减),低速自动收敛停止
  • 飞行中按水平速度轻微倾斜(±11°),落地回正
  • 撞击(>450px/s)触发挤压&拉伸形变(1.14/0.86 → 1.0,240ms)
  • 速度上限 5200/7000 px/s,避免高速穿透
  • 全面使用配置里的 throwPower / restitution / groundFriction / ceilingBounce

记忆(三层,纯本地)

  • L1 短期:最近 20 轮原文(chat_history,存 40 条)
  • L2 长期记忆卡:MemoryStore(JSON 存 pet_prefs.memory_cards,上限 300 条) 字段:title / content / tags / importance(1~3) / created / updated / hits
  • L3 召回:关键词切分命中(2.0/词) + 标签命中(1.5) + 重要度(0.8×i) + 新鲜度(≤1.2) + 使用次数(0.15×hits),取 Top5 注入 system 第二段
  • 抽取:每 2 轮对话异步调一次 ChatClient.extractMemories(单独 prompt,输出 JSON,失败静默)
  • 管理:设置 → 扩展 → 记忆(自动记忆开关 / 长按删除 / 手动添加 / 清空)
  • 不走 embedding:DeepSeek 无 embedding API,本地向量模型会让 APK 膨胀