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

16 KiB
Raw Blame History

ChatScope API Contract

CHAT-002 | 版本 v1.0 | 2026-06-06

本文档是 ChatScope 前后端接口的唯一权威契约。 所有响应 shape、错误码、SSE 格式以本文档为准。 设计逻辑参见 ChatScope 设计文档


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

type ChatScopeType =
  | "knowledge_base"
  | "folder"
  | "material"
  | "knowledge_item"
  | "global";

2.2 CreatedFrom

type CreatedFrom =
  | "knowledge_base_detail"
  | "material_detail"
  | "material_reader"
  | "knowledge_item_detail"
  | "folder_detail"
  | "global_ai_entry"
  | "legacy_migration";

2.3 ModelMode

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:

{
  "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 (已有会话):

{
  "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 (新建会话):

{
  "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:

{
  "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:

[
  {
    "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:

{
  "content": "TCP 三次握手的过程是什么?"
}
字段 类型 必需 说明
content string 消息正文(最长 10000 字符)

Response 200:

{
  "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 (被拦截):

{
  "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 类型定义:

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. doneerror → 结束流

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 (全部可选):

{
  "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:

{
  "success": true,
  "message": "会话已删除"
}

5. 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)

// 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. 错误响应格式

所有错误统一格式:

{
  "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 入口接入

本文档是前后端接口的唯一权威契约。如有冲突,以本文档为准。