一个日期输入框的跨年陷阱:保存前先把那一年读回来
站点里有个页面叫 /masturbation,公开展示每天的次数和一张年度热力图,后台 /dash/masturbation 供管理员补记、修改、清除。数据很私人,页面很普通:一条按年查询的 RPC、一张日历、一个日期输入框加一个次数输入框。 上线当天就出了个会静默吃掉数据的 bug。 0 既是「这天没有」,也是「把这天清零」 热力图按年加载,编辑器的 selectedCount 从当前这一年的查询结果里取。管理员在 2025 年的页面上,把日期改成 2024-03-12—— 输入框里的日期变了,但页面还是 2025 年的数据; selectedCount 在 2025 年的数组里找不到这一天,回落到 0; 次数输入框显示 0; 点保存,写入 0。 如果 2024-03-12 原本记了 7 次,就被这一次「什么都没改」的保存抹掉了。0 在这里同时是「没有记录」的展示默认值和「清除记录」的写入值,两者长得一模一样。 让日期自己带上年份 第一反应可能是加一句提示「跨年日期请先切换年份」。但更可靠的是把年份变成选中状态的一部分: 选中一个不属于当前年的日期时,直接 navig...
从 Vercel 搬走之后,平台替我兜的底一条条露出来了
9 月 29 到 30 号,两天里二十一个提交,把 nav-site 从 Vercel + Neon 搬到一台新加坡的 VPS 上:Cloudflare Tunnel 进来,源站不开任何公网端口,postgres 只绑 127.0.0.1:5432,应用跑在 Docker Compose 里,构建也在服务器上用专用 buildx 构建器做。 迁移最后是成功的。但前面那些提交几乎全在补同一类东西:平台以前替我做的假设。这篇按发现顺序记,不按提交顺序。 一跳就跳到 0.0.0.0:3000 切生产前在 preview 实测,/r/<渠道码> 的 302 落点是 https://0.0.0.0:3000/,Google 登录成功和出错的跳回也一样。投放入口如果在切生产当天才发现,就是全站短链一起失效。 根因在 next-server.js 的 attachRequestMeta:standalone 模式下 next-server 持有监听的 hostname/port,构造请求 URL 时优先用它们,experimental.trustHostHeader 那条分支走不...
限流挡在写行之前:一次验证码被自己作废的排查
九月的认证里程碑里,邮箱验证码、找回密码和 Google 登录一起收尾。review 第一轮揪出的东西里,有一条跟限流有关,写下来是因为它跟「配几个数字」完全不是一回事。 先看代码顺序 better-auth 的 email-otp 插件在端点里先调 resolveOTP(email-otp/routes.mjs:31),无条件插一行验证码;发信的 sendVerificationOTP 回调在那之后才跑。而消费时,consumeVerificationValue(internal-adapter.mjs:818-860)永远只认「该 identifier 最新的一行」,并在消费时把全部行 deleteMany 掉。 旧代码把限流判断挂在发信回调里,也就是挂在那次插入之后。一旦命中: resolveOTP 已经写了一行新的码; 回调判断限流,拒发,这行码从没进过任何人的信箱; 用户输入手里那封信上的旧码,匹配到的是最新那行,INVALID_OTP; 两行一起被删光。 人被卡死:信箱里的码已死,冷却窗内又要不到新码,而错误信息说「验证码错误」,把账算在用户头上。 挪到插入之...
客户端 CSV 就是唯一事实来源:C1 到 C68,把数值一条条交还回去
先交代背景:这是我自己的《海岛奇兵》私服,客户端版本 57.135,除了服务端,还有一个在浏览器里重建基地渲染与编辑的 webclient。客户端自己随包带了一整套 csv/——建筑、障碍物、装饰、陷阱、词条、globals,连「移除这块石头要多少黄金、掉多少木材、司令部要到几级」都写在里面。 以前这些东西在引擎和外壳里各抄一份。最近十天把抄的部分一条条删掉,改成直接读表,编号从 C35 排到 C68。这篇不打算按提交顺序复述,只挑几件值得记的事:为什么非读表不可、怎么让「读没读」变成会红的测试,以及表里没有的东西为什么不能自己编。 手抄的代价不是「抄错一次」 手抄最坏的形态不是抄错,是抄对之后没人知道它已经过期。客户端表里一格改了,引擎里那份副本不会跟着动,最后的表现往往离源头很远:水面的颜色差 1、障碍物清除不扣钱、同一件装饰可以摆一百个,而测试全绿。 这次清出来的几处都挺典型: 位置 表里的真值 手抄时的表现 src/terrain.h globals.csv 三张水面调色板,共 12 格 像素对比只会报「某片水颜色差 1」 removeAt obsta...
AI 写得出功能,写不好 Dockerfile:Next.js 自托管踩坑记录
文中涉及的官方文档:Next.js 自托管、Serwist、PM2 配置项、postgres 镜像说明 最近连着给三四个 Next.js 项目做自托管,这里记录下踩过的几个坑捏。功能部分 Claude 写得是真快,页面、tRPC 路由、Prisma schema 一口气糊出来都能跑;可一到部署收尾就开始反复,同一个文件改回来改过去。下面全是「build 通过但跑不起来」那一类,本地一点征兆都没有… 不废话了,干正事! standalone 多阶段镜像:只有一条是真踩了坑 本来想标题写「四个坑」的,结果翻 git 记录被打脸了~ vns-next 的 Dockerfile 全生命周期只有 4 笔提交动过它:404a769 创建,之后 0b7117c、037ccf4、fefd366 各改一次。而下面这几条里有三条在首次提交就已经写对了,压根没「踩」过。 先看真正踩了的那条。404a769 里的构建基底原本是纯 oven/bun 镜像,跑起来直接挂,0b7117c 才补上 node: # 注意:不能用纯 oven/bun 镜像——prisma/next 的 CLI shebang ...
多个 AI 一起写代码时,我给它们划的边界
最近在几个项目里,同一个仓库同时被两个 AI 编码工具改过,加上几个人自己也在推代码… 一开始觉得没什么,反正都是写代码嘛,结果踩了一圈才发现问题基本都不在代码本身,而在「谁读到了哪份规则」「谁能碰机器上的什么东西」这类协作机制上。这里记录下我最后划出来的几条边界。 先说个反转:所谓的「多方」 翻我另一个仓库时撞上件好玩的事。git shortlog -sn 出来两行、两个名字,看着像俩人在推;一查邮箱是同一个,多出来那个只是我在网页端点 merge 时的账号名。而 78 个 commit 里 63 个带 Co-Authored-By: Claude——真正的「多方」压根不是多个人,是一个人加好几个 AI 会话。并行的痕迹也不在 .claude/worktrees/(那目录空的,git worktree list 只有主仓一行),而在分支名上:五个 claude/ 前缀分支,有一次是两条同期开着最后互相合并的。 所以下面这些边界,我原本以为是给人和人定的,回头看更多是给「我和我自己开的几个会话」定的——会话之间没有共同记忆,这点比同事还严重哦。 规则文件是会分裂的 其中一个团...
9 个 commit,没有一个人类作者:AI 独自写完一个项目之后
最近接手了一个别人扔过来的项目,git log 一拉愣了一下——9 个 commit,author 全是同一个 AI Dev <ai-dev@local>,一个人类提交都没有。下面提到的 commit 我就不贴 hash 了,按干的事来称呼。仓库里没有 CLAUDE.md、没有 AGENTS.md、没有 .claude/、没有 .cursor/、没有 CI,连 lint 都只是 tsc --noEmit,测试更是零。翻了一天,记录下它做得好的和做砸的… 先交代下这项目是什么:一个纯客户端的 React SPA,后端是 Express 打包成一个 Vercel Function 入口,api/index.ts 加 api/lib/ 下六个模块,数据库是 Neon Postgres,对象存储走 S3 兼容 API。就这么点东西,不复杂。 先说它做得好的 部分 commit 带了可核对的验证结果 9 条 commit 里有 4 条在结尾带了验证结果,而且带可核对的数字。 接对象存储那条,结尾写的是「本地端到端冒烟通过: presign → PUT → public GE...
Next.js 渲染模式排查笔记:force-dynamic、hydration 与构建期没有数据库
最近连着写了几个 Next.js 项目,功能本身都挺顺,结果全卡在同一类问题上,本地 next dev 好好的,一 build 或者一上线就出事。这些坑清一色不属于「代码写错了」,而属于「渲染模式没搞对」,报错信息还经常指向别处,特别费时间… 这里记录下我踩过的几个,尽量把文件行号都写上~ 坑一:构建期是没有数据库的 vns-next 是 Galgame 资源站,Prisma + Postgres,部署走 Docker Compose 自托管。本地跑得好好的,一进 Dockerfile 的多阶段构建就炸。原因是 Next 默认会在 next build 阶段把 RSC 页面静态预渲染一遍,而 builder 那一层根本没有数据库容器,任何查库页面必然在 build 时抛错。 修法在 commit b07e20d:根 layout 里加一行 export const dynamic = "force-dynamic",sitemap.ts 加上 feed.xml、llms.txt、index.xml 三个 route handler 同步声明。 // src/...
让 Claude 少写错数据层:Prisma、Drizzle 和几条结构化做法
最近几个项目分别用了 Prisma、Drizzle 和干脆不用 ORM 直接写 SQL。 同一个 AI 在这三种上犯的错完全不一样,这点挺出乎我意料的… Prisma 上它写的是「上个大版本的正确写法」,Drizzle 上它卡在类型和 API 签名,裸 SQL 上它压根不知道那个 driver 有什么限制。 这里记录下我从 schema 到迁移这条链路上用来兜底的几条做法,纯属备忘。 Prisma 7 / Drizzle 0.45 / Next 16。版本对不上的话,下面这些写法都会变哦。 Prisma 7 有几个 AI 一定会写错的地方 acg-site 那个仓库的 CLAUDE.md:18-25 有一节直接叫「关键版本差异(AI 常见错误)」,六条 bullet 里跟 Prisma 相关的就一条: Prisma 7 generator 为 "prisma-client"(不是 "prisma-client-js"),导入路径 @/generated/prisma/client 一条塞了两件事。同一个仓库的 .cursor/rul...
我的 CLAUDE.md 只有一行:约定文件到底该写多少
最近把一个 Galgame 资源站从 Hugo 整个重写成了 Next.js,仓库是 vns-next。从 create-next-app 那个初始 commit 到能挂上线,一共 13 个 commit,时间跨度不到 20 小时。 事后翻仓库的时候发现一件挺好笑的事——我给 Claude 写的那份约定文件,全文只有一行。而且那一行还不是我写的喽… 这篇不聊渲染模式也不聊部署,就只说「项目起步时该给 AI 留什么约定」这一件事。记录下我在几个项目里来回试出来的结论… 我的 CLAUDE.md 全文就一行 先上原文,vns-next 根目录的 CLAUDE.md 全文: @AGENTS.md 对,就这一行,用 import 语法指向隔壁的 AGENTS.md。那再看 AGENTS.md,全文 5 行: <!-- BEGIN:nextjs-agent-rules --> # This is NOT the Next.js you know This version has breaking changes — APIs, conventions, and file stru...
