diff --git a/docs/03-接口文档.md b/docs/03-接口文档.md index f26a79a..3f7d28f 100644 --- a/docs/03-接口文档.md +++ b/docs/03-接口文档.md @@ -13,16 +13,28 @@ ``` 浏览器 Go Gateway :8080 - WebSocket Client <--> /ws (实时对话) - HTTP Client --> GET /api/health - HTTP Client <--> POST/DELETE /api/sessions + WebSocket Client <--> /ws?token= (实时对话,需 JWT 认证) + HTTP Client --> GET /api/health (健康检查) + HTTP Client <--> POST /api/auth/* (注册/登录/刷新/登出) + HTTP Client <--> GET/POST/PATCH/DELETE (对话 CRUD) + /api/conversations/* + HTTP Client <--> GET /api/conversations/:id (历史消息) + /messages + HTTP Client ~~> POST/DELETE /api/sessions (已废弃,保留兼容) ``` --- ## 一、WebSocket 协议 -连接地址:`ws://localhost:8080/ws` +连接地址:`ws://localhost:8080/ws?token=&conversation_id=` + +| 参数 | 必填 | 说明 | +|------|------|------| +| `token` | 是 | JWT access_token,缺失或无效时返回 401 | +| `conversation_id` | 否 | 恢复已有对话;省略则创建新对话 | + +> 详见"REST API → WebSocket 认证变更"章节。 ### 消息格式约定 @@ -95,8 +107,9 @@ interface PingMessage { ```typescript interface ConnectedMessage { type: "connected"; - session_id: string; // 服务端生成的会话 ID - server_version: string; // 服务端版本号,如 "0.1.0" + session_id: string; // 服务端生成的会话 ID + conversation_id: string; // 同 session_id,便于前端统一使用 + server_version: string; // 服务端版本号,如 "0.1.0" } ``` @@ -265,13 +278,446 @@ Client Server ## 二、REST API +### 通用约定 + +#### 认证方式 + +需要认证的接口在请求头携带 JWT access token: + +``` +Authorization: Bearer +``` + +未认证或 token 过期时返回 `401 Unauthorized`。 + +#### 错误响应格式 + +所有错误响应统一结构: + +```typescript +interface ApiError { + code: string; // 机器可读错误码 + message: string; // 人类可读描述 +} +``` + +示例: + +```json +{ + "code": "USERNAME_TAKEN", + "message": "username already taken" +} +``` + +#### 输入校验规则 + +| 字段 | 规则 | +|------|------| +| `username` | 3-64 字符,仅允许字母、数字、下划线 | +| `password` | 8-72 字符 | + +--- + +### 认证接口(`/api/auth`) + +#### 注册 + +``` +POST /api/auth/register +Content-Type: application/json +``` + +**请求体**: + +```typescript +interface RegisterRequest { + username: string; // 3-64 字符 + password: string; // 8-72 字符 +} +``` + +**成功响应** `201 Created`: + +```typescript +interface AuthResponse { + user: { + id: string; // UUID + username: string; + created_at: string; // ISO 8601 + }; + access_token: string; // JWT,15 分钟有效 + refresh_token: string; // JWT,7 天有效 +} +``` + +```json +{ + "user": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "username": "alice", + "created_at": "2026-06-14T10:00:00Z" + }, + "access_token": "eyJhbGciOiJIUzI1NiIs...", + "refresh_token": "eyJhbGciOiJIUzI1NiIs..." +} +``` + +**错误响应**: + +| 状态码 | code | 场景 | +|--------|------|------| +| 400 | `INVALID_INPUT` | 用户名/密码不符合校验规则 | +| 409 | `USERNAME_TAKEN` | 用户名已存在 | + +#### 登录 + +``` +POST /api/auth/login +Content-Type: application/json +``` + +**请求体**: + +```typescript +interface LoginRequest { + username: string; + password: string; +} +``` + +**成功响应** `200 OK`:同 `AuthResponse` 结构。 + +**错误响应**: + +| 状态码 | code | 场景 | +|--------|------|------| +| 400 | `INVALID_INPUT` | 请求参数缺失或格式错误 | +| 401 | `INVALID_CREDENTIALS` | 用户名或密码错误 | + +#### 刷新 Token + +``` +POST /api/auth/refresh +Content-Type: application/json +``` + +**请求体**: + +```typescript +interface RefreshRequest { + refresh_token: string; // 之前签发的 refresh_token +} +``` + +**成功响应** `200 OK`:同 `AuthResponse` 结构(返回新的 access_token + refresh_token,旧 refresh_token 失效——Token 轮转)。 + +**错误响应**: + +| 状态码 | code | 场景 | +|--------|------|------| +| 401 | `INVALID_TOKEN` | refresh_token 无效或已过期 | + +#### 登出 + +``` +POST /api/auth/logout +Content-Type: application/json +Authorization: Bearer +``` + +**请求体**: + +```typescript +interface LogoutRequest { + refresh_token: string; // 要废弃的 refresh_token +} +``` + +**成功响应** `204 No Content`(无响应体)。 + +**错误响应**: + +| 状态码 | code | 场景 | +|--------|------|------| +| 401 | `INVALID_TOKEN` | access_token 无效或已过期 | + +--- + +### 对话接口(`/api/conversations`) + +> 以下所有接口均需认证(`Authorization: Bearer `),省略不重复标注。 + +#### 对话列表 + +``` +GET /api/conversations?page=1&size=20 +``` + +**查询参数**: + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `page` | int | 1 | 页码,从 1 开始 | +| `size` | int | 20 | 每页条数,最大 50 | + +**成功响应** `200 OK`: + +```typescript +interface ConversationListResponse { + conversations: ConversationSummary[]; + total: number; // 总条数 + page: number; + size: number; +} + +interface ConversationSummary { + id: string; // 对话 ID(即 session_id) + title: string; // 对话标题(首条消息前 20 字) + last_message: string; // 最后一条消息内容预览 + message_count: number; // 消息总数 + updated_at: string; // ISO 8601,最后活跃时间 +} +``` + +```json +{ + "conversations": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "title": "这是一朵红色的玫瑰…", + "last_message": "它看起来很美丽。", + "message_count": 4, + "updated_at": "2026-06-14T10:05:30Z" + } + ], + "total": 1, + "page": 1, + "size": 20 +} +``` + +**错误响应**: + +| 状态码 | code | 场景 | +|--------|------|------| +| 401 | `INVALID_TOKEN` | 未认证或 token 过期 | + +#### 创建对话 + +``` +POST /api/conversations +Content-Type: application/json +``` + +**请求体**(可选,全部有默认值): + +```typescript +interface CreateConversationRequest { + config?: { + tts_enabled?: boolean; // 默认 true + detail_level?: "low" | "high"; // 默认 "low" + language?: string; // 默认 "zh-CN" + }; +} +``` + +**成功响应** `201 Created`: + +```typescript +interface ConversationDetail { + id: string; + title: string; + config: { + tts_enabled: boolean; + detail_level: "low" | "high"; + language: string; + }; + created_at: string; // ISO 8601 +} +``` + +```json +{ + "id": "660e8400-e29b-41d4-a716-446655440001", + "title": "新对话", + "config": { + "tts_enabled": true, + "detail_level": "low", + "language": "zh-CN" + }, + "created_at": "2026-06-14T11:00:00Z" +} +``` + +**错误响应**: + +| 状态码 | code | 场景 | +|--------|------|------| +| 401 | `INVALID_TOKEN` | 未认证或 token 过期 | + +#### 获取对话详情 + +``` +GET /api/conversations/:id +``` + +**成功响应** `200 OK`:同 `ConversationDetail` 结构。 + +**错误响应**: + +| 状态码 | code | 场景 | +|--------|------|------| +| 401 | `INVALID_TOKEN` | 未认证或 token 过期 | +| 404 | `SESSION_NOT_FOUND` | 对话不存在或不属于当前用户 | + +#### 更新对话标题 + +``` +PATCH /api/conversations/:id +Content-Type: application/json +``` + +**请求体**: + +```typescript +interface UpdateTitleRequest { + title: string; // 1-100 字符 +} +``` + +**成功响应** `200 OK`: + +```json +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "title": "新的自定义标题" +} +``` + +**错误响应**: + +| 状态码 | code | 场景 | +|--------|------|------| +| 400 | `INVALID_INPUT` | title 为空或超长 | +| 401 | `INVALID_TOKEN` | 未认证或 token 过期 | +| 404 | `SESSION_NOT_FOUND` | 对话不存在或不属于当前用户 | + +#### 删除对话 + +``` +DELETE /api/conversations/:id +``` + +**成功响应** `204 No Content`(无响应体)。 + +**错误响应**: + +| 状态码 | code | 场景 | +|--------|------|------| +| 401 | `INVALID_TOKEN` | 未认证或 token 过期 | +| 404 | `SESSION_NOT_FOUND` | 对话不存在或不属于当前用户 | + +#### 获取对话消息 + +``` +GET /api/conversations/:id/messages?limit=50&before= +``` + +**查询参数**: + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `limit` | int | 50 | 返回条数,最大 100 | +| `before` | int64 | — | 游标分页:返回此 message_id 之前的消息(不含),用于加载更多 | + +**成功响应** `200 OK`: + +```typescript +interface MessagesResponse { + messages: StoredMessage[]; + has_more: boolean; // 是否还有更早的消息 +} + +interface StoredMessage { + id: number; // 自增 ID,用于游标分页 + role: "user" | "assistant"; + content: string; + tokens_used: number; // 该条消息消耗的 token 数 + created_at: string; // ISO 8601 +} +``` + +```json +{ + "messages": [ + { + "id": 1001, + "role": "user", + "content": "这是什么花?", + "tokens_used": 0, + "created_at": "2026-06-14T10:01:00Z" + }, + { + "id": 1002, + "role": "assistant", + "content": "这是一朵红色的玫瑰。", + "tokens_used": 42, + "created_at": "2026-06-14T10:01:02Z" + } + ], + "has_more": false +} +``` + +**分页用法**:首次请求不带 `before`,获取最新消息。滚动到顶部时,取当前列表最小的 `id` 作为 `before` 参数请求更早的消息。 + +**错误响应**: + +| 状态码 | code | 场景 | +|--------|------|------| +| 401 | `INVALID_TOKEN` | 未认证或 token 过期 | +| 404 | `SESSION_NOT_FOUND` | 对话不存在或不属于当前用户 | + +--- + +### WebSocket 认证变更 + +连接地址变更为带 token 的查询参数: + +``` +ws://localhost:8080/ws?token=&conversation_id= +``` + +| 参数 | 必填 | 说明 | +|------|------|------| +| `token` | 是 | JWT access_token | +| `conversation_id` | 否 | 恢复已有对话;省略则创建新对话 | + +**认证失败响应**(HTTP 升级前返回): + +| 状态码 | 场景 | +|--------|------| +| 401 | token 缺失、无效或已过期 | + +**conversation_id 校验失败**: + +| 场景 | 处理 | +|------|------| +| 对话不存在 | 返回 401,`{"error": "SESSION_NOT_FOUND"}` | +| 对话不属于当前用户 | 返回 401,`{"error": "SESSION_NOT_FOUND"}`(与不存在相同,避免信息泄露) | + +--- + ### 健康检查 ``` GET /api/health ``` -响应: +无需认证。 + +**成功响应** `200 OK`: ```json { @@ -282,43 +728,21 @@ GET /api/health } ``` -### 创建会话(可选,MVP 自动创建) +--- + +### ~~旧会话接口~~(已废弃) + +> 以下端点已废弃,保留仅为向后兼容。新代码应使用 `/api/conversations` 系列接口。 ``` -POST /api/sessions -Content-Type: application/json - -{ - "config": { - "tts_enabled": true, - "detail_level": "low", - "language": "zh-CN" - } -} +POST /api/sessions → 改用 POST /api/conversations +DELETE /api/sessions/{id} → 改用 DELETE /api/conversations/{id} ``` -响应: - -```json -{ - "session_id": "550e8400-e29b-41d4-a716-446655440000", - "created_at": "2026-06-12T15:41:00Z" -} -``` - -### 销毁会话 - -``` -DELETE /api/sessions/{session_id} -``` - -响应:`204 No Content` - ### 预留端点(暂不实现) | 端点 | 方法 | 用途 | |------|------|------| -| `/api/sessions/{id}/messages` | GET | 查询对话历史 | | `/api/usage` | GET | 查询用量统计 | | `/api/users/{id}/preferences` | GET/PUT | 用户偏好管理 | @@ -689,6 +1113,7 @@ type Config struct { Redis RedisConfig `mapstructure:"redis"` AI AIConfig `mapstructure:"ai"` Storage StorageConfig `mapstructure:"storage"` + Auth AuthConfig `mapstructure:"auth"` Log LogConfig `mapstructure:"log"` } @@ -746,6 +1171,12 @@ type StorageConfig struct { DSN string `mapstructure:"dsn"` // PostgreSQL 连接串,driver=postgres 时必填 } +type AuthConfig struct { + JWTSecret string `mapstructure:"jwt_secret"` // 必须通过 CAMTALK_AUTH_JWT_SECRET 设置 + AccessTTL int `mapstructure:"access_ttl"` // 分钟,默认 15 + RefreshTTL int `mapstructure:"refresh_ttl"` // 分钟,默认 10080(7 天) +} + type LogConfig struct { Level string `mapstructure:"level"` // "debug" | "info" | "warn" | "error",默认 "info" Format string `mapstructure:"format"` // "json" | "console",生产用 json @@ -791,6 +1222,10 @@ ai: storage: driver: memory +auth: + access_ttl: 15 # access token 有效期(分钟) + refresh_ttl: 10080 # refresh token 有效期(分钟,7 天) + log: level: info format: console @@ -811,6 +1246,9 @@ Viper 自动将配置项映射为环境变量,规则:**前缀 `CAMTALK_` + | `ai.llm.model` | `CAMTALK_AI_LLM_MODEL` | `gpt-4o` | | `storage.driver` | `CAMTALK_STORAGE_DRIVER` | `postgres` | | `storage.dsn` | `CAMTALK_STORAGE_DSN` | — | +| `auth.jwt_secret` | `CAMTALK_AUTH_JWT_SECRET` | —(必填,仅环境变量) | +| `auth.access_ttl` | `CAMTALK_AUTH_ACCESS_TTL` | `15` | +| `auth.refresh_ttl` | `CAMTALK_AUTH_REFRESH_TTL` | `10080` | | `app.env` | `CAMTALK_APP_ENV` | `prod` | | `log.level` | `CAMTALK_LOG_LEVEL` | `warn` | | `log.format` | `CAMTALK_LOG_FORMAT` | `json` | @@ -892,6 +1330,7 @@ CAMTALK_AI_STT_API_KEY=xxx \ CAMTALK_AI_TTS_API_KEY=xxx \ CAMTALK_STORAGE_DRIVER=postgres \ CAMTALK_STORAGE_DSN="postgres://user:pass@db:5432/camtalk?sslmode=disable" \ +CAMTALK_AUTH_JWT_SECRET="$(openssl rand -hex 32)" \ CAMTALK_LOG_LEVEL=warn \ CAMTALK_LOG_FORMAT=json \ ./bin/camtalk @@ -910,7 +1349,10 @@ CAMTALK_LOG_FORMAT=json \ type Session struct { ID string `json:"session_id"` + UserID string `json:"user_id"` // 关联用户,空串表示匿名 + Title string `json:"title"` // 对话标题 CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` Config SessionConfig `json:"config"` } @@ -932,6 +1374,33 @@ type Message struct { Role string `json:"role"` // "user" | "assistant" Content string `json:"content"` } + +// ---- 用户模块 ---- + +type User struct { + ID string `json:"id"` + Username string `json:"username"` + PasswordHash string `json:"-"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ConversationSummary struct { + ID string `json:"id"` + Title string `json:"title"` + LastMessage string `json:"last_message"` + MessageCount int `json:"message_count"` + UpdatedAt time.Time `json:"updated_at"` +} + +type StoredMessage struct { + ID int64 `json:"id"` + SessionID string `json:"-"` + Role string `json:"role"` + Content string `json:"content"` + TokensUsed int `json:"tokens_used"` + CreatedAt time.Time `json:"created_at"` +} ``` ### TypeScript 前端模型 @@ -957,6 +1426,60 @@ interface ChatMessage { tokensUsed?: number; } +// ---- 用户模块 ---- + +interface AuthTokens { + accessToken: string; + refreshToken: string; +} + +interface User { + id: string; // UUID + username: string; + created_at: string; // ISO 8601 +} + +interface AuthResponse { + user: User; + access_token: string; + refresh_token: string; +} + +interface ConversationSummary { + id: string; + title: string; + last_message: string; + message_count: number; + updated_at: string; +} + +interface ConversationListResponse { + conversations: ConversationSummary[]; + total: number; + page: number; + size: number; +} + +interface ConversationDetail { + id: string; + title: string; + config: SessionConfig; + created_at: string; +} + +interface StoredMessage { + id: number; + role: "user" | "assistant"; + content: string; + tokens_used: number; + created_at: string; +} + +interface MessagesResponse { + messages: StoredMessage[]; + has_more: boolean; +} + // WebSocket 消息联合类型 type ServerMessage = | ConnectedMessage @@ -1054,18 +1577,22 @@ func NewApp(cfg *Config) *App { ## 九、错误码 -| 错误码 | 含义 | 客户端处理建议 | -|--------|------|--------------| -| `INVALID_MESSAGE` | 消息格式不合法 | 检查 JSON 结构,不重试 | -| `SESSION_NOT_FOUND` | 会话不存在或已过期 | 重新建立 WebSocket 连接 | -| `RATE_LIMITED` | 请求频率超限 | 延迟后重试,提示用户稍等 | -| `IMAGE_TOO_LARGE` | 图像超过 4MB 限制 | 降低分辨率或压缩质量 | -| `AUDIO_TOO_SHORT` | 音频片段 < 250ms | 忽略,等待下次语音输入 | -| `LLM_TIMEOUT` | LLM 推理超时(>10s) | 提示用户重试 | -| `LLM_ERROR` | LLM 服务异常 | 提示用户重试,服务端记录日志 | -| `STT_ERROR` | 语音识别失败 | 回退到纯文本输入模式 | -| `TTS_ERROR` | 语音合成失败 | 静默回退到纯文本回复 | -| `INTERNAL_ERROR` | 服务端内部错误 | 提示用户重试 | +| 错误码 | HTTP 状态码 | 含义 | 客户端处理建议 | +|--------|-----------|------|--------------| +| `INVALID_MESSAGE` | — | 消息格式不合法(WS) | 检查 JSON 结构,不重试 | +| `SESSION_NOT_FOUND` | 404 | 会话/对话不存在或已过期 | 重新建立连接或刷新列表 | +| `RATE_LIMITED` | 429 | 请求频率超限 | 延迟后重试,提示用户稍等 | +| `IMAGE_TOO_LARGE` | — | 图像超过 4MB 限制(WS) | 降低分辨率或压缩质量 | +| `AUDIO_TOO_SHORT` | — | 音频片段 < 250ms(WS) | 忽略,等待下次语音输入 | +| `LLM_TIMEOUT` | — | LLM 推理超时 >10s(WS) | 提示用户重试 | +| `LLM_ERROR` | — | LLM 服务异常(WS) | 提示用户重试,服务端记录日志 | +| `STT_ERROR` | — | 语音识别失败(WS) | 回退到纯文本输入模式 | +| `TTS_ERROR` | — | 语音合成失败(WS) | 静默回退到纯文本回复 | +| `INTERNAL_ERROR` | 500 | 服务端内部错误 | 提示用户重试 | +| `USERNAME_TAKEN` | 409 | 用户名已被注册 | 提示换一个用户名 | +| `INVALID_CREDENTIALS` | 401 | 用户名或密码错误 | 提示检查输入 | +| `INVALID_TOKEN` | 401 | JWT 无效或已过期 | 尝试 refresh,失败则重新登录 | +| `INVALID_INPUT` | 400 | 请求参数校验失败 | 检查字段规则后重试 | ## 十、连接管理 diff --git a/docs/PLAN_USER_MODULE.md b/docs/PLAN_USER_MODULE.md index 2bc764e..6e1dd9f 100644 --- a/docs/PLAN_USER_MODULE.md +++ b/docs/PLAN_USER_MODULE.md @@ -713,6 +713,588 @@ func main() { --- +## 前端 API 接口参考 + +本章节为前端开发者提供完整的 REST API 契约。所有接口以 JSON 通信,基地址与 WebSocket 同源(开发环境 `http://localhost:8080`,生产环境通过 Nginx 反代)。 + +### 通用约定 + +#### 认证方式 + +需要认证的接口在请求头携带 JWT access token: + +``` +Authorization: Bearer +``` + +未认证或 token 过期时返回 `401 Unauthorized`。 + +#### 错误响应格式 + +所有错误响应统一结构: + +```typescript +interface ApiError { + code: string; // 机器可读错误码 + message: string; // 人类可读描述 +} +``` + +示例: + +```json +{ + "code": "USERNAME_TAKEN", + "message": "username already taken" +} +``` + +#### 新增错误码 + +| 错误码 | HTTP 状态码 | 含义 | +|--------|-----------|------| +| `USERNAME_TAKEN` | 409 | 用户名已被注册 | +| `INVALID_CREDENTIALS` | 401 | 用户名或密码错误 | +| `INVALID_TOKEN` | 401 | JWT 无效或已过期 | +| `INVALID_INPUT` | 400 | 请求参数校验失败 | +| `SESSION_NOT_FOUND` | 404 | 对话不存在或无权访问 | + +#### 输入校验规则 + +| 字段 | 规则 | +|------|------| +| `username` | 3-64 字符,仅允许字母、数字、下划线 | +| `password` | 8-72 字符 | + +--- + +### 一、认证接口(`/api/auth`) + +#### 1.1 注册 + +``` +POST /api/auth/register +Content-Type: application/json +``` + +**请求体**: + +```typescript +interface RegisterRequest { + username: string; // 3-64 字符 + password: string; // 8-72 字符 +} +``` + +**成功响应** `201 Created`: + +```typescript +interface AuthResponse { + user: { + id: string; // UUID + username: string; + created_at: string; // ISO 8601 + }; + access_token: string; // JWT,15 分钟有效 + refresh_token: string; // JWT,7 天有效 +} +``` + +```json +{ + "user": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "username": "alice", + "created_at": "2026-06-14T10:00:00Z" + }, + "access_token": "eyJhbGciOiJIUzI1NiIs...", + "refresh_token": "eyJhbGciOiJIUzI1NiIs..." +} +``` + +**错误响应**: + +| 状态码 | code | 场景 | +|--------|------|------| +| 400 | `INVALID_INPUT` | 用户名/密码不符合校验规则 | +| 409 | `USERNAME_TAKEN` | 用户名已存在 | + +--- + +#### 1.2 登录 + +``` +POST /api/auth/login +Content-Type: application/json +``` + +**请求体**: + +```typescript +interface LoginRequest { + username: string; + password: string; +} +``` + +**成功响应** `200 OK`:同 `AuthResponse` 结构。 + +**错误响应**: + +| 状态码 | code | 场景 | +|--------|------|------| +| 400 | `INVALID_INPUT` | 请求参数缺失或格式错误 | +| 401 | `INVALID_CREDENTIALS` | 用户名或密码错误 | + +--- + +#### 1.3 刷新 Token + +``` +POST /api/auth/refresh +Content-Type: application/json +``` + +**请求体**: + +```typescript +interface RefreshRequest { + refresh_token: string; // 之前签发的 refresh_token +} +``` + +**成功响应** `200 OK`:同 `AuthResponse` 结构(返回新的 access_token + refresh_token,旧 refresh_token 失效——Token 轮转)。 + +**错误响应**: + +| 状态码 | code | 场景 | +|--------|------|------| +| 401 | `INVALID_TOKEN` | refresh_token 无效或已过期 | + +--- + +#### 1.4 登出 + +``` +POST /api/auth/logout +Content-Type: application/json +Authorization: Bearer +``` + +**请求体**: + +```typescript +interface LogoutRequest { + refresh_token: string; // 要废弃的 refresh_token +} +``` + +**成功响应** `204 No Content`(无响应体)。 + +**错误响应**: + +| 状态码 | code | 场景 | +|--------|------|------| +| 401 | `INVALID_TOKEN` | access_token 无效或已过期 | + +--- + +### 二、对话接口(`/api/conversations`) + +> 以下所有接口均需认证(`Authorization: Bearer `),省略不重复标注。 + +#### 2.1 对话列表 + +``` +GET /api/conversations?page=1&size=20 +``` + +**查询参数**: + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `page` | int | 1 | 页码,从 1 开始 | +| `size` | int | 20 | 每页条数,最大 50 | + +**成功响应** `200 OK`: + +```typescript +interface ConversationListResponse { + conversations: ConversationSummary[]; + total: number; // 总条数 + page: number; + size: number; +} + +interface ConversationSummary { + id: string; // 对话 ID(即 session_id) + title: string; // 对话标题(首条消息前 20 字) + last_message: string; // 最后一条消息内容预览 + message_count: number; // 消息总数 + updated_at: string; // ISO 8601,最后活跃时间 +} +``` + +```json +{ + "conversations": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "title": "这是一朵红色的玫瑰…", + "last_message": "它看起来很美丽。", + "message_count": 4, + "updated_at": "2026-06-14T10:05:30Z" + } + ], + "total": 1, + "page": 1, + "size": 20 +} +``` + +**错误响应**: + +| 状态码 | code | 场景 | +|--------|------|------| +| 401 | `INVALID_TOKEN` | 未认证或 token 过期 | + +--- + +#### 2.2 创建对话 + +``` +POST /api/conversations +Content-Type: application/json +``` + +**请求体**(可选,全部有默认值): + +```typescript +interface CreateConversationRequest { + config?: { + tts_enabled?: boolean; // 默认 true + detail_level?: "low" | "high"; // 默认 "low" + language?: string; // 默认 "zh-CN" + }; +} +``` + +**成功响应** `201 Created`: + +```typescript +interface ConversationDetail { + id: string; + title: string; + config: { + tts_enabled: boolean; + detail_level: "low" | "high"; + language: string; + }; + created_at: string; // ISO 8601 +} +``` + +```json +{ + "id": "660e8400-e29b-41d4-a716-446655440001", + "title": "新对话", + "config": { + "tts_enabled": true, + "detail_level": "low", + "language": "zh-CN" + }, + "created_at": "2026-06-14T11:00:00Z" +} +``` + +**错误响应**: + +| 状态码 | code | 场景 | +|--------|------|------| +| 401 | `INVALID_TOKEN` | 未认证或 token 过期 | + +--- + +#### 2.3 获取对话详情 + +``` +GET /api/conversations/:id +``` + +**成功响应** `200 OK`:同 `ConversationDetail` 结构。 + +**错误响应**: + +| 状态码 | code | 场景 | +|--------|------|------| +| 401 | `INVALID_TOKEN` | 未认证或 token 过期 | +| 404 | `SESSION_NOT_FOUND` | 对话不存在或不属于当前用户 | + +--- + +#### 2.4 更新对话标题 + +``` +PATCH /api/conversations/:id +Content-Type: application/json +``` + +**请求体**: + +```typescript +interface UpdateTitleRequest { + title: string; // 1-100 字符 +} +``` + +**成功响应** `200 OK`: + +```json +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "title": "新的自定义标题" +} +``` + +**错误响应**: + +| 状态码 | code | 场景 | +|--------|------|------| +| 400 | `INVALID_INPUT` | title 为空或超长 | +| 401 | `INVALID_TOKEN` | 未认证或 token 过期 | +| 404 | `SESSION_NOT_FOUND` | 对话不存在或不属于当前用户 | + +--- + +#### 2.5 删除对话 + +``` +DELETE /api/conversations/:id +``` + +**成功响应** `204 No Content`(无响应体)。 + +**错误响应**: + +| 状态码 | code | 场景 | +|--------|------|------| +| 401 | `INVALID_TOKEN` | 未认证或 token 过期 | +| 404 | `SESSION_NOT_FOUND` | 对话不存在或不属于当前用户 | + +--- + +#### 2.6 获取对话消息 + +``` +GET /api/conversations/:id/messages?limit=50&before= +``` + +**查询参数**: + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `limit` | int | 50 | 返回条数,最大 100 | +| `before` | int64 | — | 游标分页:返回此 message_id 之前的消息(不含),用于加载更多 | + +**成功响应** `200 OK`: + +```typescript +interface MessagesResponse { + messages: StoredMessage[]; + has_more: boolean; // 是否还有更早的消息 +} + +interface StoredMessage { + id: number; // 自增 ID,用于游标分页 + role: "user" | "assistant"; + content: string; + tokens_used: number; // 该条消息消耗的 token 数 + created_at: string; // ISO 8601 +} +``` + +```json +{ + "messages": [ + { + "id": 1001, + "role": "user", + "content": "这是什么花?", + "tokens_used": 0, + "created_at": "2026-06-14T10:01:00Z" + }, + { + "id": 1002, + "role": "assistant", + "content": "这是一朵红色的玫瑰。", + "tokens_used": 42, + "created_at": "2026-06-14T10:01:02Z" + } + ], + "has_more": false +} +``` + +**分页用法**:首次请求不带 `before`,获取最新消息。滚动到顶部时,取当前列表最小的 `id` 作为 `before` 参数请求更早的消息。 + +**错误响应**: + +| 状态码 | code | 场景 | +|--------|------|------| +| 401 | `INVALID_TOKEN` | 未认证或 token 过期 | +| 404 | `SESSION_NOT_FOUND` | 对话不存在或不属于当前用户 | + +--- + +### 三、WebSocket 认证变更 + +连接地址变更为带 token 的查询参数: + +``` +ws://localhost:8080/ws?token=&conversation_id= +``` + +| 参数 | 必填 | 说明 | +|------|------|------| +| `token` | 是 | JWT access_token | +| `conversation_id` | 否 | 恢复已有对话;省略则创建新对话 | + +**认证失败响应**(HTTP 升级前返回): + +| 状态码 | 场景 | +|--------|------| +| 401 | token 缺失、无效或已过期 | + +**conversation_id 校验失败**: + +| 场景 | 处理 | +|------|------| +| 对话不存在 | 返回 401,`{"error": "SESSION_NOT_FOUND"}` | +| 对话不属于当前用户 | 返回 401,`{"error": "SESSION_NOT_FOUND"}`(与不存在相同,避免信息泄露) | + +**连接成功后**:`connected` 消息不变,新增 `conversation_id` 字段标识当前对话: + +```typescript +interface ConnectedMessage { + type: "connected"; + session_id: string; // 对话 ID + conversation_id: string; // 同 session_id,便于前端统一使用 + server_version: string; +} +``` + +--- + +### 四、前端调用示例 + +#### 认证状态管理 + +```typescript +// 存储 token(建议 localStorage 或内存,视安全需求) +interface AuthTokens { + accessToken: string; + refreshToken: string; +} + +// 请求拦截器:自动附加 Authorization 头 +async function authFetch(url: string, options: RequestInit = {}): Promise { + const tokens = getStoredTokens(); + const headers = { + ...options.headers, + "Authorization": `Bearer ${tokens.accessToken}`, + }; + + let resp = await fetch(url, { ...options, headers }); + + // 401 时尝试刷新 token + if (resp.status === 401 && tokens.refreshToken) { + const refreshResp = await fetch("/api/auth/refresh", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ refresh_token: tokens.refreshToken }), + }); + + if (refreshResp.ok) { + const newTokens: AuthResponse = await refreshResp.json(); + storeTokens({ + accessToken: newTokens.access_token, + refreshToken: newTokens.refresh_token, + }); + // 用新 token 重试原请求 + headers["Authorization"] = `Bearer ${newTokens.access_token}`; + resp = await fetch(url, { ...options, headers }); + } else { + // refresh 也失败,跳转登录 + redirectToLogin(); + } + } + + return resp; +} +``` + +#### 注册 + 登录 + +```typescript +async function register(username: string, password: string): Promise { + const resp = await fetch("/api/auth/register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username, password }), + }); + + if (!resp.ok) { + const err: ApiError = await resp.json(); + throw new Error(err.message); // "username already taken" 等 + } + + return resp.json(); +} +``` + +#### 获取对话列表 + +```typescript +async function getConversations(page = 1, size = 20): Promise { + const resp = await authFetch( + `/api/conversations?page=${page}&size=${size}` + ); + if (!resp.ok) throw new Error("Failed to load conversations"); + return resp.json(); +} +``` + +#### 加载对话历史消息 + +```typescript +async function getMessages( + conversationId: string, + limit = 50, + before?: number +): Promise { + let url = `/api/conversations/${conversationId}/messages?limit=${limit}`; + if (before !== undefined) url += `&before=${before}`; + + const resp = await authFetch(url); + if (!resp.ok) throw new Error("Failed to load messages"); + return resp.json(); +} +``` + +#### 建立 WebSocket 连接(带认证) + +```typescript +function connectWebSocket(accessToken: string, conversationId?: string): WebSocket { + let url = `/ws?token=${encodeURIComponent(accessToken)}`; + if (conversationId) { + url += `&conversation_id=${encodeURIComponent(conversationId)}`; + } + return new WebSocket(url); +} +``` + +--- + ## 关键文件清单 ```