Probe(run 343)实测结论: - job 跑在 Docker 容器内,容器里没有 java,也没有 /opt/jdk17、/opt/android-sdk、/opt/gradle - 127.0.0.1 不可达(容器内 127 指向自身);172.17.0.1(Docker 网关)17.6 MB/s;公网 370 KB/s 改动: - 素材地址顺序改为 172.17.0.1 → 127.0.0.1 → server_url(按容器/host 两种模式的实际可达性排) - 构建改用探测到的系统 gradle:本项目没有 gradlew(build.sh 用的是 /opt/gradle/gradle-8.5) - 找不到 SDK 时的报错补上「容器看不到宿主机 /opt」这一层,并给出改 host 模式的具体命令
小鲸娘 · 悬浮宠物(安卓原生 App)
一个独立的安卓「悬浮宠物」应用:把一只透明动画宠物做成 TYPE_APPLICATION_OVERLAY 悬浮窗,浮在手机所有 App 之上,并起一个本机 HTTP 控制端口(127.0.0.1:7350),让你(或 DSH 里的 Agent)用 curl 远程指挥它说话、换动作。
从 v1.0 起已演进为完整的小鲸娘:106 个动画、2366 张表情包、三层记忆、心情系统、9 个可配置插件、10 个技能、DeepSeek 对话直连、MCP/HTTP 接入、动画随机链 + 物理引擎。
定位说明:这不是对
dsh-petnpm 插件的修改——网页插件改不出原生悬浮窗。它是 dsh-pet 的原生伴生 App,复用了 dsh-pet 的控制契约(/broadcast)与动画素材,跑在安卓系统层。
1. 目录结构
dsh-pet-floating/
├── README.md
├── settings.gradle.kts
├── build.gradle.kts
├── gradle.properties
├── gradle/wrapper/gradle-wrapper.properties # wrapper jar 未包含(二进制)
└── app/
├── build.gradle.kts
└── src/main/
├── AndroidManifest.xml
├── assets/pet.html # 悬浮窗前端(气泡 + 轮询 /state)
└── java/com/dsh/petfloat/
├── MainActivity.kt # 索要悬浮窗权限 + 启动服务
├── FloatingPetService.kt # 前台服务:悬浮窗 + WebView + 拖拽
└── PetServer.kt # 本地 HTTP 控制端口(127.0.0.1:7350)
2. 为什么动画用 WebP 而不是 dsh-pet 的 webm
这是本工程最关键的设计点:
- dsh-pet 的动画是 VP9-alpha webm;
- 安卓 WebView(Chromium)对「透明 webm」支持很差,大概率渲染成黑底;
- 而 Chromium 对动态 WebP(animated WebP)+ alpha 支持良好,
<img>就能透明播放。
所以:把 dsh-pet 的 webm 转成动态 WebP,丢进 app/src/main/assets/,即可透明悬浮。默认没放素材时,App 会用一只 🐋 emoji 兜底,保证能跑。
3. 前置条件
- 一台能跑 Android Studio 的电脑;
- JDK 17;
- Android Studio(会自带 AGP / Gradle);本工程锁定 AGP 8.2.2 + Kotlin 1.9.22 + Gradle 8.2,Gradle Wrapper 的
gradle-wrapper.jar未提交,用 Android Studio 打开即可自动生成,或在本机 Gradle 下执行gradle wrapper。
4. 构建
cd dsh-pet-floating
./gradlew :app:assembleDebug # 或 assembleRelease(需要自己配签名)
产物:app/build/outputs/apk/debug/app-debug.apk。
release 未配签名,直接装会失败;先用 debug,或自行加 signingConfig。
5. 安装 & 授权悬浮窗
-
安装 APK(任选其一):
- 电脑
adb install app-debug.apk; - 或让宿主的 Agent 用 pilot 的
app.install {"upload":"<文件名>"}(先把 APK 放进/opt/pilot/run/uploads/)装。
- 电脑
-
首次启动:App 会跳到系统「显示在其他应用上层」授权页,勾选允许。
-
也可用 Agent 侧 appops 直接开(android 8+):
appops.set { package: "com.dsh.petfloat", op: "android:system_alert_window", mode: "allow" } -
授权后回到 App(或由
app.launch拉起com.dsh.petfloat),悬浮宠物即出现,可按住拖动位置;前台服务常驻,通知栏有一条「悬浮宠物运行中」,关闭该通知/强杀即可停。
6. 控制接口(本机 127.0.0.1:7350)
契约对齐 dsh-pet 的 /broadcast,Agent 用同一套 curl 就能指挥。
让宠物头顶冒气泡
curl -X POST 'http://127.0.0.1:7350/broadcast' \
-H 'Content-Type: application/json' \
-d '{"text":"搞定啦,回来看看","duration":8000}'
text:气泡正文(原样显示);空 → 收起。duration:显示毫秒,缺省 8000。
换动画
curl -X POST 'http://127.0.0.1:7350/anim' \
-H 'Content-Type: application/json' \
-d '{"name":"dance"}'
- 前端会尝试加载
/dance.webp;若assets/dance.webp不存在,保持当前形象。
收起气泡
curl -X POST 'http://127.0.0.1:7350/bubble/clear'
读取状态 / 探活
curl 'http://127.0.0.1:7350/state'
对照表
| 本 App | dsh-pet 原接口 | 说明 |
|---|---|---|
POST /broadcast |
POST /dsh-pet-7340/broadcast |
气泡接力,语义一致 |
POST /anim |
POST /dsh-pet-7340/anim |
简化为「按名换 webp」 |
POST /bubble/clear |
POST /dsh-pet-7340/bubble/clear |
语义一致 |
7. 素材转换(VP9-alpha webm → animated WebP)
在一台有 ffmpeg(含 libwebp 编码器)的机器上:
ffmpeg -i 输入.webm -c:v libwebp -q:v 80 -lossless 0 -loop 0 -pix_fmt yuva420p -an 输出.webp
- 默认形象:转一个待机动画,命名
pet.webp,放app/src/main/assets/pet.webp; - 点播动画:每个动作命名
<name>.webp,用/anim {"name":"<name>"}切换; - dsh-pet 素材源在
dsh-pet/assets/webm/(106 个、约 53MB、含中文文件名;放进 assets 的 webp 文件名建议用[a-zA-Z0-9._-],本服务的静态路由只放行该字符集)。
验证 alpha:
ffprobe -v error -show_entries stream=pix_fmt 输出.webp应看到yuva420p。
8. Agent 如何对接(在宿主机上)
App 的控制端口绑定 127.0.0.1:7350,与 dsh-pet 一样走本机 HTTP,因此 Agent 直接:
curl -X POST 'http://127.0.0.1:7350/broadcast' -H 'Content-Type: application/json' \
-d '{"text":"…","duration":8000}'
即可让手机上的悬浮宠物说话;/anim、/bubble/clear 同理。
9. 已知限制 / 风险
- 本机没有安卓构建链(无 JDK/Gradle/SDK),本工程源码在此环境下未实际编译验证,需在 Android Studio 里首次构建时排错;
- 透明动画依赖「webm→动画 WebP」转码,未在此环境验证最终观感;
- 悬浮窗在系统设置、部分锁屏/支付界面可能被系统临时隐藏(安卓安全策略);
- 控制端口只绑回环地址、无鉴权,仅限本机可信环境使用,别开放公网。
10. 对外接入(速览)
详细文档见 INTEGRATION.md 与各端口 /docs / /docs.md / /llms.txt。
| 能力 | 入口 |
|---|---|
| HTTP 控制 | POST /api/task /api/notify /api/anim /api/meme /api/skill 等(127.0.0.1:7350,写请求需 X-Pet-Token) |
| SSE 互动事件 | GET /api/events?token=… |
| MCP Server | POST /mcp(JSON-RPC,7 个工具) |
| 能力目录 | GET /api/catalog(106 动作 + 2366 表情 + 语义标签) |
| 插件 | GET/POST /api/plugin、GET /api/plugins.js(JS,WebView 内跑) |
11. 素材再生成(新机器还原)
三个大体积资源目录不纳入 git(合计约 650MB,多为可再生/可下载的二进制), 在新机器上按下面顺序重建:
# ① 动画 webp(assets_hq 640×360 / assets_small 320×180,共 106 个)
# 从 dsh-pet 的 webm 源转码,映射表在 scripts/anim_index.json
DSH_PET=/path/to/dsh-pet ./scripts/regenerate_assets.sh
# ② 表情包图(assets_net,~2300 张)
# 从「梗鲸档案馆」(aigengtu.com) 下载并转码,元数据在 scripts/meme_meta.json
python3 scripts/download_memes.py
# ③(可选)动画配置 config.json 若需从 dsh-pet 重建,运行后保留 memes 列表
DSH_PET=/path/to/dsh-pet python3 scripts/gen_config.py
依赖:ffmpeg(libvpx-vp9 解码 + libwebp_anim 编码)、requests、Pillow。
config.json、candy.ttf、pet.html、内置表情包(m01–m51)在 assets_common/,已随仓库提交。
仓库只存「源码 + 脚本 + 源数据 + 小体积源资产」,构建产物(APK)与可再生资源(hq/small/net) 始终留在本机,不被 git 追踪——这是为了让仓库能稳定推送、克隆,而不是塞进 650MB 二进制。
分支约定
main(默认,轻量 ~8MB):源码 + 脚本 + 源数据 + 小体积源资产,新机器跑scripts/再生成大资源。full-assets(完整快照 ~650MB):在main基础上额外包含assets_hq/assets_small/assets_net,克隆后无需再生成资源即可直接./build.sh构建完整 APK。
默认维护
main;需要交付/备份完整素材时,切到full-assets或从它检出。
12. 发布与更新
两条链路:CI 构建(Gitea Actions → Release)与应用内更新(App 读 Release → 下载 → 安装)。
发版流程
git push origin main # 代码先上 main
git tag v1.0.1002 && git push origin v1.0.1002 # 打 tag 触发构建 + 发版
打 tag 后 .gitea/workflows/build.yml 会:检出当前 ref 的代码 → 从 full-assets 分支叠加
assets_hq / assets_small / assets_net(main 保持轻量,不追踪这三个目录)→ 用 KEYSTORE_BASE64
注入的固定密钥签 release 包 → 发布 APK 与 version.json 到该 tag 的 Release。
versionCode = 1000 + run_number,由 CI 注入。必须单调递增——系统只允许更高 versionCode
覆盖安装,版本号回退会导致手机装不上。
必需 secrets
仓库 → Settings → Actions,配 4 个:
| Secret | 内容 |
|---|---|
KEYSTORE_BASE64 |
发布密钥的 base64:base64 -i petfloat-release.jks | tr -d '\n' |
KEYSTORE_PASSWORD |
keystore 口令 |
KEY_ALIAS |
petfloat |
KEY_PASSWORD |
密钥口令(与上面相同即可) |
GITEA_TOKEN 由 Gitea 自动注入,不需要配(同名 secret 会被拒 400)。
签名是硬约束:所有版本必须用同一把密钥。换密钥后已装用户无法覆盖升级,只能卸载重装, 记忆数据会一起丢。keystore 请离线备份。
应用内更新
UpdateChecker 的流程:
GET /api/v1/repos/mtfyun/dsh-pet-floating/releases?limit=10,跳过 draft,取 versionCode 最大的一条- 优先读
version.json拿 versionCode / sha256 / 体积;读不到再从 tag 尾段解析 - 与当前安装版本比 versionCode(用 versionName 按字符串比会把
1.0.9判成比1.0.10新) - 下载到
cacheDir/update/,校验体积与 SHA256,不一致立即删除 - 经自带
ApkProvider(content://com.dsh.petfloat.apk/)交给系统安装器
入口:设置页 → 关于 → 检查更新。Android 8+ 首次需「安装未知来源应用」授权,未授权时先引导去开, 而不是下载完才失败。
项目零第三方依赖,用不了
androidx.core的FileProvider,所以ApkProvider是自带的只读 ContentProvider:只暴露cacheDir/update下的.apk,路径取 basename 防../穿越。
runner
需要一台带 JDK 17 + Android SDK 的 act_runner,标签 ubuntu-latest。
推荐 host 模式(直接复用机器上现成的 /opt/jdk17、/opt/android-sdk,不必再做 Android 镜像):
# 注册令牌:Gitea → 站点管理 → Actions → Runners
REG_TOKEN=<令牌> bash scripts/install-runner.sh
13. 参考与借鉴
本工程并非从零造轮子,素材、行为系统、记忆策略、UI 语言分别借鉴/参考了以下项目:
核心素材与契约
- dsh-pet(
github.com/PC2005-cloud/dsh-pet,npmdsh-pet@0.3.8)——106 个 VP9-alpha 动画 webm、config.jsonc动画定义、27 张原版表情包、/broadcast控制契约、碎碎念与对话设计。本工程是其「原生伴生 App」:复用素材与控制语义,改用原生悬浮窗实现。
三个行为系统(逻辑对齐 Yuuyuko-uu 的插件)
Yuuyuko-uu/dsh-fish-memory「鱼的记忆」——带半衰期(30 天)的淡忘式长期记忆、长期事实不过期且每轮固定带、按词稀有度召回。Yuuyuko-uu/dsh-feiyu-mood「鱼的心情」——0–100 心情、基线 70 回落(半衰 3h)、七根情绪(半衰 90min)、confidence 门槛。Yuuyuko-uu/dsh-speak-up「主动搭话」——定时器 + 本地掷骰子,中签才开一轮对话(不花 token)。@duke-dsh-plugins/dsh-whaleharbor-pet——心情状态机(moodName/mood-*切换)的早期参考。
记忆框架(调研后采纳的语义)
- Mem0(
github.com/mem0ai/mem0)——ADD / UPDATE / DELETE 事实抽取语义(MemoryStore.applyOps采用)。 - Zep(
github.com/getzep/zep)——时间戳 + 新鲜度衰减的召回思路。 - (Letta / MemGPT、LangMem 调研过;因需要 embedding 接口,本机无、本地向量模型成本高,未采用。)
插件 / Skill / MCP
- Hermes Agent 插件生态(hermes-agent.nousresearch.com)——插件与 Skill 分层、插件安装协议的设计参考,以及
dsh-hermes-bridge桥接思路。 - MCP(Model Context Protocol,modelcontextprotocol.io)——
/mcp的 JSON-RPC 工具暴露(initialize / tools/list / tools/call)。
表情包素材(社区二创)
- 梗鲸档案馆(aigengtu.com) 及
the-beating-light-of-the-nail/deepseek-chan-meme-pack——2366 张 DeepSeek 娘二创表情包,由scripts/download_memes.py批量取得。
UI 设计语言
- DeepSeek App——白底分组列表(Inset Grouped)、细分割线、品牌蓝
#4D6BFE、深浅色跟随系统。
版权提醒:动画素材来自 dsh-pet(MIT);表情包为社区二创,用于个人桌面宠物没有问题, 若要公开发布 App,建议先取得原作者授权。三个行为系统插件来自 Yuuyuko-uu,本项目按其 参数/逻辑做了原生重实现,沿用其设计意图。