VoiceInput/Sources/Utils/Config.swift
JOJO 26bf147adf fix: Agent 配置路径统一到用户目录,修复工具异常和权限错误
- config.js: ensureConfig 接受 opts.path,避免始终读 bundle 内空配置
- bridge.js: executeTool 异常时也写入 tool 消息,防止 tool_calls 历史不一致
- search_workspace.js: 加 followSymbolicLinks:false + suppressErrors + try-catch
- AgentBridge: 日志写用户目录,传 --config 给 bridge.js
- Config: Agent models.json 存储到 ~/Library/Application Support/VoiceInput
- AppDelegate: 合并权限请求为一次弹窗,适配 macOS 15
- .gitignore: 移除 agent_debug.log(已迁至用户目录)
2026-04-29 20:16:20 +08:00

424 lines
14 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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"
// LLMAI
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") }
}
// MARK: - models.json
/// Agent bundle Resources models.json Agent/
var agentDir: String {
// 0bundle Resources/AgentRelease
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"
}
// 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)
}
///
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
}
}
}