🔧 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.getQueryCompilerWasmModulerequire("fs").readFileSync(<cwd>/query_compiler_bg.wasm) 读 wasm 文件Cloudflare workerd 无文件系统readFileSync 抛错 → 每次 API 500。

之前所有”复制 wasm + import .wasm”方案都错在:Prisma 根本不走 import,它走 fs.readFileSync

终局修复(3 件套,缺一不可)

  1. wrangler.json

    "find_additional_modules": true

    (让 wrangler 收集 .wasm 为额外 module——来自 OpenNext issue #139 评论区 vicb 建议)

  2. wasm 复制到 handler 同级,文件名精确匹配 import 名:

    cp node_modules/.prisma/client/query_compiler_bg.wasm \
       .open-next/server-functions/default/query_compiler_bg.wasm
  3. 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(Prisma new WebAssembly.Instance(module, imports) 正需要)

🧾 报价编辑器「添加 item 到已有 section」逻辑

「添加工种」按钮全绑 addGroup()(总是新建 section 级 Lumsum item,sortOrder: items.length)→ 每次点都在表格末尾新开一个该工种 section,而不是加进现有 section。

修法

  • 点击添加 → 该工种已有 section 就调 addItemToGroupisLumsum:false 加 item);没有才 addGroup
  • addItemToGroup / insertFromLibrarylastIndexOf(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 修复后部署 Version 6e942c2b,用户确认页码正常
  • 禁用浏览器默认 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)