🔧 AI 技术经验归档(Sozo 基建专题)
反复踩的坑 + 已验证的终局方案。遇到同类问题先查这里,别重复踩。关联业务图见 Company_Graph。
🟥🔴 最高优先:Prisma + Cloudflare Workers (OpenNext) — master app 500 根因
反复踩 4 次的坑,每次都要重查半小时。终局方案在此,直接照做。
现象
- Next.js 15 + Prisma 6.19 driverAdapters + D1 → 部署后所有 API 500 空响应,前端
Unexpected end of JSON input - 首页 200(静态),但走数据库的
/api/*全 500
根因(非直觉!)
不是”wasm 没打包”,而是 Prisma 6.19 的 driver-adapter 生成的 config.compilerWasm.getQueryCompilerWasmModule 用 require("fs").readFileSync(<cwd>/query_compiler_bg.wasm) 读 wasm 文件。Cloudflare workerd 无文件系统 → readFileSync 抛错 → 每次 API 500。
之前所有”复制 wasm + import .wasm”方案都错在:Prisma 根本不走 import,它走 fs.readFileSync。
终局修复(3 件套,缺一不可)
-
wrangler.json加"find_additional_modules": true(让 wrangler 收集 .wasm 为额外 module——来自 OpenNext issue #139 评论区 vicb 建议)
-
wasm 复制到 handler 同级,文件名精确匹配 import 名:
cp node_modules/.prisma/client/query_compiler_bg.wasm \ .open-next/server-functions/default/query_compiler_bg.wasm -
patch
<项目>/.open-next/server-functions/default/handler.mjs:- 顶部加:
import __prisma_wasm from "./query_compiler_bg.wasm"; globalThis.__PRISMA_BINARY = __prisma_wasm; - 把
config.compilerWasm={getRuntime:async()=>require_query_compiler_bg(),getQueryCompilerWasmModule:async()=>{...readFileSync...}}替换为:config.compilerWasm={getRuntime:async()=>require_query_compiler_bg(),getQueryCompilerWasmModule:async()=>globalThis.__PRISMA_BINARY}
- 顶部加:
一键脚本
patch_handler_wasm.py(位于 D:\hermes\hsdesign_work\glm5.2_cashflow\src\hsd-system\)——每次 opennext build 后必跑(build 会重置 .open-next,注入会丢)。
部署流程(master app 正确姿势)
npm run build (或 npx opennextjs-cloudflare build)
→ 清旧 wasm 注入 & 复制 wasm 到 handler 同级
→ python patch_handler_wasm.py
→ npx wrangler deploy
→ curl 测试(首次可能 500 冷启动,重试即可)
参考
- opennextjs/opennextjs-cloudflare #139(open:wasm 无法 import)
- workerd 中
import wasm from "x.wasm"返回WebAssembly.Module(Prismanew WebAssembly.Instance(module, imports)正需要)
🧾 报价编辑器「添加 item 到已有 section」逻辑
坑
「添加工种」按钮全绑 addGroup()(总是新建 section 级 Lumsum item,sortOrder: items.length)→ 每次点都在表格末尾新开一个该工种 section,而不是加进现有 section。
修法
- 点击添加 → 该工种已有 section 就调
addItemToGroup(isLumsum:false加 item);没有才addGroup addItemToGroup/insertFromLibrary用lastIndexOf(tradeGroup)+ splice 插到该工种最后一项后面(不能[...items, new]数组末尾追加,否则isGroupStart判定成新 section)
📄 Cloudflare Workers PDF 打印(@page 页码)
- 页码必须写
@page { @bottom-center { content: "Page " counter(page) " of " counter(pages) " · Quotation " "QUO-XXXX" } } - ⚠️ @bottom-center 是 CSS Paged Media 的 margin-box,必须嵌套在
@page {}里面;独立写在 @page 外是非法语法 → Chrome 直接忽略,页码不渲染(8/5 实测踩坑:为了躲 PostCSS 拆出来单独写 = 页码消失) - ✅ 正确写法(2026-08-05 14:42 验证上线):单一
@page块内嵌 margin-box——@page { size:A4; margin:...; @bottom-center {...} }。PostCSS 不会清单一 @page 块内的嵌套规则;之前担心被清理而拆开写是错的(多个 @page 块才会触发 PostCSS 误判) - ⚠️ Cloudflare CDN 会缓存旧 PDF 响应:部署后测试/用户看到的可能还是旧版 → 验证必须带 cache-buster(URL 加
?v=随机数);用户端需 Ctrl+Shift+R 强制刷新 - 报价单 PDF(
/api/pdf/quotation/[id])和 VO PDF(/api/pdf/vo/[voId])两处都要同样修;8/5 修复后部署 Version6e942c2b,用户确认页码正常 - 禁用浏览器默认 footer(防暴露内部网址):
@media print { a[href]:after{content:none!important} }+ 自定义 margin;浏览器打印面板的「页眉和页脚」仍需用户手动关一次(关掉后自定义页码照常显示,两套独立机制) - Chrome/Edge 支持 counter(pages),Safari 不支持(可接受)
- 旧报价单数据没存 termsConditions 时 PDF 运行时 fallback 默认 T&C——旧单打印自动获得新条款/新版式,无需重建
🧾 报价系统业务规则(2026-08-05 拍板)
- VO 支付条款(柔佛惯例):确认时 50% + 完工时 50%;不参与主报价付款百分比(T&C 已更新)
- VO 独立成文:每个 VO 单独 PDF(
/api/pdf/vo/[voId],含签名+IC+页码+50/50 条款),不混在报价单 PDF 里 - 签名后 Quotation = 有效合同(Contracts Act 1950);Invoice 只是账单。签名区 = Customer Signature + IC + Date + Witnessed by
- 报价单每页底部:
Page X of N · Quotation No(防拆页争议)
🤖 D1 / Cloudflare 常见坑
- SQLITE_TOOBIG:写入大 JSON/HTML(>100KB)必须用参数化绑定(
INSERT ... VALUES (?, ?)),不能拼 SQL 字符串 getCloudflareContext({async:true})必须 await(同步模式在动态路由报错)- 空白页 = wrangler.json 缺
assets配置 - 部署用一体化
npm run deploy:cf或清 .open-next 缓存重建,避免 worker.js 旧 / assets 新不同步
🐍 Windows cron 脚本
- 一律 .py,不要 .sh:MSYS bash 吞反斜杠(
D:\hermes\...变D:hermes...) - 不要手动
cronjob run一次性提醒任务:会消耗一次性任务 - 慢任务必须主动报进度(防用户以为卡死)
🛠️ 部署经验
- 改代码 → build → 重新应用 wasm 修复 → deploy → curl 验证(处理冷启动 500)
find_additional_modules让 wrangler 收集 .wasm 为 module- master app 回滚只回退代码,如果根因是 wasm 配置则回滚无效(本次已证:回退 8/4 仍 500)