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 AgentReminderMode: String, CaseIterable { case agent = "agent" case system = "system" case both = "both" var displayName: String { switch self { case .agent: return "智能体通知" case .system: return "系统通知" case .both: return "同时通知" } } } 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 assistSpeakEnabled = "assist_speak_enabled" // 智能体模式朗读开关 static let agentSpeakEnabled = "agent_speak_enabled" // 智能体模式 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 assistSpeakEnabled: Bool { get { defaults.bool(forKey: Keys.assistSpeakEnabled) } set { defaults.set(newValue, forKey: Keys.assistSpeakEnabled) } } // 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) } } var agentSpeakEnabled: Bool { get { defaults.bool(forKey: Keys.agentSpeakEnabled) } set { defaults.set(newValue, forKey: Keys.agentSpeakEnabled) } } // 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") } } // Agent 待办提醒通知方式 var agentReminderMode: AgentReminderMode { get { guard let raw = defaults.string(forKey: "agent_reminder_mode"), let mode = AgentReminderMode(rawValue: raw) else { return .agent } return mode } set { defaults.set(newValue.rawValue, forKey: "agent_reminder_mode") } } // MARK: - 助手个性化配置 var assistantName: String { get { defaults.string(forKey: "assistant_name") ?? "小助手" } set { defaults.set(newValue, forKey: "assistant_name") saveUserProfile() } } var assistantTone: String { get { defaults.string(forKey: "assistant_tone") ?? "友好" } set { defaults.set(newValue, forKey: "assistant_tone") saveUserProfile() } } var userName: String { get { defaults.string(forKey: "user_name") ?? "" } set { defaults.set(newValue, forKey: "user_name") saveUserProfile() } } var agentShowTools: Bool { get { if defaults.object(forKey: "agent_show_tools") == nil { return true } return defaults.bool(forKey: "agent_show_tools") } set { defaults.set(newValue, forKey: "agent_show_tools") } } var agentToolsMode: String { get { defaults.string(forKey: "agent_tools_mode") ?? "all" } set { defaults.set(newValue, forKey: "agent_tools_mode") } } var agentNewConversationMode: String { get { defaults.string(forKey: "agent_new_conversation_mode") ?? "manual" } set { defaults.set(newValue, forKey: "agent_new_conversation_mode") } } /// 自动新建对话的 token 阈值(仅 newConversationMode == "auto" 时生效) var agentAutoNewTokenThreshold: Int { get { let val = defaults.integer(forKey: "agent_auto_new_token_threshold") return val > 0 ? val : 40000 } set { defaults.set(newValue, forKey: "agent_auto_new_token_threshold") } } /// 语音输入后是否自动发送(false 时只填入输入框) var agentVoiceAutoSend: Bool { get { if defaults.object(forKey: "agent_voice_auto_send") == nil { return true } return defaults.bool(forKey: "agent_voice_auto_send") } set { defaults.set(newValue, forKey: "agent_voice_auto_send") } } // MARK: - 用户配置文件(user_profile.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" } /// 用户数据目录(配置持久化,覆盖安装不丢失) private var userDataDir: String { let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! let dir = appSupport.appendingPathComponent("VoiceInput").path // 确保目录存在 if !FileManager.default.fileExists(atPath: dir) { try? FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true) } return dir } /// 用户目录下的 models.json(覆盖安装不丢失) var userAgentModelsPath: String { userDataDir + "/models.json" } /// 用户配置文件路径(助手名字/语气) var userProfilePath: String { userDataDir + "/user_profile.json" } /// 记忆文件路径 var memoryPath: String { userDataDir + "/memory/memory.json" } // MARK: - models.json 读写 func loadAgentModels() -> AgentModelsFile { let userPath = userAgentModelsPath // 用户目录已有配置 → 直接读取 if FileManager.default.fileExists(atPath: userPath), let data = try? Data(contentsOf: URL(fileURLWithPath: userPath)) { let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase if let models = try? decoder.decode(AgentModelsFile.self, from: data) { return models } } // 首次启动:从 bundle 模板复制到用户目录 let bundlePath = agentModelsPath if FileManager.default.fileExists(atPath: bundlePath), let data = try? Data(contentsOf: URL(fileURLWithPath: bundlePath)) { let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase if let models = try? decoder.decode(AgentModelsFile.self, from: data) { // 复制到用户目录 try? data.write(to: URL(fileURLWithPath: userPath), options: .atomic) return models } } return AgentModelsFile(tavilyApiKey: "", defaultModel: "", models: []) } func saveAgentModels(_ models: AgentModelsFile) { let path = userAgentModelsPath let encoder = JSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase guard let data = try? encoder.encode(models) else { return } try? data.write(to: URL(fileURLWithPath: path), options: .atomic) } /// 保存用户个性化配置到 user_profile.json func saveUserProfile() { let profile: [String: String] = [ "assistant_name": assistantName, "assistant_tone": assistantTone, "user_name": userName, ] let encoder = JSONEncoder() guard let data = try? encoder.encode(profile) else { return } try? data.write(to: URL(fileURLWithPath: userProfilePath), options: .atomic) } /// 从 user_profile.json 同步到 UserDefaults(用于窗口打开时刷新) func syncUserProfile() { guard let data = try? Data(contentsOf: URL(fileURLWithPath: userProfilePath)), let profile = try? JSONDecoder().decode([String: String].self, from: data) else { return } if let name = profile["assistant_name"], !name.isEmpty { defaults.set(name, forKey: "assistant_name") } if let tone = profile["assistant_tone"], !tone.isEmpty { defaults.set(tone, forKey: "assistant_tone") } if let userName = profile["user_name"], !userName.isEmpty { defaults.set(userName, forKey: "user_name") } } /// 有效的模型列表(过滤掉配置不完整的) 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 } } }