diff --git a/deploy/openresty-all-kefu.conf.template b/deploy/openresty-all-kefu.conf.template index 8fe22c1..fd68c89 100644 --- a/deploy/openresty-all-kefu.conf.template +++ b/deploy/openresty-all-kefu.conf.template @@ -4,7 +4,8 @@ # 拓扑(spec §9):所有域名(租户域名 × N + 管理后台)反代到同一个容器 # chat.agent.hnxinyouhe.com (租户 auto) ─┐ # chat2.agent.hnxinyouhe.com (租户 jiancai) ─┼→ 127.0.0.1:8018 → all-kefu 容器 -# (形态 A,部署前由需求方确定)─┘ +# (形态 A,部署前由需求方确定)─┤ +# <共享域名>(路径模式 /t//,见下方共享域名 server 块)─┘ # # 两种用法: # · 独立 openresty/nginx:整份可抄——upstream + server 块齐备,按需删掉 @@ -32,6 +33,15 @@ upstream all_kefu { # 含:全站反代 + WS 单套升级头 + /api/public(widget 头像/品牌,放行即全站 # 反代的一部分,单独点名是防"按路径收紧"时漏掉它——漏配的症状是 widget # 头像 404)。 +# +# XFF 注记(本模板所有 `X-Forwarded-For $proxy_add_x_forwarded_for` 行同款 +# 语义,含 /ws/、admin 两形态与共享域名块——改任何一处前先读这里): +# 登录限流取 XFF **末段**(app/platform/routes/auth_routes.py _client_ip) +# ——依赖本指令的**追加**语义(客户端自带 XFF 在前、反代把真实来源 +# $remote_addr 追加在尾)。**勿改成 `X-Forwarded-For $remote_addr` 覆盖式** +# ——多 CDN 场景语义漂移;单跳拓扑下两者等价,但保持追加式。同一行的 +# 首段是引擎 IP/区域解析的消费位(取第一段),两个消费方共用这一行, +# 动它两头都伤。 server { listen 80; # 1Panel/证书层负责 443 与 HTTPS;独立部署自行补 listen 443 ssl server_name chat2.agent.hnxinyouhe.com; # ← 每个租户域名一份:chat.(auto)/ chat2.(jiancai) @@ -44,7 +54,7 @@ server { proxy_set_header Connection "upgrade"; proxy_set_header Host $host; # 纪律 1:WS 也按 Host 解析租户 proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 引擎 IP/区域解析取 XFF 第一段 + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 首段=引擎 IP/区域、末段=登录限流(注记见块首) proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 3600s; proxy_send_timeout 3600s; @@ -58,7 +68,7 @@ server { proxy_http_version 1.1; proxy_set_header Host $host; # 纪律 1 proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 追加式,勿改覆盖式(注记见块首) proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 300s; client_max_body_size 5m; # 纪律 3(头像上传) @@ -94,6 +104,55 @@ server { # } } +# ---- 共享域名 server 块(路径模式 /t//,注释示例——启用 = 取消注释) ---- +# 单域名承载任意多租户(spec 2026-10-19,部署手册 §3.6):租户标识在 URL +# 路径 /t// 中,与上方"每租户一域名"的 Host 模式共存(优先级: +# 路径前缀 > Host)。接入零 DNS:平台设置配 shared_widget_domain → 建租户 +# (域名可留空)→ 后台复制"共享域名形态"嵌入代码贴到目标网站。地址形态: +# 聊天页 https://<共享域名>/t// 挂件 .../t//static/widget.js +# 已知边界(照抄前先读): +# 1. 路径模式 WS 的真实路径是 /t//ws/chat(widget.js 按 basePath +# 拼接),不带 /t 前缀的 `location /ws/` 匹配不到它——升级头用下面 +# 的 regex location 承载(nginx regex location 优先于前缀 location /, +# 正好圈中"路径模式 + /ws/"的交集;1Panel 同样放 proxy/ws.conf)。 +# 2. 路径前缀模式要求应用以**域名根**部署——uvicorn 不得加 `--root-path` +# (root_path 会前移 raw_path,/t/ 前缀解析永不命中,全站退化为 Host +# 解析 404)。 +# 3. `/t/`(无尾斜杠)→ 404 属预期(仅带尾斜杠形态剥离前缀); +# 嵌入代码生成的就是带斜杠正确形态,勿手工"修正"。 +#server { +# listen 80; +# server_name kefu.example.com; # ← 占位:共享域名部署前由需求方确定 +# +# # WebSocket(路径模式 /t//ws/chat):升级头单套 + 长读超时 +# location ~ ^/t/[a-z0-9-]+/ws/ { +# proxy_pass http://all_kefu; +# proxy_http_version 1.1; +# proxy_set_header Upgrade $http_upgrade; +# proxy_set_header Connection "upgrade"; +# proxy_set_header Host $host; # 纪律 1:路径模式按 /t/ 解析租户, +# # 但 Host 头仍须透传(豁免前缀按它工作) +# proxy_set_header X-Real-IP $remote_addr; +# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 追加式,勿改覆盖式(注记见租户域名块) +# proxy_set_header X-Forwarded-Proto $scheme; +# proxy_read_timeout 3600s; +# proxy_send_timeout 3600s; +# } +# +# # 其余全部(/t// 聊天页、/t//static/*、/t//api/*、 +# # /health …)——同租户域名块:no-cache 由应用层下发,反代别加 expires。 +# location / { +# proxy_pass http://all_kefu; +# proxy_http_version 1.1; +# proxy_set_header Host $host; # 纪律 1 +# proxy_set_header X-Real-IP $remote_addr; +# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 追加式,勿改覆盖式(注记见租户域名块) +# proxy_set_header X-Forwarded-Proto $scheme; +# proxy_read_timeout 300s; +# client_max_body_size 5m; # 纪律 3 +# } +#} + # ---- admin 形态 A:独立管理域名 server 块(与形态 B 二选一) ------------------ # 整域反代到同一容器:/admin(SPA 静态,含 assets)与 /api/platform(后台 # API,认证由路由依赖守护)同一 upstream。全站反代即可,无需按路径拆分。 @@ -112,7 +171,7 @@ server { proxy_set_header Host $host; # 形态 A 的 Host 是管理域(未知租户)—— # /admin 与 /api/platform 豁免租户解析,照常工作 proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 追加式,勿改覆盖式(注记见租户域名块) proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 300s; client_max_body_size 5m; # 头像上传 @@ -125,7 +184,10 @@ server { # 1. server 块由面板建站生成,不要手写整个 server——只把上面的 location # 片段放进站点目录: # /opt/1panel/www/sites/<域名>/proxy/root.conf ← location /(全站反代) -# /opt/1panel/www/sites/<域名>/proxy/ws.conf ← location /ws/(升级头单套) +# /opt/1panel/www/sites/<域名>/proxy/ws.conf ← location /ws/(升级头单套; +# 共享域名站为 regex 形态 +# `~ ^/t/[a-z0-9-]+/ws/`, +# 见上方共享域名块) # /opt/1panel/www/sites/<域名>/proxy/admin.conf ← 形态 B 的两块(启用时) # 片段内 proxy_pass 直接写 http://127.0.0.1:8018——upstream 块只能声明在 # http 上下文,server 级 include 文件里放不进去(面板世界没有"全局 diff --git a/docs/deploy-production.md b/docs/deploy-production.md index 46bb5c0..01195f0 100644 --- a/docs/deploy-production.md +++ b/docs/deploy-production.md @@ -210,6 +210,49 @@ curl -s -i --http1.1 -H "Connection: Upgrade" -H "Upgrade: websocket" \ https://chat2.agent.hnxinyouhe.com/ws/chat | head -1 # 期待 HTTP/1.1 101 ``` +### 3.6 共享域名模式(单域名多租户,可选) + +> 设计出处:spec `docs/superpowers/specs/2026-10-19-shared-domain-routing-design.md`。 + +**概念**:一个共享域名(如 `kefu.hnxinyouhe.com`)承载任意多租户——租户标识 +在 URL 路径前缀 `/t/<租户slug>/` 中,与 §3.1 "每租户一域名"的 Host 模式 +**共存**,解析优先级:**路径前缀 > Host**(`/t//...` 命中即剥前缀按 +slug 解析租户;不带前缀的请求照走 Host 解析——共享域名自身不是租户域名, +根路径 404 属预期)。 + +**接入流程(零 DNS 操作)**: + +1. 平台设置(仅超管)配 `shared_widget_domain` = 共享域名(运营填一次); +2. 建租户——域名可整组留空(纯路径模式租户,创建向导支持跳过域名); +3. 配置中心 → 品牌与外观 → 嵌入代码 → 复制「共享域名形态」→ 贴到目标网站。 + +**地址形态**: + +| 项 | 形态 | +|---|---| +| 聊天页 | `https://<共享域名>/t//` | +| 挂件脚本 | `https://<共享域名>/t//static/widget.js`(脚本地址即租户标识,无需新参数) | +| `data-site` 参数 | 照常可用;缺省取租户首个站点 | + +**无尾斜杠语义**:`/t/`(无 `/`)→ **404**——仅带尾斜杠形态才剥离 +前缀(spec §3 防歧义设计,不做 308 重定向);嵌入代码生成的就是带斜杠正确 +形态,勿手工"修正"。 + +**部署注记**: + +- 共享域名 = **一条反代**(1Panel 一站),§3.2 三件事同款:透传 Host 头、 + WS 升级头单套、`client_max_body_size 5m`;WS 超时 3600s 同既有。模板 + `deploy/openresty-all-kefu.conf.template` 有现成注释示例块(`kefu.example.com` + 占位)。**WS 位置差异**:路径模式 WS 的真实路径是 `/t//ws/chat`—— + 升级头必须落在 regex location `~ ^/t/[a-z0-9-]+/ws/`(照抄模板共享域名块; + 1Panel 同样放 `proxy/ws.conf`),只配 `location /ws/` 匹配不到它(症状: + 聊天页能开、WS 连不上)。 +- **uvicorn 不得加 `--root-path`**:路径前缀解析在 raw_path 上做,root_path + 会前移 raw_path → `/t/` 前缀永不命中,全站退化为 Host 解析 404(同模板注记)。 +- **登录限流取 XFF 末段**(`app/platform/routes/auth_routes.py` `_client_ip`) + ——依赖反代 `$proxy_add_x_forwarded_for` 的**追加**语义(客户端自带在前、 + 真实来源追加在尾);勿改成 `X-Forwarded-For $remote_addr` 覆盖式(同模板注记)。 + ## 4. 存量迁移(老 jc / auto 项目 → 本平台) **前提**:老项目与本项目同机(47.254.176.10);若跨机,先把导出文件 scp 到 @@ -411,6 +454,8 @@ curl -s http://127.0.0.1:8018/health **加租户 = 后台建租户 + 1Panel 反代加一条,不起容器**: 后台"租户管理 → 创建向导"(消费出厂档案/空档案)→ 站点绑定新域名 → 1Panel 为新域名建站反代 `127.0.0.1:8018`(§3 同款三件事)→ 完成。同一容器自动承载。 +已启用共享域名(§3.6)时更省:新租户零域名、零 DNS、零反代操作——直接按 +`/t//` 形态取嵌入代码即完成接入。 ### 6.6 bootstrap 复跑