已采用 Expo/React Native + Supabase Auth/Postgres/Edge Functions,成熟项目比较见 BACKEND-RESEARCH.md。客户端手工流程在本地运行;云服务代码已实现,但真实托管服务与模型调用尚未验证。不整仓复制 Supabase。移动端同一份 React Native UI 可输出浏览器验证版,浏览器验证不代表 iOS 真机或商店审核已经通过。
flowchart LR
U[用户] --> APP[Expo iPhone App / 本地浏览器预览]
APP --> LOCAL[设备本地范围/请求/回复]
APP -->|邮箱验证码| AUTH[Supabase Auth]
APP -->|显式同意 + JWT + 文本| EDGE[analyze Edge Function]
EDGE --> QUOTA[Postgres 原子用量限制]
EDGE --> MODEL[服务端模型提供商]
MODEL --> VALIDATE[结构和原文引用校验]
VALIDATE -->|仅建议| APP
U -->|确认结论 + 工时/费率| APP
APP -->|确定性计算| REPLY[可编辑回复/手动复制]
src/domain.ts:root 维护共享契约、报价与引用验证,测试在tests/domain.test.ts。app/、src/components/、src/theme.ts、src/store.tsx:前端 agent 维护。src/cloud.ts:后端 agent 维护客户端云 API 与 Auth,前端按导出契约调用。supabase/、tests/backend*:后端 agent 维护;根目录依赖由 root 安装。docs/、品牌素材、上架资料:研究/设计 agent 与 root 按分工维护。
Clause { id: string; text: string }
Verdict = 'included' | 'additional' | 'clarify'
Analysis { verdict: Verdict; summary: string; evidence: { clauseId: string; quote: string }[]; questions: string[]; source: 'ai' | 'manual'; warnings: string[] }
Project { id; name; client; clauses: Clause[]; createdAt: string; sample?: boolean }
SavedReply { id; projectId; projectName; request: string; decision: Verdict; text: string; amountCents: number | null; createdAt: string; scopeSnapshot: Clause[]; analysisSnapshot?: Analysis; sample?: boolean }
parseClauses(text)、validateAnalysis(raw, clauses)、calculateQuote(hoursText, rateText)、buildReply({ projectName, request, decision, hours, rate, note? }) 由 domain 导出。calculateQuote 返回整数分,无效值抛出可展示错误。draft 模板不调模型。
保存时复制当前范围为 scopeSnapshot,复制审阅建议/人工标记的引用、问题和警告为 analysisSnapshot;最终 decision 独立保存。快照与之后的页面编辑分离。旧本地记录允许没有 analysisSnapshot,不补造历史依据。示例项目的 sample 标记传递到保存记录。
amountCents 只记录未修改模板的结构化金额。最终正文相对生成模板发生自定义修改时(比较时忽略首尾空白),保存为 null,历史页显示“Edited reply · amount not recorded”,不从自由文本猜测金额,也不继续显示旧金额。例如工时 4、时薪 75 生成 30000 分;把正文费用改成 $500 后,正文仍可保存/复制,但独立金额为未知,不能计作 $300 或 $500 的收入。重新生成并保留模板原文时可再次记录确定金额。
src/web-tools.tsx 在浏览器暴露兼容的 document.modelContext.registerTool 时注册只读 calculate_scopepatch_estimate;原生端或不支持的浏览器不注册,注册失败不阻塞应用。输入仅为人工提供的 hours/rate 十进制字符串,复用 calculateQuote,返回整数分、USD 格式与 calculated_only 状态。
这个 WebMCP 计算器不调用 AI,不估算工时、不判断范围、不读取项目正文,也不保存、发送或收费。它不是可代替用户完成审阅/发送的 agent 接口。是否能被特定浏览器宿主发现需要另外验证。
前端 src/cloud.ts 导出:
isCloudConfigured(): booleangetSession()→ Promise<Session|null>;onAuthChange(callback)→ unsubscribesendOtp(email);verifyOtp(email, token);signOut();deleteAccount()→{ localSessionCleared: boolean }。云删除失败才抛错,云已删除但本地会话清除异常独立提示并允许重试。analyzeRequest({ clauses, request, consent: true })→ Promise
POST /functions/v1/analyze,JWT 必须经过服务端 getUser 验证。请求 JSON { clauses, request, consent: true }。用户 ID 只取验证过的 JWT,不取 body。最大 30 clauses,每条 2000 字符、合计 12000 字符,请求 4000 字符,body 64KB。每账号每天最多 20 次、两次间隔至少 10 秒(私测工程额度,不是付费权益),单次 quota RPC 通过数据库事务与行锁原子预占;已预占并发出的失败仍占一次额度。模型超时 25 秒,不自动重试;客户端总等待上限 45 秒。
响应 { analysis };未配置 503,未登录 401,无同意 400,超额度 429,无效输入 400,模型失败 502/504。模型密钥只在 Edge secrets;客户端仅用 Supabase URL 与 publishable/anon key。允许的浏览器 origin 从部署配置读取,native 无 origin 走 JWT。日志不记原始范围和请求。
POST /functions/v1/delete-account:验证登录用户,删除其云资料/用量和 Auth 身份,客户端随后清本地数据和会话。不得使用用户提供的 userId 删除任意账号。
Auth auth.users → 自有 profiles / projects / scope_versions / change_requests / replies(按版本扩展)与 ai_usage。
每张含业务内容的表必须包含 owner_id,开启 RLS,SELECT/INSERT/UPDATE/DELETE 都限制 auth.uid() = owner_id,并保证父对象同属一个 owner,不能只保护最外层表。快照不可覆盖历史。ai_usage 客户端不可写;仅服务端 service role 调受限制 RPC,撤销 public/anon/authenticated 执行权限。账号删除以外键级联清理。
MVP 保存路径是本地,云表不意味着已完成同步。当前预留的云 replies.amount_cents 为非空,未来接入同步前必须迁移为兼容本地未知金额的语义;不能把 null 转成 0 或旧估价。业务内容的 RLS、复合外键、quota 权限和删除级联已通过本地真实 Postgres 测试;真实托管环境双账号、JWT 过期与刷新、多连接并发仍待验证,见 QA.md。
- 本地 Expo 先跑无密钥手工闭环;导出 web 预览用于评审。
- 新建用户自己拥有的 Supabase 项目,选目标市场附近区域,应用 SQL 与 Edge functions,配置模型 secrets、允许 origin、认证发信服务。
- 在真实模型上跑评测集,真实 iPhone 私测,确认隐私披露。
- 只有付费/复用信号出现后再做云同步与 IAP。数字功能收费采用 StoreKit/RevenueCat 的商店内购路线,服务端校验交易/通知并做幂等权益,不相信客户端 isPro。
- App Store 需用户自己的开发者账号、签名与审核材料;EAS 配置随代码提供,实际提交和费用未产生。
不预设服务器月费/模型固定价格;单位经济以实时价、实际 token 与调用量核算。试点预算和退出阈值见验证计划。