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

183 lines
9.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 悬浮宠物(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:
```bash
# 原画质(交付用)
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 解析):
```bash
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`,随机动画链在展示时长内不会覆盖它。
示例:
```bash
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 限制**。
```bash
./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 膨胀