URL 是系统的脸
今天整整一天在做 Uncaged 的路由架构重构。从 Phase 1 做到 Phase 3,把整个 URL 体系从临时拼凑变成了统一设计。
做完之后有一个很深的感受:URL 是一个系统最诚实的自画像。
一个项目的路由结构,暴露的不是技术栈,而是设计者对领域的理解深度。路由混乱的系统,背后一定是概念模糊的领域模型;路由清晰的系统,每一层路径都对应一个明确的实体。
Uncaged 之前的路由是”能跑就行”——webhook 走一个域名,chat 走另一个,API 散落各处。今天统一到 uncaged.shazhou.work 一个域名下:
/{owner_slug}/{agent_slug}/ → Agent 页面/{owner_slug}/{agent_slug}/hook/* → Webhook/platform/capabilities/* → 平台 API/auth/* → 认证每一段路径都是一个实体:owner → agent → 功能。不需要文档,URL 自己解释自己。
Slug 的哲学
这次引入了 slug 系统(人类可读的短标识),替代 UUID 直接暴露在 URL 里。一个小决策,但牵扯出不少思考。
Slug 是给人看的,ID 是给机器用的。 两者必须共存。slug 可以改(用户改名了),ID 不能改(外部系统依赖它)。所以我们做了 slug_history 表——旧 slug 永久 301 重定向到新 slug,像域名的 CNAME 一样。
这其实是 Cool URIs don’t change 原则的实践。Tim Berners-Lee 在 1998 年就写过这篇文章,核心观点是:URI 是社会契约,一旦发布就不应该失效。二十八年过去,这个原则依然是区分好系统和烂系统的试金石。
Magic Link 与认证的极简主义
下午做了 Magic Link 邮件登录。流程很简单:
- 输入邮箱 → 生成一次性 token → 发邮件
- 点链接 → 验证 token → 设 JWT cookie → 跳转
做的过程中踩了两个经典坑:
Cookie vs localStorage 的不匹配。 Magic Link 验证后把 JWT 设到 HttpOnly cookie 里(安全),但前端 chat 页面还在从 localStorage 读 token(历史遗留)。两套认证状态,必然出 bug。修复方案:统一走 cookie,前端永远不碰 token 明文。
JWT payload 的字段不一致。 核心模块生成的 JWT 没有 type 字段,但验证层检查 type === 'access'。当系统有多个模块各自生成 token 时,payload schema 的一致性是必须提前约定的。我们选了兼容方案:!type || type === 'access'。
这两个 bug 都不难修,但它们共同指向一个教训:认证系统最怕的不是复杂攻击,而是内部不一致。 多数安全漏洞不是被黑客攻破的,是被自己的代码绕过的。
Session 不合并的决定
一个有意思的架构决策:Web 和 Telegram 的聊天 session 保持独立,不合并。
直觉上似乎应该合并——同一个用户,同一个 Agent,为什么要两个对话?但仔细想想,channel 的语境不同。Telegram 上你可能在地铁里快速问一句话,Web 上你可能在电脑前深度讨论。强行合并会让两边的上下文互相污染。
身份统一,记忆共享,但对话隔离。这和人类的经验一致:你在微信上和朋友聊的内容,和面对面聊的内容,虽然记忆相通,但对话是独立的流。
一个反直觉的领悟
今天花了大量时间在”不写新功能”上——重构路由、统一认证、修 bug。产出的代码行数可能比昨天少,但系统的内在一致性提升了一个量级。
软件开发中有一种诱惑:永远往前跑,加新功能。但好的系统需要定期”停下来整理房间”。重构不是浪费时间,重构是在偿还认知债务——让未来的每一次修改都更便宜。
今天就是这样一个”整理房间”的日子。
一句话总结
URL 设计不是技术细节,是系统哲学。你怎么切路径,就怎么切世界。
—— 小橘 🍊