29 天,5 个散装 App 变成一个平台
第1篇讲了为什么在零领域知识的情况下做算命平台。现在说说它到底是怎么做出来的。
猫咪建筑师将五个分散的小楼合并成一座统一平台
不是精选集锦。是真实的架构决策、最棘手的bug,以及让每天39个commit成为可能的基础设施。
第一阶段:五合一(第1-4天)
起步时有5个独立的Next.js应用,在5个独立仓库里。八字、星座、塔罗、解梦、占卜。每个有自己的认证、自己的Supabase客户端、自己的Tailwind配置。
用户得登录五次。这不叫平台,这叫折磨。
第1天搭Turborepo。21个commit。概念上很简单——应用搬到apps/,共享代码提取到packages/——但每条import路径都炸了。修import和验证构建占了80%的时间。
第2天是CSS地狱。Tailwind配置统一到packages/config之后,每个依赖自定义design token的应用全崩了。不是那种明显的红色报错,是那种微妙的"为什么这个按钮小了2px"。38个commit,每个都在修一个视觉回归。
但第2天也产出了第一批真正的成果:提取了四个共享package——PDF生成、聊天记录、共享聊天UI和通用React hooks。看着5个应用里的重复代码收敛成单一数据源,那种感觉极其爽。
第3天加了MBTI性格测试成为第6个应用。还做了微信浏览器检测——中国互联网有很大一部分流量来自微信内置浏览器。UA嗅探要做加固,因为微信的UA在Android、iOS和桌面端各不相同。
第4天是56个commit——统一API package。packages/api标准化了所有应用跟AI模型的通信方式。52个API路由迁移到统一模式:认证检查、积分扣除、模型选择、SSE流式输出、错误处理。
Claude Code看了一个示例后迁移了全部52个。实际耗时一小时,手动做至少一整天。
第4天还搞定了GDPR合规、中英双语法律页面,以及第一版基于DFA的敏感词过滤器。过滤器是刚需——玄学内容天然包含会触发简单关键词过滤的词(算命、破财、桃花劫),需要一套合理的白名单方案。
第二阶段:测试、品牌,和那只猫(第5-10天)
测试不光鲜,但能救命
全monorepo用Vitest。优先级想清楚了:
-
先测共享package — API流式输出helper、Supabase客户端、积分管理。这些是承重墙。
-
再测领域计算 — 八字计算、塔罗抽牌、六壬占卜。我没法验证正确性(零领域知识),但可以验证确定性输入产出预期输出。
-
最后测API路由 — mock认证token做集成测试。
GitHub Actions CI:每个PR跑lint、类型检查、构建、测试。到正式开始里程碑时,测试套件已经有34个文件、1,922个测试。
一只猫改变了一切
PanPanMao(盼盼猫)。会算命的猫。
"猫"这个主题一定下来,一切就顺理成章了。每个应用有了猫主题的装饰。积分货币变成了小鱼干。
你不拿小鱼干付,拿什么付给一只猫呢?
Stripe定价
四个美元积分套餐,从入门到高级用户各一档,中间有个"最受欢迎"锚定档。
积分抽象层是关键——人们更愿意"花5条小鱼干",而不是看到具体的美元金额。还预置了未激活的人民币套餐,为中国市场做准备。
第三阶段:硬核工程(第12-19天)
手面相识别:浏览器里跑机器学习
技术难度最高的功能。PR #1:56个文件变更,6,881行新增。
MediaPipe完全跑在浏览器端。 检测不需要服务端往返。hooks架构:
-
useMediaPipeLoader— 懒加载WASM(约3MB),模块级缓存 -
useFaceDetector— 实时BlazeFace,60fps用于摄像头预览 -
useFaceLandmarker— 478点面部网格,拍照时跑一次 -
useHandLandmarker— 21点手部关键点,拍照时跑一次
PR #50把WASM和模型文件本地化到public/mediapipe/,因为中国用户访问cdn.jsdelivr.net和storage.googleapis.com不稳定。PR #35加了手掌检测的GPU→CPU降级,解决WebGL失败的设备兼容问题。
多模态AI prompt不能只说"看看这只手"。prompt有四层结构:角色定义 → 知识注入(五官、三停、十二宫)→ 交叉验证 → 结构化输出格式。
积分经济:匿名用户转正最头疼
PR #2(M1 Foundation):100个文件变更。核心挑战是匿名用户转正式用户的账号合并。
新用户进来就是Supabase匿名账户。等他们兑换码或登录时,积分、算命记录、聊天历史需要无缝合并。兑换流程是个状态机:idle → checking → redeeming/logging_in → success/error。
跨标签页积分同步用Supabase实时订阅。有个隐蔽的bug:一个标签页买了积分,切到另一个标签页,旧余额还在。
用户以为购买失败了,又买一次。双重扣费。
修复:实时订阅在一秒内同步所有标签页。
18条流式路由的大统一
到这时候已经有18条SSE流式路由,每条自己实现ReadableStream。PR #16做了大重构:15条迁移到共享的createAIStreamResponse() helper,消除了约1,200行重复样板代码。加了生命周期钩子:initEvents、onComplete、onError、refundCreditsOnError。
有三条确实需要自定义处理:MBTI聊天(accumulated-buffer-delta模式做信号剥离)、梦境小说(多步骤生成协议)、每日运势(非流式,带服务端缓存)。
AI语气大改:最难做对的事
PR #38由真实用户反馈触发:解读太"讨好型"。什么都是正面的。每次都说你会成功。健康永远没问题。感情永远有希望。
但真正的算命先生不是这样说话的。
修复:全部6个领域的prompt做系统性改造——整体语气从"偏积极正面"改成"好的说好,不好的明确指出"。健康解读从"混合分析"改成"中立偏负面,直接给健康警示"。负面指标直接用凶/不利,后面跟化解方案。
这是少数几个AI没法独立完成的改动。语气校准需要人对文化期望的判断。
第四阶段:里程碑与基础设施(第20-29天)
一天三个里程碑
2月12日最高强度:15个PR合并。M1(积分经济)、M2(每日枢纽)、M3(PostHog数据分析)全部上线。
每日枢纽(PR #4)是留存设计。算命天然是事件驱动的——有问题来,得到答案走了。枢纽给用户一个每天回来的理由。
核心洞察:北京时间午夜cron预生成内容,不是按需生成。 用户早上打开,内容瞬间加载。没有转圈。即时和等3秒之间,差的不是3秒,是用户对产品的信任。
一天三个新产品
2月15日:流年运程(PR #85)是全新领域——生肖计算器、太岁分析、流年飞星、AI运势解读。48个文件,162个领域测试。
Portal主题bug:我最喜欢的工程故事
每个应用有自己的CSS主题(.theme-hub、.theme-tarot等)。但React portal通过createPortal(content, document.body)渲染——DOM节点跑到了应用主题wrapper外面。CSS变量对portal内容不可见。
最初修法:检测closest('.theme-hub')然后硬编码overlay类。9个主题、13个portal组件,不可扩展。
PR #67方案:ThemeProvider React context声明当前主题类,ThemedPortal组件用MutationObserver把正确的主题类和暗色模式类包裹到portal内容上。干净、可扩展、正确。
弹性AI流式传输
最后一个大基础设施PR(#105):服务端流式缓冲。
19条AI流式路由通过StreamBufferManager将数据块缓冲到数据库。客户端中途断开?通过GET /api/stream/{id}恢复完整响应。统一的useAIStream hook替换了14个组件中的3种SSE消费模式。
结果:加了整套弹性基础设施,代码净减186行。
内容过滤器的三次进化
-
V1(第4天):DFA关键词过滤。敏感消息整条拦截。
-
V2(PR #78):流感知过滤。但遇到敏感词截断整个流——用户积分扣了,只拿到残缺的解读。
-
V3(PR #104):行内遮蔽。敏感词替换为
**检测到违禁词**,流不中断。双过滤架构:严格的输入过滤器(广告+政治+色情+犯罪),宽松的输出过滤器(排除"广告"类别,防止误杀算命术语)。边界安全遮蔽处理跨chunk边界的词汇。
速度数据
-
1,134个commit,29天(日均39,峰值98)
-
109个PR,66个带详细描述
-
9个产品线,都有真实业务逻辑
-
85个API端点,19条SSE流式路由
-
约284,000行代码,16个package
-
2,021个测试通过(最终计数)
-
0个零commit日
共享package是速度的倍增器。新增一个产品线自动获得认证、积分、AI模型选择、内容过滤、流式传输、错误处理和UI组件。新产品的边际成本几乎全在prompt engineering。
第3篇是真实的教训——哪些事让我意外,哪些事我做错了,以及这一切让我对未来怎么做产品有了什么新认识。