罗德岛通讯与技术部

封装成 App

APP PACKAGING

这个项目已经有一整套手工缓存机制(bump_frontend_cache.py 139 行 +

状态:🟡 规划 · 最后核对 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(见架构讨论)
给动态发推送 见上:会变成骚扰