VoiceInput/Sources/Utils/Config.swift
JOJO e82ab6a70e feat: 从 remember 工具中拆分出独立的 todo 工具
- 新增 todo 工具:add/complete/delete/update,共享 memory.json
- remember 去掉 todo 类型和 remind_at 字段
- recall 对 todo 条目显示完成状态(未完成/完成于时间)
- 提醒触发后自动标记完成 + 清理 remind_at
- 提醒消息加上 todo ID 便于关联
- 更新 system prompt 拆分记忆/待办指导
- UI 显示适配:待办 新增/完成/删除/更新 · 内容 · 时间
2026-05-01 03:25:51 +08:00

525 lines
17 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: -
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 {
// 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"
}
/// /
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
}
}
}