docs: 补充用户模块 REST API 接口契约
- PLAN_USER_MODULE.md 新增「前端 API 接口参考」章节 - 03-接口文档.md 同步认证接口、对话接口、WebSocket 认证变更 - 新增错误码 USERNAME_TAKEN / INVALID_CREDENTIALS / INVALID_TOKEN / INVALID_INPUT - 数据模型补充 User / ConversationSummary / StoredMessage 及对应 TypeScript 类型 - 配置结构体补充 AuthConfig(JWTSecret / AccessTTL / RefreshTTL)
This commit is contained in:
623
docs/03-接口文档.md
623
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> (实时对话,需 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=<access_token>&conversation_id=<uuid>`
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `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 <access_token>
|
||||
```
|
||||
|
||||
未认证或 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 <access_token>
|
||||
```
|
||||
|
||||
**请求体**:
|
||||
|
||||
```typescript
|
||||
interface LogoutRequest {
|
||||
refresh_token: string; // 要废弃的 refresh_token
|
||||
}
|
||||
```
|
||||
|
||||
**成功响应** `204 No Content`(无响应体)。
|
||||
|
||||
**错误响应**:
|
||||
|
||||
| 状态码 | code | 场景 |
|
||||
|--------|------|------|
|
||||
| 401 | `INVALID_TOKEN` | access_token 无效或已过期 |
|
||||
|
||||
---
|
||||
|
||||
### 对话接口(`/api/conversations`)
|
||||
|
||||
> 以下所有接口均需认证(`Authorization: Bearer <access_token>`),省略不重复标注。
|
||||
|
||||
#### 对话列表
|
||||
|
||||
```
|
||||
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=<message_id>
|
||||
```
|
||||
|
||||
**查询参数**:
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `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=<access_token>&conversation_id=<uuid>
|
||||
```
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `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 | 请求参数校验失败 | 检查字段规则后重试 |
|
||||
|
||||
## 十、连接管理
|
||||
|
||||
|
||||
@@ -713,6 +713,588 @@ func main() {
|
||||
|
||||
---
|
||||
|
||||
## 前端 API 接口参考
|
||||
|
||||
本章节为前端开发者提供完整的 REST API 契约。所有接口以 JSON 通信,基地址与 WebSocket 同源(开发环境 `http://localhost:8080`,生产环境通过 Nginx 反代)。
|
||||
|
||||
### 通用约定
|
||||
|
||||
#### 认证方式
|
||||
|
||||
需要认证的接口在请求头携带 JWT access token:
|
||||
|
||||
```
|
||||
Authorization: Bearer <access_token>
|
||||
```
|
||||
|
||||
未认证或 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 <access_token>
|
||||
```
|
||||
|
||||
**请求体**:
|
||||
|
||||
```typescript
|
||||
interface LogoutRequest {
|
||||
refresh_token: string; // 要废弃的 refresh_token
|
||||
}
|
||||
```
|
||||
|
||||
**成功响应** `204 No Content`(无响应体)。
|
||||
|
||||
**错误响应**:
|
||||
|
||||
| 状态码 | code | 场景 |
|
||||
|--------|------|------|
|
||||
| 401 | `INVALID_TOKEN` | access_token 无效或已过期 |
|
||||
|
||||
---
|
||||
|
||||
### 二、对话接口(`/api/conversations`)
|
||||
|
||||
> 以下所有接口均需认证(`Authorization: Bearer <access_token>`),省略不重复标注。
|
||||
|
||||
#### 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=<message_id>
|
||||
```
|
||||
|
||||
**查询参数**:
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `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=<access_token>&conversation_id=<uuid>
|
||||
```
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `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<Response> {
|
||||
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<AuthResponse> {
|
||||
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<ConversationListResponse> {
|
||||
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<MessagesResponse> {
|
||||
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);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 关键文件清单
|
||||
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user