api-server/docs/chat-scope-api-contract.md
wangdl fe44dec567
All checks were successful
Deploy API Server / build-and-deploy (push) Successful in 46s
feat: M-CHAT ChatScope 会话系统完整实现
## 数据模型
- ChatSession +13 字段 (scopeType/scopeId/parentKnowledgeBaseId/createdFrom/isPinned/isArchived/isDeleted/modelMode/modelId/lastMessageAt)
- ChatMessage +scopeSnapshot (消息级 scope 快照)
- ChatCitation +lineStart/lineEnd +sourceId 索引
- 5 个新查询索引

## 核心能力
- open-or-create: 同 scope 继续会话 (200) / 新建 (201)
- scope 级检索: global/knowledge_base/material/knowledge_item/folder
- listSessions: scope 过滤 + isDeleted 排除 + isPinned 排序 + 分页元数据
- 自动标题: 首条消息截取 + 词边界处理
- 软删除 + 置顶/归档
- scope 字段创建后不可修改
- 全部端点 userId 鉴权

## 文档
- docs/chat-scope-design.md (设计文档 + 决策表)
- docs/chat-scope-api-contract.md (API 契约)
- docs/chat-scope-test-plan.md (33 条测试用例)
- prisma/migrations/backfill_chat_scope.sql (旧数据回填)

## Bug 修复
- #104: KnowledgeItem.sourceRef 填充 (material scope 检索修复)
- #102: sendMessageStream aiGateway null 保护
- listSessions isDeleted/isArchived 过滤 + 分页

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 17:27:40 +08:00

713 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.

