AI 模块 API 文档
基础信息
- Base URL:
/api/ai - 认证方式: JWT Bearer Token
数据库表
api_usage_logs 表(AI 调用记录)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | text (PK) | 必填 | 10位随机ID |
| user_id | text (FK→users) | 选填 | 请求用户 |
| bill_id | text (FK→bills) | 选填 | 账单 |
| provider | text | 选填 | AI 提供商 |
| model | text | 选填 | 模型名 |
| prompt_tokens | integer | 选填 | 输入 token 数 |
| completion_tokens | integer | 选填 | 输出 token 数 |
| total_tokens | integer | 选填 | 总 token 数 |
| duration_ms | integer | 选填 | 请求耗时(毫秒) |
| created_at | datetime | 必填 | 请求时间 |
接口列表
1. 文本对话(流式 SSE)
POST /api/ai/chat
触发时机: 在 首页(HomePage) 的 ChatBar 输入文字消息后点击发送(或按回车)
HomePage ChatBar → 输入文字 → 点击发送/按回车 → POST /api/ai/chat
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| billId | string | 必填 | 账单ID |
| provider | string | 必填 | AI 提供商(openai/anthropic/gemini) |
| model | string | 选填 | 模型名(不传自动选择) |
| messages | array | 必填 | 对话历史 [{role, content}] |
| owner_type | string | 选填 | 指定 Key 类型 |
| owner_id | number | 选填 | 指定 Key 所有者 |
messages 格式:
[
{ "role": "system", "content": "你是一个助手..." },
{ "role": "user", "content": "这个月花了多少钱?" }
]
后端处理:
- 校验用户是账单成员
- Key 解析(优先级:owner_type+owner_id > provider+model > provider 自动)
- 调用
proxyChatByProvider,流式输出 SSE
响应格式(SSE):
data: [DONE]
或错误时:
data: [ERROR] AI 请求失败,请稍后重试
data: [DONE]
重要问题: Anthropic 的 `proxyChatByProvider` 实现实际用的是 **non-streaming** `/v1/messages` 端点,而非流式。对 Anthropic Provider,`/chat` 实际上**不会返回真正的 SSE 流**。
2. 文本对话(非流式)
POST /api/ai/chat-simple
触发时机: HomePage 加载时(可能用于获取欢迎语),或 ChatBar 的 useEffect 初始化
HomePage 加载 → 可能在首次打开 ChatBar 时调用 → POST /api/ai/chat-simple
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| billId | string | 必填 | 账单ID |
| provider | string | 选填 | AI 提供商(不传自动选择) |
| model | string | 选填 | 模型名 |
| messages | array | 必填 | 对话历史 |
后端处理:
- 与
/chat类似,但不输出 SSE - 无可用 Key 时返回
{ content: "", info: "no_ai_configured" }
3. 语音转写(Whisper)
POST /api/ai/transcribe
触发时机: 在 HomePage ChatBar 按住麦克风按钮说话,松开后自动触发
HomePage ChatBar → 按住麦克风按钮 → 说话 → 松开 → POST /api/ai/transcribe
Content-Type: multipart/form-data
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| billId | string | 必填 | 账单ID(FormData) |
| provider | string | 选填 | 提供商(默认 openai,Whisper) |
| audio | File | 必填 | 音频文件,最大 12MB |
后端处理:
- 校验用户是账单成员
- 获取 OpenAI Whisper Key
- POST 到 OpenAI Transcription API(语言
zh,格式json,温度0)
依赖: 必须配置了 OpenAI Key(Whisper 仅支持 OpenAI)
响应:
{ "text": "转写后的文字内容" }
错误:
- 无 OpenAI Key → 400:
语音转写需要可用的 OpenAI Key - 音频无法识别 → 422:
未识别到有效语音内容
4. 语音直发对话
POST /api/ai/voice-chat
触发时机: 在 HomePage ChatBar 点击「语音直发」模式,说完一段话后自动发送
HomePage ChatBar → 切换到语音直发模式 → 按住说话 → 松开发送 → POST /api/ai/voice-chat
Content-Type: multipart/form-data
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| billId | string | 必填 | 账单ID |
| provider | string | 选填 | 偏好提供商(默认 openai) |
| systemPrompt | string | 选填 | 系统提示词 |
| history | string | 选填 | 历史消息(JSON 字符串) |
| audio | File | 必填 | 音频文件,最大 12MB |
后端处理:
- 获取语音候选 Key(
resolveNativeVoiceCandidates) - 遍历候选,用每个 Provider 的 TTS/语音模型处理
- 第一个成功立即返回
问题: 所有 Provider 都失败时返回 502,无友好提示
Key 解析机制(后端内部逻辑)
优先级
owner_type + owner_id 精确匹配
↓ 不存在
provider + model 精确匹配(当前用户 Key)
↓ 不存在
provider 自动选择(priority 最小的)
↓ 都不存在
返回 400 错误
可用 Provider(chatProviders.js)
| Provider | 模型示例 | API Endpoint | 流式 |
|---|---|---|---|
| openai | gpt-4o, gpt-4o-mini | api.openai.com/v1/chat/completions | 必填 |
| anthropic | claude-3-5-sonnet | api.anthropic.com/v1/messages | 选填(实际不走流) |
| gemini | gemini-1.5-pro | generativelanguage.googleapis.com | 必填 |
| ollama | llama3, qwen | localhost:11434/api/chat | 必填 |
| azure | gpt-4o | (Azure URL) | 必填 |
| deepseek | deepseek-chat | api.deepseek.com/v1/chat/completions | 必填 |
API 调用日志记录
成功调用后由 proxyChatByProvider 记录到 api_usage_logs 表:
// 在 chatProviders.js 中,成功响应后:
db.knex('api_usage_logs').insert({
user_id, bill_id, provider, model,
prompt_tokens, completion_tokens, total_tokens,
duration_ms: elapsed
})
注意: 日志记录是在 `proxyChatByProvider` 中进行的,如果 Provider 返回错误,可能不会记录日志
已知问题汇总
- Anthropic 流式假象:
/chat对 Anthropic 实际不走 SSE(非流式/v1/messages) - Key 明文存储:
api_key_encrypted= Base64 编码,可直接解码 - Ollama 无认证: 通常本地运行,无 API Key 验证
- 语音直发全部失败: 返回 502,无友好提示
- API 日志记录不完整: Provider 请求失败时不记录
api_usage_logs