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
对应 Webweb/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();

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

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-reasonerdelta.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"

使用短超时客户端 shortClientAiProviderDetailFragment 用其填充模型选择下拉。

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 管理:

详见 数据模型 · ai_providers 表