import Foundation // MARK: - Agent 模型配置(对应 models.json) struct AgentModelConfig: Codable, Identifiable { var id: String { name } var url: String var name: String var apikey: String var modes: String var multimodal: String var maxOutput: Int var maxContext: Int init(url: String = "", name: String = "", apikey: String = "", modes: String = "快速,思考", multimodal: String = "", maxOutput: Int = 32000, maxContext: Int = 256000) { self.url = url self.name = name self.apikey = apikey self.modes = modes self.multimodal = multimodal self.maxOutput = maxOutput self.maxContext = maxContext } /// 是否支持思考模式 var supportsThinking: Bool { modes.contains("思考") || modes.contains("thinking") } /// 是否支持快速模式 var supportsFast: Bool { modes.contains("快速") || modes.contains("fast") } /// 脱敏显示 API Key var maskedKey: String { apikey.count <= 8 ? apikey : "\(apikey.prefix(3))...\(apikey.suffix(3))" } } struct AgentModelsFile: Codable { var tavilyApiKey: String var defaultModel: String var models: [AgentModelConfig] } enum RecognitionLanguage: String, CaseIterable { case chineseSimplified = "zh-CN" case english = "en-US" case chineseTraditional = "zh-TW" case japanese = "ja-JP" case korean = "ko-KR" var displayName: String { switch self { case .chineseSimplified: return "简体中文" case .english: return "English" case .chineseTraditional: return "繁體中文" case .japanese: return "日本語" case .korean: return "한국어" } } } enum LLMMode { case input // 语音输入纠错 case assist // AI 助手对话 case agent // 智能体调用 } enum TriggerMode: String { case input = "input" // 语音输入 case assist = "assist" // AI 助手 case agent = "agent" // 智能体调用 } enum AssistOutputMode: String, CaseIterable { case inject = "inject" // 直接输入到输入栏 case notch = "notch" // 刘海弹窗展示 var displayName: String { switch self { case .inject: return "直接输入" case .notch: return "刘海弹窗" } } } struct LLMConfig { var enabled: Bool var baseURL: String var apiKey: String var model: String var isConfigured: Bool { !baseURL.isEmpty && !apiKey.isEmpty && !model.isEmpty } } final class Config { static let shared = Config() private init() {} private let defaults = UserDefaults.standard private enum Keys { static let language = "recognition_language" static let triggerMode = "trigger_mode" // 输入模式 LLM(语音输入纠错) static let inputLLMEnabled = "input_llm_enabled" static let inputLLMBaseURL = "input_llm_base_url" static let inputLLMAPIKey = "input_llm_api_key" static let inputLLMModel = "input_llm_model" // 助手模式 LLM(AI 助手对话) static let assistLLMEnabled = "assist_llm_enabled" static let assistLLMBaseURL = "assist_llm_base_url" static let assistLLMAPIKey = "assist_llm_api_key" static let assistLLMModel = "assist_llm_model" // 助手模式输出方式 static let assistOutputMode = "assist_output_mode" // 智能体模式 static let agentLLMEnabled = "agent_llm_enabled" static let agentLLMBaseURL = "agent_llm_base_url" static let agentLLMAPIKey = "agent_llm_api_key" static let agentLLMModel = "agent_llm_model" static let agentWorkspace = "agent_workspace" } var language: RecognitionLanguage { get { guard let raw = defaults.string(forKey: Keys.language), let lang = RecognitionLanguage(rawValue: raw) else { return .chineseSimplified } return lang } set { defaults.set(newValue.rawValue, forKey: Keys.language) } } // MARK: - 触发模式(菜单栏切换) var triggerMode: TriggerMode { get { guard let raw = defaults.string(forKey: Keys.triggerMode), let mode = TriggerMode(rawValue: raw) else { return .input } return mode } set { defaults.set(newValue.rawValue, forKey: Keys.triggerMode) } } // MARK: - 输入模式配置(语音输入纠错) var inputLLM: LLMConfig { get { LLMConfig( enabled: defaults.bool(forKey: Keys.inputLLMEnabled), baseURL: defaults.string(forKey: Keys.inputLLMBaseURL) ?? "", apiKey: defaults.string(forKey: Keys.inputLLMAPIKey) ?? "", model: defaults.string(forKey: Keys.inputLLMModel) ?? "" ) } set { defaults.set(newValue.enabled, forKey: Keys.inputLLMEnabled) defaults.set(newValue.baseURL, forKey: Keys.inputLLMBaseURL) defaults.set(newValue.apiKey, forKey: Keys.inputLLMAPIKey) defaults.set(newValue.model, forKey: Keys.inputLLMModel) } } // 兼容旧版本 var llmEnabled: Bool { get { inputLLM.enabled } set { var cfg = inputLLM cfg.enabled = newValue inputLLM = cfg } } var llmBaseURL: String { get { inputLLM.baseURL } set { var cfg = inputLLM cfg.baseURL = newValue inputLLM = cfg } } var llmAPIKey: String { get { inputLLM.apiKey } set { var cfg = inputLLM cfg.apiKey = newValue inputLLM = cfg } } var llmModel: String { get { inputLLM.model } set { var cfg = inputLLM cfg.model = newValue inputLLM = cfg } } var isLLMConfigured: Bool { inputLLM.isConfigured } // MARK: - Control 键模式配置(AI 助手) var assistLLM: LLMConfig { get { LLMConfig( enabled: defaults.bool(forKey: Keys.assistLLMEnabled), baseURL: defaults.string(forKey: Keys.assistLLMBaseURL) ?? "", apiKey: defaults.string(forKey: Keys.assistLLMAPIKey) ?? "", model: defaults.string(forKey: Keys.assistLLMModel) ?? "" ) } set { defaults.set(newValue.enabled, forKey: Keys.assistLLMEnabled) defaults.set(newValue.baseURL, forKey: Keys.assistLLMBaseURL) defaults.set(newValue.apiKey, forKey: Keys.assistLLMAPIKey) defaults.set(newValue.model, forKey: Keys.assistLLMModel) } } // MARK: - 助手模式输出方式 var assistOutputMode: AssistOutputMode { get { guard let raw = defaults.string(forKey: Keys.assistOutputMode), let mode = AssistOutputMode(rawValue: raw) else { return .inject } return mode } set { defaults.set(newValue.rawValue, forKey: Keys.assistOutputMode) } } // MARK: - 智能体模式配置 var agentLLM: LLMConfig { get { LLMConfig( enabled: defaults.bool(forKey: Keys.agentLLMEnabled), baseURL: defaults.string(forKey: Keys.agentLLMBaseURL) ?? "", apiKey: defaults.string(forKey: Keys.agentLLMAPIKey) ?? "", model: defaults.string(forKey: Keys.agentLLMModel) ?? "" ) } set { defaults.set(newValue.enabled, forKey: Keys.agentLLMEnabled) defaults.set(newValue.baseURL, forKey: Keys.agentLLMBaseURL) defaults.set(newValue.apiKey, forKey: Keys.agentLLMAPIKey) defaults.set(newValue.model, forKey: Keys.agentLLMModel) } } var agentWorkspace: String { get { defaults.string(forKey: Keys.agentWorkspace) ?? NSHomeDirectory() } set { defaults.set(newValue, forKey: Keys.agentWorkspace) } } // Agent 运行模式 var agentAllowMode: String { get { defaults.string(forKey: "agent_allow_mode") ?? "full_access" } set { defaults.set(newValue, forKey: "agent_allow_mode") } } // Agent 输入方式 var agentInputMode: String { get { defaults.string(forKey: "agent_input_mode") ?? "voice" } set { defaults.set(newValue, forKey: "agent_input_mode") } } // Agent 思考模式 var agentThinkingMode: Bool { get { defaults.bool(forKey: "agent_thinking_mode") } set { defaults.set(newValue, forKey: "agent_thinking_mode") } } // MARK: - models.json 路径 /// Agent 目录路径:优先查找 bundle Resources,其次逐级往上搜索包含 models.json 的 Agent/ var agentDir: String { // 优先级 0:bundle Resources/Agent(Release 打包后) if let resourcePath = Bundle.main.resourcePath { let bundledAgent = resourcePath + "/Agent/models.json" if FileManager.default.fileExists(atPath: bundledAgent) { print("[Config] found Agent in bundle Resources: \(resourcePath)/Agent") return resourcePath + "/Agent" } } // 优先级 1:从 bundlePath 开始逐级往上搜索(Debug 开发模式) var dir = Bundle.main.bundlePath print("[Config] bundlePath: \(Bundle.main.bundlePath)") for i in 0..<6 { let candidate = dir + "/Agent/models.json" let exists = FileManager.default.fileExists(atPath: candidate) print("[Config] level \(i): \(candidate) → \(exists ? "YES" : "NO")") if exists { return dir + "/Agent" } dir = (dir as NSString).deletingLastPathComponent } // 回退:当前工作目录 let cwd = FileManager.default.currentDirectoryPath + "/Agent/models.json" print("[Config] cwd: \(cwd) → \(FileManager.default.fileExists(atPath: cwd) ? "YES" : "NO")") if FileManager.default.fileExists(atPath: cwd) { return FileManager.default.currentDirectoryPath + "/Agent" } // 最后回退到开发默认路径 return Bundle.main.bundlePath + "/../../../Agent" } var agentModelsPath: String { agentDir + "/models.json" } // MARK: - models.json 读写 func loadAgentModels() -> AgentModelsFile { let path = agentModelsPath guard FileManager.default.fileExists(atPath: path), let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else { return AgentModelsFile(tavilyApiKey: "", defaultModel: "", models: []) } let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase guard let models = try? decoder.decode(AgentModelsFile.self, from: data) else { return AgentModelsFile(tavilyApiKey: "", defaultModel: "", models: []) } return models } func saveAgentModels(_ models: AgentModelsFile) { let path = agentModelsPath let encoder = JSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase guard let data = try? encoder.encode(models) else { return } try? data.write(to: URL(fileURLWithPath: path), options: .atomic) } /// 有效的模型列表(过滤掉配置不完整的) var validAgentModels: [AgentModelConfig] { loadAgentModels().models.filter { m in !m.url.isEmpty && !m.name.isEmpty && !m.apikey.isEmpty && !m.modes.isEmpty } } func getLLMConfig(for mode: LLMMode) -> LLMConfig { switch mode { case .input: return inputLLM case .assist: return assistLLM case .agent: return agentLLM } } func setLLMConfig(_ config: LLMConfig, for mode: LLMMode) { switch mode { case .input: inputLLM = config case .assist: assistLLM = config case .agent: agentLLM = config } } }