refactor: 只保留 Web,移除 Capacitor 原生壳

删除 android/、ios/ 两个 Capacitor 原生工程与 capacitor.config.ts、
ionic.config.json,以及 scripts/android、scripts/ios.sh 构建脚本;
根与 apps/web 的 @capacitor/* 依赖和相关 npm scripts 一并移除。

Web 侧去掉原生分支:登录只走云乐坊容器授权面板或顶层重定向,
暗色模式不再改原生状态栏,移除 Android 物理返回键处理。

保留 @nuxtjs/ionic(Web UI 框架)、云乐坊 window.ylf 桥接与 PWA。

决策记录见 docs/decisions/0004-web-only.md。
This commit is contained in:
toor
2026-10-21 12:27:50 +08:00
committed by trae-solo-agent
parent 8ded87a3a8
commit 76a5d71252
117 changed files with 204 additions and 3023 deletions
+2 -2
View File
@@ -34,9 +34,8 @@ export default defineConfig({
text: '开发文档',
items: [
{ text: 'Cook CLI', link: '/dev/cli' },
{ text: '移动应用', link: '/dev/app' },
{ text: 'Web 应用', link: '/dev/app' },
{ text: '菜谱数据', link: '/dev/data' },
{ text: 'iOS App 打磨目标', link: '/dev/ios-roadmap' },
],
},
],
@@ -48,6 +47,7 @@ export default defineConfig({
{ text: '0001 菜谱数据', link: '/decisions/0001-recipe-data' },
{ text: '0002 反馈与固定 id', link: '/decisions/0002-feedback-and-ids' },
{ text: '0003 原生 iOS 与 monorepo', link: '/decisions/0003-native-ios-monorepo' },
{ text: '0004 只保留 Web', link: '/decisions/0004-web-only' },
],
},
],
+2 -2
View File
@@ -1,6 +1,6 @@
# 0003 原生 iOS App 与 monorepo 结构
- 状态:已采纳
- 状态:被 [0004 只保留 Web,移除 Capacitor 原生壳](./0004-web-only) 取代
- 日期:2026-10-18
- 关联:调整 [#98](https://github.com/YunYouJun/cook/issues/98) 的阶段 4
@@ -68,4 +68,4 @@ docs/ 不变
- iOS 开发、构建和 UI 调试需要 macOS + Xcode 26,云端环境只能写代码和跑纯 Swift / TS 测试。
- 推荐逻辑的改动成本变成两份,靠共享用例兜底;核心逻辑应尽量保持小而纯。
- 搬迁会和进行中的 PR 冲突,需要等它们合并后再开始,且搬迁 PR 要尽快合并。
- 打磨计划和验收标准见 [iOS App 打磨目标](/dev/ios-roadmap)。
- 打磨计划和验收标准见 iOS App 打磨目标(该计划已随 [0004](/decisions/0004-web-only) 作废)。
+62
View File
@@ -0,0 +1,62 @@
# 0004 只保留 Web,移除 Capacitor 原生壳
- 状态:已采纳
- 日期:2026-10-21
- 关联:取代 [0003 原生 iOS App 与 monorepo 结构](./0003-native-ios-monorepo)
## 背景
- 仓库同时维护着三套东西:Nuxt Web 应用、Capacitor 的 iOS 壳、Capacitor 的 Android 壳。
- Capacitor 依赖比想象中深:登录走原生浏览器 + 原生回调 URL,暗色模式改原生状态栏,Android 有物理返回键处理,
SSO 事务存在 `Preferences` 里。为此多出了 `nativeSso.ts`、`createNativeYunlefunAuthorizationAdapter`、
`resolveYunlefunSsoConfig` 的 `platform` 分支,以及 6 个只为原生分支存在的测试文件。
- 0003 决定用 SwiftUI 原生版替换 Capacitor 版,把 iOS 当作要上架收费的主力。但 SwiftUI 工程
(`apps/ios`)至今没有开工,Capacitor 壳反而成了长期负担:每次升级 `@capacitor/*` 都要在 Web 侧
重新验证原生分支,而这些分支在浏览器里根本走不到。
- 实际使用中,Web 版已经同时服务普通浏览器和云乐坊容器(`window.ylf` JSAPI),原生壳带来的能力
(状态栏配色、物理返回键、原生 SSO 回跳)都有 Web 侧的等价实现。
## 方案对比
| 问题 | 方案 | 取舍 |
| ---------- | ---------------------------- | ------------------------------------------------------------ |
| 原生壳去留 | 保留 Capacitor iOS + Android | 两条壳都要跟 `@capacitor/*` 版本走;Web 侧原生分支长期没人跑 |
| | 只删 Android,保留 iOS | iOS 壳仍在,Capacitor 依赖无法卸掉,清理不彻底 |
| | **全部移除,只保留 Web** | 失去上架 App 与原生 SSO;换来单一代码路径和持续可维护的 Web |
## 决定
### 删除
- `android/`、`ios/` 两个 Capacitor 原生壳工程。
- `capacitor.config.ts`、根 `ionic.config.json`。
- `scripts/android/`、`scripts/ios.sh`。
- 根与 `apps/web` 的 `package.json` 里全部 `@capacitor/*` 依赖,以及 `dev:android`、`build:android`、
`dev:ios`、`ios`、`android`、`open:ios`、`open:android`、`sync` 等脚本。
### Web 侧去 Capacitor 化
- `composables/dark.ts`:只改 `meta[name="theme-color"]`,不再设置原生状态栏。
- `pages/tabs.vue`:移除 Android 物理返回键处理(浏览器自带返回,无需拦截)。
- `adapters/yunlefunAuth.ts`:删掉原生授权适配器和基于 `Preferences` 的事务存储,只保留 Web 授权适配器。
- `utils/nativeSso.ts`:整体删除。它的产物只是原生 SSO 的 PKCE 事务与回调解析,Web 端走
`@yunlefun/sso` 的顶层重定向,不需要。
- `utils/yunlefunAuth.ts` / `utils/yunlefunSsoConfig.ts`:控制器和配置解析去掉 `platform` 分支,
只保留 Web 一种形态。
- `nuxt.config.ts`:去掉 `yunlefunSsoNativeClientId` / `yunlefunSsoNativeRedirectUri`。
### 保留
- `@nuxtjs/ionic` 与 Ionic UI:Ionic 是 Web UI 框架,与 Capacitor 无关。
- `utils/yunleHost.ts` 与云乐坊容器(`window.ylf`)集成:这是 Web 被嵌入第三方 App 容器时的适配层,
和 Capacitor 无关。
- PWA 安装、Service Worker、离线能力:都是 Web 能力。
## 影响
- 不再有 iOS / Android 安装包。若之后要上架,需要另起仓库做原生工程,而不是把 Capacitor 壳捡回来。
- 云乐坊 SSO 在 Web 上继续可用:容器内走宿主授权面板,容器外走顶层重定向回到
`https://cook.yunyoujun.cn/auth/callback`。PKCE 与 HTTPS 回跳地址的约束不变。
- `docs/dev/ios-roadmap.md` 与 `docs/decisions/0003-native-ios-monorepo.md` 随之作废。
- `@nuxtjs/ionic` 自身仍会传递依赖 `@capacitor/cli` / `@capacitor/core`(构建期使用,运行时不会调用),
`pnpm-workspace.yaml` 里对应的 override 保留。
+6 -5
View File
@@ -10,8 +10,9 @@
## 列表
| 编号 | 标题 | 状态 |
| ---------------------------------- | --------------------------------- | ------ |
| [0001](./0001-recipe-data) | 菜谱数据的来源、质量与反馈闭环 | 提议中 |
| [0002](./0002-feedback-and-ids) | 菜谱固定 id、应用内反馈与筛选信号 | 已采纳 |
| [0003](./0003-native-ios-monorepo) | 原生 iOS App 与 monorepo 结构 | 已采纳 |
| 编号 | 标题 | 状态 |
| ---------------------------------- | --------------------------------- | ------------ |
| [0001](./0001-recipe-data) | 菜谱数据的来源、质量与反馈闭环 | 提议中 |
| [0002](./0002-feedback-and-ids) | 菜谱固定 id、应用内反馈与筛选信号 | 已采纳 |
| [0003](./0003-native-ios-monorepo) | 原生 iOS App 与 monorepo 结构 | 被 0004 取代 |
| [0004](./0004-web-only) | 只保留 Web,移除 Capacitor 原生壳 | 已采纳 |
+3 -3
View File
@@ -6,7 +6,7 @@ Cook 在文档初次进入时记录来源。`firstTouch` 保存在当前域名
来源只携带落地域名、路径、外部 referrer 域名以及 `utm_source`、`utm_medium`、`utm_campaign`。`yunle.fun`、其全部子域、`cook.yunyoujun.cn` 和当前域名的站内跳转不会计入外部 referrer,避免 SSO 或 Apps 往返被统计成获客渠道。查询、hash 和凭证不会写入来源记录。没有 referrer/UTM 表示来源缺失,不能据此断言为直接访问。
登录前读取同一份 `{ version: 1, firstTouch, registerTouch }`,同时覆盖 Web 顶层重定向、Apps host bridge 和 Cook 原生浏览器。来源为客户端上报数据;统一登录服务依据 Registry 验证应用入口并依据 Auth 判断账号是否真正新注册,不能按 Cook 首次登录或公开资料创建时间判断。后续登录不会覆盖账号已保存的注册来源。
登录前读取同一份 `{ version: 1, firstTouch, registerTouch }`,同时覆盖 Web 顶层重定向和 Apps host bridge。来源为客户端上报数据;统一登录服务依据 Registry 验证应用入口并依据 Auth 判断账号是否真正新注册,不能按 Cook 首次登录或公开资料创建时间判断。后续登录不会覆盖账号已保存的注册来源。
## 本地联调
@@ -25,10 +25,10 @@ node scripts/qa/verify-acquisition-sdk.mjs --sdk-root=/path/to/ylf --build --typ
1. 先发布账户私有来源集合/写入 API 与统一登录、SSO 服务端(它们可先接收可选字段并继续兼容旧客户端)。
2. 从 `YunLeFun/ylf` 发布已验证的 `@yunlefun/sso@0.7.0`;该版本给现有 PKCE 协议增加可选来源字段。
3. 在 Cook 解析并更新 0.7.0 的 lockfile,复核保留当前其他依赖改动,再执行 frozen install、typecheck 和构建并发布。
4. 发布 Apps 的来源透传代码;原生“使用其他账号”分支还需要新的 iOS/Android 容器版本。
4. 发布 Apps 的来源透传代码。
`@yunlefun/sso@0.7.0` 已由公开的 `YunLeFun/ylf` 仓库通过 OIDC 发布,附带 GitHub Actions provenance。Cook 的 lockfile 使用实际 registry integrity,安装与发布不依赖临时本地路径或 override。`minimumReleaseAgeExclude` 只为已核验的自有版本 `@yunlefun/sso@0.7.0` 提供精确例外,其余依赖继续遵循 24 小时等待和供应链策略。
Cook 主域名 `cook.yunyoujun.cn` 对应 EdgeOne Upload 项目 `yunyoujun-cook`,发布时使用该项目名;`cook.yunle.fun` 是另一个 Git 集成项目。原生容器与应用商店发布仍按各自流程验收。
Cook 主域名 `cook.yunyoujun.cn` 对应 EdgeOne Upload 项目 `yunyoujun-cook`,发布时使用该项目名;`cook.yunle.fun` 是另一个 Git 集成项目。云乐坊容器内的发布仍按各自流程验收。
历史用户最多保留来自 SSO 时间关联的应用入口推断;不能把 GA 总体流量比例回填为某个 UID 的外部渠道。各域名、不同浏览器和被清除的存储也无法恢复此前未采集的触达。
+1 -1
View File
@@ -17,7 +17,7 @@ NUXT_PUBLIC_CLARITY_PROJECT_ID=ks1ebw03i5
NUXT_PUBLIC_CLARITY_DOMAINS=cook.yunyoujun.cn,cook.yunle.fun
```
显式设置空项目 ID 可关闭统计。开发模式、未授权域名、原生 Capacitor WebView、Do Not Track 和 Global Privacy Control 开启的浏览器不加载统计。页面就绪后异步加载 Clarity,不代理或打包第三方脚本。
显式设置空项目 ID 可关闭统计。开发模式、未授权域名、Do Not Track 和 Global Privacy Control 开启的浏览器不加载统计。页面就绪后异步加载 Clarity,不代理或打包第三方脚本。
静态托管须在**构建前**设置配置,执行 `pnpm build` 并发布 `dist`。容器启动后修改环境变量不会改变静态资源。Docker 支持同名 `--build-arg`,GitHub Docker 工作流读取同名 Repository variables,未设置时使用 Cook 默认值。工作流中需要禁用时可将项目 ID 设置为 `disabled`,它无法通过 ID 校验。
+17 -26
View File
@@ -1,33 +1,24 @@
# APP
# Web 应用
考虑到跨平台和开发效率,计划使用:
Cook 只保留 Web 一套代码,服务普通浏览器和云乐坊容器。技术栈:
- [Capacitor](https://capacitorjs.com/)
- [Ionic](https://ionicframework.com/)
- [Nuxt Ionic](https://ionic.nuxtjs.org/)
- [Nuxt 4](https://nuxt.com/)(Vue 3,CSR-only,静态生成)
- [Ionic](https://ionicframework.com/) + [Nuxt Ionic](https://ionic.nuxtjs.org/)(Web UI 框架,与 Capacitor 无关)
- [UnoCSS](https://unocss.dev/) 原子化样式
## 云乐坊 SSO 回调
原生 App(iOS / Android)的代码已随 [0004 只保留 Web,移除 Capacitor 原生壳](/decisions/0004-web-only) 一并移除。
移动端使用系统浏览器打开云乐坊授权页,并通过已验证的 HTTPS Universal Links / App Links
返回 `https://cook.yunyoujun.cn/auth/callback?platform=native`。不要改用自定义 URL Scheme;
PKCE 能阻止授权码被旁路窃取,但不能阻止恶意 App 主动冒充公开客户端发起授权。
## 云乐坊 SSO
iOS 的 Associated Domains 与站点 `apple-app-site-association` 已纳入项目。Android 上架前还需用
Google Play App Signing 的发布证书 SHA-256 生成站点文件
`apps/web/public/.well-known/assetlinks.json`,关系必须限定为:
Web 端有两种登录路径:
```json
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "cn.yunyoujun.cook",
"sha256_cert_fingerprints": ["<PLAY_APP_SIGNING_SHA256>"]
}
}
]
```
- **在云乐坊 App 容器内**:通过宿主注入的 `window.ylf` JSAPI 直接请求宿主授权面板,用户不离开当前页面。
- **在普通浏览器中**:走顶层重定向到云乐坊授权页,授权完成后回到
`https://cook.yunyoujun.cn/auth/callback`,由该页面换回登录票据。
不要把本机 debug keystore 指纹发布到生产站点。完成证书配置后,需在真机验证已登录快捷授权、
显式同意、取消、返回键、超时、伪造回调和授权码重放。
回跳地址必须是已在云乐坊登记的 HTTPS 地址,且不能带 `?platform=native` 之类的原生标记。
PKCE 能阻止授权码被旁路窃取,但不能阻止恶意客户端主动冒充公开客户端发起授权,
因此不要放宽回跳地址的登记要求。
回跳域名有多个时(`cook.yunyoujun.cn`、`cook.yunle.fun`),按当前页面 origin 选对应的那个,
`yunlefunSsoRedirectUris` 里没登记的域名会直接判定为配置错误并拒绝登录。
-58
View File
@@ -1,58 +0,0 @@
# iOS App 打磨目标
目标:做出一个能在 App Store 上架、值得用户付费的「食用手册」iOS App,同时 Web 继续可用。背景和取舍见 [0003 原生 iOS App 与 monorepo 结构](/decisions/0003-native-ios-monorepo)。
每个阶段完成验收项后才进入下一阶段。数字类指标是起始目标,做出第一版后按实测调整。
## M0 仓库改成 monorepo
- [x] 把 Nuxt 应用搬到 `apps/web`、菜谱 CSV 和生成的 JSON 搬到 `packages/data`,只改路径不改逻辑。根目录的 `pnpm build` 仍然输出到 `dist/`,CI、EdgeOne、Netlify、Vercel、Docker、Capacitor 不用改配置。
- [x] 食材词典、常备调料、别名改成 `packages/data` 里的 JSON(`food.json`、`pantry.json`、`aliases.json`),Web 和 iOS 读同一份。
- [x] 两端共用的推荐用例 `packages/data/fixtures/recommend.json`,`pnpm fixtures` 生成,`pnpm test` 检查是否过期。`packages/cook` 的推荐逻辑本身不依赖 Vue / Node,不再单独拆 `packages/core`。
验收:`pnpm lint`、`typecheck`、`test`、`build` 全绿;线上 Web 部署后行为不变;`pnpm eval:search` 指标与搬迁前一致。
## M1 数据与核心逻辑两端一致
- JSON 加 `schemaVersion`、`dataVersion` 和 JSON Schema。
- 共享用例补上相克判断和搜索;收集到真实筛选信号后,把 `test/fixtures/search-eval.json` 也纳入。
- `apps/ios/CookCore`(Swift Package)实现菜谱加载、推荐、搜索、相克判断。
验收:TS 和 Swift 跑同一份用例全部通过;CI 两边都跑(Swift 部分可先用 Linux 上的 `swift test`,或 macOS runner)。
## M2 iOS 第一版(可上 TestFlight)
- 食材选择 → 推荐结果、菜谱详情(B 站视频跳转)、随机一道菜、收藏、浏览历史、设置。
- 只用系统控件搭界面:`TabView`、`NavigationStack`、`List`、sheet,自动获得 Liquid Glass。
- SwiftData 本地存储,CloudKit 同步收藏和历史。
- 深色模式、动态字体、VoiceOver、横竖屏和 iPad 基本可用。
验收:核心流程不依赖网络;冷启动到可点击不超过 1 秒(iPhone 12 级别设备);滚动无明显掉帧;VoiceOver 能走完「选食材 → 打开菜谱」;TestFlight 内测无崩溃。
## M3 打磨
- Widget:「今天吃什么」随机推荐、最近收藏。
- App Intents / 快捷指令 / Siri:「我有土豆和鸡蛋,能做什么」。
- Spotlight 索引菜谱,系统分享面板,触感反馈,空状态和引导页。
- 菜谱数据在线更新(按 `dataVersion` 从 CDN 拉取),失败时用内置数据。
- 从 Capacitor 版迁移:首次启动导入旧版收藏和历史(方案待验证,备选是旧版先同步到云端)。
验收:每项功能有截图走查记录;旧版用户升级后收藏不丢;崩溃率和卡顿通过 Xcode Organizer 观察一周无异常。
## M4 会员与上架
- StoreKit 2 会员:CloudBase 跨平台同步,以及之后确定的高级功能;恢复购买;Web 已购会员登录后解锁。
- 付费墙、隐私清单、App Store 截图和文案、审核备注。
- 以更新的方式替换现有 `cn.yunyoujun.cook` 商店条目。
验收:沙盒环境走通购买、续订、退订、恢复;审核通过并上架。
## M5 收尾
- 移除 Capacitor(`ios/`、`capacitor.config.ts`、`@capacitor/*`、`nativeSso` 等);`android/` 去留单独决定。
- 更新 #98 的阶段 4 和 `docs/dev/app.md`。
## 分工说明
- 云端 Claude 可以完成 M0、M1 的 TS 部分、Swift 代码编写和纯逻辑测试。
- Xcode 构建、模拟器和真机调试、TestFlight、App Store Connect 需要在你的 Mac 上进行(可以用 Remote Control 让 Claude 在你的电脑上跑)。
+11 -33
View File
@@ -55,48 +55,26 @@ pnpm build
pnpm preview
```
### 移动应用
#### iOS
```bash
# 构建 iOS 应用
pnpm build:ios
# 在 Xcode 中打开
open ios/App/App.xcworkspace
```
#### Android
```bash
# 构建 Android 应用
pnpm build:android
# 在 Android Studio 中打开
# File > Open > android/
```
## 项目结构
```
cook/
├── app/ # 应用源码
│ ├── components/ # Vue 组件
│ ├── composables/ # 组合式函数
│ ├── data/ # 菜谱数据 (CSV + JSON)
│ ├── pages/ # 页面路由
│ └── utils/ # 工具函数
├── apps/web/ # Web 应用(Nuxt)
│ ├── app/ # 应用源码
│ │ ├── components/ # Vue 组件
│ │ ├── composables/# 组合式函数
│ │ ├── pages/ # 页面路由
│ │ └── utils/ # 工具函数
│ └── public/ # 静态资源
├── docs/ # 文档网站
├── packages/
│ └── cook/ # CLI 工具
├── android/ # Android 应用
├── ios/ # iOS 应用
└── public/ # 静态资源
│ ├── cook/ # CLI 工具与推荐/搜索核心
│ └── data/ # 菜谱数据 (CSV + JSON)
└── test/ # 测试
```
## 下一步
- 📖 查看 [CLI 文档](/dev/cli) 了解数据管理
- 📱 查看 [移动应用开发](/dev/app) 了解跨平台构建
- 💻 查看 [Web 应用开发](/dev/app) 了解技术栈与登录流程
- 🍳 开始添加你喜欢的菜谱!