# LemonTwin 專案研讀報告(Joe / Copilot CLI on joyce628) > 資料來源: > - PRD + 127 檔離線匯出 `/home/joyce628/.copilot/session-state/25e2ccd5-…/files/lemontwin_brief.md` > - 線上實機 SSH `joycechen@192.168.1.83`(macOS 25.2.0 / Mac mini ARM64) > - 對外 https://orchard.graceai.net (Cloudflare tunnel → localhost:3010) > 探測時間:2026-05-28 09:48 +08:00 --- ## 1. 📚 Study — 產品定位 / 技術棧 / 系統架構 / 模組相依 / 部署現況 ### 1.1 產品定位 - **一句話**:檸檬果園的「數位分身」MVP — 以 3D 互動果園地圖 + 單株樹履歷 + 巡園紀錄 + 派工 + AI 初篩,把果園從「憑記憶管理」推進到「資料化決策」。 - **差異化**:核心是 **3D Orchard Map**(React Three Fiber,開心農場風格的 low-poly),PRD 明說 60% Phase 1 工時都花在這。AI 是加值,不是賣點。 - **第一版不做**:完整病害診斷、農藥處方、GIS 精準地圖、IoT 灌溉硬體。 ### 1.2 技術棧(PRD 宣稱 vs 實機現況) | 層 | PRD 宣稱 | 實機觀察 (192.168.1.83) | 差異 / 風險 | |---|---|---|---| | 前端 | Next.js 14 App Router + R3F + drei + postprocessing + Zustand + Tailwind + shadcn | `next-server v16.2.6` + Turbopack **dev mode** on :3010 | ⚠️ 線上跑的是 dev server(HMR script 在 HTML 中可見),不是 `next build && next start` | | 後端 | NestJS 10 + Prisma + **PostgreSQL 16 + PostGIS** + Redis 7 + BullMQ | NestJS + Prisma + **SQLite (`file:./dev.db`)**,無 Redis、無佇列 | 🚨 違反 PRD 自己 §2.2,也違反 joyce628 Iron Law #5(Postgres only) | | 存檔 | AWS S3 / MinIO | 看不到 minio 行程 | 照片可能直接寫本機 `uploads/` | | AI | OpenAI GPT-4o Vision → Roboflow → 自訓 YOLOv8 | `OPENAI_API_KEY` 可空,空時走 mock;`ai.service.ts` 存在 | 目前 Phase 1,無 cost guard / retry / 佇列 | | 部署 | Docker Compose + Nginx + GH Actions | 🚨 **無 docker**(`zsh: command not found: docker`)、無 nginx、用 `tsx watch` 與 `next dev` 裸跑 + Cloudflare Tunnel | 完全沒走 PRD 的部署 | | 邊界 | — | Cloudflared `e7851f46-…` tunnel → `localhost:3010`(hostnames: orchard.graceai.net、orchid.graceai.net) | 同時跑 **3 條** cloudflared(見下) | ### 1.3 系統架構(實際拓樸) ``` [Internet] │ HTTPS ▼ Cloudflare Edge (104.21.41.195 / 172.67.166.213) │ tunnel e7851f46-… (joyce 家裡 Mac mini) ▼ ┌─────────────────────────── Mac mini 192.168.1.83 ──────────────────────────┐ │ │ │ cloudflared (PID 91282, /Users/.../web/cloudflared-config.yml) ←主用 │ │ cloudflared (PID 28623, /etc/cloudflared/config.yml, 8 days) │ │ cloudflared (PID 90658, quick tunnel --url http://localhost:3010) │ ← ⚠️ 三條 tunnel 同時存在 │ │ │ ┌───────────────── Web ────────────────┐ ┌─────── API ──────────┐ │ │ │ next dev -p 3010 (Turbopack HMR) │───▶│ NestJS :3001 │ │ │ │ + postcss daemon │ │ tsx watch src/main.ts │ │ │ │ PID 8370 / 9146 / 9147 │ │ PID 18508(active) │ │ │ └──────────────────────────────────────┘ │ + 殘留 PID 2709/8348/ │ │ │ │ 18166 watcher 殭屍 │ │ │ └──────────┬──────────┘ │ │ │ Prisma │ │ ▼ │ │ SQLite `api/dev.db` │ │ │ │ openclaw gateway :18789 (PID 60664) — 已在這台上跑 │ └────────────────────────────────────────────────────────────────────────────┘ ``` ### 1.4 模組相依(API 端,主要 service) - `app.module` ← `farms / trees / observations / tasks / harvest / inventory / finance / camera / camera-scheduler / dispatch / ai / health-engine / cwa.client / weather / orchard-workflow / post-harvest.util / auth / jwt` - 關鍵流程: - **巡園 → 健康等級**:`observations.service` → `health-engine.computeHealthLevel(tags, prev, days)` → 0–4 等級 + `generateTasks()` 自動派工 - **採收 → 庫存 → 採後工**:`harvest.service.create()` → 寫 `harvestRecord` + 開 `inventoryLot` + `buildPostHarvestTasks()` 生 6 張採後任務 - **每月農務**:`orchard-workflow.ts` 內建 12 個月 workflow → `dispatch.service` 派工 - **相機**:`camera.service` + `camera-scheduler.service` + `infra/mediamtx.yml`(RTSP relay),每小時截圖丟 `ai.service` 做 vision 初篩 ### 1.5 部署現況(活線探測) - `GET https://orchard.graceai.net/` → HTTP 200,title `LemonTwin`,HTML 顯示徽章 **「未登入 · Mock」**、初始畫面 **「載入果園經營總覽…」**(前端跑在 client-side bootstrap),證實線上跑的是 dev build 且預設走 mock 資料 - `GET https://orchard.graceai.net/api/health` → `{"ok":true}`(這條走 Next.js → API 代理) - 直接 `GET http://localhost:3001/health`(在 Mac 上)→ NestJS 404,代表健康檢查只接在 `/api/health` 而不是 `/health`,API 自己沒原生 `/health` route - 機器 load avg `4.36 3.89 4.09`,uptime 8 天 21 小時 - 看不到 `nginx`、`postgres`、`redis`、`minio` 監聽埠 - 沒有 `*.log` 檔(API/Web 全跑前景 dev 模式,log 只在 terminal) --- ## 2. 💡 Brainstorm — 關鍵問題(套用 brainstorming HARD-GATE) > HARD-GATE 探索使用者意圖:在改動任何程式碼前,這些題目必須由 **Joycechen** 親自回答。 > 我(Joe)的角色是把問題列齊、給選項,不是替她決定。 ### Q1【產品 / 受眾】這個 MVP 的「真實第一位使用者」是誰? - (a) Joycechen 自家 / 親戚的果園主,當作內部工具,先跑 6 個月再說 - (b) 雲嘉南檸檬產區 5–20 位農友的「邀請制 closed beta」,要做 onboarding 流程 - (c) B2B:對接合作社 / 加工廠(要 SLA、發票、合約) - → 不同答案會直接決定要不要做多租戶、auth、權限、繳費 ### Q2【產品 / 範疇】3D 地圖是「demo 噱頭」還是「日常使用的主介面」? - (a) 噱頭:保留為 landing / 報表展示,日常用清單表格(行動裝置友善) - (b) 主介面:必須投資 LOD、行動裝置 fallback、低階機效能、Three.js bundle 分割 - (c) 雙軌:3D 給辦公室、清單給果園現場(推薦但工程量 1.5 倍) ### Q3【技術 / 資料庫】要不要把 SQLite 升級成 PostgreSQL + PostGIS? - (a) 留 SQLite:單機、單農場、零維運;但未來無法做地理查詢、多 user 寫衝突 - (b) 升 Postgres(PRD 原樣):可用 PostGIS 做樹的 GPS、可多 user、可備份 - (c) Supabase 託管 Postgres:省維運,但要處理 cold start 與費用 - 🔴 對照 Joyce628 Iron Law #5「Postgres only」: 若這套未來要併入 AirCore / Joyce 生態,**(b) 或 (c) 必選** ### Q4【技術 / 部署】生產要不要離開 `next dev`? - (a) `next build && next start` + pm2/launchd(最低工) - (b) Docker Compose(照 PRD)+ Caddy/Traefik - (c) 拆雲端:Web → Vercel、API → Fly.io / Render、DB → Supabase - 現況跑 dev server 直接被 Cloudflare 暴露,違反 Iron Law #39(web app must always work)— HMR socket 連不到、source map 外洩、記憶體會漲 ### Q5【商業 / 收費】LemonTwin 的 v1 商業模式? - (a) 一次性顧問費(Joyce 接案做) - (b) 月費 SaaS(NT$ 1,500–3,000 / 果園) - (c) 政府補助 / 智慧農業計畫案 - (d) 開源 + 訓練服務(接 LINE bot、相機安裝服務) - → 影響 priority:(b)(c) 需要 multi-tenant、發票、報表匯出 ### Q6【安全】目前 `未登入 · Mock` 直接暴露在公網的 acceptable risk? - (a) 內部測試階段,加 Cloudflare Access(Zero Trust)擋外人,1 小時可上 - (b) 完成 `auth.service` + `auth.guard` 的 login 流程,把 mock badge 拿掉 - (c) 把 orchard.graceai.net 改成 IP allowlist + Basic Auth(過渡) - → 也要看 AI 端點:截圖 + GPT-4o vision 若被亂用,是 token cost attack 面 ### Q7【UX】3D 場景在現場手機(戶外、烈日、4G)能用嗎? - (a) 做 3D-only:必須加 mobile fallback、降階為 2D grid(建議) - (b) PWA + offline cache(巡園可能無訊號) - (c) LINE / Telegram bot 取代部份巡園功能(拍照 + tag) - → 不解決這題,現場農友會直接放棄 ### Q8【AI】OpenAI vision 的 cost / token 控管? - (a) Daily budget cap + per-tree quota - (b) 升 Phase 2 Roboflow 或本機 YOLOv8(在 Joyce 的 Mac 上跑 onnx) - (c) 用 joyce628 OpenClaw 路由:先打本機 ollama 模型,失敗才 fallback OpenAI - 現況:`OPENAI_API_KEY` 空時走 mock,**已有 graceful path**,但沒有預算閾值 ### Q9【可觀測性】出事了怎麼知道? - (a) Cloudflare Analytics + Logs(最低) - (b) 把 NestJS log 寫檔 + logrotate + 每日 email - (c) 接 Grafana Cloud / Better Stack(免費 tier 夠用) - 現況:**完全沒有持久 log**(process 死了就什麼都沒了) ### Q10【災備】Mac mini 壞了 / 被偷 / 電源斷掉的恢復計畫? - (a) `dev.db` 每日 rsync 到 NAS / iCloud - (b) Postgres dump → S3 / B2 - (c) Cloudflare Tunnel credentials 備份(現在只在這台 Mac) - 現況:**0 備份**,單點故障 --- ## 3. 🛠️ Improve Plan > 標註:**why** / **how** / **affected files** / **effort (S/M/L)** / **risk (L/M/H)** ### 3.1 短期(1–2 週,立即改善) | # | 項目 | why | how | files | effort | risk | |---|---|---|---|---|---|---| | S1 | **線上改跑 `next build && next start`** | dev server 在公網=記憶體洩漏 + HMR socket 失敗 + source map 外洩,違反 Iron Law #39 | 在 `web/package.json` 加 `start:prod`;用 launchd plist 取代手動 `npm run dev`;Cloudflared 改指 `:3010` 仍可 | `web/package.json`、新增 `~/Library/LaunchAgents/net.graceai.lemontwin-web.plist`、`web/cloudflared-config.yml` | S | L | | S2 | **殺掉殘留 watcher 行程** | `tsx watch` 在 API 留了 PID 2709/8348/18166 三個殭屍,CPU 與 inode 浪費,可能搶 :3001 | `kill 2709 8348 18166`;改用 pm2 / launchd 管 single instance | (無 code)| S | L | | S3 | **整理 Cloudflared 多 tunnel** | 同時跑 3 條 cloudflared,路由不可預期;其中 PID 90658 是 `quick tunnel`(10 小時前手動測試殘留) | 停掉 PID 90658 與 28623,只留 PID 91282 的 `web/cloudflared-config.yml`;把 `/etc/cloudflared/config.yml` 與 user-level config 二選一 | `web/cloudflared-config.yml`、`/etc/cloudflared/config.yml` | S | M | | S4 | **API 加 `/health` 與 `/readyz`** | 目前 `/api/health` 只回 `{ok:true}`,不查 DB;炸了 Cloudflare 也不會切流量 | `app.controller.ts` 加 `/health` 查 `prisma.$queryRaw('select 1')`,回 `{db:'up',uptime}` | `api/src/app.controller.ts` | S | L | | S5 | **NestJS 日誌落地** | 沒 log = 沒辦法 debug | 加 `pino` + `nestjs-pino`,輸出 `~/LemonTwin/api/logs/api-YYYY-MM-DD.log`,配 logrotate | `api/src/main.ts`、`api/package.json` | S | L | | S6 | **把「未登入 · Mock」徽章在 prod 隱藏** | 公網看得到「Mock」對潛在客戶觀感差 | 用 `NEXT_PUBLIC_SHOW_MOCK_BADGE` 環境變數控制;prod build 設 `false` | `web/src/components/FarmBootstrap.tsx`、`web/.env.production` | S | L | | S7 | **dev.db 每日備份** | SQLite 單檔,壞了就沒了 | launchd job 每日 03:00 `sqlite3 .backup`;備到 `~/Backups/lemontwin/` 並上傳 iCloud Drive | 新增 `scripts/backup-db.sh`、`~/Library/LaunchAgents/net.graceai.lemontwin-backup.plist` | S | L | | S8 | **Cloudflare Access 上 Zero Trust** | 在 auth 還沒完成前先擋外人,避免 AI cost attack | Cloudflare Dashboard → Access → Application 加 email allowlist | (無 code) | S | L | | S9 | **OpenAI 用量上限** | 沒 cost guard,被掃站可破產 | `ai.service.ts` 加 `dailyTokenBudget`、用 `prisma` 記 `AIUsageLog` 表,超額退回 mock | `api/src/ai.service.ts`、`api/prisma/schema.prisma`(新 model) | S | M | | S10 | **3D 場景 mobile fallback** | 手機端可能直接 OOM | `OrchardScene.tsx` 偵測 `navigator.deviceMemory < 4` 或 viewport < 768 改渲染 `OrchardListView` | `web/src/components/orchard-3d/OrchardScene.tsx`、新 `OrchardListView.tsx` | M | M | ### 3.2 中期(1–2 個月,結構性改善) | # | 項目 | why | how | files | effort | risk | |---|---|---|---|---|---|---| | M1 | **SQLite → PostgreSQL 16 + PostGIS** | PRD §2.2 + Iron Law #5 要求;未來才能做地理、多 user、備援 | 用 `docker-compose.yml` 起 Postgres → `prisma migrate dev` → 寫 `scripts/sqlite-to-pg.ts` 一次性搬資料;切 `DATABASE_URL` | `api/prisma/schema.prisma`、`api/.env`、`docker-compose.yml`、新 migration | M | H | | M2 | **`auth.guard` 真正啟用** | 現在每個 controller 應該有的 guard 沒掛,前端走 mock | 在 `app.module` 加 `APP_GUARD` provider;前端 `lib/auth.ts` 寫真實 login flow;移除 mock | `api/src/app.module.ts`、`api/src/*.controller.ts`、`web/src/lib/auth.ts`、`web/src/app/login/page.tsx` | M | M | | M3 | **照片儲存 → MinIO 或 R2** | 目前若寫 `uploads/`,跟著 dev.db 一起單點 | 啟 MinIO container;或直接走 Cloudflare R2(同帳號);改 `camera.service` + `ai.service` 寫 S3-compatible | `api/src/camera.service.ts`、`api/src/ai.service.ts`、`docker-compose.yml` | M | M | | M4 | **BullMQ + Redis 跑 AI 截圖** | 目前 `camera-scheduler` 直接同步呼 OpenAI,會塞住 event loop | Redis 7 容器 + BullMQ;scheduler 只丟 job,worker 跑 vision | `api/src/camera-scheduler.service.ts`、`api/src/ai.service.ts`、新 `worker.ts` | M | M | | M5 | **CI/CD(GitHub Actions)** | 現在每次都手改 Mac 上的檔,沒有 rollback | repo push → Actions build → SSH deploy → launchd restart;保留 `git tag` | `.github/workflows/deploy.yml`、`scripts/deploy.sh` | M | M | | M6 | **可觀測性 stack** | 出事盲飛 | 接 Better Stack / Grafana Cloud:API 用 OpenTelemetry,Cloudflare logs push 到同一處;每日 email 摘要走 SMTP(沿用 joyce628 SMTP 規範) | `api/src/main.ts`、`scripts/daily-digest.ts` | M | L | | M7 | **i18n + a11y** | PRD 全中文 hardcode;行動現場可能要日語(果園移工) | next-intl;把 `health-engine.healthStatusLabel` 改 lookup | `web/src/lib/i18n/*`、各 page | M | L | | M8 | **PWA + offline 巡園** | 果園 4G 不穩 | `next-pwa`;observations 先寫 IndexedDB,回線同步 | `web/next.config.ts`、`web/src/lib/sync-queue.ts` | L | M | | M9 | **3D 效能:Instanced mesh + LOD** | 樹數量上升後 R3F 會掉到 < 30 fps | 改 `TreeMesh` 用 `InstancedMesh`,依距離切 3 級 LOD | `web/src/components/orchard-3d/TreeMesh.tsx`、`PlantingRows.tsx` | M | M | | M10 | **Postgres backup → R2** | M1 之後仍要遠端備份 | `pg_dump` cron → R2 bucket + 90 天保留 | `scripts/backup-pg.sh` | S | L | --- ## 4. 🚀 Evo Plan — 6 個月路線圖 ### v1.1(M+1,~2026-06 底)「Production-grade MVP」 - **新功能**:真正的 login、照片上傳走 R2、Cloudflare Access 擋外人、3D 手機 fallback - **技術升級**:`next start` 取代 `next dev`、Postgres + PostGIS、pino 日誌、`/health` 含 DB 探測 - **Market positioning**:「可以給合作社看的 demo 站」 - **預期使用者**:3–5 位內測果園主(Joyce 親友 + 1–2 位邀請) - **成本**:Cloudflare R2 ~ US$2/mo、Postgres 自架(電費攤入既有 Mac mini)、OpenAI 估 US$10/mo(daily cap US$0.50) ### v1.5(M+3,~2026-08)「Beta 小規模付費」 - **新功能**:派工 LINE 通知、採後庫存 PDF 出貨單、財務月報(已有 finance.service 雛形)、PWA 巡園離線模式 - **技術升級**:BullMQ + Redis、GitHub Actions CI/CD、OpenTelemetry → Better Stack、AI 改走 OpenClaw(先 ollama gpt-oss:20b 在本機跑,fallback OpenAI) - **Market positioning**:雲嘉南檸檬產區 closed beta,月費 NT$ 1,500 - **預期使用者**:10–20 個果園、~30 個 active user - **成本**:Better Stack 免費 tier、AI 因走本機 ollama 成本 ~ US$5/mo(fallback only) ### v2.0(M+6,~2026-11)「Multi-tenant SaaS / 公開上線」 - **新功能**:多農場 / 多 user / RBAC、Roboflow 病斑偵測、相機 RTSP 雲端 relay、政府補助申請報表匯出 - **技術升級**:把 API 拆 Fly.io 多區(高雄 + 東京 fallback)、Postgres 升 Supabase 託管、3D Instanced mesh + R3F r161、Cloudflare Workers 做 edge cache - **Market positioning**:智慧農業 SaaS,提案農委會 / 縣府補助;接合作社 - **預期使用者**:50–100 個果園、200+ MAU - **成本**:Fly.io ~US$25/mo、Supabase Pro US$25/mo、Cloudflare Pro US$20/mo、AI US$30/mo;總 ~US$100/mo,營收目標 ≥ NT$ 45,000/mo(30 個果園 × NT$ 1,500) --- ## 5. 🐞 Fix Plan(含 Iron Laws 自我檢查) | # | 類型 | 症狀(現場證據) | 根因假設 | 修復步驟 | 驗證方法 | Iron Law | |---|---|---|---|---|---|---| | F1 | 部署 bug | HTML 含 turbopack HMR script、socket 連不到 | 線上跑 `next dev` 而非 `next start` | 改 launchd + `next start` | `curl orchard.graceai.net \| grep -c hmr-client` = 0;Lighthouse Performance ≥ 80 | **#39** web app must always work | | F2 | 環境 bug | `tsx watch` 三隻 zombie,watch fd 漏 | 改檔時舊 process 沒收乾淨 | 用 pm2/launchd 包裝 `tsx src/main.ts`(非 watch);prod 直接 `tsc + node dist/main.js` | `ps -ax \| grep tsx \| wc -l` = 1(dev)或 0(prod) | #29 CPU headroom | | F3 | 資料庫違規 | `api/.env` `DATABASE_URL="file:./dev.db"`、prisma `provider = "sqlite"` | PRD 說 Postgres,但實際走 SQLite | 起 Postgres 容器 → `prisma migrate` → 跑 sqlite→pg 搬遷 script → 切 env | `psql -c '\dt'` 看得到所有表;`api/health` 回 `{db:'pg', ok:true}` | **#5** Postgres only | | F4 | 安全漏洞 | 線上徽章「未登入 · Mock」+ 任何人可呼 `/api/*` | `auth.guard` 沒掛全域;前端 mock fallback 開著 | (a) Cloudflare Access 先擋;(b) 把 `APP_GUARD` 註冊 + login flow | curl 未帶 token → 401;登入後 200 | — | | F5 | 安全 / 成本 | `ai.service.ts` 沒 budget guard | 沒有 quota | 加 `AIUsageLog` table + daily/per-user cap | 連按 100 次 vision → 第 N 次回 429 + mock | — | | F6 | 安全 | 同時三條 cloudflared,含 quick tunnel(PID 90658)對外可能洩漏其他 localhost 服務 | 之前手動測試殘留 | `launchctl unload` 多餘的;單一 named tunnel | `pgrep -fl cloudflared \| wc -l` = 1 | #39 | | F7 | 效能 | Mac mini load avg 4.36 | watcher + 多 next dev | F2 解掉 + `next start` 後 RAM/CPU 應降 30%+ | `top -l 1 \| head -5` load < 2 | #29 | | F8 | 效能 / UX | 3D 場景在 mid-range Android 會 crash(推測,未實測) | 沒 LOD、沒 fallback | M9 + 加 `