diff --git a/docs/superpowers/specs/2026-10-19-shared-domain-routing-design.md b/docs/superpowers/specs/2026-10-19-shared-domain-routing-design.md index 9d7f8a3..09b8c0d 100644 --- a/docs/superpowers/specs/2026-10-19-shared-domain-routing-design.md +++ b/docs/superpowers/specs/2026-10-19-shared-domain-routing-design.md @@ -30,17 +30,17 @@ Host 模式(不动):chat.xxx.com → registry.resolve_site(host) 优先级:**平台豁免 → 路径前缀解析 → Host 解析**。 -1. **slug 形态校验先行**:前缀段须 `^[a-z0-9-]+$`(与租户创建 `SLUG_RE` 一致,tenant_routes.py:65 同款正则常量提取共用);不匹配(含 `..`、编码畸形、大小写不符)直接走 Host 解析(共享域名下即 404),**不做任何剥离**。 -2. **路径改写**:匹配 `/t//` 或精确 `/t/`(后者无尾斜杠 → 不剥离,走 Host 解析 → 共享域名 404,语义记录在案);剥离时同步改写 `scope["path"]` 与 `scope["raw_path"]`(bytes,存在才改);http 与 websocket 两类 scope 同构处理。 +1. **slug 形态校验先行**:在**解码后的 `scope["path"]`** 上校验前缀段须 `^[a-z0-9-]+$`(与租户创建 `SLUG_RE` 一致,tenant_routes.py:65 同款正则常量提取共用);不匹配(含 `..`、大小写不符、含编码百分号的原始形态)直接走 Host 解析(共享域名下即 404),**不做任何剥离**——注意 `/t/%61uto/` 解码后虽成合法 slug,但原始形态含编码即不匹配校验(校验看 path 的原始段需由未编码字符构成:实现取 raw_path 对应段做 ASCII 直判,或等价地在 path 段上拒绝含 `%` 的输入——`%` 不在合法 slug 字符集内,解码后 slug 不可能含 `%`,故「解码后含 %」必为编码形态,拒绝)。 +2. **路径改写**:匹配 `/t//` 或精确 `/t/`(后者无尾斜杠 → 不剥离,走 Host 解析 → 共享域名 404,语义记录在案);`scope["path"]` 剥前缀;`scope["raw_path"]` **按剥离后的解码 path 重新编码重算**(不按长度截断——编码形态下解码/原始长度不一致);http 与 websocket 两类 scope 同构处理。 3. **orig_path 留痕**:原始 path 存 `scope["state"]["orig_path"]`(排障/日志对账用;uvicorn 访问日志显示剥离后路径为已知性状)。 -4. **解析与错误语义**:slug → `registry._by_slug` 查租户(缺停用判断与 resolve_site 同款:未知 slug → http 404 / ws close(1008);停用 → 503 暂停页 / ws close(1013));站点 = 租户**首个站点**为默认(`?site=` 覆盖链路既有),`state["site"]` 同样写入。 +4. **解析与错误语义**:slug 经 registry 公开方法 `resolve_by_slug`(见 §5;中间件不触私有 `_by_slug`)——未知 slug → http 404 / ws close(1008);停用 → 503 暂停页 / ws close(1013)(与 resolve_site 同款语义);站点 = 租户**首个站点**为默认(`?site=` 覆盖链路既有),`state["site"]` 同样写入。 5. **平台豁免优先级在前**:`/health`、`/api/platform/`、`/admin` 不受影响;`/t/x/api/platform/...` 剥离后命中平台路由属 by design(认证守护,与未知 Host 下 admin 面可达同级)。 6. 已知边界(记录不修):StaticFiles 目录 308 的 Location 不带前缀(挂件/页面只引用具体文件,不触发)。 ## 4. 静态面前缀自推导 -- **widget.js**:`var basePath = new URL(script.src).pathname` 剥掉尾部 `/static/widget.js...`(得 "" 或 `/t/`);brand / avatar(含 DEFAULT_AVATAR_PATH)/ WS 三类端点构造统一 `wsBase + basePath + 原路径`。Host 模式 basePath="" 零变化;`tests/test_widget_static.py` 风格静态断言钉住。 -- **chat.html**:新增 15 行内联引导脚本——按 `location.pathname` 推导 basePath(`/t//` → `/t/`;`/` → ""),改写自身 `/