VoiceInput/Sources/App/MenuBarController.swift
JOJO f07a72de2b fix(agent): 多模态工具过滤 + memory 新类型 + UI 优化
- 根据模型 multimodal 字段过滤 read_mediafile 工具
- prompt 新增模型多模态能力说明
- 记忆新增 machine 类型(本机路径/脚本/配置)
- 记忆内容统一用「用户」指代,禁止用名字
- done 后按 Fn 不再清空之前内容
- 设置窗口 checkbox 文字与其他控件对齐
- 菜单和设置窗口去掉所有 emoji
2026-04-30 21:09:08 +08:00

435 lines
15 KiB
Swift
Raw Permalink 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 Cocoa
///
final class MenuBarController: NSObject {
private let statusItem: NSStatusItem
private let menu = NSMenu()
private var languageMenuItems: [NSMenuItem] = []
private var modeMenuItems: [NSMenuItem] = []
// LLM
private let inputLLMSubmenu = NSMenu()
private let inputLLMEnabledItem: NSMenuItem
private let assistLLMSubmenu = NSMenu()
private let assistLLMEnabledItem: NSMenuItem
private let inputSettingsWindow: SettingsWindow
private let assistSettingsWindow: SettingsWindow
private let agentSettingsWindow: SettingsWindow
private let agentLLMEnabledItem: NSMenuItem
private let agentVoiceInputItem: NSMenuItem
private let agentTextInputItem: NSMenuItem
private let memoryWindow: MemoryWindow
override init() {
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
inputLLMEnabledItem = NSMenuItem(
title: "Enable",
action: #selector(toggleInputLLM),
keyEquivalent: ""
)
assistLLMEnabledItem = NSMenuItem(
title: "Enable",
action: #selector(toggleAssistLLM),
keyEquivalent: ""
)
inputSettingsWindow = SettingsWindow(mode: .input)
assistSettingsWindow = SettingsWindow(mode: .assist)
agentSettingsWindow = SettingsWindow(mode: .agent)
memoryWindow = MemoryWindow()
agentLLMEnabledItem = NSMenuItem(
title: "Enable",
action: #selector(toggleAgentLLM),
keyEquivalent: ""
)
agentVoiceInputItem = NSMenuItem(
title: "语音输入",
action: #selector(selectAgentVoiceInput),
keyEquivalent: ""
)
agentTextInputItem = NSMenuItem(
title: "打字输入",
action: #selector(selectAgentTextInput),
keyEquivalent: ""
)
super.init()
configureStatusItem()
buildMenu()
}
private func configureStatusItem() {
if let button = statusItem.button {
button.image = NSImage(
systemSymbolName: "mic.fill",
accessibilityDescription: "Voice Input"
)
button.toolTip = "Voice Input — 按住 Fn 键录音"
}
}
private func buildMenu() {
//
let currentLanguageItem = NSMenuItem(
title: "Language: \(Config.shared.language.displayName)",
action: nil,
keyEquivalent: ""
)
currentLanguageItem.isEnabled = false
menu.addItem(currentLanguageItem)
menu.addItem(.separator())
//
let languageMenu = NSMenu()
for lang in RecognitionLanguage.allCases {
let item = NSMenuItem(
title: lang.displayName,
action: #selector(selectLanguage(_:)),
keyEquivalent: ""
)
item.target = self
item.representedObject = lang
item.state = lang == Config.shared.language ? .on : .off
languageMenu.addItem(item)
languageMenuItems.append(item)
}
// autoenablesItems
languageMenu.autoenablesItems = false
let languageSubmenuItem = NSMenuItem()
languageSubmenuItem.title = "Language"
languageSubmenuItem.submenu = languageMenu
menu.addItem(languageSubmenuItem)
//
for item in languageMenuItems {
print("[Menu Debug] Language item: '\(item.title)' enabled=\(item.isEnabled) target=\(String(describing: item.target)) action=\(String(describing: item.action))")
}
menu.addItem(.separator())
// ===== =====
let modeMenu = NSMenu()
let inputModeItem = NSMenuItem(
title: "语音输入",
action: #selector(selectMode(_:)),
keyEquivalent: ""
)
inputModeItem.target = self
inputModeItem.representedObject = TriggerMode.input
inputModeItem.state = Config.shared.triggerMode == .input ? .on : .off
modeMenuItems.append(inputModeItem)
modeMenu.addItem(inputModeItem)
let assistModeItem = NSMenuItem(
title: "AI 助手",
action: #selector(selectMode(_:)),
keyEquivalent: ""
)
assistModeItem.target = self
assistModeItem.representedObject = TriggerMode.assist
assistModeItem.state = Config.shared.triggerMode == .assist ? .on : .off
modeMenuItems.append(assistModeItem)
modeMenu.addItem(assistModeItem)
let agentModeItem = NSMenuItem(
title: "智能体",
action: #selector(selectMode(_:)),
keyEquivalent: ""
)
agentModeItem.target = self
agentModeItem.representedObject = TriggerMode.agent
agentModeItem.state = Config.shared.triggerMode == .agent ? .on : .off
modeMenuItems.append(agentModeItem)
modeMenu.addItem(agentModeItem)
// autoenablesItems
modeMenu.autoenablesItems = false
let modeSubmenuItem = NSMenuItem()
modeSubmenuItem.title = "Mode"
modeSubmenuItem.submenu = modeMenu
menu.addItem(modeSubmenuItem)
//
for item in modeMenuItems {
print("[Menu Debug] Mode item: '\(item.title)' enabled=\(item.isEnabled) target=\(String(describing: item.target)) action=\(String(describing: item.action))")
}
menu.addItem(.separator())
// ===== Input LLM =====
inputLLMSubmenu.addItem(inputLLMEnabledItem)
inputLLMEnabledItem.target = self
inputLLMEnabledItem.action = #selector(toggleInputLLM)
updateInputLLMMenuState()
let inputSettingsItem = NSMenuItem(
title: "Settings...",
action: #selector(openInputSettings),
keyEquivalent: ""
)
inputSettingsItem.target = self
inputLLMSubmenu.addItem(inputSettingsItem)
let inputSubmenuItem = NSMenuItem()
inputSubmenuItem.title = "Input LLM"
inputSubmenuItem.submenu = inputLLMSubmenu
menu.addItem(inputSubmenuItem)
menu.addItem(.separator())
// ===== Assist LLM =====
assistLLMSubmenu.addItem(assistLLMEnabledItem)
assistLLMEnabledItem.target = self
assistLLMEnabledItem.action = #selector(toggleAssistLLM)
updateAssistLLMMenuState()
let assistSettingsItem = NSMenuItem(
title: "Settings...",
action: #selector(openAssistSettings),
keyEquivalent: ""
)
assistSettingsItem.target = self
assistLLMSubmenu.addItem(assistSettingsItem)
let assistSubmenuItem = NSMenuItem()
assistSubmenuItem.title = "Assist LLM"
assistSubmenuItem.submenu = assistLLMSubmenu
menu.addItem(assistSubmenuItem)
menu.addItem(.separator())
// ===== Agent LLM =====
let agentLLMSubmenu = NSMenu()
agentLLMEnabledItem.target = self
agentLLMSubmenu.addItem(agentLLMEnabledItem)
updateAgentLLMMenuState()
agentLLMSubmenu.addItem(.separator())
agentVoiceInputItem.target = self
agentVoiceInputItem.state = Config.shared.agentInputMode == "voice" ? .on : .off
agentLLMSubmenu.addItem(agentVoiceInputItem)
agentTextInputItem.target = self
agentTextInputItem.state = Config.shared.agentInputMode == "text" ? .on : .off
agentLLMSubmenu.addItem(agentTextInputItem)
agentLLMSubmenu.addItem(.separator())
let agentSettingsItem = NSMenuItem(
title: "Settings...",
action: #selector(openAgentSettings),
keyEquivalent: ""
)
agentSettingsItem.target = self
agentSettingsItem.isEnabled = true
agentLLMSubmenu.addItem(agentSettingsItem)
let agentSubmenuItem = NSMenuItem()
agentSubmenuItem.title = "Agent LLM"
agentSubmenuItem.submenu = agentLLMSubmenu
menu.addItem(agentSubmenuItem)
menu.addItem(.separator())
//
let assistantSettingsItem = NSMenuItem(
title: "助手设置...",
action: #selector(openAgentSettings),
keyEquivalent: ""
)
assistantSettingsItem.target = self
menu.addItem(assistantSettingsItem)
//
let memoryItem = NSMenuItem(
title: "记忆管理...",
action: #selector(openMemoryManager),
keyEquivalent: ""
)
memoryItem.target = self
menu.addItem(memoryItem)
menu.addItem(.separator())
// 退
let quitItem = NSMenuItem(
title: "Quit",
action: #selector(quit),
keyEquivalent: "q"
)
quitItem.target = self
menu.addItem(quitItem)
//
menu.autoenablesItems = false
languageMenu.autoenablesItems = false
modeMenu.autoenablesItems = false
inputLLMSubmenu.autoenablesItems = false
assistLLMSubmenu.autoenablesItems = false
agentLLMSubmenu.autoenablesItems = false
print("[Menu Debug] Agent Settings item: title=\(agentSettingsItem.title) enabled=\(agentSettingsItem.isEnabled) target=\(String(describing: agentSettingsItem.target)) action=\(String(describing: agentSettingsItem.action)) submenu=\(String(describing: agentSettingsItem.menu))")
statusItem.menu = menu
}
// MARK: - NSMenuItemValidation
///
@objc func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
// target/action
return true
}
func refreshModeMenu() {
updateModeMenuState()
}
private func updateInputLLMMenuState() {
inputLLMEnabledItem.state = Config.shared.inputLLM.enabled ? .on : .off
}
private func updateAssistLLMMenuState() {
assistLLMEnabledItem.state = Config.shared.assistLLM.enabled ? .on : .off
}
private func updateAgentLLMMenuState() {
let modelsFile = Config.shared.loadAgentModels()
let validModels = modelsFile.models.filter { !$0.url.isEmpty && !$0.name.isEmpty && !$0.apikey.isEmpty && !$0.modes.isEmpty }
agentLLMEnabledItem.state = validModels.isEmpty ? .off : .on
}
private func updateModeMenuState() {
for item in modeMenuItems {
guard let mode = item.representedObject as? TriggerMode else { continue }
item.state = mode == Config.shared.triggerMode ? .on : .off
}
}
private func updateLanguageMenuState() {
for item in languageMenuItems {
guard let lang = item.representedObject as? RecognitionLanguage else { continue }
item.state = lang == Config.shared.language ? .on : .off
}
if let first = menu.items.first {
first.title = "Language: \(Config.shared.language.displayName)"
}
}
@objc private func selectLanguage(_ sender: NSMenuItem) {
print("[Menu Debug] selectLanguage called! sender.title=\(sender.title) sender.representedObject=\(String(describing: sender.representedObject))")
guard let lang = sender.representedObject as? RecognitionLanguage else {
print("[Menu Debug] selectLanguage FAILED: representedObject is not RecognitionLanguage, actual type: \(type(of: sender.representedObject))")
return
}
Config.shared.language = lang
updateLanguageMenuState()
print("[Menu] Language switched to: \(lang.displayName)")
}
@objc private func selectMode(_ sender: NSMenuItem) {
print("[Menu Debug] selectMode called! sender.title=\(sender.title) sender.representedObject=\(String(describing: sender.representedObject))")
guard let mode = sender.representedObject as? TriggerMode else {
print("[Menu Debug] selectMode FAILED: representedObject is not TriggerMode, actual type: \(type(of: sender.representedObject))")
return
}
Config.shared.triggerMode = mode
updateModeMenuState()
print("[Menu] Mode switched to: \(mode)")
}
@objc private func toggleInputLLM() {
var config = Config.shared.inputLLM
config.enabled.toggle()
Config.shared.inputLLM = config
updateInputLLMMenuState()
print("[Menu] Input LLM: \(config.enabled ? "ON" : "OFF")")
}
@objc private func toggleAssistLLM() {
var config = Config.shared.assistLLM
config.enabled.toggle()
Config.shared.assistLLM = config
updateAssistLLMMenuState()
print("[Menu] Assist LLM: \(config.enabled ? "ON" : "OFF")")
}
@objc private func toggleAgentLLM() {
let modelsFile = Config.shared.loadAgentModels()
let validModels = modelsFile.models.filter { !$0.url.isEmpty && !$0.name.isEmpty && !$0.apikey.isEmpty && !$0.modes.isEmpty }
// Settings
if validModels.isEmpty {
openAgentSettings()
}
}
@objc private func openInputSettings() {
showWindowWithDock(inputSettingsWindow)
}
@objc private func openAssistSettings() {
showWindowWithDock(assistSettingsWindow)
}
@objc private func openAgentSettings() {
print("[MenuBar] openAgentSettings called")
agentSettingsWindow.refreshAgentModelTable()
showWindowWithDock(agentSettingsWindow)
}
@objc private func openMemoryManager() {
showWindowWithDock(memoryWindow)
}
@objc private func selectAgentVoiceInput() {
Config.shared.agentInputMode = "voice"
agentVoiceInputItem.state = .on
agentTextInputItem.state = .off
}
@objc private func selectAgentTextInput() {
Config.shared.agentInputMode = "text"
agentVoiceInputItem.state = .off
agentTextInputItem.state = .on
}
/// Dock
private var dockVisibleWindowCount = 0
private func showWindowWithDock(_ window: NSWindow) {
dockVisibleWindowCount += 1
if NSApp.activationPolicy() != .regular {
NSApp.setActivationPolicy(.regular)
}
window.delegate = self
window.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
}
@objc private func quit() {
NSApplication.shared.terminate(nil)
}
}
// MARK: - NSWindowDelegateDock
extension MenuBarController: NSWindowDelegate {
func windowWillClose(_ notification: Notification) {
dockVisibleWindowCount -= 1
if dockVisibleWindowCount <= 0 {
dockVisibleWindowCount = 0
NSApp.setActivationPolicy(.accessory)
}
}
}