# 用户对话 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;默认要求至少 32 个可打印 ASCII 字符。 - 首轮创建随机本地 `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 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_ALLOW_LEGACY_SHORT_TOKEN` | `false` | 仅为已交付旧客户端短凭证的受控测试/迁移临时兼容开关;新环境不得开启 | | `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`。 `FIRE_SAFETY_CHAT_ALLOW_LEGACY_SHORT_TOKEN` 默认为 `false`。只有在旧客户端已经拿到、且无法立即更换的短凭证时,才可在受控测试或迁移窗口显式设置为 `true`。无论开关取值如何,`FIRE_SAFETY_CHAT_AUTH_TOKEN` 都必须非空、不超过 4096 字节,并且每个字符都在 ASCII `0x21-0x7e` 范围内(不得包含空格、控制字符或 Unicode);开关只影响 Chat 原生 Bearer 和兼容入口的 `xtoken`,不放宽 MCP Token 至少 32 个可打印 ASCII 字符的门禁。三种凭证仍必须使用不同值。凭证轮换完成后必须恢复 `false`;新环境不得以该开关绕过默认门禁。开关启用时,服务启动日志会记录不含 Secret 的安全 warning,便于后续清理。 ## 7. CORS 与鉴权边界 - 无 `Origin` 的 CLI/服务端请求可以进入 Bearer 校验。 - 带 `Origin` 的浏览器请求必须精确匹配配置列表;不支持 `*`。 - 预检只允许 `POST` 以及 `Authorization`、`Content-Type` Header。 - 不启用 Cookie 身份或 CORS credentials。 - 静态 Bearer 只适合首版受控联调;legacy 短凭证开关只适合明确的迁移窗口,公网最终用户入口必须接入真实身份认证、速率限制和动态授权。 ## 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 `FIRE_SAFETY_CHAT_ALLOW_LEGACY_SHORT_TOKEN` 未开启,When Chat Token 少于 32 个字符,Then 服务启动配置校验失败。 - Given `FIRE_SAFETY_CHAT_ALLOW_LEGACY_SHORT_TOKEN=true`,When 旧短凭证用于受控测试/迁移,Then 原生 Bearer 和兼容 `xtoken` 均可校验,但非空、4096 字节上限和 ASCII `0x21-0x7e` 约束仍生效,且启动日志只记录不含 Secret 的安全 warning。 - 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 提交。