- 默认切换 AgentProvider 为 HttpAgentProvider,增强网络请求的容错和重试机制 - 优化 Run 逻辑,支持多场景去重与并发保护 - 添加 Redis 发布失败的日志记录以提升问题排查效率 - 扩展 OpenAPI 规范,新增 Error 和 Run 状态相关模型 - 增强测试覆盖,验证调度策略和重复请求的幂等性 - 增加数据库索引以优化查询性能 - 更新所有相关文档和配置文件
223 lines
11 KiB
Markdown
223 lines
11 KiB
Markdown
# ChatSession & Message API(MVP-1 + Agent Run MVP-0)
|
||
|
||
基地址:`http://localhost:8000/api`(FrankenPHP 容器 8000 端口)
|
||
认证方式:JWT,`Authorization: Bearer {token}`
|
||
自然语言:中文
|
||
|
||
## 变更记录
|
||
- 2025-02-14:新增 ChatSession 创建、消息追加、增量查询接口;支持状态门禁与 dedupe 幂等。
|
||
- 2025-02-14:MVP-1.1 增加会话列表、会话更新(重命名/状态变更),列表附带最后一条消息摘要。
|
||
- 2025-02-15:Agent Run MVP-0 —— RunDispatcher + AgentRunJob + DummyProvider;自动在 user.prompt 后触发一次 Run,落地 run.status / agent.message。
|
||
- 2025-12-18:Agent Run 可靠性增强 —— 并发幂等、终态去重、取消语义加强、Provider 超时/重试/错误归一,SSE gap 回补与心跳。
|
||
|
||
## 本次变更摘要(2025-12-18)
|
||
- RunDispatcher 并发幂等:同 trigger_message_id 只产生一个 RUNNING,且仅新建时 dispatch。
|
||
- RunLoop/OutputSink 幂等:agent.message 与 run.status 采用 dedupe_key;重复执行不重复写。
|
||
- Cancel 强化:多检查点取消,确保不落 agent.message 且落 CANCELED 终态。
|
||
- Provider 可靠性:超时/重试/429/5xx,错误落库包含 retryable/http_status/provider/latency_ms。
|
||
- SSE 可靠性:gap 触发回补,心跳保活,publish 异常不影响主流程。
|
||
|
||
## 领域模型
|
||
- `ChatSession`:`session_id`(UUID)、`session_name`、`status`(`OPEN`/`LOCKED`/`CLOSED`)、`last_seq`
|
||
- `Message`:`message_id`(UUID)、`session_id`、`role`(`USER`/`AGENT`/`TOOL`/`SYSTEM`)、`type`(字符串)、`content`、`payload`(json)、`seq`(会话内递增)、`reply_to`(UUID)、`dedupe_key`
|
||
- 幂等:`UNIQUE (session_id, dedupe_key)`;同一 dedupe_key 返回已有消息。
|
||
- 状态门禁:`CLOSED` 禁止追加,例外 `role=SYSTEM && type in [run.status, error]`;`LOCKED` 禁止 `role=USER && type=user.prompt`。
|
||
- 会话缓存:`chat_sessions.last_message_id` 记录最后一条消息;`appendMessage` 事务内同步更新 `last_seq`、`last_message_id`、`updated_at`。
|
||
|
||
## 接口
|
||
### 创建会话
|
||
- `POST /sessions`
|
||
- 请求体字段
|
||
|
||
| 字段 | 必填 | 类型 | 说明 |
|
||
| --- | --- | --- | --- |
|
||
| session_name | 否 | string(≤255) | 会话名称 |
|
||
- 响应 201(JSON)
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
| --- | --- | --- |
|
||
| session_id | uuid | 主键 |
|
||
| session_name | string|null | 会话名 |
|
||
| status | enum | `OPEN|LOCKED|CLOSED` |
|
||
| last_seq | int | 当前最大 seq |
|
||
| last_message_id | uuid|null | 最后一条消息 |
|
||
| created_at, updated_at | datetime | 时间戳 |
|
||
- 错误:401 未授权
|
||
|
||
### 追加消息
|
||
- `POST /sessions/{session_id}/messages`
|
||
- 请求体字段
|
||
|
||
| 字段 | 必填 | 类型 | 说明 |
|
||
| --- | --- | --- | --- |
|
||
| role | 是 | enum | `USER|AGENT|TOOL|SYSTEM` |
|
||
| type | 是 | string(≤64) | 如 `user.prompt`/`agent.message` 等 |
|
||
| content | 否 | string | 文本内容 |
|
||
| payload | 否 | object | jsonb 结构 |
|
||
| reply_to | 否 | uuid | 引用消息 |
|
||
| dedupe_key | 否 | string(≤128) | 幂等键 |
|
||
- 响应 201(JSON)
|
||
字段:`message_id, session_id, seq, role, type, content, payload, reply_to, dedupe_key, created_at`
|
||
- 幂等:同 session + dedupe_key 返回已存在的消息(同 `message_id/seq`)。
|
||
- 错误:401 未授权;403 违反状态门禁(CLOSED 禁止,LOCKED 禁止 user.prompt);404 session 不存在;422 校验失败。
|
||
|
||
### 按序增量查询
|
||
- `GET /sessions/{session_id}/messages?after_seq=0&limit=50`
|
||
- 查询参数
|
||
|
||
| 参数 | 默认 | 类型 | 说明 |
|
||
| --- | --- | --- | --- |
|
||
| after_seq | 0 | int | 仅返回 seq 大于该值 |
|
||
| limit | 50 | int(≤200) | 返回数量上限 |
|
||
- 响应 200:`data` 数组,元素字段同“追加消息”响应。
|
||
- 错误:401/404/422
|
||
|
||
### 会话列表
|
||
- `GET /sessions?page=1&per_page=15&status=OPEN&q=keyword`
|
||
- 查询参数
|
||
|
||
| 参数 | 默认 | 类型 | 说明 |
|
||
| --- | --- | --- | --- |
|
||
| page | 1 | int | 分页页码 |
|
||
| per_page | 15 | int(≤100) | 分页大小 |
|
||
| status | - | enum | 过滤 `OPEN|LOCKED|CLOSED` |
|
||
| q | - | string | ILIKE 模糊匹配 session_name |
|
||
- 响应 200:分页结构(`data/links/meta`),`data` 每项字段:
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
| --- | --- | --- |
|
||
| session_id | uuid | 会话主键 |
|
||
| session_name | string|null | 名称 |
|
||
| status | enum | `OPEN|LOCKED|CLOSED` |
|
||
| last_seq | int | 当前最大 seq |
|
||
| last_message_id | uuid|null | 最后一条消息 |
|
||
| last_message_at | datetime|null | 最后一条消息时间 |
|
||
| last_message_preview | string | content 截断 120,空内容返回空字符串 |
|
||
| last_message_role | string|null | 最后消息角色 |
|
||
| last_message_type | string|null | 最后消息类型 |
|
||
| created_at, updated_at | datetime | 时间戳 |
|
||
- 排序:`updated_at` DESC
|
||
- 错误:401/422
|
||
|
||
### 会话更新
|
||
- `PATCH /sessions/{session_id}`
|
||
请求体(至少一项,否则 422)
|
||
|
||
| 字段 | 必填 | 类型 | 说明 |
|
||
| --- | --- | --- | --- |
|
||
| session_name | 否 | string 1..255 | 自动 trim |
|
||
| status | 否 | enum | `OPEN|LOCKED|CLOSED` |
|
||
- 规则:
|
||
- `CLOSED` 不可改回 `OPEN`(返回 403)。
|
||
- 任意更新都会刷新 `updated_at`。
|
||
- 响应 200:字段同“会话列表”项。
|
||
- 错误:401 未授权;403 状态门禁;404 session 不存在;422 校验失败。
|
||
|
||
### 获取会话详情
|
||
- `GET /sessions/{session_id}`
|
||
- 响应 200:字段同“会话列表”项。
|
||
- 错误:401 未授权;404 session 不存在。
|
||
|
||
### 归档会话(Archive)
|
||
- `POST /sessions/{session_id}/archive`
|
||
- 行为:将 `status` 置为 `CLOSED`,更新 `updated_at`,幂等(重复归档返回当前状态)。
|
||
- 响应 200:字段同“会话列表”项(status=CLOSED)。
|
||
- 错误:401 未授权;404 session 不存在。
|
||
|
||
### 获取单条消息(带会话校验)
|
||
- `GET /sessions/{session_id}/messages/{message_id}`
|
||
- 行为:校验 `message.session_id` 与路径参数一致,否则 404。
|
||
- 响应 200:字段同“追加消息”响应。
|
||
- 错误:401 未授权;404 不存在或不属于该会话。
|
||
|
||
### SSE 实时增量
|
||
- `GET /sessions/{session_id}/sse?after_seq=123`
|
||
- 头部:`Accept: text/event-stream`,可带 `Last-Event-ID`(优先于 query)用于断线续传。
|
||
- 查询参数
|
||
|
||
| 参数 | 默认 | 类型 | 说明 |
|
||
| --- | --- | --- | --- |
|
||
| after_seq | 0 | int | backlog 起始 seq(若有 Last-Event-ID 则覆盖) |
|
||
| limit | 200 | int(≤500) | backlog 最多条数 |
|
||
- SSE 输出格式:
|
||
```
|
||
id: {seq}
|
||
event: message
|
||
data: {...message json...}
|
||
|
||
```
|
||
- `id` 为消息 `seq`,便于续传;`data` 为消息 JSON(同追加消息响应字段)。
|
||
- Backlog:建立连接后先补发 `seq > after_seq` 的消息(order asc,最多 `limit` 条),再进入实时订阅。
|
||
- 实时:Redis channel `session:{session_id}:messages` 发布消息 ID,SSE 侧读取后按 seq 去重、推送。
|
||
- Gap 回补:若订阅推送的 seq 与 last_sent_seq 存在缺口,会主动回补 backlog。
|
||
- 心跳:周期输出 `: ping` 保活。
|
||
- 错误:401 未授权;404 session 不存在。
|
||
|
||
## Agent Run MVP-0(RunDispatcher + AgentRunJob)
|
||
### 流程概述
|
||
1. 用户追加 `role=USER && type=user.prompt` 后,Controller 自动调用 `RunDispatcher->dispatchForPrompt`。
|
||
2. 并发保护:同会话只允许一个 RUNNING;同一个 `trigger_message_id` 幂等复用已有 `run_id`。
|
||
3. 立即写入 `run.status`(SYSTEM/run.status,payload `{run_id,status:'RUNNING',trigger_message_id}`,dedupe_key=`run:trigger:{message_id}`)。
|
||
4. 仅在新建 RUNNING 时推送 `AgentRunJob(session_id, run_id)` 到队列(测试环境 QUEUE=sync 会同步执行)。
|
||
5. RunLoop(默认 HttpAgentProvider,未配置 endpoint 时回退 DummyAgentProvider):
|
||
- 终态检测:若已 DONE/FAILED/CANCELED 则直接返回。
|
||
- Cancel 检查:存在 `run.cancel.request`(payload.run_id) 则写入 `run.status=CANCELED`,不产出 agent.message。
|
||
- ContextBuilder:提取最近 20 条 USER/AGENT 消息(type in user.prompt/agent.message),seq 升序提供给 Provider。
|
||
- Provider 返回一次性文本回复(内置超时/重试/退避)。
|
||
- OutputSink 依次写入:`agent.message`(payload 含 run_id, provider,dedupe_key=`run:{run_id}:agent:message`)、`run.status=DONE`(dedupe_key=`run:{run_id}:status:DONE`)。
|
||
6. 异常:ProviderException 写入 `error` + `run.status=FAILED`(dedupe),error payload 包含 retryable/http_status/provider/latency_ms。
|
||
|
||
### Run 相关消息类型(落库即真相源)
|
||
| type | role | payload 关键字段 | 说明 |
|
||
| --- | --- | --- | --- |
|
||
| run.status | SYSTEM | run_id, status(RUNNING/DONE/CANCELED/FAILED), trigger_message_id?, error? | Run 生命周期事件,CLOSED 状态下允许写入 |
|
||
| agent.message | AGENT | run_id, provider | Provider 的一次性回复 |
|
||
| run.cancel.request | USER/SYSTEM | run_id | CancelChecker 依据该事件判断是否中止 |
|
||
| error | SYSTEM | run_id, message, retryable?, http_status?, provider?, latency_ms?, raw_message? | 任务异常时落库 |
|
||
|
||
### 触发 Run(调试入口)
|
||
- `POST /sessions/{session_id}/runs`
|
||
- 请求体字段
|
||
|
||
| 字段 | 必填 | 类型 | 说明 |
|
||
| --- | --- | --- | --- |
|
||
| trigger_message_id | 是 | uuid | 通常为 `user.prompt` 消息 ID |
|
||
- 行为:同 `trigger_message_id` 幂等;若已有 RUNNING 则复用其 run_id。
|
||
- 响应 201:`{ run_id }`
|
||
- 错误:401 未授权;404 session 不存在或 trigger_message 不属于该 session。
|
||
|
||
## cURL 示例
|
||
```bash
|
||
# 创建会话
|
||
SESSION_ID=$(curl -s -X POST http://localhost:8000/api/sessions \
|
||
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
||
-d '{"session_name":"Demo"}' | jq -r '.data.session_id')
|
||
|
||
# 追加消息(支持 dedupe_key 幂等)
|
||
curl -s -X POST http://localhost:8000/api/sessions/$SESSION_ID/messages \
|
||
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
||
-d '{"role":"USER","type":"user.prompt","content":"hello","dedupe_key":"k1"}'
|
||
|
||
# 增量查询
|
||
curl -s "http://localhost:8000/api/sessions/$SESSION_ID/messages?after_seq=0&limit=50" \
|
||
-H "Authorization: Bearer $TOKEN"
|
||
|
||
# 归档
|
||
curl -X POST http://localhost:8000/api/sessions/$SESSION_ID/archive \
|
||
-H "Authorization: Bearer $TOKEN"
|
||
|
||
# 获取单条消息
|
||
curl -s http://localhost:8000/api/sessions/$SESSION_ID/messages/{message_id} \
|
||
-H "Authorization: Bearer $TOKEN"
|
||
|
||
# SSE(断线续传:可带 Last-Event-ID)
|
||
curl -N http://localhost:8000/api/sessions/$SESSION_ID/sse?after_seq=10 \
|
||
-H "Authorization: Bearer $TOKEN" \
|
||
-H "Accept: text/event-stream"
|
||
|
||
# 手动触发 Run(调试用,实际 user.prompt 会自动触发)
|
||
curl -s -X POST http://localhost:8000/api/sessions/$SESSION_ID/runs \
|
||
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
||
-d '{"trigger_message_id":"'$MESSAGE_ID'"}'
|
||
```
|