jkooe 665baa0831 docs: 补充「发布与更新」章节,归档 act_runner 安装脚本
- README 新增第 12 节:发版流程(打 tag)、必需 secrets、签名硬约束、
  应用内更新原理、runner 要求(host 模式复用现成 JDK/SDK)
- scripts/install-runner.sh:act_runner 安装与注册(host 模式),
  参数化 GITEA_URL / REG_TOKEN / LABELS,装成 systemd 服务
2026-11-09 17:32:01 +08:00

小鲸娘 · 悬浮宠物(安卓原生 App)

一个独立的安卓「悬浮宠物」应用:把一只透明动画宠物做成 TYPE_APPLICATION_OVERLAY 悬浮窗,浮在手机所有 App 之上,并起一个本机 HTTP 控制端口(127.0.0.1:7350),让你(或 DSH 里的 Agent)用 curl 远程指挥它说话、换动作。

从 v1.0 起已演进为完整的小鲸娘:106 个动画、2366 张表情包、三层记忆、心情系统、9 个可配置插件、10 个技能、DeepSeek 对话直连、MCP/HTTP 接入、动画随机链 + 物理引擎。

定位说明:这不是对 dsh-pet npm 插件的修改——网页插件改不出原生悬浮窗。它是 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. 安装 & 授权悬浮窗

  1. 安装 APK(任选其一):

    • 电脑 adb install app-debug.apk;
    • 或让宿主的 Agent 用 pilot 的 app.install {"upload":"<文件名>"}(先把 APK 放进 /opt/pilot/run/uploads/)装。
  2. 首次启动:App 会跳到系统「显示在其他应用上层」授权页,勾选允许。

  3. 也可用 Agent 侧 appops 直接开(android 8+):

    appops.set { package: "com.dsh.petfloat", op: "android:system_alert_window", mode: "allow" }
    
  4. 授权后回到 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. 已知限制 / 风险

  1. 本机没有安卓构建链(无 JDK/Gradle/SDK),本工程源码在此环境下未实际编译验证,需在 Android Studio 里首次构建时排错;
  2. 透明动画依赖「webm→动画 WebP」转码,未在此环境验证最终观感;
  3. 悬浮窗在系统设置、部分锁屏/支付界面可能被系统临时隐藏(安卓安全策略);
  4. 控制端口只绑回环地址、无鉴权,仅限本机可信环境使用,别开放公网。

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 的流程:

  1. GET /api/v1/repos/mtfyun/dsh-pet-floating/releases?limit=10,跳过 draft,取 versionCode 最大的一条
  2. 优先读 version.json 拿 versionCode / sha256 / 体积;读不到再从 tag 尾段解析
  3. 与当前安装版本比 versionCode(用 versionName 按字符串比会把 1.0.9 判成比 1.0.10 新)
  4. 下载到 cacheDir/update/,校验体积与 SHA256,不一致立即删除
  5. 经自带 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,npm dsh-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,本项目按其 参数/逻辑做了原生重实现,沿用其设计意图。

S
Description
小鲸娘 Android 悬浮宠物:106 动画 + 记忆 + 心情 + 插件 + MCP + DeepSeek 直连
Readme
909 MiB
0 Stars 1 Watchers 0 Forks
Languages
Kotlin 95.4%
Python 2.3%
Shell 1.2%
HTML 1.1%