# ChatScope API Contract
> CHAT-002 | 版本 v1.0 | 2026-06-06
>
> 本文档是 ChatScope 前后端接口的**唯一权威契约**。
> 所有响应 shape、错误码、SSE 格式以本文档为准。
> 设计逻辑参见 [ChatScope 设计文档](./chat-scope-design.md)。
---
## 1. 基础信息
| 项目 | 值 |
|------|----|
| Base Path | `/rag-chat` |
| Auth | Bearer JWT (所有端点需要) |
| Content-Type (Request) | `application/json` |
| Content-Type (Response) | `application/json` (除 SSE 端点) |
| Date Format | ISO 8601 (`2026-06-06T12:00:00.000Z`) |
---
## 2. 枚举 & 常量
### 2.1 ChatScopeType
```typescript
type ChatScopeType =
| "knowledge_base"
| "folder"
| "material"
| "knowledge_item"
| "global";
```
### 2.2 CreatedFrom
```typescript
type CreatedFrom =
| "knowledge_base_detail"
| "material_detail"
| "material_reader"
| "knowledge_item_detail"
| "folder_detail"
| "global_ai_entry"
| "legacy_migration";
```
### 2.3 ModelMode
```typescript
type ModelMode = "normal" | "deep_think" | "web_search";
```
---
## 3. 端点清单
| Method | Path | 说明 | Issue |
|--------|------|------|-------|
| POST | `/rag-chat/sessions` | 创建/打开会话 (open-or-create) | #81 #76 |
| GET | `/rag-chat/sessions` | 会话列表(支持 scope 过滤) | #75 |
| GET | `/rag-chat/sessions/:id/messages` | 获取消息历史 | (已有) |
| POST | `/rag-chat/sessions/:id/messages` | 发送消息(同步) | (已有) |
| POST | `/rag-chat/sessions/:id/stream` | 发送消息SSE 流式) | (已有) |
| PATCH | `/rag-chat/sessions/:id` | 更新会话属性 | NEW |
| DELETE | `/rag-chat/sessions/:id` | 软删除会话 | (已有) |
---
## 4. 端点详细定义
### 4.1 POST /rag-chat/sessions — open-or-create
创建新会话,或在匹配的 scope 下返回已有会话。
**Request Body:**
```json
{
"scopeType": "material",
"scopeId": "clx7abc123",
"parentKnowledgeBaseId": "clx7kb456",
"createdFrom": "material_detail",
"title": "新对话"
}
```
| 字段 | 类型 | 必需 | 默认值 | 说明 |
|------|------|------|--------|------|
| scopeType | ChatScopeType | **是** | — | 会话绑定范围 |
| scopeId | string \| null | 否 | null | scope 对应的实体 ID |
| parentKnowledgeBaseId | string \| null | 否 | null | 所属知识库 |
| createdFrom | CreatedFrom | 否 | `"global_ai_entry"` | 入口标识 |
| title | string | 否 | `"新对话"` | 会话标题 |
**Validation Rules:**
- `scopeType` 必须是合法枚举值,否则 400
- `scopeType === "global"` 时,`scopeId` 必须为 null
- `scopeType !== "global"` 时,`scopeId` 不能为空404 或 400
**Behavior (open-or-create):**
```
IF 存在 userId + scopeType + scopeId 完全匹配 + isDeleted=false 的会话
→ 返回该会话 (HTTP 200)
ELSE
→ 创建新会话并返回 (HTTP 201)
```
**Response 200 (已有会话):**
```json
{
"id": "clx7sess001",
"userId": "clx7user001",
"scopeType": "material",
"scopeId": "clx7abc123",
"parentKnowledgeBaseId": "clx7kb456",
"title": "数据库事务讨论",
"createdFrom": "material_detail",
"modelMode": "normal",
"modelId": null,
"isPinned": false,
"isArchived": false,
"isDeleted": false,
"lastMessageAt": "2026-06-05T15:30:00.000Z",
"createdAt": "2026-06-01T08:00:00.000Z",
"updatedAt": "2026-06-05T15:30:00.000Z"
}
```
**Response 201 (新建会话):**
```json
{
"id": "clx7sess002",
"userId": "clx7user001",
"scopeType": "material",
"scopeId": "clx7abc123",
"parentKnowledgeBaseId": "clx7kb456",
"title": "新对话",
"createdFrom": "material_detail",
"modelMode": "normal",
"modelId": null,
"isPinned": false,
"isArchived": false,
"isDeleted": false,
"lastMessageAt": null,
"createdAt": "2026-06-06T10:00:00.000Z",
"updatedAt": "2026-06-06T10:00:00.000Z"
}
```
**Errors:**
| Code | 条件 |
|------|------|
| 400 | scopeType 不合法 |
| 400 | scopeType !== "global" 且 scopeId 为空 |
| 401 | JWT 缺失或过期 |
---
### 4.2 GET /rag-chat/sessions — 会话列表
**Query Parameters:**
| 参数 | 类型 | 必需 | 默认值 | 说明 |
|------|------|------|--------|------|
| scopeType | ChatScopeType | 否 | — | 过滤 scope 类型 |
| scopeId | string | 否 | — | 过滤 scope ID需配合 scopeType |
| parentKnowledgeBaseId | string | 否 | — | 知识库下所有会话 |
| isArchived | boolean | 否 | false | 是否返回已归档 |
| page | number | 否 | 1 | 页码 |
| limit | number | 否 | 20 | 每页数量(最大 50 |
**过滤逻辑:**
```
IF scopeType + scopeId → 精确匹配(同一 scope 的历史)
ELIF parentKnowledgeBaseId → 该 KB 下所有会话(不区分 scope
ELSE → 全局列表(所有 scope排除 isDeleted
```
**Response 200:**
```json
{
"data": [
{
"id": "clx7sess001",
"userId": "clx7user001",
"scopeType": "material",
"scopeId": "clx7abc123",
"parentKnowledgeBaseId": "clx7kb456",
"title": "数据库事务讨论",
"createdFrom": "material_detail",
"modelMode": "normal",
"modelId": null,
"isPinned": true,
"isArchived": false,
"isDeleted": false,
"lastMessageAt": "2026-06-05T15:30:00.000Z",
"createdAt": "2026-06-01T08:00:00.000Z",
"updatedAt": "2026-06-05T15:30:00.000Z"
}
],
"meta": {
"page": 1,
"limit": 20,
"total": 42
}
}
```
**排序规则:**
- `isPinned === true` 优先
- 同优先级按 `lastMessageAt DESC`(最近活跃在前)
- 无消息的会话按 `createdAt DESC`
---
### 4.3 GET /rag-chat/sessions/:id/messages — 消息历史
**Response 200:**
```json
[
{
"id": "clx7msg001",
"sessionId": "clx7sess001",
"role": "user",
"content": "TCP 三次握手的过程是什么?",
"tokens": 0,
"scopeSnapshot": {
"scopeType": "material",
"scopeId": "clx7abc123",
"parentKnowledgeBaseId": "clx7kb456"
},
"createdAt": "2026-06-06T10:00:00.000Z",
"citations": []
},
{
"id": "clx7msg002",
"sessionId": "clx7sess001",
"role": "assistant",
"content": "TCP 三次握手的过程如下:...",
"tokens": 245,
"scopeSnapshot": {
"scopeType": "material",
"scopeId": "clx7abc123",
"parentKnowledgeBaseId": "clx7kb456"
},
"createdAt": "2026-06-06T10:00:05.000Z",
"citations": [
{
"id": "clx7cit001",
"messageId": "clx7msg002",
"chunkId": "clx7chk042",
"sourceId": "clx7abc123",
"sourceTitle": "计算机网络自顶向下.pdf",
"excerptText": "TCP 连接建立需要三次握手...",
"pageNumber": 156,
"lineStart": 12,
"lineEnd": 18,
"createdAt": "2026-06-06T10:00:05.000Z"
}
]
}
]
```
排序: `createdAt ASC`(时间正序)
---
### 4.4 POST /rag-chat/sessions/:id/messages — 发送消息 (同步)
**Request Body:**
```json
{
"content": "TCP 三次握手的过程是什么?"
}
```
| 字段 | 类型 | 必需 | 说明 |
|------|------|------|------|
| content | string | **是** | 消息正文(最长 10000 字符) |
**Response 200:**
```json
{
"id": "clx7msg002",
"role": "assistant",
"content": "TCP 三次握手的过程如下:...",
"tokens": 245,
"blocked": false,
"message": {
"id": "clx7msg002",
"sessionId": "clx7sess001",
"role": "assistant",
"content": "TCP 三次握手的过程如下:...",
"tokens": 245,
"createdAt": "2026-06-06T10:00:05.000Z",
"citations": [...]
},
"citations": [...]
}
```
**Response (被拦截):**
```json
{
"blocked": true,
"message": "输入包含违规内容,请修改后重试"
}
```
**Errors:**
| Code | 条件 |
|------|------|
| 404 | 会话不存在 |
| 403 | 会话不属于当前用户 |
| 400 | content 为空或超过 10000 字符 |
---
### 4.5 POST /rag-chat/sessions/:id/stream — SSE 流式
**Request Body:** 同 4.4
**Response:** `text/event-stream; charset=utf-8`
**SSE 事件格式:**
```
data: {"type":"thinking","content":"我们来看看..."}
data: {"type":"content","content":"TCP "}
data: {"type":"content","content":"三次握手是"}
data: {"type":"content","content":"..."}
data: {"type":"citations","citations":[{"sourceTitle":"...","excerptText":"..."}]}
data: {"type":"done"}
```
**Chunk 类型定义:**
```typescript
interface SSEChunk {
type: "thinking" | "content" | "citations" | "done" | "error";
content?: string;
citations?: ChatCitation[];
error?: string;
}
```
| type | 说明 | content | citations | error |
|------|------|---------|-----------|-------|
| thinking | 思考过程片段 | 思考文本 | — | — |
| content | 回答正文片段 | 增量文本 | — | — |
| citations | 引用信息(流末尾) | — | 引用数组 | — |
| done | 流结束 | — | — | — |
| error | 流错误 | — | — | 错误信息 |
**调用方必须:**
1. 逐行读取 `data: {...}\n\n`
2. JSON.parse 每行 `data:` 后的内容
3. 累积 `thinking` chunk → 思考过程
4. 累积 `content` chunk → 回答正文
5. `done``error` → 结束流
**Headers:**
```
Content-Type: text/event-stream; charset=utf-8
Cache-Control: no-cache
Connection: keep-alive
X-Accel-Buffering: no
```
---
### 4.6 PATCH /rag-chat/sessions/:id — 更新会话 (NEW)
**Request Body (全部可选):**
```json
{
"title": "三次握手深度讨论",
"isPinned": true,
"isArchived": false,
"modelMode": "deep_think",
"modelId": "deepseek-v4-pro"
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| title | string | 新标题(最长 200 字符) |
| isPinned | boolean | 置顶 |
| isArchived | boolean | 归档 |
| modelMode | ModelMode | 模型模式 |
| modelId | string \| null | 模型 ID |
**不可变字段:**
- `scopeType` — 创建后不可修改
- `scopeId` — 创建后不可修改
- `parentKnowledgeBaseId` — 创建后不可修改(由后端推导)
- `createdFrom` — 创建后不可修改
尝试修改这些字段 → 字段被静默忽略(不报错)
**Response 200:** 更新后的 ChatSession 对象
**Errors:**
| Code | 条件 |
|------|------|
| 404 | 会话不存在 |
| 403 | 会话不属于当前用户 |
---
### 4.7 DELETE /rag-chat/sessions/:id — 软删除
**Behavior:**
```
UPDATE ChatSession SET isDeleted = true WHERE id = :id AND userId = :userId
```
消息不物理删除。
**Response 200:**
```json
{
"success": true,
"message": "会话已删除"
}
```
---
## 5. TypeScript 类型 (后端)
```typescript
// === Request DTOs ===
interface CreateSessionDto {
scopeType: ChatScopeType;
scopeId?: string | null;
parentKnowledgeBaseId?: string | null;
createdFrom?: CreatedFrom;
title?: string;
}
interface ListSessionsQuery {
scopeType?: ChatScopeType;
scopeId?: string;
parentKnowledgeBaseId?: string;
isArchived?: boolean;
page?: number;
limit?: number;
}
interface SendMessageDto {
content: string;
}
interface UpdateSessionDto {
title?: string;
isPinned?: boolean;
isArchived?: boolean;
modelMode?: ModelMode;
modelId?: string | null;
}
// === Response Types ===
interface ChatSessionResponse {
id: string;
userId: string;
scopeType: ChatScopeType;
scopeId: string | null;
parentKnowledgeBaseId: string | null;
title: string;
createdFrom: CreatedFrom;
modelMode: ModelMode;
modelId: string | null;
isPinned: boolean;
isArchived: boolean;
isDeleted: boolean;
lastMessageAt: string | null;
createdAt: string;
updatedAt: string;
}
interface ChatMessageResponse {
id: string;
sessionId: string;
role: "user" | "assistant";
content: string;
tokens: number;
scopeSnapshot: ChatScope | null;
createdAt: string;
citations: ChatCitationResponse[];
}
interface ChatCitationResponse {
id: string;
messageId: string;
chunkId: string | null;
sourceId: string | null;
sourceTitle: string | null;
excerptText: string | null;
pageNumber: number | null;
lineStart: number | null;
lineEnd: number | null;
createdAt: string;
}
interface PaginatedResponse<T> {
data: T[];
meta: {
page: number;
limit: number;
total: number;
};
}
interface SendMessageResponse {
id?: string;
role?: string;
content?: string;
tokens?: number;
blocked?: boolean;
message?: ChatMessageResponse;
citations?: ChatCitationResponse[];
}
```
---
## 6. Swift 类型 (iOS)
```swift
// MARK: - Request DTOs
struct CreateChatSessionRequest: Codable {
let scopeType: String
let scopeId: String?
let parentKnowledgeBaseId: String?
let createdFrom: String
let title: String?
}
struct SendMessageRequest: Codable {
let content: String
}
struct UpdateChatSessionRequest: Codable {
var title: String?
var isPinned: Bool?
var isArchived: Bool?
var modelMode: String?
var modelId: String?
}
// MARK: - Response Types
struct ChatSessionResponse: Codable, Identifiable {
let id: String
let userId: String?
let scopeType: String
let scopeId: String?
let parentKnowledgeBaseId: String?
let title: String?
let createdFrom: String?
let modelMode: String?
let modelId: String?
let isPinned: Bool?
let isArchived: Bool?
let isDeleted: Bool?
let lastMessageAt: String?
let createdAt: String?
let updatedAt: String?
}
struct ChatMessageResponse: Codable, Identifiable {
let id: String
let sessionId: String?
let role: String
let content: String
let tokens: Int?
let scopeSnapshot: ChatScopeSnapshot?
let createdAt: String?
let citations: [ChatCitationResponse]?
}
struct ChatScopeSnapshot: Codable {
let scopeType: String?
let scopeId: String?
let parentKnowledgeBaseId: String?
}
struct ChatCitationResponse: Codable, Identifiable {
let id: String
let messageId: String?
let chunkId: String?
let sourceId: String?
let sourceTitle: String?
let excerptText: String?
let pageNumber: Int?
let lineStart: Int?
let lineEnd: Int?
let createdAt: String?
}
struct SendMessageResponse: Codable {
let id: String?
let role: String?
let content: String?
let tokens: Int?
let blocked: Bool?
let message: ChatMessageResponse?
let citations: [ChatCitationResponse]?
}
struct PaginatedChatSessions: Codable {
let data: [ChatSessionResponse]
let meta: PaginationMeta
}
// MARK: - SSE Chunk
struct SSEChunk: Decodable {
let type: String // "thinking" | "content" | "citations" | "done" | "error"
let content: String?
let citations: [ChatCitationResponse]?
let error: String?
}
```
---
## 7. 错误响应格式
所有错误统一格式:
```json
{
"statusCode": 400,
"message": "scopeType must be one of: knowledge_base, folder, material, knowledge_item, global",
"error": "Bad Request"
}
```
| HTTP Code | 场景 |
|-----------|------|
| 400 | 参数校验失败 |
| 401 | JWT 缺失/过期 |
| 403 | 会话不属于当前用户 |
| 404 | 会话不存在 |
| 413 | content 超过 10000 字符 |
| 500 | 服务端未知错误 |
---
## 8. 版本兼容性
| 版本 | 日期 | 变更 |
|------|------|------|
| v1.0 | 2026-06-06 | 初始版本 — 完整 ChatScope API |
向后兼容策略:
- 新增字段:前端未传 → 使用默认值
- 新增响应字段前端忽略未知字段Codable 默认行为)
- 不可变字段PATCH 时静默忽略(不报错)
---
## 9. 依赖
```
本文档依赖:
chat-scope-design.md (CHAT-001) — ChatScope 类型定义、规则、决策表、open-or-create 算法
本文档被依赖:
#81 M7-05 — createSession 实现
#76 M7-08 — open-or-create 实现
#75 M7-07 — listSessions 实现
#45-#50 — iOS AI Chat View
#39-#44 — iOS 入口接入
```
---
> **本文档是前后端接口的唯一权威契约。如有冲突,以本文档为准。**