=== 记忆系统 === - 新增 remember(记录)和 recall(搜索)工具,持久化到 ~/Library/Application Support/VoiceInput/memory/memory.json - 新增 set_identity 工具,独立管理助手名字/用户称呼/语气 - prompt 重写为个人助手身份,注入 macOS 能力 + 记忆规则 - prompt 中积极调用记忆工具,按需搜索、自然融入回答 - 禁止相对时间,待办必须绝对日期 === 对话持久化 === - 集成 conversation_store,done 前自动写入对话文件 - token 记录:prompt/completion 累加,total 覆盖为最新上下文长度 - 启动时加载最新对话继续(使用 eagent /resume 逻辑) - 去重:末尾连续 assistant 消息自动合并 - 修复消息加载顺序 bug(旧对话覆盖用户新消息) === 新建对话 === - 手动/自动两种模式(设置窗口可切换) - 自动模式:total > 阈值时隐藏按钮,下次输入自动新建 - 自动阈值在设置窗口可配置(默认 40000) === 弹窗 UI 统一 === - 语音输入按 Fn 即显示刘海弹窗(和打字输入统一) - 语音识别内容自动填入并发送(可关闭自动发送) - 工作期间(thinking/running)隐藏输入框,idle/done 时显示 - 工具显示三种模式:全部显示/最后隐藏/一直不显示 === 设置窗口增强 === - 助手名字 / 用户称呼 / 语气可配置 - 工具显示模式 / 新建对话模式 / 自动阈值 - 语音输入后自动发送开关(默认开) - 记忆管理窗口可视化编辑(Dock 可访问) - 窗口高度 840→860 === 菜单增强 === - Agent LLM 二级菜单切换语音/打字输入 - 助手设置和记忆管理入口(Dock 可访问) === 修复 === - Fn 按下时停止正在进行的朗读 - 弹窗已存在时不再覆盖 isTextInputMode - voiceInputText 加 @Published 以触发 UI 更新
922 lines
43 KiB
Swift
922 lines
43 KiB
Swift
import Cocoa
|
||
|
||
/// LLM 设置窗口(支持双模式)
|
||
final class SettingsWindow: NSWindow {
|
||
|
||
private let mode: LLMMode
|
||
private let baseURLField: NSTextField
|
||
private let apiKeyField: NSSecureTextField
|
||
private let modelField: NSTextField
|
||
private let testButton: NSButton
|
||
private let saveButton: NSButton
|
||
private let copyFromOtherButton: NSButton
|
||
private let statusLabel: NSTextField
|
||
|
||
// Assist 模式专属:输出方式
|
||
private let outputModeLabel: NSTextField
|
||
private let outputModePopup: NSPopUpButton
|
||
|
||
// Assist 模式专属:朗读开关
|
||
private let speakCheckbox: NSButton
|
||
|
||
// Agent 模式专属:朗读开关
|
||
private let agentSpeakCheckbox: NSButton
|
||
|
||
// Agent 模式专属:语音自动发送开关
|
||
private let voiceAutoSendCheckbox: NSButton
|
||
|
||
// Agent 模式专属:助手名字/语气
|
||
private let assistantNameField: NSTextField
|
||
private let assistantTonePopup: NSPopUpButton
|
||
private let userNameField: NSTextField
|
||
|
||
// Agent 模式专属:工具显示模式
|
||
private let toolsModePopup: NSPopUpButton
|
||
|
||
// Agent 模式专属:新建对话模式
|
||
private let newConversationModePopup: NSPopUpButton
|
||
|
||
// Agent 模式专属:自动新建阈值
|
||
private let autoNewTokenThresholdField: NSTextField
|
||
|
||
// Agent 模式专属
|
||
private let workspaceField: NSTextField
|
||
private let browseButton: NSButton
|
||
private let modelPopup: NSPopUpButton
|
||
private let allowModePopup: NSPopUpButton
|
||
private let inputModePopup: NSPopUpButton
|
||
private let thinkingModePopup: NSPopUpButton
|
||
private let tavilyKeyField: NSSecureTextField
|
||
private let modelTableView: NSTableView
|
||
private let addModelButton: NSButton
|
||
private let editModelButton: NSButton
|
||
private let deleteModelButton: NSButton
|
||
private var agentModelList: [AgentModelConfig] = []
|
||
private var agentUIBuilt = false
|
||
|
||
init(mode: LLMMode) {
|
||
self.mode = mode
|
||
|
||
baseURLField = NSTextField(frame: .zero)
|
||
apiKeyField = NSSecureTextField(frame: .zero)
|
||
modelField = NSTextField(frame: .zero)
|
||
testButton = NSButton(title: "Test", target: nil, action: nil)
|
||
saveButton = NSButton(title: "Save", target: nil, action: nil)
|
||
copyFromOtherButton = NSButton(title: "", target: nil, action: nil)
|
||
statusLabel = NSTextField(labelWithString: "")
|
||
outputModeLabel = NSTextField(labelWithString: "")
|
||
outputModePopup = NSPopUpButton(frame: .zero, pullsDown: false)
|
||
speakCheckbox = NSButton(checkboxWithTitle: "朗读 AI 回复", target: nil, action: nil)
|
||
agentSpeakCheckbox = NSButton(checkboxWithTitle: "任务完成时朗读 AI 最终输出", target: nil, action: nil)
|
||
voiceAutoSendCheckbox = NSButton(checkboxWithTitle: "语音输入后自动发送", target: nil, action: nil)
|
||
assistantNameField = NSTextField(frame: .zero)
|
||
assistantTonePopup = NSPopUpButton(frame: .zero, pullsDown: false)
|
||
userNameField = NSTextField(frame: .zero)
|
||
toolsModePopup = NSPopUpButton(frame: .zero, pullsDown: false)
|
||
newConversationModePopup = NSPopUpButton(frame: .zero, pullsDown: false)
|
||
autoNewTokenThresholdField = NSTextField(frame: .zero)
|
||
workspaceField = NSTextField(frame: .zero)
|
||
browseButton = NSButton(title: "浏览...", target: nil, action: nil)
|
||
modelPopup = NSPopUpButton(frame: .zero, pullsDown: false)
|
||
allowModePopup = NSPopUpButton(frame: .zero, pullsDown: false)
|
||
inputModePopup = NSPopUpButton(frame: .zero, pullsDown: false)
|
||
thinkingModePopup = NSPopUpButton(frame: .zero, pullsDown: false)
|
||
tavilyKeyField = NSSecureTextField(frame: .zero)
|
||
modelTableView = NSTableView(frame: .zero)
|
||
addModelButton = NSButton(title: "+", target: nil, action: nil)
|
||
editModelButton = NSButton(title: "编辑", target: nil, action: nil)
|
||
deleteModelButton = NSButton(title: "删除", target: nil, action: nil)
|
||
|
||
let height: CGFloat = mode == .agent ? 860 : (mode == .assist ? 400 : 320)
|
||
let width: CGFloat = mode == .agent ? 500 : 420
|
||
let windowRect = NSRect(x: 0, y: 0, width: width, height: height)
|
||
super.init(
|
||
contentRect: windowRect,
|
||
styleMask: [.titled, .closable, .miniaturizable],
|
||
backing: .buffered,
|
||
defer: false
|
||
)
|
||
|
||
configureWindow()
|
||
buildUI()
|
||
loadCurrentConfig()
|
||
}
|
||
|
||
private func configureWindow() {
|
||
switch mode {
|
||
case .input: title = "Input LLM Settings (Fn)"
|
||
case .assist: title = "Assist LLM Settings (Control)"
|
||
case .agent: title = "Agent LLM Settings"
|
||
}
|
||
isReleasedWhenClosed = false
|
||
center()
|
||
}
|
||
|
||
// MARK: - 快捷键支持
|
||
|
||
override func performKeyEquivalent(with event: NSEvent) -> Bool {
|
||
guard event.modifierFlags.contains(.command) else {
|
||
return super.performKeyEquivalent(with: event)
|
||
}
|
||
|
||
let selector: Selector?
|
||
switch event.charactersIgnoringModifiers?.lowercased() {
|
||
case "a": selector = #selector(NSText.selectAll(_:))
|
||
case "c": selector = Selector(("copy:"))
|
||
case "v": selector = Selector(("paste:"))
|
||
case "x": selector = Selector(("cut:"))
|
||
default: selector = nil
|
||
}
|
||
|
||
guard let sel = selector else {
|
||
return super.performKeyEquivalent(with: event)
|
||
}
|
||
|
||
if NSApp.sendAction(sel, to: nil, from: self) {
|
||
return true
|
||
}
|
||
|
||
return super.performKeyEquivalent(with: event)
|
||
}
|
||
|
||
private func makeLabel(_ text: String) -> NSTextField {
|
||
let label = NSTextField(labelWithString: text)
|
||
label.font = .systemFont(ofSize: 11, weight: .semibold)
|
||
label.textColor = .secondaryLabelColor
|
||
label.alignment = .right
|
||
label.translatesAutoresizingMaskIntoConstraints = false
|
||
return label
|
||
}
|
||
|
||
private func buildUI() {
|
||
if mode == .agent {
|
||
buildAgentUI()
|
||
return
|
||
}
|
||
|
||
guard let content = contentView else { return }
|
||
|
||
let baseURLLabel = makeLabel("API Base URL:")
|
||
let apiKeyLabel = makeLabel("API Key:")
|
||
let modelLabel = makeLabel("Model:")
|
||
|
||
baseURLField.placeholderString = "https://api.openai.com"
|
||
baseURLField.font = .systemFont(ofSize: 13)
|
||
baseURLField.bezelStyle = .roundedBezel
|
||
baseURLField.translatesAutoresizingMaskIntoConstraints = false
|
||
|
||
apiKeyField.placeholderString = "sk-..."
|
||
apiKeyField.font = .systemFont(ofSize: 13)
|
||
apiKeyField.bezelStyle = .roundedBezel
|
||
apiKeyField.translatesAutoresizingMaskIntoConstraints = false
|
||
|
||
modelField.placeholderString = mode == .input ? "deepseek-v4-flash" : "gpt-4"
|
||
modelField.font = .systemFont(ofSize: 13)
|
||
modelField.bezelStyle = .roundedBezel
|
||
modelField.translatesAutoresizingMaskIntoConstraints = false
|
||
|
||
testButton.bezelStyle = .rounded
|
||
testButton.translatesAutoresizingMaskIntoConstraints = false
|
||
testButton.target = self
|
||
testButton.action = #selector(testConnection)
|
||
|
||
saveButton.bezelStyle = .rounded
|
||
saveButton.translatesAutoresizingMaskIntoConstraints = false
|
||
saveButton.keyEquivalent = "\r"
|
||
saveButton.target = self
|
||
saveButton.action = #selector(saveConfig)
|
||
|
||
let otherModeName = mode == .input ? "AI 助手" : "语音输入"
|
||
copyFromOtherButton.title = "从「\(otherModeName)」复制配置"
|
||
copyFromOtherButton.bezelStyle = .rounded
|
||
copyFromOtherButton.translatesAutoresizingMaskIntoConstraints = false
|
||
copyFromOtherButton.target = self
|
||
copyFromOtherButton.action = #selector(copyFromOther)
|
||
|
||
statusLabel.font = .systemFont(ofSize: 11)
|
||
statusLabel.textColor = .secondaryLabelColor
|
||
statusLabel.alignment = .center
|
||
statusLabel.maximumNumberOfLines = 2
|
||
statusLabel.translatesAutoresizingMaskIntoConstraints = false
|
||
|
||
let grid = NSGridView(views: [
|
||
[baseURLLabel, baseURLField],
|
||
[apiKeyLabel, apiKeyField],
|
||
[modelLabel, modelField]
|
||
])
|
||
grid.translatesAutoresizingMaskIntoConstraints = false
|
||
grid.column(at: 0).xPlacement = .trailing
|
||
grid.column(at: 1).width = 260
|
||
grid.rowSpacing = 12
|
||
grid.columnSpacing = 8
|
||
|
||
let buttonStack = NSStackView(views: [testButton, saveButton])
|
||
buttonStack.translatesAutoresizingMaskIntoConstraints = false
|
||
buttonStack.orientation = .horizontal
|
||
buttonStack.spacing = 12
|
||
buttonStack.distribution = .fillEqually
|
||
|
||
content.addSubview(grid)
|
||
content.addSubview(buttonStack)
|
||
content.addSubview(copyFromOtherButton)
|
||
content.addSubview(statusLabel)
|
||
|
||
let outputLabel: NSTextField?
|
||
if mode == .assist {
|
||
let label = makeLabel("输出方式:")
|
||
outputModePopup.translatesAutoresizingMaskIntoConstraints = false
|
||
outputModePopup.addItems(withTitles: AssistOutputMode.allCases.map { $0.displayName })
|
||
outputModePopup.selectItem(at: Config.shared.assistOutputMode == .notch ? 1 : 0)
|
||
outputModePopup.target = self
|
||
outputModePopup.action = #selector(outputModeChanged)
|
||
|
||
content.addSubview(label)
|
||
content.addSubview(outputModePopup)
|
||
|
||
NSLayoutConstraint.activate([
|
||
label.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20),
|
||
label.topAnchor.constraint(equalTo: grid.bottomAnchor, constant: 16),
|
||
label.widthAnchor.constraint(equalToConstant: 90),
|
||
outputModePopup.leadingAnchor.constraint(equalTo: label.trailingAnchor, constant: 8),
|
||
outputModePopup.centerYAnchor.constraint(equalTo: label.centerYAnchor),
|
||
outputModePopup.widthAnchor.constraint(equalToConstant: 160)
|
||
])
|
||
|
||
// 朗读开关
|
||
speakCheckbox.translatesAutoresizingMaskIntoConstraints = false
|
||
speakCheckbox.state = Config.shared.assistSpeakEnabled ? .on : .off
|
||
speakCheckbox.target = self
|
||
speakCheckbox.action = #selector(speakToggled)
|
||
|
||
content.addSubview(speakCheckbox)
|
||
|
||
NSLayoutConstraint.activate([
|
||
speakCheckbox.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20),
|
||
speakCheckbox.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 10)
|
||
])
|
||
|
||
outputLabel = nil
|
||
} else {
|
||
outputLabel = nil
|
||
}
|
||
|
||
let buttonTopItem: NSView
|
||
if mode == .assist {
|
||
buttonTopItem = speakCheckbox
|
||
} else {
|
||
buttonTopItem = outputLabel ?? grid
|
||
}
|
||
|
||
NSLayoutConstraint.activate([
|
||
grid.topAnchor.constraint(equalTo: content.topAnchor, constant: 24),
|
||
grid.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20),
|
||
grid.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -20),
|
||
|
||
buttonStack.topAnchor.constraint(equalTo: buttonTopItem.bottomAnchor, constant: 20),
|
||
buttonStack.centerXAnchor.constraint(equalTo: content.centerXAnchor),
|
||
buttonStack.widthAnchor.constraint(equalToConstant: 220),
|
||
|
||
copyFromOtherButton.topAnchor.constraint(equalTo: buttonStack.bottomAnchor, constant: 10),
|
||
copyFromOtherButton.centerXAnchor.constraint(equalTo: content.centerXAnchor),
|
||
|
||
statusLabel.topAnchor.constraint(equalTo: copyFromOtherButton.bottomAnchor, constant: 10),
|
||
statusLabel.centerXAnchor.constraint(equalTo: content.centerXAnchor),
|
||
statusLabel.widthAnchor.constraint(equalToConstant: 380)
|
||
])
|
||
}
|
||
|
||
private func loadCurrentConfig() {
|
||
let config = Config.shared.getLLMConfig(for: mode)
|
||
baseURLField.stringValue = config.baseURL
|
||
apiKeyField.stringValue = config.apiKey
|
||
modelField.stringValue = config.model
|
||
if mode == .assist {
|
||
outputModePopup.selectItem(at: Config.shared.assistOutputMode == .notch ? 1 : 0)
|
||
speakCheckbox.state = Config.shared.assistSpeakEnabled ? .on : .off
|
||
}
|
||
}
|
||
|
||
@objc private func testConnection() {
|
||
let url = baseURLField.stringValue.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
||
let key = apiKeyField.stringValue
|
||
let model = modelField.stringValue
|
||
|
||
guard !url.isEmpty, !key.isEmpty, !model.isEmpty else {
|
||
statusLabel.stringValue = "Please fill all fields"
|
||
statusLabel.textColor = .systemRed
|
||
return
|
||
}
|
||
|
||
statusLabel.stringValue = "Testing..."
|
||
statusLabel.textColor = .systemBlue
|
||
testButton.isEnabled = false
|
||
saveButton.isEnabled = false
|
||
|
||
Task { @MainActor in
|
||
let refiner = LLMRefiner()
|
||
let testConfig = LLMConfig(enabled: true, baseURL: url, apiKey: key, model: model)
|
||
|
||
do {
|
||
let result = try await refiner.testConnection(config: testConfig)
|
||
statusLabel.stringValue = "✅ Connected! Response: \(result)"
|
||
statusLabel.textColor = .systemGreen
|
||
} catch {
|
||
statusLabel.stringValue = "❌ Error: \(error.localizedDescription)"
|
||
statusLabel.textColor = .systemRed
|
||
}
|
||
|
||
testButton.isEnabled = true
|
||
saveButton.isEnabled = true
|
||
}
|
||
}
|
||
|
||
@objc private func copyFromOther() {
|
||
let otherMode: LLMMode = mode == .input ? .assist : .input
|
||
let config = Config.shared.getLLMConfig(for: otherMode)
|
||
baseURLField.stringValue = config.baseURL
|
||
apiKeyField.stringValue = config.apiKey
|
||
modelField.stringValue = config.model
|
||
statusLabel.stringValue = "✅ 已复制「\(otherMode == .input ? "语音输入" : "AI 助手")」的配置"
|
||
statusLabel.textColor = .systemGreen
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
|
||
self?.statusLabel.stringValue = ""
|
||
}
|
||
}
|
||
|
||
@objc private func outputModeChanged() {
|
||
let mode = AssistOutputMode.allCases[outputModePopup.indexOfSelectedItem]
|
||
Config.shared.assistOutputMode = mode
|
||
statusLabel.stringValue = "✅ 输出方式已更新"
|
||
statusLabel.textColor = .systemGreen
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
|
||
self?.statusLabel.stringValue = ""
|
||
}
|
||
}
|
||
|
||
@objc private func speakToggled() {
|
||
Config.shared.assistSpeakEnabled = speakCheckbox.state == .on
|
||
statusLabel.stringValue = speakCheckbox.state == .on ? "✅ 朗读已开启" : "✅ 朗读已关闭"
|
||
statusLabel.textColor = .systemGreen
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
|
||
self?.statusLabel.stringValue = ""
|
||
}
|
||
}
|
||
|
||
@objc private func saveConfig() {
|
||
let url = baseURLField.stringValue.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
||
let key = apiKeyField.stringValue
|
||
let model = modelField.stringValue
|
||
|
||
let config = LLMConfig(enabled: true, baseURL: url, apiKey: key, model: model)
|
||
Config.shared.setLLMConfig(config, for: mode)
|
||
|
||
statusLabel.stringValue = "✅ Saved"
|
||
statusLabel.textColor = .systemGreen
|
||
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { [weak self] in
|
||
self?.statusLabel.stringValue = ""
|
||
}
|
||
}
|
||
|
||
// MARK: - Agent UI
|
||
|
||
override func makeKeyAndOrderFront(_ sender: Any?) {
|
||
if mode == .agent && !agentUIBuilt {
|
||
refreshAgentModelTable()
|
||
}
|
||
super.makeKeyAndOrderFront(sender)
|
||
}
|
||
|
||
func refreshAgentModelTable() {
|
||
Config.shared.syncUserProfile()
|
||
buildAgentUI()
|
||
let modelsFile = Config.shared.loadAgentModels()
|
||
print("[SettingsWindow] loaded models count: \(modelsFile.models.count)")
|
||
agentModelList = modelsFile.models
|
||
modelTableView.reloadData()
|
||
|
||
modelPopup.removeAllItems()
|
||
let validModels = modelsFile.models.filter {
|
||
!$0.url.isEmpty && !$0.name.isEmpty && !$0.apikey.isEmpty && !$0.modes.isEmpty
|
||
}
|
||
for m in validModels {
|
||
modelPopup.addItem(withTitle: m.name)
|
||
}
|
||
if let idx = validModels.firstIndex(where: { $0.name == modelsFile.defaultModel }) {
|
||
modelPopup.selectItem(at: idx)
|
||
} else if !validModels.isEmpty {
|
||
modelPopup.selectItem(at: 0)
|
||
}
|
||
|
||
tavilyKeyField.stringValue = modelsFile.tavilyApiKey
|
||
workspaceField.stringValue = Config.shared.agentWorkspace
|
||
agentSpeakCheckbox.state = Config.shared.agentSpeakEnabled ? .on : .off
|
||
voiceAutoSendCheckbox.state = Config.shared.agentVoiceAutoSend ? .on : .off
|
||
assistantNameField.stringValue = Config.shared.assistantName
|
||
userNameField.stringValue = Config.shared.userName
|
||
let tones = ["友好", "专业", "幽默", "简洁", "温暖", "正式"]
|
||
let toneIdx = tones.firstIndex(of: Config.shared.assistantTone) ?? 0
|
||
assistantTonePopup.selectItem(at: toneIdx)
|
||
let modeMap = ["all", "hide_final", "never"]
|
||
let currentMode = Config.shared.agentToolsMode
|
||
toolsModePopup.selectItem(at: modeMap.firstIndex(of: currentMode) ?? 0)
|
||
let convModeMap = ["manual", "auto"]
|
||
let currentConvMode = Config.shared.agentNewConversationMode
|
||
newConversationModePopup.selectItem(at: convModeMap.firstIndex(of: currentConvMode) ?? 0)
|
||
autoNewTokenThresholdField.stringValue = String(Config.shared.agentAutoNewTokenThreshold)
|
||
}
|
||
|
||
private func buildAgentUI() {
|
||
guard !agentUIBuilt else { return }
|
||
guard let content = contentView else { return }
|
||
agentUIBuilt = true
|
||
|
||
let modelLabel = makeLabel("默认模型:")
|
||
modelPopup.translatesAutoresizingMaskIntoConstraints = false
|
||
modelPopup.target = self
|
||
modelPopup.action = #selector(agentModelChanged)
|
||
|
||
let allowLabel = makeLabel("运行模式:")
|
||
allowModePopup.translatesAutoresizingMaskIntoConstraints = false
|
||
allowModePopup.addItems(withTitles: ["Read Only(只读)", "Full Access(无限制)"])
|
||
allowModePopup.selectItem(at: Config.shared.agentAllowMode == "read_only" ? 0 : 1)
|
||
allowModePopup.target = self
|
||
allowModePopup.action = #selector(agentAllowModeChanged)
|
||
|
||
let inputLabel = makeLabel("输入方式:")
|
||
inputModePopup.translatesAutoresizingMaskIntoConstraints = false
|
||
inputModePopup.addItems(withTitles: ["🎤 语音输入", "⌨️ 打字输入"])
|
||
inputModePopup.selectItem(at: Config.shared.agentInputMode == "text" ? 1 : 0)
|
||
inputModePopup.target = self
|
||
inputModePopup.action = #selector(agentInputModeChanged)
|
||
|
||
let thinkLabel = makeLabel("思考模式:")
|
||
thinkingModePopup.translatesAutoresizingMaskIntoConstraints = false
|
||
thinkingModePopup.addItems(withTitles: ["⚡ 快速", "🧠 思考"])
|
||
thinkingModePopup.selectItem(at: Config.shared.agentThinkingMode ? 1 : 0)
|
||
thinkingModePopup.target = self
|
||
thinkingModePopup.action = #selector(agentThinkingModeChanged)
|
||
|
||
// 工具显示模式
|
||
let toolsModeLabel = makeLabel("工具显示:")
|
||
toolsModePopup.translatesAutoresizingMaskIntoConstraints = false
|
||
toolsModePopup.addItems(withTitles: ["全部显示", "最后隐藏", "一直不显示"])
|
||
let modeMap = ["all", "hide_final", "never"]
|
||
let currentMode = Config.shared.agentToolsMode
|
||
toolsModePopup.selectItem(at: modeMap.firstIndex(of: currentMode) ?? 0)
|
||
|
||
// 新建对话模式
|
||
let newConvLabel = makeLabel("新建对话:")
|
||
newConversationModePopup.translatesAutoresizingMaskIntoConstraints = false
|
||
newConversationModePopup.addItems(withTitles: ["手动", "自动"])
|
||
let convModeMap = ["manual", "auto"]
|
||
let currentConvMode = Config.shared.agentNewConversationMode
|
||
newConversationModePopup.selectItem(at: convModeMap.firstIndex(of: currentConvMode) ?? 0)
|
||
|
||
// 自动新建阈值
|
||
let thresholdLabel = makeLabel("自动新建阈值:")
|
||
autoNewTokenThresholdField.placeholderString = "40000"
|
||
autoNewTokenThresholdField.font = .systemFont(ofSize: 13)
|
||
autoNewTokenThresholdField.bezelStyle = .roundedBezel
|
||
autoNewTokenThresholdField.translatesAutoresizingMaskIntoConstraints = false
|
||
autoNewTokenThresholdField.stringValue = String(Config.shared.agentAutoNewTokenThreshold)
|
||
autoNewTokenThresholdField.target = self
|
||
autoNewTokenThresholdField.action = #selector(agentAutoNewThresholdChanged)
|
||
|
||
agentSpeakCheckbox.translatesAutoresizingMaskIntoConstraints = false
|
||
agentSpeakCheckbox.state = Config.shared.agentSpeakEnabled ? .on : .off
|
||
agentSpeakCheckbox.target = self
|
||
agentSpeakCheckbox.action = #selector(agentSpeakToggled)
|
||
|
||
voiceAutoSendCheckbox.translatesAutoresizingMaskIntoConstraints = false
|
||
voiceAutoSendCheckbox.state = Config.shared.agentVoiceAutoSend ? .on : .off
|
||
voiceAutoSendCheckbox.target = self
|
||
voiceAutoSendCheckbox.action = #selector(agentVoiceAutoSendToggled)
|
||
|
||
// 助手名字
|
||
let assistantNameLabel = makeLabel("助手名字:")
|
||
assistantNameField.placeholderString = "小助手"
|
||
assistantNameField.font = .systemFont(ofSize: 13)
|
||
assistantNameField.bezelStyle = .roundedBezel
|
||
assistantNameField.translatesAutoresizingMaskIntoConstraints = false
|
||
assistantNameField.stringValue = Config.shared.assistantName
|
||
|
||
// 用户称呼
|
||
let userNameLabel = makeLabel("我的称呼:")
|
||
userNameField.placeholderString = "如 jojo、张总(留空则称「用户」)"
|
||
userNameField.font = .systemFont(ofSize: 13)
|
||
userNameField.bezelStyle = .roundedBezel
|
||
userNameField.translatesAutoresizingMaskIntoConstraints = false
|
||
userNameField.stringValue = Config.shared.userName
|
||
|
||
// 助手语气
|
||
let assistantToneLabel = makeLabel("助手语气:")
|
||
assistantTonePopup.translatesAutoresizingMaskIntoConstraints = false
|
||
assistantTonePopup.addItems(withTitles: ["友好", "专业", "幽默", "简洁", "温暖", "正式"])
|
||
let toneIndex = ["友好", "专业", "幽默", "简洁", "温暖", "正式"].firstIndex(of: Config.shared.assistantTone) ?? 0
|
||
assistantTonePopup.selectItem(at: toneIndex)
|
||
|
||
let tavilyLabel = makeLabel("Tavily Key:")
|
||
tavilyKeyField.placeholderString = "tvly-...(网络搜索用)"
|
||
tavilyKeyField.font = .systemFont(ofSize: 13)
|
||
tavilyKeyField.bezelStyle = .roundedBezel
|
||
tavilyKeyField.translatesAutoresizingMaskIntoConstraints = false
|
||
|
||
let wsLabel = makeLabel("工作目录:")
|
||
workspaceField.placeholderString = NSHomeDirectory()
|
||
workspaceField.font = .systemFont(ofSize: 12)
|
||
workspaceField.bezelStyle = .roundedBezel
|
||
workspaceField.translatesAutoresizingMaskIntoConstraints = false
|
||
|
||
browseButton.bezelStyle = .rounded
|
||
browseButton.translatesAutoresizingMaskIntoConstraints = false
|
||
browseButton.target = self
|
||
browseButton.action = #selector(browseWorkspace)
|
||
|
||
let tableHeaderLabel = NSTextField(labelWithString: "模型列表")
|
||
tableHeaderLabel.font = .systemFont(ofSize: 12, weight: .semibold)
|
||
tableHeaderLabel.translatesAutoresizingMaskIntoConstraints = false
|
||
|
||
addModelButton.bezelStyle = .rounded
|
||
addModelButton.font = .systemFont(ofSize: 13, weight: .bold)
|
||
addModelButton.translatesAutoresizingMaskIntoConstraints = false
|
||
addModelButton.target = self
|
||
addModelButton.action = #selector(addAgentModel)
|
||
|
||
let scrollView = NSScrollView(frame: .zero)
|
||
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
||
scrollView.hasVerticalScroller = true
|
||
scrollView.borderType = .bezelBorder
|
||
|
||
modelTableView.translatesAutoresizingMaskIntoConstraints = false
|
||
modelTableView.headerView = nil
|
||
modelTableView.selectionHighlightStyle = .regular
|
||
modelTableView.dataSource = self
|
||
modelTableView.delegate = self
|
||
modelTableView.rowHeight = 24
|
||
modelTableView.intercellSpacing = NSSize(width: 4, height: 2)
|
||
|
||
let colName = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("name"))
|
||
colName.title = "模型名称"; colName.width = 120; colName.minWidth = 80
|
||
let colURL = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("url"))
|
||
colURL.title = "API URL"; colURL.width = 150; colURL.minWidth = 100
|
||
let colKey = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("key"))
|
||
colKey.title = "API Key"; colKey.width = 90; colKey.minWidth = 70
|
||
let colModes = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("modes"))
|
||
colModes.title = "模式"; colModes.width = 70; colModes.minWidth = 50
|
||
|
||
modelTableView.addTableColumn(colName)
|
||
modelTableView.addTableColumn(colURL)
|
||
modelTableView.addTableColumn(colKey)
|
||
modelTableView.addTableColumn(colModes)
|
||
|
||
scrollView.documentView = modelTableView
|
||
|
||
editModelButton.bezelStyle = .rounded
|
||
editModelButton.translatesAutoresizingMaskIntoConstraints = false
|
||
editModelButton.target = self
|
||
editModelButton.action = #selector(editAgentModel)
|
||
|
||
deleteModelButton.bezelStyle = .rounded
|
||
deleteModelButton.translatesAutoresizingMaskIntoConstraints = false
|
||
deleteModelButton.target = self
|
||
deleteModelButton.action = #selector(deleteAgentModel)
|
||
|
||
let editDeleteStack = NSStackView(views: [editModelButton, deleteModelButton])
|
||
editDeleteStack.translatesAutoresizingMaskIntoConstraints = false
|
||
editDeleteStack.orientation = .horizontal
|
||
editDeleteStack.spacing = 8
|
||
|
||
saveButton.bezelStyle = .rounded
|
||
saveButton.translatesAutoresizingMaskIntoConstraints = false
|
||
saveButton.keyEquivalent = "\r"
|
||
saveButton.target = self
|
||
saveButton.action = #selector(saveAgentConfig)
|
||
|
||
statusLabel.font = .systemFont(ofSize: 11)
|
||
statusLabel.textColor = .secondaryLabelColor
|
||
statusLabel.alignment = .center
|
||
statusLabel.maximumNumberOfLines = 2
|
||
statusLabel.translatesAutoresizingMaskIntoConstraints = false
|
||
|
||
content.addSubview(modelLabel)
|
||
content.addSubview(modelPopup)
|
||
content.addSubview(allowLabel)
|
||
content.addSubview(allowModePopup)
|
||
content.addSubview(inputLabel)
|
||
content.addSubview(inputModePopup)
|
||
content.addSubview(thinkLabel)
|
||
content.addSubview(thinkingModePopup)
|
||
content.addSubview(toolsModeLabel)
|
||
content.addSubview(toolsModePopup)
|
||
content.addSubview(newConvLabel)
|
||
content.addSubview(newConversationModePopup)
|
||
content.addSubview(thresholdLabel)
|
||
content.addSubview(autoNewTokenThresholdField)
|
||
content.addSubview(agentSpeakCheckbox)
|
||
content.addSubview(voiceAutoSendCheckbox)
|
||
content.addSubview(assistantNameLabel)
|
||
content.addSubview(assistantNameField)
|
||
content.addSubview(userNameLabel)
|
||
content.addSubview(userNameField)
|
||
content.addSubview(assistantToneLabel)
|
||
content.addSubview(assistantTonePopup)
|
||
content.addSubview(tavilyLabel)
|
||
content.addSubview(tavilyKeyField)
|
||
content.addSubview(wsLabel)
|
||
content.addSubview(workspaceField)
|
||
content.addSubview(browseButton)
|
||
content.addSubview(tableHeaderLabel)
|
||
content.addSubview(addModelButton)
|
||
content.addSubview(scrollView)
|
||
content.addSubview(editDeleteStack)
|
||
content.addSubview(saveButton)
|
||
content.addSubview(statusLabel)
|
||
|
||
NSLayoutConstraint.activate([
|
||
modelLabel.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20),
|
||
modelLabel.topAnchor.constraint(equalTo: content.topAnchor, constant: 24),
|
||
modelLabel.widthAnchor.constraint(equalToConstant: 85),
|
||
modelPopup.leadingAnchor.constraint(equalTo: modelLabel.trailingAnchor, constant: 8),
|
||
modelPopup.centerYAnchor.constraint(equalTo: modelLabel.centerYAnchor),
|
||
modelPopup.widthAnchor.constraint(equalToConstant: 200),
|
||
|
||
allowLabel.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20),
|
||
allowLabel.topAnchor.constraint(equalTo: modelLabel.bottomAnchor, constant: 16),
|
||
allowLabel.widthAnchor.constraint(equalToConstant: 85),
|
||
allowModePopup.leadingAnchor.constraint(equalTo: allowLabel.trailingAnchor, constant: 8),
|
||
allowModePopup.centerYAnchor.constraint(equalTo: allowLabel.centerYAnchor),
|
||
allowModePopup.widthAnchor.constraint(equalToConstant: 200),
|
||
|
||
inputLabel.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20),
|
||
inputLabel.topAnchor.constraint(equalTo: allowLabel.bottomAnchor, constant: 16),
|
||
inputLabel.widthAnchor.constraint(equalToConstant: 85),
|
||
inputModePopup.leadingAnchor.constraint(equalTo: inputLabel.trailingAnchor, constant: 8),
|
||
inputModePopup.centerYAnchor.constraint(equalTo: inputLabel.centerYAnchor),
|
||
inputModePopup.widthAnchor.constraint(equalToConstant: 200),
|
||
|
||
thinkLabel.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20),
|
||
thinkLabel.topAnchor.constraint(equalTo: inputLabel.bottomAnchor, constant: 16),
|
||
thinkLabel.widthAnchor.constraint(equalToConstant: 85),
|
||
thinkingModePopup.leadingAnchor.constraint(equalTo: thinkLabel.trailingAnchor, constant: 8),
|
||
thinkingModePopup.centerYAnchor.constraint(equalTo: thinkLabel.centerYAnchor),
|
||
thinkingModePopup.widthAnchor.constraint(equalToConstant: 200),
|
||
|
||
toolsModeLabel.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20),
|
||
toolsModeLabel.topAnchor.constraint(equalTo: thinkLabel.bottomAnchor, constant: 16),
|
||
toolsModeLabel.widthAnchor.constraint(equalToConstant: 85),
|
||
toolsModePopup.leadingAnchor.constraint(equalTo: toolsModeLabel.trailingAnchor, constant: 8),
|
||
toolsModePopup.centerYAnchor.constraint(equalTo: toolsModeLabel.centerYAnchor),
|
||
toolsModePopup.widthAnchor.constraint(equalToConstant: 200),
|
||
|
||
newConvLabel.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20),
|
||
newConvLabel.topAnchor.constraint(equalTo: toolsModeLabel.bottomAnchor, constant: 16),
|
||
newConvLabel.widthAnchor.constraint(equalToConstant: 85),
|
||
newConversationModePopup.leadingAnchor.constraint(equalTo: newConvLabel.trailingAnchor, constant: 8),
|
||
newConversationModePopup.centerYAnchor.constraint(equalTo: newConvLabel.centerYAnchor),
|
||
newConversationModePopup.widthAnchor.constraint(equalToConstant: 200),
|
||
|
||
thresholdLabel.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20),
|
||
thresholdLabel.topAnchor.constraint(equalTo: newConvLabel.bottomAnchor, constant: 16),
|
||
thresholdLabel.widthAnchor.constraint(equalToConstant: 85),
|
||
autoNewTokenThresholdField.leadingAnchor.constraint(equalTo: thresholdLabel.trailingAnchor, constant: 8),
|
||
autoNewTokenThresholdField.centerYAnchor.constraint(equalTo: thresholdLabel.centerYAnchor),
|
||
autoNewTokenThresholdField.widthAnchor.constraint(equalToConstant: 100),
|
||
|
||
agentSpeakCheckbox.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20),
|
||
agentSpeakCheckbox.topAnchor.constraint(equalTo: thresholdLabel.bottomAnchor, constant: 10),
|
||
|
||
voiceAutoSendCheckbox.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20),
|
||
voiceAutoSendCheckbox.topAnchor.constraint(equalTo: agentSpeakCheckbox.bottomAnchor, constant: 6),
|
||
|
||
// 助手名字
|
||
assistantNameLabel.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20),
|
||
assistantNameLabel.topAnchor.constraint(equalTo: voiceAutoSendCheckbox.bottomAnchor, constant: 14),
|
||
assistantNameLabel.widthAnchor.constraint(equalToConstant: 85),
|
||
assistantNameField.leadingAnchor.constraint(equalTo: assistantNameLabel.trailingAnchor, constant: 8),
|
||
assistantNameField.centerYAnchor.constraint(equalTo: assistantNameLabel.centerYAnchor),
|
||
assistantNameField.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -20),
|
||
|
||
// 用户称呼
|
||
userNameLabel.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20),
|
||
userNameLabel.topAnchor.constraint(equalTo: assistantNameLabel.bottomAnchor, constant: 14),
|
||
userNameLabel.widthAnchor.constraint(equalToConstant: 85),
|
||
userNameField.leadingAnchor.constraint(equalTo: userNameLabel.trailingAnchor, constant: 8),
|
||
userNameField.centerYAnchor.constraint(equalTo: userNameLabel.centerYAnchor),
|
||
userNameField.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -20),
|
||
|
||
// 助手语气
|
||
assistantToneLabel.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20),
|
||
assistantToneLabel.topAnchor.constraint(equalTo: userNameLabel.bottomAnchor, constant: 14),
|
||
assistantToneLabel.widthAnchor.constraint(equalToConstant: 85),
|
||
assistantTonePopup.leadingAnchor.constraint(equalTo: assistantToneLabel.trailingAnchor, constant: 8),
|
||
assistantTonePopup.centerYAnchor.constraint(equalTo: assistantToneLabel.centerYAnchor),
|
||
assistantTonePopup.widthAnchor.constraint(equalToConstant: 200),
|
||
|
||
tavilyLabel.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20),
|
||
tavilyLabel.topAnchor.constraint(equalTo: assistantToneLabel.bottomAnchor, constant: 14),
|
||
tavilyLabel.widthAnchor.constraint(equalToConstant: 85),
|
||
tavilyKeyField.leadingAnchor.constraint(equalTo: tavilyLabel.trailingAnchor, constant: 8),
|
||
tavilyKeyField.centerYAnchor.constraint(equalTo: tavilyLabel.centerYAnchor),
|
||
tavilyKeyField.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -20),
|
||
|
||
wsLabel.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20),
|
||
wsLabel.topAnchor.constraint(equalTo: tavilyLabel.bottomAnchor, constant: 16),
|
||
wsLabel.widthAnchor.constraint(equalToConstant: 85),
|
||
workspaceField.leadingAnchor.constraint(equalTo: wsLabel.trailingAnchor, constant: 8),
|
||
workspaceField.centerYAnchor.constraint(equalTo: wsLabel.centerYAnchor),
|
||
workspaceField.trailingAnchor.constraint(equalTo: browseButton.leadingAnchor, constant: -6),
|
||
browseButton.centerYAnchor.constraint(equalTo: wsLabel.centerYAnchor),
|
||
browseButton.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -20),
|
||
browseButton.widthAnchor.constraint(equalToConstant: 60),
|
||
|
||
tableHeaderLabel.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 22),
|
||
tableHeaderLabel.topAnchor.constraint(equalTo: wsLabel.bottomAnchor, constant: 22),
|
||
addModelButton.centerYAnchor.constraint(equalTo: tableHeaderLabel.centerYAnchor),
|
||
addModelButton.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -20),
|
||
addModelButton.widthAnchor.constraint(equalToConstant: 28),
|
||
|
||
scrollView.topAnchor.constraint(equalTo: tableHeaderLabel.bottomAnchor, constant: 6),
|
||
scrollView.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20),
|
||
scrollView.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -20),
|
||
scrollView.heightAnchor.constraint(equalToConstant: 170),
|
||
|
||
editDeleteStack.topAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 8),
|
||
editDeleteStack.centerXAnchor.constraint(equalTo: content.centerXAnchor),
|
||
|
||
saveButton.topAnchor.constraint(equalTo: editDeleteStack.bottomAnchor, constant: 12),
|
||
saveButton.centerXAnchor.constraint(equalTo: content.centerXAnchor),
|
||
saveButton.widthAnchor.constraint(equalToConstant: 140),
|
||
|
||
statusLabel.topAnchor.constraint(equalTo: saveButton.bottomAnchor, constant: 8),
|
||
statusLabel.centerXAnchor.constraint(equalTo: content.centerXAnchor),
|
||
statusLabel.widthAnchor.constraint(equalToConstant: 460)
|
||
])
|
||
|
||
refreshAgentModelTable()
|
||
}
|
||
|
||
@objc private func browseWorkspace() {
|
||
let panel = NSOpenPanel()
|
||
panel.canChooseDirectories = true
|
||
panel.canChooseFiles = false
|
||
panel.message = "选择智能体工作目录"
|
||
panel.begin { [weak self] response in
|
||
guard response == .OK, let url = panel.url else { return }
|
||
self?.workspaceField.stringValue = url.path
|
||
Config.shared.agentWorkspace = url.path
|
||
}
|
||
}
|
||
|
||
// MARK: - Agent 事件
|
||
|
||
@objc private func agentModelChanged() {}
|
||
@objc private func agentAllowModeChanged() {
|
||
Config.shared.agentAllowMode = allowModePopup.indexOfSelectedItem == 0 ? "read_only" : "full_access"
|
||
}
|
||
@objc private func agentInputModeChanged() {
|
||
Config.shared.agentInputMode = inputModePopup.indexOfSelectedItem == 0 ? "voice" : "text"
|
||
}
|
||
@objc private func agentThinkingModeChanged() {
|
||
Config.shared.agentThinkingMode = thinkingModePopup.indexOfSelectedItem == 1
|
||
}
|
||
|
||
@objc private func agentSpeakToggled() {
|
||
Config.shared.agentSpeakEnabled = agentSpeakCheckbox.state == .on
|
||
statusLabel.stringValue = agentSpeakCheckbox.state == .on ? "✅ 朗读已开启" : "✅ 朗读已关闭"
|
||
statusLabel.textColor = .systemGreen
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
|
||
self?.statusLabel.stringValue = ""
|
||
}
|
||
}
|
||
|
||
@objc private func agentVoiceAutoSendToggled() {
|
||
Config.shared.agentVoiceAutoSend = voiceAutoSendCheckbox.state == .on
|
||
}
|
||
|
||
@objc private func agentAutoNewThresholdChanged() {
|
||
let val = Int(autoNewTokenThresholdField.stringValue) ?? 40000
|
||
Config.shared.agentAutoNewTokenThreshold = max(1000, val)
|
||
autoNewTokenThresholdField.stringValue = String(Config.shared.agentAutoNewTokenThreshold)
|
||
}
|
||
|
||
@objc private func saveAgentConfig() {
|
||
var modelsFile = Config.shared.loadAgentModels()
|
||
modelsFile.tavilyApiKey = tavilyKeyField.stringValue.trimmingCharacters(in: .whitespaces)
|
||
modelsFile.defaultModel = modelPopup.titleOfSelectedItem ?? ""
|
||
modelsFile.models = agentModelList
|
||
Config.shared.saveAgentModels(modelsFile)
|
||
Config.shared.agentWorkspace = workspaceField.stringValue
|
||
// 保存助手个性化配置
|
||
Config.shared.assistantName = assistantNameField.stringValue.trimmingCharacters(in: .whitespaces)
|
||
let tones = ["友好", "专业", "幽默", "简洁", "温暖", "正式"]
|
||
let toneIdx = assistantTonePopup.indexOfSelectedItem
|
||
if toneIdx >= 0, toneIdx < tones.count {
|
||
Config.shared.assistantTone = tones[toneIdx]
|
||
}
|
||
Config.shared.userName = userNameField.stringValue.trimmingCharacters(in: .whitespaces)
|
||
let modeMap = ["all", "hide_final", "never"]
|
||
let modeIdx = toolsModePopup.indexOfSelectedItem
|
||
if modeIdx >= 0, modeIdx < modeMap.count {
|
||
Config.shared.agentToolsMode = modeMap[modeIdx]
|
||
}
|
||
let convModeMap = ["manual", "auto"]
|
||
let convIdx = newConversationModePopup.indexOfSelectedItem
|
||
if convIdx >= 0, convIdx < convModeMap.count {
|
||
Config.shared.agentNewConversationMode = convModeMap[convIdx]
|
||
}
|
||
// 保存自动新建阈值
|
||
let thresholdVal = Int(autoNewTokenThresholdField.stringValue) ?? 40000
|
||
Config.shared.agentAutoNewTokenThreshold = max(1000, thresholdVal)
|
||
refreshAgentModelTable()
|
||
statusLabel.stringValue = "✅ Agent 配置已保存"
|
||
statusLabel.textColor = .systemGreen
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
|
||
self?.statusLabel.stringValue = ""
|
||
}
|
||
}
|
||
|
||
@objc private func addAgentModel() {
|
||
AgentModelEditor.show(for: nil) { [weak self] newModel in
|
||
guard let self = self else { return }
|
||
self.agentModelList.append(newModel)
|
||
self.modelTableView.reloadData()
|
||
self.statusLabel.stringValue = "✅ 模型已添加(点击 Save 保存到文件)"
|
||
self.statusLabel.textColor = .systemGreen
|
||
}
|
||
}
|
||
|
||
@objc private func editAgentModel() {
|
||
let selected = modelTableView.selectedRow
|
||
guard selected >= 0, selected < agentModelList.count else {
|
||
statusLabel.stringValue = "请先选中一个模型"
|
||
statusLabel.textColor = .systemRed
|
||
return
|
||
}
|
||
let original = agentModelList[selected]
|
||
AgentModelEditor.show(for: original) { [weak self] newModel in
|
||
guard let self = self else { return }
|
||
if let idx = self.agentModelList.firstIndex(where: { $0.name == original.name }) {
|
||
self.agentModelList[idx] = newModel
|
||
}
|
||
self.modelTableView.reloadData()
|
||
self.statusLabel.stringValue = "✅ 模型已更新(点击 Save 保存到文件)"
|
||
self.statusLabel.textColor = .systemGreen
|
||
}
|
||
}
|
||
|
||
@objc private func deleteAgentModel() {
|
||
let selected = modelTableView.selectedRow
|
||
guard selected >= 0, selected < agentModelList.count else {
|
||
statusLabel.stringValue = "请先选中一个模型"
|
||
statusLabel.textColor = .systemRed
|
||
return
|
||
}
|
||
let model = agentModelList[selected]
|
||
let alert = NSAlert()
|
||
alert.messageText = "删除模型"
|
||
alert.informativeText = "确定要删除模型「\(model.name)」吗?"
|
||
alert.alertStyle = .warning
|
||
alert.addButton(withTitle: "删除")
|
||
alert.addButton(withTitle: "取消")
|
||
if alert.runModal() == .alertFirstButtonReturn {
|
||
agentModelList.remove(at: selected)
|
||
modelTableView.reloadData()
|
||
statusLabel.stringValue = "✅ 已删除(点击 Save 保存到文件)"
|
||
statusLabel.textColor = .systemGreen
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
// MARK: - NSTableView
|
||
|
||
extension SettingsWindow: NSTableViewDataSource {
|
||
func numberOfRows(in tableView: NSTableView) -> Int { agentModelList.count }
|
||
}
|
||
|
||
extension SettingsWindow: NSTableViewDelegate {
|
||
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
|
||
guard row < agentModelList.count, let column = tableColumn else { return nil }
|
||
let model = agentModelList[row]
|
||
let textField: NSTextField
|
||
if let reused = tableView.makeView(withIdentifier: column.identifier, owner: nil) as? NSTextField {
|
||
textField = reused
|
||
} else {
|
||
textField = NSTextField(labelWithString: "")
|
||
textField.identifier = column.identifier
|
||
textField.font = .systemFont(ofSize: 11)
|
||
textField.lineBreakMode = .byTruncatingTail
|
||
textField.maximumNumberOfLines = 1
|
||
}
|
||
switch column.identifier.rawValue {
|
||
case "name": textField.stringValue = model.name
|
||
case "url": textField.stringValue = model.url
|
||
case "key": textField.stringValue = model.maskedKey
|
||
case "modes": textField.stringValue = model.modes
|
||
default: textField.stringValue = ""
|
||
}
|
||
return textField
|
||
}
|
||
}
|