Files
fire-safety-ymd/docs/project/operations/nginx-public-entry.md
T
2026-09-06 00:25:25 +08:00

255 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Nginx 公网入口(受控联调示例)
| 项 | 内容 |
| --- | --- |
| 目标 | 让 `agent.nianxx.com` 通过 HTTPS 访问本项目的可选测试页面、兼容对话接口和 MCP |
| 状态 | 示例配置;公网机器、证书、网络白名单和真实鉴权仍待联调 |
| 上游 | 仅反代本机 `127.0.0.1:16587`;容器内部仍监听 8080 |
## 1. 与旧配置的变化
旧配置把整个 `/` 转发到 DashScope,并在 Nginx 中比较 `xtoken`、注入 DashScope API Key。新边界是:
```text
公网 HTTPS
-> Nginx(TLS、精确路径、限流、SSE 传输设置)
-> 127.0.0.1:16587(Docker 发布端口)
-> 容器内 :8080(fire-safety-ymd)
-> Go 侧校验 xtoken / MCP Bearer
```
Nginx 不再调用 DashScope,也不保存或注入 SuperAgent Open API Key、Chat `xtoken`、MCP Bearer 或数据库凭证。Go 服务负责把用户兼容请求转发到 SuperAgent,并负责 `/mcp` 的独立 Bearer 校验。这样可避免把 Secret 写入 Nginx 配置,也避免使用 Nginx `if` 比较 Secret。
## 2. 可复制的配置示例
配置文件位于 [`../../../deploy/nginx/fire-safety-ymd.conf.example`](../../../deploy/nginx/fire-safety-ymd.conf.example)。它只公开以下路径:
| 路径 | 用途 | 鉴权与限制 |
| --- | --- | --- |
| `/api/v1/apps/<safe-app-id>/completion` | 截图所示的 DashScope-compatible 用户对话 SSE | Go 校验 `xtoken`;请求体 128 KiB;示例限流 5 req/s、burst 20 |
| `/chat`、`/chat/` | 可选的受控测试对话页面 | `/chat` 由 Go 规范化重定向到 `/chat/`(308);页面 GET 不要求 Token;Go 由 `FIRE_SAFETY_CHAT_PAGE_ENABLED` 决定返回 HTML 或 404;示例限流 5 req/s、burst 20 |
| `/chat/app.css`、`/chat/app.js` | 测试页面静态资源 | 页面开关关闭时由 Go 返回 404;开启时返回对应资源;示例限流 5 req/s、burst 20 |
| `/mcp` | SuperAgent 调用本项目的 MCP | Go 校验独立 `Authorization: Bearer`;请求体 256 KiB;示例限流 20 req/s、burst 40 |
| `/health` | 可选进程存活检查 | 不访问数据库;如不希望公开可删除该 location |
| 其他路径 | 不对外提供 | Nginx 固定返回 404 |
将示例放入 Nginx 的 `http` 配置范围(例如 `/etc/nginx/conf.d/fire-safety-ymd.conf`)前,必须完成以下替换:
1. 把 `location = /api/v1/apps/replace-with-fire-safety-app-id/completion` 中的占位符替换为安全的 app ID。
2. 该 app ID 必须与 Go 服务的 `FIRE_SAFETY_CHAT_COMPAT_APP_ID` 完全一致;两处不一致会导致请求被拒绝。
3. 确认证书路径 `/cert/agent.nianxx.com.pem` 和 `/cert/agent.nianxx.com.key` 在公网机器上存在,并由 Nginx 进程可读。
4. 直接在宿主机运行 Go 时,应选择未占用的回环端口并同步修改 Nginx upstream;使用本仓库 Compose 时,容器内监听 `:8080`,端口映射保持为 `127.0.0.1:16587:8080`。不要把 16587、容器 8080 或 PostgreSQL 5432 暴露到公网。
5. 按环境配置 DNS、云防火墙/安全组和 SuperAgent 对 `/mcp` 的来源 IP/TLS 要求;这些部署事实尚未由本项目验证。
示例中的 `location = /chat`、`location = /chat/`、`location = /chat/app.css` 和
`location = /chat/app.js` 是页面及其资源的精确入口,均反代到同一个 `127.0.0.1:16587`
上游。Nginx 不判断页面开关,也不提供备用 HTML、CSS 或 JavaScript:
- `FIRE_SAFETY_CHAT_PAGE_ENABLED=true` 且依赖完整时,Go 返回页面 HTML;
- 页面请求 `/chat` 时,Go 返回 308 并规范化到 `/chat/`;随后 `/chat/` 返回页面 HTML;
- 页面请求 CSS/JavaScript 资源时,Go 返回资源内容;
- 开关为 `false` 或页面依赖不满足时,Go 对页面和资源路径返回 404;
- 不要把这些 location 改成旧配置的 `location /`,也不要在 Nginx 中注入 xtoken。
页面默认关闭。测试开启时,Go 进程配置的 `FIRE_SAFETY_CHAT_ALLOWED_ORIGINS` 必须包含精确
Origin `https://agent.nianxx.com`;该值是浏览器从页面同源发出 completion 请求时的来源。不要
用 `*`,也不要把 Token 写入 Nginx。
`limit_req_zone` 必须位于 Nginx `http` context,不能放进 `server` 或 `location`。示例文件假定它被 `conf.d/*.conf` 从 `http {}` 中 include;如果部署系统不是这样 include,应把两条 `limit_req_zone` 指令单独移到 `http {}`,并保留 `server`/`upstream` 在合法上下文。
## 3. Go 服务配置
在服务器的未提交 Secret 管理方式(例如权限收紧的 `.env`、systemd EnvironmentFile 或容器 Secret)中配置,不要写入 Nginx 文件或 Git:
```text
# Compose 会覆盖为容器内的 :8080,并发布到宿主机回环地址 16587。
# 若直接在宿主机运行,则使用 127.0.0.1:16587 并保持 Nginx upstream 一致。
FIRE_SAFETY_HTTP_ADDR=127.0.0.1:16587
FIRE_SAFETY_SUPERAGENT_ENABLED=true
FIRE_SAFETY_SUPERAGENT_BASE_URL=https://<superagent-domain>
FIRE_SAFETY_SUPERAGENT_OPEN_API_KEY=<superagent-open-api-key>
FIRE_SAFETY_CHAT_ENABLED=true
FIRE_SAFETY_CHAT_AUTH_TOKEN=<chat-xtoken>
# Keep false for new environments. Only set true in a controlled migration
# when an already-issued legacy Chat credential is shorter than 32 characters.
FIRE_SAFETY_CHAT_ALLOW_LEGACY_SHORT_TOKEN=false
FIRE_SAFETY_CHAT_COMPAT_APP_ID=<same-safe-app-id-as-nginx-location>
# Optional public test page; keep false unless this test surface is needed.
FIRE_SAFETY_CHAT_PAGE_ENABLED=false
# If the page is enabled, this exact same-origin value is required.
FIRE_SAFETY_CHAT_ALLOWED_ORIGINS=https://agent.nianxx.com
FIRE_SAFETY_MCP_ENABLED=true
FIRE_SAFETY_MCP_AUTH_TOKEN=<different-mcp-bearer-at-least-32-printable-ascii-characters>
FIRE_SAFETY_POSTGIS_ENABLED=true
FIRE_SAFETY_POSTGIS_DSN=<readonly-postgresql-dsn>
```
当前仍是受控联调静态门禁:
- `FIRE_SAFETY_CHAT_AUTH_TOKEN` 由用户请求的 `xtoken` Header 携带,必须与 SuperAgent Open API Key、MCP Token 使用不同值。
- 默认要求 Chat Token 至少 32 个可打印 ASCII 字符;若已交付的旧客户端短凭证无法立即更换,只能在 Go 服务环境中显式设置 `FIRE_SAFETY_CHAT_ALLOW_LEGACY_SHORT_TOKEN=true` 做受控测试/迁移。无论开关如何,Token 都必须非空、不超过 4096 字节,并且只含 ASCII `0x21-0x7e`(无空格、控制字符或 Unicode);轮换完成后恢复 `false`,新环境不得开启。该开关同时适用于原生 Bearer 和兼容 `xtoken`,不放宽 MCP Token 的至少 32 字符门禁。
- `FIRE_SAFETY_MCP_AUTH_TOKEN` 只用于 SuperAgent -> Go `/mcp`,不应复用 Chat Token。
- Chat、SuperAgent、MCP 和 PostGIS 的完整配置校验以 `.env.example` 和对应项目文档为准。
- 浏览器会看到 `xtoken`;它不能代表最终用户身份、角色、租户或数据授权。公网真实用户入口仍需身份提供方、动态授权、限流、Secret 轮换和持久审计。
- `FIRE_SAFETY_CHAT_PAGE_ENABLED` 默认必须为 `false`。开启页面只适用于受控测试;页面 GET 本身
不需要 Token,用户在页面输入的 `xtoken` 只在当前页面内存中使用,不写入 HTML、Cookie、URL、
localStorage 或 sessionStorage。
- 页面与兼容接口同源时,`FIRE_SAFETY_CHAT_ALLOWED_ORIGINS` 必须包含精确的
`https://agent.nianxx.com`。这不是把页面变成生产认证;Token 仍为静态测试凭证,必须由用户
手动输入并按测试范围管理。
- Nginx 不读取、比较或存储 Chat Token,也不需要为 legacy 开关增加配置;请求 Header 原样转发给 Go 校验。开关启用时 Go 仅记录不含 Secret 的安全 warning,便于迁移完成后清理。
测试服务器使用 Docker Compose 的完整目录、启动、更新和回滚步骤见 [`docker-test-deployment.md`](docker-test-deployment.md)。
## 4. SSE 代理边界
兼容对话响应是长连接 SSE。示例对该精确路径设置:
- `proxy_http_version 1.1`、清空 `Connection`,保持上游流式连接。
- `proxy_buffering off`、`proxy_cache off`、`proxy_request_buffering off` 和 `gzip off`,并返回 `X-Accel-Buffering: no`。
- `proxy_read_timeout`/`proxy_send_timeout` 为 660 秒,覆盖 Go 默认 10 分钟总运行时限并留出少量余量;如提高 Go 的运行时限,必须同步提高 Nginx 超时。
- `proxy_next_upstream off`,避免 POST 流中断后 Nginx 自动重试而产生重复 SuperAgent Run。
- 限流拒绝使用 HTTP 429 和稳定 JSON;客户端仍需把网关层非 SSE 响应视为失败,不能等待 `finish_reason=stop`。
- 透传 `Host`、`X-Forwarded-For`、`X-Forwarded-Proto`、`X-Forwarded-Host`、`X-Forwarded-Port`、`X-Forwarded-Server` 和 `X-Request-ID`。
- 不设置 `proxy_set_header xtoken ...` 或 `proxy_set_header Authorization ...`;客户端送来的 `xtoken` 原样到 Go,由 Go 做常量时间比较,MCP Bearer 同理。
MCP 是普通 JSON 请求,不需要 SSE 的关闭响应缓冲设置;示例使用 30 秒读写超时,仍关闭上游自动重试。
## 5. 启用与检查
启动 Go 服务前,在服务器的进程环境中加载 Secret。不要把 Secret 直接写进命令行或 shell 历史;可以在受保护的环境文件中加载后,再通过 Header 环境变量展开:
直接在宿主机运行时可使用 `go run ./cmd/server`;测试服务器的默认方式是由 Compose 启动镜像,不在宿主机安装或运行 Go 工具链。
在 reload 前先检查 Nginx 配置;示例机器没有安装 Nginx 时,本地无法代替目标服务器完成这一检查:
```bash
sudo nginx -t
sudo nginx -s reload
```
### 首轮兼容对话 SSE
以下命令不会把 Token 字面量写入命令历史;`CHAT_XTOKEN` 应由受保护的环境文件或 Secret 管理器注入当前 shell:
```bash
curl -N --fail \
-H "xtoken: ${CHAT_XTOKEN}" \
-H 'Content-Type: application/json' \
-H 'Accept: text/event-stream' \
--data '{"input":{"prompt":"观水镇附近有哪些水源候选?"},"parameters":{}}' \
'https://agent.nianxx.com/api/v1/apps/<same-safe-app-id>/completion'
```
客户端应读取 `event: result` 的 SSE 数据,等待 `output.finish_reason` 为 `stop` 后再把最终 `output.text` 视为完成;中途断流不能当作可信答案。首轮返回的 `output.session_id` 是本项目的本地会话 ID,后续是否能继续多轮取决于兼容接口的契约,不能自行把它当作 SuperAgent Provider Session。
### 多轮兼容对话 SSE
使用上一轮服务返回并确认成功的会话 ID;不要把 SuperAgent 的内部 Session ID 放入客户端请求:
```bash
curl -N --fail \
-H "xtoken: ${CHAT_XTOKEN}" \
-H 'Content-Type: application/json' \
-H 'Accept: text/event-stream' \
--data '{"input":{"prompt":"再说明这些候选的限制","session_id":"<session-id-from-previous-response>"},"parameters":{}}' \
'https://agent.nianxx.com/api/v1/apps/<same-safe-app-id>/completion'
```
如果目标客户端不接受 `text/event-stream` 或只需要一次性 JSON,应先以接口 Spec 为准;不要让 Nginx 擅自将 SSE 缓冲成普通 JSON。
### 受控测试页面
页面由 Go 在开关开启时提供,Nginx 只做四个精确路径的反代(页面、规范化入口和两个资源):
```bash
curl -i https://agent.nianxx.com/chat
curl -i https://agent.nianxx.com/chat/
```
预期行为:
- `FIRE_SAFETY_CHAT_PAGE_ENABLED=false`:`/chat` 和 `/chat/` 均直接返回 HTTP 404;
- `FIRE_SAFETY_CHAT_PAGE_ENABLED=true` 且 Chat 兼容配置完整:`/chat` 返回 HTTP 308,
`/chat/` 返回 HTTP 200,`Content-Type` 为 `text/html`;
- 开启时页面加载的 `/chat/app.css` 和 `/chat/app.js` 也应返回 HTTP 200;关闭时均为 404。
需要跟随 `/chat` 的规范化重定向时使用:
```bash
curl -iL https://agent.nianxx.com/chat
```
浏览器打开 `https://agent.nianxx.com/chat/` 后,手动输入受控测试 `xtoken` 和不含敏感信息的
问题。页面使用 `fetch` 向同源的
`/api/v1/apps/<same-safe-app-id>/completion` 发起 POST,Header 为 `xtoken`、
`Content-Type: application/json`、`Accept: text/event-stream`,请求体仍是:
```json
{"input":{"prompt":"观水镇附近有哪些地点候选?"},"parameters":{}}
```
页面只在内存中保存最终成功的 `output.session_id`,下一轮把它放入 `input.session_id`;刷新或
关闭页面后不恢复。只有 `finish_reason=stop` 才显示为成功,初始 `finish_reason=null`、HTTP
错误、`event: error` 或断流不能当作完整答案。页面不会把 xtoken 写入 HTML、浏览器存储、Cookie、
URL、Nginx 或日志。
浏览器 Network 面板应能确认:请求为同源 POST、Origin 为 `https://agent.nianxx.com`、使用正确
的公开 App ID、没有跨域失败,并且第二次请求包含上一轮返回的 `session_id`。如果出现 403,优先
检查 `FIRE_SAFETY_CHAT_ALLOWED_ORIGINS` 是否包含精确 Origin;如果出现 404,检查页面开关和
容器是否已 recreate;如果出现 401,重新输入正确的测试 xtoken。
### MCP 探活/初始化
MCP 请求需要独立 Token,具体 JSON-RPC body 以 MCP Spec 为准:
```bash
curl --fail \
-H "Authorization: Bearer ${MCP_BEARER}" \
-H 'Content-Type: application/json' \
--data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"manual-probe","version":"1"}}}' \
'https://agent.nianxx.com/mcp'
curl --fail --output /dev/null --write-out 'initialized HTTP %{http_code}\n' \
-H "Authorization: Bearer ${MCP_BEARER}" \
-H 'Content-Type: application/json' \
--data '{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'https://agent.nianxx.com/mcp'
curl --fail \
-H "Authorization: Bearer ${MCP_BEARER}" \
-H 'Content-Type: application/json' \
--data '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
'https://agent.nianxx.com/mcp'
```
预期 `initialize` 返回协议版本 `2025-06-18`,initialized notification 返回 HTTP 202,`tools/list` 返回 7 个固定工具。只收到 HTTP 200 但 JSON-RPC 中含 `error` 也属于失败,不能把它当作 MCP 已就绪。
## 6. 验收边界与回滚
完成 `nginx -t` 和 reload 后,应至少验证:
1. `/health` 返回 200(如果保留可选 location)。
2. 页面开关关闭时 `/chat` 和 `/chat/` 均直接返回 404;开启且依赖完整时 `/chat` 返回 308、`/chat/` 返回 200 HTML,且两个资源路径返回 200。
3. 兼容对话首轮在正确 `xtoken` 下返回 SSE,末尾出现 `finish_reason: stop`;错误 Token 返回 401,且 Go 不会创建 SuperAgent Run。
4. 兼容对话多轮使用返回的本地会话 ID;服务重启或会话失效后按契约返回会话不存在,不把 ID 当成 Provider Session。
5. `/mcp` 使用独立 Bearer,错误或缺失 Bearer 返回 401;Nginx 没有注入任何 MCP Token。
6. 未列出的路径(例如 `/api/chat`、`/api/v1/apps/other/completion` 和 `/anything`)返回 404。
如果 reload 后发现兼容路由、证书或 SSE 行为异常,先恢复上一个已验证的 Nginx 配置并保留 `nginx -t` 输出;不要通过放开 `location /`、关闭 Go 鉴权或把 Secret 写入 Nginx 来排障。
如只需紧急关闭测试页面,不必改动 Nginx:将 Go 环境中的
`FIRE_SAFETY_CHAT_PAGE_ENABLED` 改为 `false`,重新创建容器后确认 `/chat`、`/chat/` 和资源
路径均直接返回 404。这样不会自动关闭兼容 completion;如果也要关闭对话 API,再按 Chat 配置和对应回滚流程
处理。页面开关和 Nginx 路由均应保留清晰的变更记录。
## 7. 未确认事项
- 目标公网机器的 Nginx 版本、include 层级、TLS 终止位置和证书续期方式;示例同时监听 80 做 HTTPS 跳转,安全组需按实际策略决定是否允许 80。
- `agent.nianxx.com` 的 DNS、安全组、反向代理来源 IP 和 SuperAgent 对 MCP 回调的网络策略。
- 兼容接口在目标 SuperAgent/Profile 中对 `input.session_id`、`parameters` 和 SSE `result` 事件的最终协议;实现以本项目 Spec 和实际联调为准。
- 生产最终用户认证、动态授权、会话共享/持久化、主动取消、配额、审计和 Secret 轮换。