SayCraft 工具链踩坑笔记:Clash、Drizzle、Clerk 和一堆杂货

做 SayCraft 这半年,踩的坑比写的功能还多。这篇不是什么系统总结,就是把散落在各处的工具链笔记捞出来,记一下那些"查了半天才搞明白"的东西。

Clash 和 SSH:AI 都被骗了

有段时间海外服务器 SSH 死活连不上,kex 阶段直接断开。我让 AI 帮忙排查,它很笃定地说是服务器 OOM 把 sshd 杀了。重启了好几次,换了配置,还是不行。

最后发现跟服务器一点关系没有——是本地 Clash 把 22 端口的流量吃了。海外 IP 不命中 GEOIP,CN 规则,直接落到 MATCH,Final,走代理出去,SSH 握手被代理截断。加一条 DST-PORT,22,DIRECT 就好了。

更烦的是,Clash 的订阅更新会覆盖自定义规则。每次机场推送新配置,这条 DIRECT 就没了,SSH 又断。反复出现过三四次才形成肌肉记忆:SSH 连不上,先看 Clash。

Clash 的 DNS 劫持:另一个坑

写了个脚本往 IndexNow 推 URL,跑起来 curl 直接返回 000,exit code 35。查了半天,发现域名被 Clash 的 fake IP 池劫持了,解析到 198.18.x.x 这种地址。脚本里有个 DNS 校验逻辑,检测到非预期 IP 就拒绝发请求。

这个 fake IP 机制是 Clash 的 TUN/DNS 特性,用来加速代理路由。但它对本地脚本来说就是个陷阱——你以为在跟真实服务器通信,实际上 TLS 握手对象是 Clash 的内部端口。

Drizzle ORM:SQL 看着对,跑着错

Drizzle 的 sql 模板标签很好用,但有两个暗坑让我连栽两次。

第一个:JS 的数字通过 sql 模板传进 Postgres 时是 untyped 参数。如果你对这个参数做除法,Postgres 不知道它是 integer 还是 bigint,类型推导失败,直接报错。解法是手动 cast 成 bigint。这种错误 TypeScript 编译完全不报,只有真跑 SQL 才炸。

第二个更隐蔽:同一个 sql fragment 在 SELECT 和 GROUP BY 里复用时,Drizzle 给它们生成了不同的 placeholder 名字。Postgres 认为 GROUP BY 里的表达式跟 SELECT 里的不是同一个,拒绝执行。绕过去的办法是用 sql.raw() 把表达式内联成字符串,不走参数绑定。

教训:凡是超出简单 CRUD 的 Drizzle SQL,写完先拿 psql 跑一遍。TypeScript 类型检查和 mock 测试都救不了你。

DashScope 国际版 vs 国内版

阿里云百炼和 Model Studio 共享"DashScope"这个品牌名,但它们是两套独立系统。国内账号的 API key 在国际版上用不了,反过来也一样。

如果你的产品同时服务国内和海外用户,需要分别开通两个账号、拿两个 key。好消息是国际版现在也上了 deepseek-v4-flash、deepseek-v4-pro 和 kimi-k2.5,模型覆盖已经对齐了。

Clerk OAuth:GitHub App 不是 OAuth App

给 SayCraft 接 GitHub 登录的时候,我在 GitHub 上建了个 GitHub App,把 client ID 填进 Clerk。登录页加载出来了,但点击跳转到 GitHub 授权时直接报错。

翻 Clerk 文档才发现,社交登录要的是 OAuth App,不是 GitHub App。这两个在 GitHub Developer Settings 里是并排的两个入口,名字长得像,协议完全不同。

还有个更隐蔽的问题:Clerk 的 publishable key 是个 base64 字符串,里面编码了绑定的 custom domain。如果你配了 auth.yourdomain.com 但 DNS 还没生效,前端 SDK 初始化时就直接拒请求,报一个很模糊的错误。这个排查花了不少时间。

移动端审计:工具链全军覆没

想在手机宽度下看看 SayCraft 的页面效果,试了一圈工具,全有问题。

Preview 浏览器(Claude 内置的)被沙箱限制在 localhost,打不开线上地址。Claude-in-Chrome 倒是能连真实 Chrome,但用它调整浏览器窗口大小之后扩展就断连了。想在本地 dev 环境指向生产 API 来测?Clerk 的登录门控过不去,COS 上的 archive 数据也因为跨域拿不到。

最后的方案很土:Playwright 无头浏览器,直接对线上 URL 截图,指定 viewport 宽度。没有什么 fancy 的热重载,但至少截出来的是真实渲染结果。

移动端 CSS:hover 在触屏上不存在

项目里有些 Edit/Delete 按钮用了 opacity:0 加 :hover 显形的交互——鼠标移上去才出现。在桌面上很优雅,到手机上这些按钮就彻底消失了,因为触屏没有 hover 状态。

修法是加 @media(hover:none) 让这些按钮在触屏设备上常显。

顺便说一句,整个项目没有用过一次 Tailwind 的响应式断点。所有移动端适配全靠一个 JS hook useIsMobile(768px) 加内联 style。这不是什么最佳实践,但项目演进到现在确实就是这样。

Dashboard 永远显示 100:前端分桶的锅

有个 KPI 卡片"MEETINGS 14D"一直显示 100,不管实际有多少会议。查了半天,原因是前端调 listMeetings 时传了 limit: 100,拿到 100 条之后在客户端按日期分桶计数。超过 100 条的会议永远统计不到。

修法很直接:把分桶逻辑移到服务端,写一条 SQL 聚合,前端只拿结果。这种"先全量拉再客户端算"的模式在数据量小的时候没问题,数据一多就全是 bug。


这些坑单个看都不大,但每个都能让你卡半天。写下来是因为下次再遇到类似问题,至少有个地方可以搜。如果你也在用类似的技术栈,希望能省你一点时间。

SayCraft 是我在做的一个产品,用对话的方式生成网页应用。感兴趣可以看看 saycraft.ai

感谢您的收看 祝你天天开心~
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