初始化第一版

This commit is contained in:
andy committed 2026-09-05 15:46:37 +08:00
commit 8a6c31c14d
83 files changed
+14302

No files matched your search

+164
View File
@@ -0,0 +1,164 @@
# 用户对话 API v1 Spec
| 项 | 内容 |
| --- | --- |
| 状态 | Implemented |
| 日期 | 2026-09-05 |
| 负责人 | fire-safety-ymd 后端 |
| 需求来源 | 用户要求“先做对话 API” |
| 关联 Change Request | 无 |
## 1. 背景
项目已经具备 SuperAgent Open API 出站 Adapter 和空间只读 MCP,但用户侧应用还没有安全的服务端对话入口。前端不能直接持有 SuperAgent Open API Key,也不能自行指定 SuperAgent Session、用户身份或授权范围。
本 checkpoint 提供一个最小可联调的对话 API。它由 Go 服务创建并复用 SuperAgent Session,通过 SSE 返回安全进度和最终回答。真实用户认证、动态区域授权、数据库会话持久化和生产审计后续单独设计。
## 2. 目标
- 新增默认关闭的 `POST /api/chat`。
- 使用独立静态 Bearer Token 保护首版测试入口,不复用 SuperAgent Key 或 MCP Token。
- 首轮创建随机本地 `conversation_id` 和 SuperAgent Session;后续轮次复用映射。
- 同一 `conversation_id` 同时只允许一个活动 Run,冲突时返回 `409`。
- 通过 SSE 返回对话 ID、安全进度、严格完成后的最终回答和完成事件。
- 不把消息正文、回答正文、Secret、原始工具参数或输出写入日志。
- 对输入大小、总运行时间、内存会话数量、空闲过期时间和浏览器 Origin 设上限。
## 3. 非目标
- 不实现注册、登录、JWT、SSO、角色、租户或最终用户级数据授权。
- 不接受客户端提交的 `user_id`、`external_subject_id`、SuperAgent `session_id`、角色或区域范围。
- 不持久化对话;进程重启或多实例切换后,旧 `conversation_id` 不可继续使用。
- 不返回逐字 token、模型原始思考、原始 Trace、工具输入或工具输出。
- 不实现历史消息查询、会话列表、主动取消、重试队列、限流或 WebSocket。
- 不把 Agent 回答写入消防业务事实。
## 4. HTTP 契约
### 4.1 请求
```http
POST /api/chat
Authorization: Bearer <FIRE_SAFETY_CHAT_AUTH_TOKEN>
Content-Type: application/json
Accept: text/event-stream
{
"message": "观水镇附近有哪些可用水源?",
"conversation_id": "conv_..."
}
```
- `message` 必填,去除首尾空白后不能为空,并服从 SuperAgent 单条消息字节上限。
- `conversation_id` 首轮省略;后续使用服务返回的随机值。
- 未知字段、多个 JSON 值、非法媒体类型和超大请求体均拒绝。
- 客户端不得提交身份、权限、Provider Session 或任意 metadata。
### 4.2 成功响应
响应媒体类型为 `text/event-stream`。事件顺序如下:
```text
event: conversation
data: {"conversation_id":"conv_...","reused":false}
event: progress
data: {"event":"tool.started","tool_name":"fire_safety_search_place_candidates","status":"running"}
event: message
data: {"conversation_id":"conv_...","answer":"..."}
event: done
data: {"conversation_id":"conv_...","run_id":"...","usage":{"input":0,"output":0,"total":0}}
```
- `conversation` 在 Provider Session 准备完成后首先发送。
- `progress` 只包含经过现有 Adapter 清洗的事件名、工具名和状态;不包含文本、ID、参数或工具结果。
- 没有业务事件时,服务每 15 秒发送一个 SSE comment heartbeat,保持长连接且不携带业务数据。
- `message` 只在 Adapter 同时确认最终内容、成功 `run.completed` 和顶层 `end` 后发送。
- `done` 是成功终止事件。
### 4.3 错误响应
在 SSE 开始前,错误使用 HTTP 状态码和稳定 JSON 错误,例如:
```json
{
"error": {
"code": "CHAT_AUTH_INVALID",
"message": "Chat authentication failed."
},
"request_id": "..."
}
```
SSE 开始后的 Provider 失败使用终止 `error` 事件,不返回部分回答。主要错误类别:
- `CHAT_REQUEST_INVALID`:输入或 JSON 不合法,HTTP 400。
- `CHAT_AUTH_INVALID`:Bearer 无效,HTTP 401。
- `CHAT_ORIGIN_FORBIDDEN`:浏览器 Origin 未获准,HTTP 403。
- `CHAT_CONVERSATION_NOT_FOUND`:会话不存在、已过期或服务已重启,HTTP 404。
- `CHAT_CONVERSATION_BUSY`:同一会话已有活动 Run,HTTP 409。
- `CHAT_CAPACITY_REACHED`:内存会话达到上限,HTTP 503。
- `CHAT_UPSTREAM_TIMEOUT`、`CHAT_UPSTREAM_UNAVAILABLE`、`CHAT_UPSTREAM_PROTOCOL_ERROR`、`CHAT_RUN_FAILED`:上游运行失败;SSE 尚未开始时使用 502/504,否则发送 `error` 事件。
## 5. 会话与并发规则
- 映射仅保存在当前 Go 进程内:`conversation_id -> SuperAgent session_id`。
- `conversation_id` 由加密安全随机数生成,不包含用户、镇街或业务语义。
- 新会话使用服务端固定测试主体 `FIRE_SAFETY_CHAT_SUBJECT_ID` 创建;客户端不能覆盖。
- 每次发送消息使用新的幂等键和请求关联 ID。
- 同一会话的 Run 使用互斥占用;不同会话可并发。
- 空闲会话超过 TTL 后惰性清理;活动 Run 不清理。
- 达到最大会话数后先清理过期会话,仍满则拒绝创建。
固定测试主体只是首版联调边界,不代表真实用户认证,也不能用于用户级授权或审计。
## 6. 配置契约
| 环境变量 | 默认值 | 说明 |
| --- | --- | --- |
| `FIRE_SAFETY_CHAT_ENABLED` | `false` | 对话 API 总开关;启用时要求 SuperAgent 同时启用 |
| `FIRE_SAFETY_CHAT_AUTH_TOKEN` | 空 | 独立高熵静态 Bearer,至少 32 个可打印 ASCII 字符 |
| `FIRE_SAFETY_CHAT_SUBJECT_ID` | `fire-safety-ymd-chat-test-subject` | 服务端固定测试主体,不使用真实用户标识 |
| `FIRE_SAFETY_CHAT_ALLOWED_ORIGINS` | 空 | 逗号分隔的精确 HTTP(S) Origin;空值只允许无 Origin 的服务端/CLI 调用 |
| `FIRE_SAFETY_CHAT_MAX_BODY_BYTES` | `131072` | HTTP JSON 请求体上限,最大 1 MiB |
| `FIRE_SAFETY_CHAT_RUN_TIMEOUT` | `10m` | 创建 Session 加单轮 Run 的总时限,最大 30 分钟 |
| `FIRE_SAFETY_CHAT_SESSION_TTL` | `30m` | 空闲内存会话保留时间,最大 24 小时 |
| `FIRE_SAFETY_CHAT_MAX_SESSIONS` | `1000` | 单进程最大内存会话数,最大 10000 |
`FIRE_SAFETY_CHAT_AUTH_TOKEN` 必须分别不同于 `FIRE_SAFETY_SUPERAGENT_OPEN_API_KEY` 和 `FIRE_SAFETY_MCP_AUTH_TOKEN`。
## 7. CORS 与鉴权边界
- 无 `Origin` 的 CLI/服务端请求可以进入 Bearer 校验。
- 带 `Origin` 的浏览器请求必须精确匹配配置列表;不支持 `*`。
- 预检只允许 `POST` 以及 `Authorization`、`Content-Type` Header。
- 不启用 Cookie 身份或 CORS credentials。
- 静态 Bearer 只适合首版受控联调;公网最终用户入口必须接入真实身份认证、速率限制和动态授权。
## 8. 日志与数据边界
- 只记录 request ID、结果类别、是否复用会话和耗时。
- 不记录 Bearer、Open API Key、消息、回答、Provider payload、坐标、工具参数或工具输出。
- 对外错误不包含上游响应正文、URL、Session ID、堆栈或 Secret。
- SuperAgent 回答属于辅助内容,不自动升级为权威事实,也不替代报警、撤离和现场指挥。
## 9. 验收标准
- Given 对话 API 未启用,When 请求 `/api/chat`,Then 返回 404 且不创建 SuperAgent Client 调用。
- Given 未授权或 Origin 不在白名单,When 请求 API,Then 在读取和转发消息前拒绝。
- Given 首轮合法消息,When Provider 严格成功,Then 返回新 `conversation_id`、最终回答和 `done`。
- Given 后续合法消息,When 传入同一 `conversation_id`,Then 复用原 SuperAgent Session。
- Given 同一会话已有活动 Run,When 再次发送,Then 返回 `409` 且不发第二个上游请求。
- Given Provider 流不完整或 Run 失败,When 处理结束,Then 不发送 `message` 或 `done`,只返回安全错误。
- Given 服务重启、会话过期或随机 ID 不存在,When 继续对话,Then 返回 `404` 而不把客户端值当作 Provider Session。
- Given 无真实网络和 Secret,When 执行自动化测试,Then 使用本地 fake/模拟 Provider 并全部通过。
## 10. Definition of Done
- 配置、Service、SuperAgent 适配、HTTP Handler 和应用装配满足本文契约。
- 覆盖鉴权、CORS、输入、SSE、复用、并发、过期、容量、上游错误和敏感信息边界测试。
- `gofmt`、`go test ./...`、`go test -race ./...` 和 `go vet ./...` 通过。
- `.env.example`、集成指南、安全边界、项目索引和 `PROJECT_STATE.md` 同步。
- 不提交真实 Secret,不自动创建 Git 提交。
@@ -0,0 +1,143 @@
# DashScope 风格兼容对话 API v1 Spec
| 项 | 内容 |
| --- | --- |
| 状态 | Implemented |
| 日期 | 2026-09-05 |
| 负责人 | fire-safety-ymd 后端 |
| 需求来源 | 复用既有客户端的 `/api/v1/apps/{app_id}/completion`、`xtoken` 与 `event: result` 契约 |
## 1. 背景
既有用户端按照 DashScope Application HTTP 形式调用固定应用路径,并以 SSE `result` 事件读取 `output.session_id`、`finish_reason` 和 `text`。本项目原生 `/api/chat` 的请求体与事件名称不同。为避免客户端一次性重写,本 checkpoint 在同一 Chat Service 之上增加一个受限兼容适配层,并保留原生接口。
本契约参考阿里云官方 Application API 的路径、`input.prompt`、`input.session_id` 和 SSE 结果外形,但不是 DashScope 全量代理或完整复刻。
## 2. 目标
- 新增可选的 `POST /api/v1/apps/{app_id}/completion`。
- 接收既有客户端的 `xtoken`、`input.prompt`、可选 `input.session_id` 和 `parameters` 空对象。
- 返回 `event: result` SSE;首个事件提供会话 ID,严格成功事件以 `finish_reason: "stop"` 终止。
- 复用 `/api/chat` 的会话容量、TTL、同会话并发排斥、SuperAgent 严格完成判定和错误分类。
- Nginx 只终止 TLS、限制路径、限流并反代本 Go 服务,不保存或注入任何应用 Secret。
## 3. 非目标与兼容边界
- 不移除或改变原生 `POST /api/chat`。
- 不实现 DashScope 的 `messages`、`biz_params`、文件、图像、知识库、思考过程或任意 `parameters`。
- 不代理到 DashScope,也不把兼容 `app_id` 当成真正的 SuperAgent Profile ID。
- 不向客户端暴露 Provider Session、Run、Trace、工具参数/结果或半截回答。
- 不提供非流式 JSON 模式;即使请求未带 `X-DashScope-SSE: enable`,本兼容路径也始终返回 SSE,以匹配现有客户端。
- `parameters.incremental_output` 仅为输入兼容而接受。v1 只有严格完成后的一个正文结果,因此 `true` 与 `false` 不改变正文分片方式。
- 静态 `xtoken` 只用于受控联调,不是最终用户身份或动态授权。
## 4. 配置与路由
| 环境变量 | 默认值 | 说明 |
| --- | --- | --- |
| `FIRE_SAFETY_CHAT_COMPAT_APP_ID` | 空 | 非空时注册兼容路径;1 至 128 个 ASCII 字母、数字、下划线或连字符 |
| `FIRE_SAFETY_CHAT_AUTH_TOKEN` | 空 | 兼容路径期望的 `xtoken`,仍必须与 SuperAgent Key、MCP Token 不同 |
兼容路径只有在 `FIRE_SAFETY_CHAT_ENABLED=true` 且 App ID 非空时注册。App ID 是用于路径匹配的公开标识,不是 Secret;不匹配的路径返回 404。
## 5. 请求契约
```http
POST /api/v1/apps/fire-safety-public-app/completion
xtoken: <FIRE_SAFETY_CHAT_AUTH_TOKEN>
Content-Type: application/json
Accept: text/event-stream
{
"input": {
"prompt": "杨家盘瞭望哨 3 公里内的水源?"
},
"parameters": {}
}
```
后续轮次把前一轮成功响应的 `output.session_id` 放回 `input`:
```json
{
"input": {
"prompt": "再说明这些候选的限制",
"session_id": "conv_..."
},
"parameters": {
"incremental_output": true
},
"debug": {}
}
```
约束:
- `input.prompt` 必填,非空,并服从现有消息大小上限。
- `input.session_id` 只能是本服务此前返回的本地会话 ID;服务端仍负责映射 Provider Session。
- `parameters` 可省略、为空对象,或只包含布尔型 `incremental_output`。
- `debug` 可省略或只能是空对象。
- 未知字段、`null`、错误类型、尾随 JSON 和超大请求体均拒绝。
- 兼容 DTO 不接收用户身份、角色、区域、租户、Provider Session 或 metadata。
## 6. 成功 SSE
Prepare 成功后先发送会话事件。`finish_reason` 的初始值按官方示例使用字符串 `"null"`,不是 JSON `null`:
```text
id: 1
event: result
:HTTP_STATUS/200
data: {"output":{"session_id":"conv_...","finish_reason":"null"},"usage":{},"request_id":"..."}
```
只有 SuperAgent Adapter 同时验证最终内容、成功 `run.completed` 和顶层 `end` 后,才发送终止事件:
```text
id: 2
event: result
:HTTP_STATUS/200
data: {"output":{"session_id":"conv_...","finish_reason":"stop","text":"..."},"usage":{"models":[{"input_tokens":12,"output_tokens":34,"model_id":"qwen-plus-latest"}]},"request_id":"..."}
```
- 两个事件的 `session_id` 和 `request_id` 必须相同。
- `session_id` 是本地随机会话 ID,不是 Provider Session。
- `model_id` 在上游未提供有效模型名时省略;token 数量是当前 Provider 返回的单轮聚合值。
- 等待期间每 15 秒可发送不含业务数据的 SSE comment heartbeat。
- 客户端只有看到 `event: result` 且 `output.finish_reason == "stop"` 才能把本轮视为成功。
## 7. 错误
SSE 开始前使用 HTTP 状态和 DashScope 风格安全 JSON:
```json
{"code":"CHAT_AUTH_INVALID","message":"Chat authentication failed.","request_id":"..."}
```
SSE 开始后的失败发送 `event: error`,只包含稳定 code、通用 message、request ID 和本地 session ID;不发送 `finish_reason: "stop"`,也不返回任何已接收的部分回答。
沿用原生 Chat 的主要状态:400 输入错误、401 token 错误、403 Origin 错误、404 App/会话不存在、409 会话忙、503 容量不足,以及 502/504 上游失败或超时。
## 8. CORS、Nginx 与 Secret
- 浏览器 Origin 必须精确出现在 `FIRE_SAFETY_CHAT_ALLOWED_ORIGINS`;不允许 `*` 或 credentials。
- 预检只允许 `POST` 以及 `Content-Type`、`xtoken`、`X-DashScope-SSE`、`X-Request-ID`。
- Nginx 示例只公开精确兼容路径、`/mcp` 和可选 `/health`,其余路径返回 404。
- Nginx 不比较或注入 `xtoken`、SuperAgent Open API Key、MCP Bearer 或数据库凭证;Header 原样交给 Go 验证。
- 对话 SSE 必须关闭代理缓冲、缓存、gzip 和上游自动重试,并让代理超时覆盖 Chat 总运行时限。
## 9. 验收标准
- Given App ID 未配置,When 请求兼容路径,Then 路由返回 404,原生 `/api/chat` 行为不变。
- Given App ID 或 `xtoken` 错误,When 请求,Then 在读取问题和调用 SuperAgent 前拒绝。
- Given 首轮请求成功,When 读取 SSE,Then 先收到字符串 `"null"` 会话事件,再收到同会话 `"stop"` 最终正文。
- Given 后续请求携带成功返回的 `session_id`,When 调用,Then 复用同一服务端会话映射。
- Given Provider 流失败,When SSE 已开始,Then 收到安全 `error`,不收到部分正文或 `stop`。
- Given Nginx 配置生效,When 请求未列出的路径,Then 不会转发到 Go 或外部 DashScope。
## 10. 协议来源
- [Alibaba Cloud Model Studio Application API reference](https://www.alibabacloud.com/help/en/model-studio/application-api-reference)
- [Call a Model Studio application by using an API](https://www.alibabacloud.com/help/en/model-studio/application-calling-guide)
外部文档会演进;本项目以本 Spec 和自动化测试定义的受限兼容子集为准。
@@ -0,0 +1,210 @@
# 森林防火空间只读 MCP v1 Spec
| 项 | 内容 |
| --- | --- |
| 状态 | Implemented;live readiness 与本地 7 tools/call 已通过,SuperAgent 回调待联调 |
| 日期 | 2026-09-05 |
| Checkpoint | `fire-safety-ymd-superagent-mcp-spatial-readonly-v1` |
| 需求来源 | 用户提供 8 张 PostgreSQL/PostGIS 表结构与每表 2 条样例,要求先基于现有数据建设 MCP |
## 1. 背景与已核对输入
`docs/import/db-samples/` 中包含以下表的结构和样例:
- `st_2_mpslfh_t_slfh_syd`:水源地,点。
- `st_2_xianyouxushuichiguan`:蓄水池,点。
- `st_2_xianyoufanghuotongdao`:防火通道,线。
- `st_2_fanghuojianchazhan`:防火检查站,点。
- `st_2_fanghuoliaowangshao`:防火瞭望哨,点。
- `st_2_fanghuowangge`:防火网格,面。
- `st_2_linqugongkuangqiye`:林区工矿企业,面。
- `st_2_mudifenqu_mian`:墓地坟区,面。
SQL 文件仅作为结构和数据契约参考。文件包含 `DROP TABLE` 等导出语句,本项目不会执行这些文件。
当前样例足以确定首版字段映射,但不能证明全库的数据完整性、坐标系、几何类型一致性、索引、时效或资源当前可用性。尤其是:
- 所有 `geom` 列均声明为宽泛的 `geometry(GEOMETRY)`,没有 typmod SRID。
- 现场全量导入进一步确认:`st_2_xianyoufanghuotongdao.geom` 的源记录混合二维与 Z 维度,原 `geometry(GEOMETRY)` 二维 typmod 会拒绝 Z;用户改用裸 `geometry` 保留原始维度后报告重导成功。该导入反馈仍需只读 probe 核验最终数量和分布。
- 样例几何十六进制是未携带 SRID 的 WKB;坐标数值及水源表独立经纬度字段看起来符合 WGS84,但这只能作为待验证线索。
- 样例 DDL 只有防火网格明确包含 GiST 几何索引。
- 多张表含姓名、电话等受限字段;首版 MCP 不返回这些字段。
## 2. 目标
- 在现有 Go HTTP 服务内提供默认关闭的 `POST /mcp`。
- 与已经验证的 SuperAgent 配置保持兼容,采用 MCP `2025-06-18`、JSON-RPC 2.0 和单请求 JSON 响应。
- 通过独立 Bearer Token 鉴权;不得复用 SuperAgent Open API Key。
- 只提供固定、参数化、只读、按服务端可信数据范围执行的空间查询工具;默认使用镇街白名单,数据库全范围必须显式启用。
- 支持在现有业务记录中按地名搜索有界候选,让没有坐标的用户先确认一个候选,再进入距离和包含关系查询。
- 使用 PostgreSQL/PostGIS 作为事实来源,并在启用 MCP 前校验 PostGIS、表、SRID、几何类型和有效性。
- 工具结果明确表达数据来源、生成时间、限制与需要现场确认的事项。
- 提供一个只输出 schema/几何统计、不输出业务记录或联系人数据的 PostGIS readiness probe。
## 3. 非目标
- 不开放任意 SQL、任意表查询、文件读取或通用 HTTP 代理。
- 不写数据库,不调度人员或资源,不生成/下发指挥命令。
- 不声称防火通道是可通行路线,不实现路网拓扑、最短路、坡度或实时封路计算。
- 不声称网格队伍的实时位置或集结点;现有表只提供责任队伍和区域信息。
- 不依据资源记录存在就宣称水源、检查站或瞭望哨当前可用。
- 不返回负责人、队长、值班人员、书记、联系电话或图片地址。
- 不提供任意地址、山名或道路的外部地理编码,不实现完整地名库、拼音/别名纠错,也不自动选定地名候选。
- 不在本 checkpoint 内建立最终用户身份、动态角色或多租户模型;首版使用每环境 MCP 凭证和服务端静态数据库全范围/镇街白名单。
## 4. MCP 传输与安全契约
### 4.1 支持的方法
- `initialize`
- `notifications/initialized`
- `tools/list`
- `tools/call`
服务端声明协议版本 `2025-06-18` 和 `tools` capability。首版是无会话、非流式的 Streamable HTTP 子集:`POST` 返回 `application/json`;`GET /mcp` 返回 `405 Method Not Allowed`。
### 4.2 入站控制
- MCP 默认关闭,关闭时不注册 `/mcp`。
- 只接受 `Content-Type: application/json`。
- 使用 `Authorization: Bearer <FIRE_SAFETY_MCP_AUTH_TOKEN>`,常量时间比较。
- Token 至少 32 个可打印 ASCII 字符,且不能等于 SuperAgent Open API Key。
- 请求体默认最大 256 KiB,最大可配置 1 MiB。
- 首版只接受服务到服务请求;带非空 `Origin` 的请求拒绝,避免浏览器和 DNS rebinding 风险。
- 不接受模型提供的用户、角色、租户或授权范围。范围模式和可选镇街列表只来自服务端配置。
- 每次工具调用有硬超时;日志只记录 request ID、方法/工具名、耗时和结果类别,不记录参数、坐标、Token、SQL 或结果正文。
### 4.3 服务端数据范围
- `FIRE_SAFETY_MCP_SCOPE_MODE=town_allowlist` 是默认值;启用 MCP 时要求 `FIRE_SAFETY_MCP_ALLOWED_TOWNS` 非空,并按数据库镇街字段精确过滤。
- `FIRE_SAFETY_MCP_SCOPE_MODE=all` 必须显式配置;它授权查询当前配置数据库中 MCP 固定查询表的全部镇街以及镇街字段为空的记录,此时 `FIRE_SAFETY_MCP_ALLOWED_TOWNS` 必须为空。
- `all` 不会放宽只读、固定 SQL、空间半径、结果数量、字段脱敏、SRID 或 readiness 限制。
- 业务表中的镇街值不能自动决定授权范围;范围只能由可信服务端配置建立。
## 5. MCP 工具
除地名候选工具外,所有工具的坐标参数使用 WGS84:`longitude` 范围 `[-180, 180]`,`latitude` 范围 `[-90, 90]`。需要距离的工具接收 `radius_meters` 和 `limit`,同时有工具级默认值和硬上限。
### 5.1 `fire_safety_search_place_candidates`
输入 `place_name`(去除首尾空白后 2 至 100 字符)和可选 `limit`(默认 10、最大 20),在 8 张现有业务表的名称、镇街和村庄字段中做不区分大小写的文字包含匹配。百分号和下划线按普通文字处理,不作为 SQL 通配符。
返回稳定资源类型、记录 ID、名称、镇街、村庄、命中字段、`exact | partial`、WGS84 坐标和 `location_kind`:
- 点记录返回 `recorded_point`。
- 线记录返回首个组成线的起点,面记录返回 `ST_PointOnSurface` 代表点,两者标记为 `representative_point`。
候选不等于用户位置。无结果时 Agent 必须请用户补充名称或地图选点;多结果时必须请用户选择;即使只有一个候选,也要先回显确认,不能直接调用后续距离工具。该工具不返回联系人字段,不调用外部地图服务。
### 5.2 `fire_safety_resolve_incident_context`
输入演练点坐标,查询覆盖该点的防火网格,返回网格 ID、镇街、区域标签。用于建立后续查询上下文,不返回个人信息。
### 5.3 `fire_safety_find_nearby_water_sources`
合并水源地和蓄水池,按球面距离返回候选水源。返回名称/标签、类型、镇街、村、坐标、距离、容量(数据存在时)、源表报告状态和未经解释的源时间原值(数据存在时)。结果固定标记“当前可用性未验证”;时间原值的单位和时区待数据所有者确认。
### 5.4 `fire_safety_find_command_post_candidates`
合并防火检查站和防火瞭望哨,返回附近候选设施、坐标、距离及源表报告状态。结果只代表空间候选,必须由现场核验安全、通信、容量、可达性和火势上风向等条件。
### 5.5 `fire_safety_list_nearby_access_lines`
返回附近防火通道名称、按 WGS84 geometry 计算的米制长度、最近接入点和未经解释的源更新时间原值。该工具不称为 route planner;它不返回“推荐路线”或“可通行”结论。
### 5.6 `fire_safety_get_responsible_units`
根据坐标查询覆盖网格及其防火中队名称。明确返回 `live_location_available=false` 和 `assembly_site_available=false`,因为当前表没有队伍实时位置、战备状态或正式集结点字段。
### 5.7 `fire_safety_find_nearby_risk_areas`
合并墓地坟区和林区工矿企业,返回点位周边风险区域类别、名称/标签、镇街、村、方位、是否覆盖演练点和距离。联系人字段不返回。
## 6. 统一结果语义
工具成功结果使用:
```json
{
"status": "ok | no_results",
"data": {},
"metadata": {
"generated_at": "RFC3339 UTC",
"data_sources": [],
"spatial_reference": "EPSG:4326",
"result_count": 0
},
"warnings": []
}
```
`structuredContent` 保存上述对象,同时在 `content[0].text` 返回同一对象的 JSON 文本,兼容只读取文本内容的 MCP 客户端。
`data_sources` 只使用稳定领域名称(如 `water_source`、`fire_grid`),不向模型暴露历史物理表名。
`warnings` 使用 `results_limited_to_server_authorized_towns` 表示镇街白名单模式,使用 `results_include_all_towns_in_configured_database` 表示显式数据库全范围模式,避免 Agent 混淆结果范围。所有工具同时返回 `source_records_with_invalid_geometries_are_excluded`,防止 Agent 把排除后的无结果解释成数据库确认不存在相关记录。
工具级失败仍使用 JSON-RPC 成功响应中的 `isError=true`,结构化错误只公开稳定错误码:
- `INVALID_ARGUMENT`
- `TOOL_NOT_FOUND`
- `DATA_SOURCE_UNAVAILABLE`
- `QUERY_TIMEOUT`
- `INTERNAL_ERROR`
数据库 DSN、SQL、表结构细节和原始驱动错误不得进入 MCP 响应。
## 7. PostgreSQL/PostGIS 契约
- 使用 `pgx/v5` 原生连接池;当前仅面向 PostgreSQL,不引入 ORM。
- 连接设置 `default_transaction_read_only=on`、`statement_timeout` 和应用名。
- SQL 固定在 Repository,所有值使用 `$n` 参数;只有代码内表白名单可参与标识符拼接。
- 查询始终应用服务端范围、半径和返回数量上限;`town_allowlist` 使用参数化镇街数组,`all` 使用服务端布尔参数显式跳过镇街条件,模型不能提供该参数。
- 地名查询使用参数化的文字包含匹配,先按精确名称、精确村庄、精确镇街,再按前缀和普通包含排序;不拼接用户输入。线面候选的二维代表点只在只读查询中计算,不修改原始几何。
- MCP v1 只支持已实库确认的 EPSG:4326。启用 MCP 时 `FIRE_SAFETY_POSTGIS_EXPECTED_SRID` 必须显式配置为 `4326`。
- readiness 校验要求每张已用表存在、PostGIS 可用、非空几何的 SRID 均为 4326、类型符合点/线/面预期且坐标不越过 WGS84 范围;表/SRID/类型/越界不符合时服务拒绝启用 MCP。
- 无效几何、空几何、空表或缺少空间索引作为 readiness warning。固定查询使用 `ST_IsValid` 排除无效几何,不自动调用 `ST_MakeValid`,不修改原始事实;生产前应评估数据缺口并补齐查询表达式适用的 GiST 索引。
- 原始防火通道列保持裸 `geometry` 以容纳混合二维/Z。不得用 `ST_Force2D` 或重写 WKB 修改原始数据;确需二维投影时仅在只读查询/派生层显式执行并测试其结果语义。
- 不自动执行样例 SQL、SRID 修复或索引 DDL。
## 8. 配置
| 环境变量 | 默认值 | 启用时要求 |
| --- | --- | --- |
| `FIRE_SAFETY_MCP_ENABLED` | `false` | 显式为 `true` |
| `FIRE_SAFETY_MCP_AUTH_TOKEN` | 空 | 必填,独立高熵 Token |
| `FIRE_SAFETY_MCP_SCOPE_MODE` | `town_allowlist` | `town_allowlist` 或 `all` |
| `FIRE_SAFETY_MCP_ALLOWED_TOWNS` | 空 | `town_allowlist` 时必填;`all` 时必须为空 |
| `FIRE_SAFETY_MCP_MAX_BODY_BYTES` | `262144` | `1..1048576` |
| `FIRE_SAFETY_MCP_TOOL_TIMEOUT` | `5s` | `>0` 且不超过 `30s` |
| `FIRE_SAFETY_POSTGIS_ENABLED` | `false` | MCP 启用时必须为 `true` |
| `FIRE_SAFETY_POSTGIS_DSN` | 空 | PostGIS 启用时必填;Secret |
| `FIRE_SAFETY_POSTGIS_EXPECTED_SRID` | 空/`0` | MCP 启用时必须显式为 `4326` |
| `FIRE_SAFETY_POSTGIS_CONNECT_TIMEOUT` | `5s` | `>0` 且不超过 `30s` |
| `FIRE_SAFETY_POSTGIS_QUERY_TIMEOUT` | `3s` | `>0` 且不超过 `30s` |
| `FIRE_SAFETY_POSTGIS_MAX_CONNS` | `4` | `1..20` |
## 9. 验收标准
- MCP 关闭时 `/mcp` 不暴露,健康检查保持可用。
- MCP 启用但 Token、合法范围、PostGIS 或显式 SRID 缺失时配置加载失败;`all` 与非空镇街列表同时出现时失败,错误不含 Secret。
- 无 Token、错误 Token、错误 Content-Type、非空 Origin、超大 body 和非法 JSON 均被稳定拒绝。
- `initialize`、initialized notification、`tools/list` 和 7 个 `tools/call` 契约通过本地测试。
- 工具 schema 限制地名长度、经纬度、半径、数量和未知字段;工具不接受授权身份参数。
- Service 测试证明可信数据库全范围/镇街白名单来自构造时配置,并覆盖互斥校验、无结果、超时和仓储失败。
- Repository 只使用参数化值,MCP 结果不包含联系人字段。
- readiness 测试证明无效几何不阻塞启动、会产生排除 warning,且全部固定查询显式包含 `ST_IsValid`;SRID、类型和越界仍为硬失败。
- 地名 Service/Repository 测试覆盖首尾空白、长度、控制字符、结果上限、服务端范围、8 张固定表和候选确认 warning。
- readiness probe 不输出业务行、电话、负责人、DSN 或 SQL。
- `gofmt`、`go test -count=1 ./...`、`go test -race -count=1 ./...` 和 `go vet ./...` 通过。
## 10. 上线前未确认项
- 实库源 CRS 已由数据提供方确认为 EPSG:4326 且无偏移;2026-09-05 已通过单独审核、显式确认和单事务迁移为 4,048 条非空几何补齐 SRID 4326,严格 readiness 已通过。重新导入无 SRID 的原始 SQL 时仍必须重新经过该流程,应用查询不得静默赋值。
- 实库 35 条无效面几何按首版决策排除;需评估由此造成的网格、责任单位和风险区域覆盖缺口是否满足生产要求。
- 各资源状态字段的枚举、更新时间含义和数据刷新责任人。
- MCP 回调网络地址、TLS、SuperAgent 实际 Header 行为及 Token 轮换方式。
- 最终用户身份、区域权限、精确位置权限和审计保留策略。
- 真实数据量下地名重复率、字段质量和无索引包含搜索性能;后续是否引入标准地名表、别名词典、`pg_trgm` 或经审批的外部地理编码服务。
- 路线规划需要的路网拓扑、路面/宽度/坡度/车辆限制、实时封路、火场和天气数据。
- 队伍集结需要的正式集结点、实时位置、战备状态、装备和容量数据。
@@ -0,0 +1,159 @@
# SuperAgent Open API 连通性基线 Spec
| 项 | 内容 |
| --- | --- |
| 状态 | Implemented |
| 日期 | 2026-09-04 |
| 负责人 | fire-safety-ymd 后端 |
| 需求来源 | 用户 checkpoint `fire-safety-ymd-superagent-openapi-connectivity` |
| 关联 Change Request | 无 |
## 1. 背景
fire-safety-ymd 需要由 Go 后端调用既有 SuperAgent 平台,并在后续让 SuperAgent 通过本项目 MCP 工具查询 PostgreSQL/PostGIS 消防数据。
TH Hotel 项目已经验证了“创建 Open Agent Session、流式发送消息、解析公开 Trace、严格判断完成状态和断流恢复”的调用形态。本 checkpoint 只迁移协议经验和安全边界,使用 Go 独立实现,不复制 Java、酒店业务、AgentBus、邮件、OSS 或任务结果写入逻辑。
## 2. 目标
- 建立独立、可测试的 Go SuperAgent Open API Adapter。
- 分离 `CreateSession` 与 `StreamMessage`,为后续一个本地对话复用一个 SuperAgent Session 做准备。
- 使用严格 SSE 成功条件,拒绝部分回答和不完整协议结果。
- 初始 SSE 断流后通过既有 Run 恢复,不重新发送原始消息。
- 提供默认关闭的 CLI 连通性探针,只发送固定无敏感信息消息。
- 所有自动化测试使用本地模拟 Provider,不需要真实 Secret 或网络。
## 3. 非目标
- 不实现浏览器或业务聊天 API。
- 不持久化本地会话与 SuperAgent Session 的映射。
- 不实现 `/mcp` endpoint 或消防工具。
- 不连接 PostgreSQL/PostGIS。
- 不创建或配置 SuperAgent Profile、外部应用、API Key 或 MCP Server。
- 不把 SuperAgent 输出写入消防业务事实。
## 4. 用户与场景
当前用户是开发和联调人员:在安全配置测试环境后,通过 CLI 探针验证 Go 服务能创建 Session、发送无敏感信息消息并取得严格完成的最终回答。
后续的用户对话 API v1 已在独立 Spec 中实现对该 Adapter 的复用、单进程会话映射、并发冲突和客户端 SSE;真实身份、持久化与主动取消仍需另行设计。
## 5. Definition of Ready
- 目标与非目标已确认:是。
- 协议基线已确认:以 TH Hotel 仓库保存的 2026-07-12 SuperAgent Open API 文档和当前实现为输入,真实环境上线前重新验证。
- 权限和安全边界已确认:Secret 仅由本项目环境变量注入;没有用户业务数据进入探针。
- 外部依赖已确认:实现只使用 Go 标准库。
- 未确认问题已列出:Profile、外部应用、scope、真实 Base URL 和 API Key 均由平台管理员后续提供。
## 6. 协议与客户端契约
### 6.1 创建 Session
```text
POST /api/open/agent-sessions
```
请求包含:
- `external_subject_id`:调用方提供的稳定、非敏感主体标识。
- `idempotency_key`:创建 Session 的稳定幂等键。
- `metadata`:不包含 Secret 的关联元数据。
客户端接受响应中的 `session_id`,并兼容 `id` 字段。
### 6.2 流式发送消息
```text
POST /api/open/agent-sessions/{session_id}/messages/stream?include_trace=true
```
请求包含 `message`、稳定 `idempotency_key` 和安全 `metadata`。Session ID 由上层明确传入,Adapter 不在每条消息前隐式创建新 Session。
### 6.3 鉴权与关联 Header
- `Authorization: Bearer <Open API Key>`
- `X-Request-ID: <稳定请求关联 ID>`
- `X-CSRF-Token` 与 `Cookie: csrf_token=...` 使用同一个每请求随机值。
- SSE 使用 `Accept: text/event-stream` 和 `Cache-Control: no-cache`。
## 7. SSE 成功与恢复规则
一次调用只有同时满足以下条件才成功:
1. 收到最终内容;优先使用 `message.final`,兼容累计 `message.delta` 和历史 `messages`/`values` AI 消息。
2. 收到 `run.completed` 且 `status=success`。
3. 收到顶层 `event: end`。
4. 没有收到顶层 `error` 或 `run.failed`。
解析器必须支持 `event:`、多行 `data:`、`id:`、心跳注释和空行分帧,并按 SSE event ID 去重。
初始流提前结束时:
- 不重新 POST 消息。
- 从 `Content-Location` 获取同源 Run URL;必要时使用已解析的 Run ID 构造 URL。
- 查询 Run 状态,再订阅 `{run_url}/events`。
- 恢复请求携带 `Last-Event-ID`。
- 使用有上限的指数退避并服从调用方 context 取消。
- 恢复耗尽或进入失败终态时返回明确错误,不返回部分答案。
## 8. 配置契约
| 环境变量 | 默认值 | 说明 |
| --- | --- | --- |
| `FIRE_SAFETY_SUPERAGENT_ENABLED` | `false` | 总开关,默认不调用真实 Provider |
| `FIRE_SAFETY_SUPERAGENT_BASE_URL` | 空 | SuperAgent Open API Base URL |
| `FIRE_SAFETY_SUPERAGENT_OPEN_API_KEY` | 空 | Secret,启用时必填 |
| `FIRE_SAFETY_SUPERAGENT_CONNECT_TIMEOUT` | `15s` | 建连超时 |
| `FIRE_SAFETY_SUPERAGENT_RECOVERY_MAX_ATTEMPTS` | `5` | SSE 恢复最大次数;最大可配置为 20 |
| `FIRE_SAFETY_SUPERAGENT_RECOVERY_INITIAL_BACKOFF` | `250ms` | 首次恢复退避 |
| `FIRE_SAFETY_SUPERAGENT_MAX_MESSAGE_BYTES` | `65536` | 单条消息 UTF-8 字节上限;最大可配置为 16 MiB |
| `FIRE_SAFETY_SUPERAGENT_PROBE_SUBJECT_ID` | 固定探针主体 | 仅 CLI 探针使用 |
| `FIRE_SAFETY_SUPERAGENT_PROBE_TIMEOUT` | `10m` | 探针总超时 |
启用时配置缺失或不合法必须启动失败;错误不得包含 API Key。
## 9. 安全规则
- 不读取 TH Hotel 的环境变量或 Secret,不共享 API Key。
- Base URL 必须是绝对 HTTP/HTTPS URL,且不得带 userinfo、query 或 fragment。
- `Content-Location` 和恢复 URL 必须与配置 Base URL 同源,防止向其他主机转发 Authorization。
- 错误只暴露安全状态和经过限制的 Provider error code,不返回响应正文。
- 客户端不记录消息、Header、Cookie、API Key 或 SSE 原始数据。
- Probe 使用固定安全消息;不得用它发送真实火情、联系人或生产数据。
## 10. 需求追踪表
| 需求项 | 后端状态 | 测试状态 | 文档位置 | 当前状态 |
| --- | --- | --- | --- | --- |
| 环境配置与安全默认值 | Done | Passed | 本文第 8 节 | Implemented |
| CreateSession | Done | Passed | 本文第 6.1 节 | Implemented |
| StreamMessage 与严格成功条件 | Done | Passed | 本文第 6.2、7 节 | Implemented |
| SSE 断流恢复 | Done | Passed | 本文第 7 节 | Implemented |
| CLI 探针 | Done | Compile passed;live test pending | 项目集成指南 | Implemented |
## 11. 验收标准
- Given 未启用 SuperAgent,When 执行真实调用,Then 返回受控禁用错误且不发起网络请求。
- Given 配置完整,When 创建 Session,Then请求具备鉴权、幂等、关联和 CSRF Header,且能解析 Session ID。
- Given完整 SSE,When 同时收到最终内容、成功完成和 `end`,Then 返回最终回答与安全元数据。
- Given SSE 缺少任一成功条件,When 无法恢复,Then 返回协议错误且不返回部分回答。
- Given 初始 SSE 提前结束且存在 Run URL,When 恢复,Then只 GET Run/events、携带 `Last-Event-ID`,消息 POST 次数仍为 1。
- Given未设置真实环境变量,When 运行全部测试,Then 不访问外网且测试通过。
## 12. 测试范围
- 配置默认值、合法值和错误值。
- Session 请求路径、Header、CSRF、请求体和响应解析。
- 当前 Trace SSE、历史 messages/values 兼容、事件 ID 去重和多行 data。
- 缺少 final/completed/end、顶层 error、run.failed 和非法 JSON。
- 提前 EOF 恢复、Last-Event-ID、同源 URL 和不重复 POST。
- HTTP 非 2xx、超大控制响应和 context 取消。
## 13. Definition of Done
- 实现满足本文契约。
- `gofmt`、`go test ./...` 和 `go vet ./...` 通过。
- 没有真实 Secret、用户数据、构建产物或无关用户变更。
- 项目索引、集成指南、安全边界和 `PROJECT_STATE.md` 已同步。
- 真实环境未配置时明确说明未做 live connectivity test。