为 polish 层引入与具体供应商解耦的抽象: - OpenLessCore: LLMProvider 协议、OpenAICompatibleConfig、LLMError - OpenLessPolish: 单一 OpenAICompatibleLLMProvider 实现,覆盖 Ark / OpenAI / DashScope / DeepSeek / Moonshot 等所有 OpenAI Chat Completions 协议端点 - DoubaoPolishClient 标记为 @available deprecated(Phase B-3 接入新 provider 后移除) - 新增 9 项 URLProtocol 桩测试,含 apiKey 不入日志的安全断言
74 lines
3.2 KiB
Swift
74 lines
3.2 KiB
Swift
import Foundation
|
||
|
||
// LLMProvider 是文本润色层与具体大模型供应商之间的抽象。
|
||
// Ark / OpenAI / 阿里云 DashScope / DeepSeek / Moonshot 等都共用 OpenAI Chat
|
||
// Completions 协议,所以我们只需要一份 Provider 实现 + 不同的 OpenAICompatibleConfig。
|
||
//
|
||
// 该协议放在 OpenLessCore 是为了避免 Core 之外的层之间互相依赖:UI/设置层通过 Core
|
||
// 拿到 Provider 抽象,OpenLessPolish 提供具体实现。
|
||
|
||
public protocol LLMProvider: Sendable {
|
||
/// 当前 Provider 使用的配置(baseURL / model / apiKey 等)。
|
||
var config: OpenAICompatibleConfig { get }
|
||
|
||
/// 把口语原始转写整理成最终输出文本。
|
||
/// - Parameters:
|
||
/// - rawText: ASR 给出的原始文本,可能包含口癖、乱断句。
|
||
/// - mode: 润色模式(原文 / 轻度 / 结构化 / 正式)。
|
||
/// - hotwords: 用户词典里的热词,仅在非空时附加到 system prompt。
|
||
/// - Returns: 经过模型整理后的纯文本,调用方仍需自己按业务做后处理(比如去前缀套话)。
|
||
func polish(rawText: String, mode: PolishMode, hotwords: [String]) async throws -> String
|
||
}
|
||
|
||
/// OpenAI Chat Completions 协议下的通用 Provider 配置。
|
||
///
|
||
/// 同一份结构能描述 Ark / OpenAI / DashScope / DeepSeek / Moonshot 等所有 OpenAI 兼容端点。
|
||
/// `providerId` 是一个稳定字符串,用于在持久化里区分不同 provider;`displayName` 给 UI 用。
|
||
public struct OpenAICompatibleConfig: Codable, Sendable, Hashable {
|
||
public let providerId: String
|
||
public let displayName: String
|
||
public let baseURL: URL
|
||
public let apiKey: String
|
||
public let model: String
|
||
public let extraHeaders: [String: String]
|
||
public let temperature: Double
|
||
public let requestTimeout: TimeInterval
|
||
|
||
public init(
|
||
providerId: String,
|
||
displayName: String,
|
||
baseURL: URL,
|
||
apiKey: String,
|
||
model: String,
|
||
extraHeaders: [String: String] = [:],
|
||
temperature: Double = 0.3,
|
||
requestTimeout: TimeInterval = 20
|
||
) {
|
||
self.providerId = providerId
|
||
self.displayName = displayName
|
||
self.baseURL = baseURL
|
||
self.apiKey = apiKey
|
||
self.model = model
|
||
self.extraHeaders = extraHeaders
|
||
self.temperature = temperature
|
||
self.requestTimeout = requestTimeout
|
||
}
|
||
}
|
||
|
||
/// LLMProvider 抛出的错误。
|
||
///
|
||
/// 这里没有把 URLError 直接暴露出去:URLError 不是 Sendable-friendly 的稳定值类型,
|
||
/// 而且我们要求 LLMError 是 Equatable(方便测试断言)。所以网络错误统一用 NSError 包装。
|
||
public enum LLMError: Error, Sendable, Equatable {
|
||
/// 缺少调用所需的凭证(apiKey 为空等)。
|
||
case missingCredentials
|
||
/// HTTP 状态码非 2xx。`body` 是响应体的前若干字符(不是完整 body,避免日志过大)。
|
||
case invalidResponse(statusCode: Int, body: String?)
|
||
/// 解析响应 JSON 失败,附带定位信息。
|
||
case parseError(String)
|
||
/// 请求超时(URLError.timedOut 等)。
|
||
case timeout
|
||
/// 其他网络错误,原始错误用 NSError 携带 domain/code/userInfo,保持 Equatable。
|
||
case network(NSError)
|
||
}
|