AI 服务
Narrator Android · AiService · OpenAI 兼容 /chat/completions · 流式 SSE + 非流式
AiService 是 Narrator 与大语言模型交互的统一封装,对应 Web 端 web/services/apiService.ts。
基于 OkHttp 4.12.0,封装 OpenAI 兼容 /chat/completions 接口,支持
流式(SSE)与非流式两种调用模式;并支持模型列表查询。
🔒 权限:
AndroidManifest.xml 声明 android.permission.INTERNET,v3 起为后续 AI 与对话功能预留。
1. 概览
| 项 | 说明 |
|---|---|
| 文件 | service/AiService.java |
| HTTP 客户端 | OkHttp(连接池 + 超时配置) |
| 目标接口 | {apiUrl}/chat/completions(POST,Bearer Token) |
| 服务商配置 | AIProviderEntity(apiUrl / apiKey / modelName)持久化于 Room ai_providers 表 |
| 对应 Web | web/services/apiService.ts sendChat / sendChatStream / fetchModels |
2. 双客户端
构造置器创建两个 OkHttpClient,按场景使用:
this.client = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS) // 长上下文/流式需要
.writeTimeout(30, TimeUnit.SECONDS)
.build();
this.shortClient = new OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS) // 用于 /models 轻型请求
.writeTimeout(15, TimeUnit.SECONDS)
.build();
getClient()— 对外提供主客户端(chat/completions)getShortClient()— 模型列表等轻量请求
3. 请求体与鉴权
3.1 buildBody(messages, stream, temperature)
{
"messages": [ ... ], // JSONArray,对话历史
"temperature": 0.7,
"max_tokens": 4096,
"stream": true // 仅流式请求带
}
3.2 buildRequest(provider, body)
使用服务商配置的模型名 + Bearer Token 鉴权:
body.put("model", provider.getModelName());
return new Request.Builder()
.url(provider.getApiUrl() + "/chat/completions")
.header("Authorization", "Bearer " + provider.getApiKey())
.header("Content-Type", "application/json")
.post(RequestBody.create(body.toString(), JSON))
.build();
4. 非流式调用 sendChat
public String sendChat(AIProviderEntity provider, JSONArray messages,
double temperature)
throws IOException, AiException
- 同步执行,返回完整文本。
- 解析
choices[0].message.content,自动trim()。 - HTTP 非 2xx 抛
AiException("API 返回 {code}: {body}")。 - 空 choices 抛
"API 返回空 choices"。
5. 流式调用 sendChatStream(SSE)
public interface StreamCallback {
void onChunk(String chunk); // 内容增量
void onReasoningChunk(String reasoning); // 推理增量(如 deepseek-reasoner)
void onComplete(String fullContent);
void onError(String error);
}
在独立线程执行;请求体带 stream: true,逐行读取 data: ...:
BufferedReader reader = new BufferedReader(
new InputStreamReader(response.body().byteStream()));
while ((line = reader.readLine()) != null) {
if (!line.startsWith("data: ")) continue;
String data = line.substring(6).trim();
if (data.equals("[DONE]")) break;
// 解析 choices[0]:优先 delta.content,再回退 message.content
}
5.1 增量来源(三层兼容)
| 来源字段 | 适用场景 |
|---|---|
choices[0].text | 非标准兼容:stream=true 仍返回完整 text |
choices[0].delta.content | 标准 OpenAI SSE 增量 |
choices[0].message.content | 非流式兼容回退 |
5.2 推理内容(reasoning_content)
对支持推理模型(如 deepseek-reasoner 的 delta.reasoning_content),单独累积并通过 onReasoningChunk 回调,与正文 content 分流。
📌 流程联结:
DeductionFragment 的「导演决策 / 角色发言 / 旁白收尾」三阶段均调用 sendChatStream(provider, messages, temperature, callback),
temperature 取自 CharData.temperature(每个角色可独立设置)。
6. 模型查询 fetchModels
public JSONArray fetchModels(String apiUrl, String apiKey)
throws IOException, JSONException
// GET {apiUrl}/models · Authorization: Bearer {apiKey}
// 解析 json.data 数组,每个元素是 JSONObject 含 "id"
使用短超时客户端 shortClient;AiProviderDetailFragment 用其填充模型选择下拉。
7. 消息构建工具
AiService 提供静态工厂方法构造 messages 数组元素:
public static final String SYSTEM_MSG = "你是一个有用的 AI 助手,请用中文回答。";
public static JSONObject userMessage(String content); // role="user"
public static JSONObject assistantMessage(String content); // role="assistant"
public static JSONObject systemMessage(String content); // role="system"
8. AiException
所有 AI 调用异常统一为内部异常类型,含 message 与可带 cause 的重载:
public static class AiException extends Exception {
public AiException(String message);
public AiException(String message, Throwable cause);
}
9. 服务商数据模型
AIProvider 配置对应 Room ai_providers 表,由 AIProviderRepository 管理:
model/entity/AIProviderEntity.java— 含 apiUrl / apiKey / modelName 等字段model/dao/AIProviderDao.java— Room DAO(增删改查)model/AIProviderRepository.java— 仓库层封装AiFragment列表展示;AiProviderDetailFragment编辑并调用fetchModels校验