- updateText 移除文本宽度硬编码下限 160,真正根据内容自适应 - showRefining/showThinking 简化为调用 updateText,复用自适应逻辑 - 面板最小宽度由 minPanelWidth(180pt) 兜底 - 打包 VoiceInput-20260425.dmg
131 lines
5.5 KiB
Swift
131 lines
5.5 KiB
Swift
import Foundation
|
||
|
||
/// 通过 OpenAI 兼容 API 进行 LLM 处理
|
||
final class LLMRefiner {
|
||
|
||
private let session: URLSession = {
|
||
let config = URLSessionConfiguration.default
|
||
config.timeoutIntervalForRequest = 30
|
||
config.timeoutIntervalForResource = 60
|
||
return URLSession(configuration: config)
|
||
}()
|
||
|
||
struct RefinementResult {
|
||
let text: String
|
||
let wasChanged: Bool
|
||
}
|
||
|
||
// MARK: - Input 模式:语音输入纠错(Fn 长按触发)
|
||
|
||
func refine(text: String, config: LLMConfig) async throws -> RefinementResult {
|
||
let systemPrompt = """
|
||
你是一个语音识别后处理工具。你唯一的任务是修正转录文本中的明显识别错误。规则:
|
||
1. 修正中文同音字/词错误。语音识别经常把技术词汇识别为同音的日常词汇,例如:
|
||
「变异」→「编译」、「程序圆」→「程序员」、「配森」→「Python」。
|
||
请根据上下文判断修正方向,将同音错误还原为正确的词汇。
|
||
2. 修正被错误转录为中文的英文技术术语。
|
||
3. 修正极其明显的中文和英文拼写错误(包括错别字、漏字、多字)。
|
||
4. 修正明显的乱码或无意义字符序列。
|
||
5. 绝不重写、润色、改进或修改任何看起来正确的内容。
|
||
6. 如果输入本身正确,请原样返回,不做任何修改。
|
||
7. 保留所有标点符号、空格、大小写和格式。
|
||
8. 只返回修正后的文本,不要添加任何解释、前缀、后缀或 Markdown 标记。
|
||
"""
|
||
|
||
return try await callLLM(
|
||
text: text,
|
||
systemPrompt: systemPrompt,
|
||
config: config
|
||
)
|
||
}
|
||
|
||
// MARK: - Control 键模式:AI 助手对话
|
||
|
||
func chat(text: String, config: LLMConfig) async throws -> RefinementResult {
|
||
let systemPrompt = """
|
||
你是一个 AI 助手,用户通过语音输入与你交流。用户的输入来自语音识别,可能存在同音字或转录错误,请结合上下文理解。
|
||
|
||
任务:
|
||
1. 回答用户的问题、提供信息或执行对话
|
||
2. 当用户询问「xx 的英文是什么」「xx 的日文是什么」等翻译请求时,直接输出对应的翻译结果,不要添加任何前缀或解释
|
||
|
||
要求:
|
||
1. 保持回答简洁自然,适合口语化场景
|
||
2. 用户输入模糊时,尝试猜测意图并给出最可能的回答
|
||
3. 使用用户正在使用的语言,或用户明确要求的语言进行回复
|
||
4. 不要包含任何解释性文字、前缀、后缀或 Markdown 标记
|
||
5. 【最重要】你的输出将直接进入用户的输入框,只返回最终回答内容
|
||
"""
|
||
|
||
return try await callLLM(
|
||
text: text,
|
||
systemPrompt: systemPrompt,
|
||
config: config
|
||
)
|
||
}
|
||
|
||
// MARK: - 通用调用
|
||
|
||
private func callLLM(text: String, systemPrompt: String, config: LLMConfig) async throws -> RefinementResult {
|
||
let baseURL = config.baseURL.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
||
let url = URL(string: "\(baseURL)/v1/chat/completions")!
|
||
|
||
var urlRequest = URLRequest(url: url)
|
||
urlRequest.httpMethod = "POST"
|
||
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||
urlRequest.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization")
|
||
|
||
let messages: [[String: String]] = [
|
||
["role": "system", "content": systemPrompt],
|
||
["role": "user", "content": text]
|
||
]
|
||
|
||
let body: [String: Any] = [
|
||
"model": config.model,
|
||
"messages": messages,
|
||
"max_tokens": 4096,
|
||
"thinking": ["type": "disabled"]
|
||
]
|
||
|
||
urlRequest.httpBody = try JSONSerialization.data(withJSONObject: body)
|
||
|
||
let (data, response) = try await session.data(for: urlRequest)
|
||
|
||
guard let httpResponse = response as? HTTPURLResponse else {
|
||
throw NSError(domain: "LLMRefiner", code: -1,
|
||
userInfo: [NSLocalizedDescriptionKey: "Invalid response"])
|
||
}
|
||
|
||
guard httpResponse.statusCode == 200 else {
|
||
let body = String(data: data, encoding: .utf8) ?? "Unknown"
|
||
throw NSError(domain: "LLMRefiner", code: httpResponse.statusCode,
|
||
userInfo: [NSLocalizedDescriptionKey: "HTTP \(httpResponse.statusCode): \(body)"])
|
||
}
|
||
|
||
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||
let choices = json["choices"] as? [[String: Any]],
|
||
let firstChoice = choices.first,
|
||
let message = firstChoice["message"] as? [String: Any],
|
||
let result = message["content"] as? String else {
|
||
throw NSError(domain: "LLMRefiner", code: -2,
|
||
userInfo: [NSLocalizedDescriptionKey: "Failed to parse response"])
|
||
}
|
||
|
||
let trimmed = result.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
let wasChanged = trimmed != text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
|
||
return RefinementResult(text: trimmed, wasChanged: wasChanged)
|
||
}
|
||
|
||
/// 测试连接
|
||
func testConnection(config: LLMConfig) async throws -> String {
|
||
let testText = "测试连接"
|
||
let result = try await callLLM(
|
||
text: testText,
|
||
systemPrompt: "You are a helpful assistant. Reply with 'OK' only.",
|
||
config: config
|
||
)
|
||
return result.text
|
||
}
|
||
}
|