封装成 App
APP PACKAGING
状态:🟡 规划 · 最后核对 2026-08-06
2026-08-02。博士定的方向:做成 app,放弃 PWA。
讨论过程见对话;这份只记结论、依据和执行顺序。
一、形态:壳 + 远程 URL(保持现状)
capacitor.config.json 现在是 server.url: https://manana.icu —— 壳只是一个
指向线上站点的 webview。三种形态里选它:
| 形态 | 改前端 | 没网 | 审核 | |
|---|---|---|---|---|
| A ⭐ | 壳 + 远程 URL | git pull,用户刷新即得 |
❌ 打不开 | ⚠️ 纯 webview 会被挑刺 |
| B | 壳 + 资源打包进 app | 每次都要重新发版 | ✅ 壳能开 | 好过 |
| C | 原生/混合重写 | — | — | — |
选 A 的唯一理由:这个项目一天推五次 commit, 「改一次前端就要重新发一次包」的摩擦会直接杀死迭代速度。
⚠️ A 有一个必须先补的洞:远程模式 + 没网 = 点开 app 一片白,没有任何提示。
浏览器里没网至少有浏览器自己的错误页;app 图标点进去一片白,用户会认为 这 app 坏了。
502.html救不了 —— 那是服务端返回的,没网根本到不了。 发包前必须打包一个最小的离线兜底页。
⚠️ TestFlight 也要过 Beta App Review,Guideline 4.2「最低功能性」专治纯 webview 壳。而推送 + 启动图 + 原生音频正好是「不只是个网页」的证据 —— 所以先接推送,也有过审这一层作用。
二、放弃 PWA
| 抛弃 | 保留 |
|---|---|
| Service Worker · 离线 · Web Push · 安卓「可安装」 | 浏览器直接访问 · iOS 添加到主屏幕(靠 apple-mobile-web-app-capable,不需要 SW,白拿的) |
最强的理由不是「原生更好」,是缓存:
这个项目已经有一整套手工缓存机制(bump_frontend_cache.py 139 行 +
「所有本地 import 要么都带 token 要么都不带」的铁律 + 白屏保护),
而它是被白屏事故逼出来的。Service Worker 是优先级更高、更难清的
第二层缓存,经典事故就是「用户装了旧版 SW,之后所有更新都推不过去」——
而你只有 13 个熟人用户,没有强制刷新的手段。
在一个已经被缓存咬过的项目上再叠一层缓存,收益不值这个风险。
次要理由:推送能从四条收敛到一条(现在 ntfy 活着、bark 零调用、 FCM 零调用且凭证错,再加 Web Push 就是第四条)。
➡️ tech-debt F7「PWA 装不上」这条债直接划掉 —— 不是修,是不做了。
三、分发
| 渠道 | 门槛 | |
|---|---|---|
| 安卓 | 直接发 apk | 零门槛,允许「未知来源」即可 |
| iOS | TestFlight(激活码 / 公开链接,最多 1 万人) | 测试者不花钱;构建 90 天过期,要定期上传新构建 |
⚠️ 构建不是永久的。 对持续在改的项目基本无感(你本来就在发版、测试者自动更新), 只要别停更超过三个月。
⚠️ 硬阻塞:开发机是 Windows。 npx cap add ios 生成的是 Xcode 工程,
构建和上传 TestFlight 必须在 macOS 上。解法不是买 Mac,是
GitHub Actions 的 macOS runner(推 tag → 自动构建 → 上传 TestFlight)。
这条要提前打通,别等到 iOS 那一步才发现卡住。
四、app 化会把「网页里能忍」变成「这 app 做得糙」
同样的缺陷,换个壳,容忍度完全不同:
| 债 | 网页里 | app 里 |
|---|---|---|
| G2 白屏干等(深聊/群聊/动态页) | 「网慢」 | 「这 app 卡」 |
| 18 处输入框 < 16px | 偶尔放大一下 | iOS 每次聚焦都跳,很显眼 |
| 没网无兜底 | 浏览器有错误页 | 一片白 |
60+ 个可点 <div>、零 aria |
没人注意 | 系统辅助功能会露馅 |
app 化不是给现有体验加个壳,是把它的下限暴露出来。
五、执行顺序
| 事 | 为什么在这个位置 | |
|---|---|---|
| 0 | 接通推送 | 没有推送,app 化就失去了最大的理由;且它是过 4.2 审核的筹码。不需要任何封装工作就能验证 |
| 1 | F1 passlib | 装在别人手机上之后,「服务起不来」的代价高一个量级 |
| 2 | 没网兜底页 | 远程模式的必修课 |
| 3 | G2 加载态 | 让它不像糙 app |
| 4 | npm i && npx cap sync android → apk,自己用两周 |
安卓分发零门槛,先在自己手上把债还掉 |
| 5 | iOS:CI 打通 → TestFlight → 发激活码 | 在一个已经打磨过的版本上给别人用 |
为什么先安卓:apk 发个文件就能装;iOS 要 Mac + CI + 审核。 先出安卓、自己用两周,等于用最低成本做一轮真机验收。
六、第 0 步:推送的六个断点
整条链路从头到尾没有一处是通的(2026-08-02 逐段核实):
| # | 断点 | 症状 |
|---|---|---|
| 1 | send_push 零调用 |
函数自己的文档写着「在 chat.py / deep_chat.py 发消息后调用」——那个调用从来没写 |
| 2 | 凭证路径是错的 | 代码读 android/app/google-services.json 传给 credentials.Certificate()。那是 Android 客户端配置,不是服务端凭证 —— 服务端要的是 service account JSON(含 private_key / client_email)。就算你把文件放进去,也会抛异常 → 被 except Exception 吞掉 → 静默不推送 |
| 3 | firebase-admin 不在 requirements.txt |
生产机上 import 失败 → 静默跳过 |
| 4 | .gitignore 没有防凭证规则 |
service account JSON 一旦落进仓库就是把整个 Firebase 项目交出去 |
| 5 | 前端零处调用 /api/config/push-token |
fcm_tokens.json 永远是空的 → send_push 拿到空 tokens 直接 return |
| 6 | 没有 google-services.json |
构建 apk 时需要(这个才是它真正的用途) |
触发时机:只推「她主动找你」,不推动态
ntfy 现在两个都推(_push_entry 动态 + _job_initiative 主动私聊)。
FCM 只接后者。
理由和世界引擎那条门槛是同一个:一条推送如果不会让你想打开 app,就不该发。 动态是每小时可能好几条的朋友圈更新,推送化就是骚扰; 而「她主动发消息给你」正是 app 化要解决的那件事 —— 现在 #48 / #54 做的主动消息只有你打开应用才看得到。
验证:不需要设备就能分辨好坏
conventions §10.3:验证步骤必须在坏掉时给出不同结果。 而第 0 步做完时还没有 app、没有任何设备 token,发不出真推送。
所以加一个自检端点,逐段报告链路状态:
firebase-admin 装了吗 / 凭证读到了吗 / 是不是 service account / 这个用户有几个 token / 调用点接了吗。
每一段分别可见,而不是笼统的「推送不可用」。
七、明确不做
| 不做 | 为什么 |
|---|---|
| Service Worker / PWA | 见第二节:给一个已经被缓存咬过的项目再叠一层缓存 |
| 资源打包进 app(形态 B) | 失去「改前端立刻生效」,那是当前最舒服的性质 |
Tauri 桌面(tauri-desktop-plan.md 246 行) |
已经走了 Capacitor,目标用户在手机上。那份文档里还有价值的只有「本地 SQLite + 同步线程」,那属于 #32/#33 |
| Postgres / 微服务 | 给「多机部署」付的钱,而这个项目没有那个问题。数据层该走的是 SQLite(见架构讨论) |
| 给动态发推送 | 见上:会变成骚扰 |