You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

807 lines
30 KiB

import Foundation
import UIKit
import Contacts
import EventKit
import CoreLocation
import MediaPlayer
import AVFoundation
/// 操作结果
public struct Result {
let success: Bool
let message: String
public init(success: Bool, message: String) {
self.success = success
self.message = message
}
}
/// 系统功能处理器 - 负责调用系统API执行各种功能以及注册系统功能
public class SystemFunctionHandler: NSObject {
private let tag = "SystemFunctionHandler"
// 位置管理器
private var locationManager: CLLocationManager?
private var locationCompletion: ((Result) -> Void)?
// 事件存储
private var eventStore: EKEventStore?
public override init() {
super.init()
setupLocationManager()
setupEventStore()
}
/// 设置位置管理器
private func setupLocationManager() {
locationManager = CLLocationManager()
locationManager?.delegate = self
locationManager?.desiredAccuracy = kCLLocationAccuracyBest
}
/// 设置事件存储
private func setupEventStore() {
eventStore = EKEventStore()
}
/// 在MCPClient中注册所有系统功能
/// - Parameter client: MCPClient实例
public func registerAllFunctions(client: MCPClient) {
do {
// 注册退出交互函数
registerExitInteractionFunction(client: client)
// 注册翻译模式函数
registerTranslationModeFunction(client: client)
// 注册发送短信函数
registerSendTextMessageFunction(client: client)
// 注册拨打电话函数
registerMakePhoneCallFunction(client: client)
// 注册设置提醒函数
registerSetReminderFunction(client: client)
// 注册获取当前时间函数
registerGetCurrentTimeFunction(client: client)
// 注册获取当前位置函数
registerGetCurrentLocationFunction(client: client)
// 注册媒体控制函数
registerMediaPlayFunction(client: client)
registerMediaPauseFunction(client: client)
registerMediaPreviousFunction(client: client)
registerMediaNextFunction(client: client)
// 注册打开录音机功能
registerOpenRecorderFunction(client: client)
print("\(tag) 所有系统功能注册完成")
} catch {
print("\(tag) 注册系统功能失败: \(error.localizedDescription)")
}
}
// MARK: - 函数注册
/// 注册退出交互函数
private func registerExitInteractionFunction(client: MCPClient) {
let parameters: [String: Any] = [
"type": "object",
"properties": [:],
"required": []
]
let handler = ExitInteractionHandler()
_ = client.registerLocalFunction(
name: "exit_interaction",
description: "结束当前交互",
parameters: parameters,
handler: handler
)
print("\(tag) 已注册退出交互功能")
}
/// 注册翻译模式函数
private func registerTranslationModeFunction(client: MCPClient) {
let parameters: [String: Any] = [
"type": "object",
"properties": [:],
"required": []
]
let handler = TranslationModeHandler()
_ = client.registerLocalFunction(
name: "enter_translation_mode",
description: "用户请求进入实时翻译模式时,启动实时翻译功能",
parameters: parameters,
handler: handler
)
print("\(tag) 已注册翻译模式功能")
}
/// 注册发送短信函数
private func registerSendTextMessageFunction(client: MCPClient) {
let parameters: [String: Any] = [
"type": "object",
"properties": [
"contact": [
"type": "string",
"description": "联系人姓名或电话号码"
],
"message": [
"type": "string",
"description": "短信内容"
]
],
"required": ["contact", "message"]
]
let handler = SendTextMessageHandler()
_ = client.registerLocalFunction(
name: "send_text_message",
description: "发送短信",
parameters: parameters,
handler: handler
)
print("\(tag) 已注册发送短信功能")
}
/// 注册拨打电话函数
private func registerMakePhoneCallFunction(client: MCPClient) {
let parameters: [String: Any] = [
"type": "object",
"properties": [
"contact": [
"type": "string",
"description": "联系人姓名或电话号码"
]
],
"required": ["contact"]
]
let handler = MakePhoneCallHandler()
_ = client.registerLocalFunction(
name: "make_phone_call",
description: "拨打电话",
parameters: parameters,
handler: handler
)
print("\(tag) 已注册拨打电话功能")
}
/// 注册设置提醒函数
private func registerSetReminderFunction(client: MCPClient) {
let parameters: [String: Any] = [
"type": "object",
"properties": [
"title": [
"type": "string",
"description": "提醒标题"
],
"content": [
"type": "string",
"description": "提醒内容"
],
"time": [
"type": "string",
"description": "提醒时间,格式为'yyyy-MM-dd HH:mm',如'2023-12-31 14:30'"
]
],
"required": ["title", "time"]
]
let handler = SetReminderHandler(eventStore: eventStore)
_ = client.registerLocalFunction(
name: "set_reminder",
description: "设置提醒事项",
parameters: parameters,
handler: handler
)
print("\(tag) 已注册设置提醒功能")
}
/// 注册获取当前时间函数
private func registerGetCurrentTimeFunction(client: MCPClient) {
let parameters: [String: Any] = [
"type": "object",
"properties": [
"format": [
"type": "string",
"enum": ["short", "date", "time", "full", "iso"],
"description": "时间输出粒度,仅可使用枚举值:short(仅时分)、date(仅日期)、time(时分秒)、full(日期+星期+时间)、iso(机器可读)。禁止传入 SimpleDateFormat/strftime 等格式串。"
]
],
"required": []
]
let handler = GetCurrentTimeHandler()
_ = client.registerLocalFunction(
name: "get_current_time",
description: "获取当前日期和时间,返回值已按当前语言本地化并对TTS友好(不含冒号等易被误读的符号)。播报或回复给用户时必须原样使用 result 字段的文本,不要自行改写为 HH:mm:ss 等格式。",
parameters: parameters,
handler: handler
)
print("\(tag) 已注册获取当前时间功能")
}
/// 注册获取当前位置函数
private func registerGetCurrentLocationFunction(client: MCPClient) {
let parameters: [String: Any] = [
"type": "object",
"properties": [:],
"required": []
]
let handler = GetCurrentLocationHandler(systemHandler: self)
_ = client.registerLocalFunction(
name: "get_current_location",
description: "获取当前地理位置",
parameters: parameters,
handler: handler
)
print("\(tag) 已注册获取当前位置功能")
}
/// 注册媒体播放功能
private func registerMediaPlayFunction(client: MCPClient) {
let parameters: [String: Any] = [
"type": "object",
"properties": [:],
"required": []
]
let handler = MediaPlayHandler()
_ = client.registerLocalFunction(
name: "media_play",
description: "播放媒体",
parameters: parameters,
handler: handler
)
print("\(tag) 已注册媒体播放功能")
}
/// 注册媒体暂停功能
private func registerMediaPauseFunction(client: MCPClient) {
let parameters: [String: Any] = [
"type": "object",
"properties": [:],
"required": []
]
let handler = MediaPauseHandler()
_ = client.registerLocalFunction(
name: "media_pause",
description: "暂停媒体播放",
parameters: parameters,
handler: handler
)
print("\(tag) 已注册媒体暂停功能")
}
/// 注册媒体上一首功能
private func registerMediaPreviousFunction(client: MCPClient) {
let parameters: [String: Any] = [
"type": "object",
"properties": [:],
"required": []
]
let handler = MediaPreviousHandler()
_ = client.registerLocalFunction(
name: "media_previous",
description: "播放上一首",
parameters: parameters,
handler: handler
)
print("\(tag) 已注册媒体上一首功能")
}
/// 注册媒体下一首功能
private func registerMediaNextFunction(client: MCPClient) {
let parameters: [String: Any] = [
"type": "object",
"properties": [:],
"required": []
]
let handler = MediaNextHandler()
_ = client.registerLocalFunction(
name: "media_next",
description: "播放下一首",
parameters: parameters,
handler: handler
)
print("\(tag) 已注册媒体下一首功能")
}
/// 注册打开录音机功能
private func registerOpenRecorderFunction(client: MCPClient) {
let parameters: [String: Any] = [
"type": "object",
"properties": [:],
"required": []
]
let handler = OpenRecorderHandler()
_ = client.registerLocalFunction(
name: "open_recorder",
description: "打开系统录音机并开始录音",
parameters: parameters,
handler: handler
)
print("\(tag) 已注册打开录音机功能")
}
// MARK: - 内部方法
/// 获取当前位置
internal func getCurrentLocation() async -> Result {
return await withCheckedContinuation { continuation in
locationCompletion = { result in
continuation.resume(returning: result)
}
guard let locationManager = locationManager else {
continuation.resume(returning: Result(success: false, message: "位置管理器未初始化"))
return
}
// 检查位置权限
let authStatus = locationManager.authorizationStatus
switch authStatus {
case .notDetermined:
locationManager.requestWhenInUseAuthorization()
return
case .denied, .restricted:
continuation.resume(returning: Result(success: false, message: "位置权限被拒绝"))
return
case .authorizedWhenInUse, .authorizedAlways:
break
@unknown default:
continuation.resume(returning: Result(success: false, message: "未知的位置权限状态"))
return
}
// 检查位置服务是否可用
guard CLLocationManager.locationServicesEnabled() else {
continuation.resume(returning: Result(success: false, message: "位置服务未启用"))
return
}
// 请求位置
locationManager.requestLocation()
}
}
}
// MARK: - CLLocationManagerDelegate
extension SystemFunctionHandler: CLLocationManagerDelegate {
public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else {
locationCompletion?(Result(success: false, message: "无法获取位置信息"))
return
}
let latitude = location.coordinate.latitude
let longitude = location.coordinate.longitude
let message = "当前位置:纬度 \(latitude),经度 \(longitude)"
locationCompletion?(Result(success: true, message: message))
locationCompletion = nil
}
public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
locationCompletion?(Result(success: false, message: "获取位置失败:\(error.localizedDescription)"))
locationCompletion = nil
}
public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if status == .authorizedWhenInUse || status == .authorizedAlways {
manager.requestLocation()
} else if status == .denied || status == .restricted {
locationCompletion?(Result(success: false, message: "位置权限被拒绝"))
locationCompletion = nil
}
}
}
// MARK: - 功能处理器实现
/// 退出交互处理器
private class ExitInteractionHandler: FunctionHandler {
func handle(arguments: [String: Any]) async throws -> String {
// 发送退出交互通知
NotificationCenter.default.post(name: NSNotification.Name("ExitInteraction"), object: nil)
return "{\"result\": \"已结束当前交互\"}"
}
}
/// 翻译模式处理器
private class TranslationModeHandler: FunctionHandler {
func handle(arguments: [String: Any]) async throws -> String {
// 发送进入翻译模式通知
NotificationCenter.default.post(name: NSNotification.Name("EnterTranslationMode"), object: nil)
return "{\"result\": \"已进入翻译模式\"}"
}
}
/// 发送短信处理器
private class SendTextMessageHandler: FunctionHandler {
func handle(arguments: [String: Any]) async throws -> String {
guard let contact = arguments["contact"] as? String,
let message = arguments["message"] as? String else {
throw OpenAIException("缺少必要参数")
}
let result = await sendTextMessage(contact: contact, message: message)
return "{\"result\": \"\(result.message)\"}"
}
private func sendTextMessage(contact: String, message: String) async -> Result {
// 在iOS中,我们只能打开短信应用,无法直接发送短信
guard let url = URL(string: "sms:\(contact)&body=\(message.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")") else {
return Result(success: false, message: "无法创建短信URL")
}
return await withCheckedContinuation { continuation in
DispatchQueue.main.async {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url) { success in
if success {
continuation.resume(returning: Result(success: true, message: "已打开短信应用"))
} else {
continuation.resume(returning: Result(success: false, message: "打开短信应用失败"))
}
}
} else {
continuation.resume(returning: Result(success: false, message: "无法打开短信应用"))
}
}
}
}
}
/// 拨打电话处理器
private class MakePhoneCallHandler: FunctionHandler {
func handle(arguments: [String: Any]) async throws -> String {
guard let contact = arguments["contact"] as? String else {
throw OpenAIException("缺少联系人参数")
}
let result = await makePhoneCall(contact: contact)
return "{\"result\": \"\(result.message)\"}"
}
private func makePhoneCall(contact: String) async -> Result {
guard let url = URL(string: "tel:\(contact)") else {
return Result(success: false, message: "无法创建电话URL")
}
return await withCheckedContinuation { continuation in
DispatchQueue.main.async {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url) { success in
if success {
continuation.resume(returning: Result(success: true, message: "已发起电话呼叫"))
} else {
continuation.resume(returning: Result(success: false, message: "发起电话呼叫失败"))
}
}
} else {
continuation.resume(returning: Result(success: false, message: "无法拨打电话"))
}
}
}
}
}
/// 设置提醒处理器
private class SetReminderHandler: FunctionHandler {
private let eventStore: EKEventStore?
init(eventStore: EKEventStore?) {
self.eventStore = eventStore
}
func handle(arguments: [String: Any]) async throws -> String {
guard let title = arguments["title"] as? String,
let timeString = arguments["time"] as? String else {
throw OpenAIException("缺少必要参数")
}
let content = arguments["content"] as? String ?? ""
let result = await setReminder(title: title, content: content, time: timeString)
return "{\"result\": \"\(result.message)\"}"
}
private func setReminder(title: String, content: String, time: String) async -> Result {
guard let eventStore = eventStore else {
return Result(success: false, message: "事件存储未初始化")
}
// 请求日历权限
let status = await requestCalendarAccess()
guard status == .authorized else {
return Result(success: false, message: "日历权限被拒绝")
}
// 解析时间
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm"
guard let date = formatter.date(from: time) else {
return Result(success: false, message: "时间格式错误")
}
// 创建提醒
let reminder = EKReminder(eventStore: eventStore)
reminder.title = title
reminder.notes = content
reminder.calendar = eventStore.defaultCalendarForNewReminders()
let alarm = EKAlarm(absoluteDate: date)
reminder.addAlarm(alarm)
do {
try eventStore.save(reminder, commit: true)
return Result(success: true, message: "提醒设置成功")
} catch {
return Result(success: false, message: "设置提醒失败:\(error.localizedDescription)")
}
}
private func requestCalendarAccess() async -> EKAuthorizationStatus {
return await withCheckedContinuation { continuation in
eventStore?.requestAccess(to: .reminder) { granted, error in
let status = EKEventStore.authorizationStatus(for: .reminder)
continuation.resume(returning: status)
}
}
}
}
/// 获取当前时间处理器
private class GetCurrentTimeHandler: FunctionHandler {
func handle(arguments: [String: Any]) async throws -> String {
let format = arguments["format"] as? String
let result = getCurrentTime(format: format)
return "{\"result\": \"\(result.message)\", \"time\": \"\(result.message)\"}"
}
private func getCurrentTime(format: String?) -> Result {
let now = Date()
let lang = Locale.current.languageCode ?? "en"
let timeStr: String
switch format?.lowercased() {
case "short":
timeStr = formatShortTime(now, lang: lang)
case "date":
timeStr = formatDate(now, lang: lang)
case "time":
timeStr = formatTime(now, lang: lang)
case "full":
timeStr = formatFull(now, lang: lang)
case "iso":
let f = DateFormatter()
f.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
timeStr = f.string(from: now)
default:
timeStr = formatDefault(now, lang: lang)
}
let resultTime: String
switch format?.lowercased() {
case "short", "time", "iso":
resultTime = timeStr
case "full":
resultTime = timeStr
default:
let weekFormatter = DateFormatter()
weekFormatter.dateFormat = "EEEE"
weekFormatter.locale = Locale.current
resultTime = "\(timeStr) \(weekFormatter.string(from: now))"
}
return Result(success: true, message: resultTime)
}
// TTS友好的时间格式,避免AI将":"分隔的时间误读为数字
// CJK 使用单位数 m/s(去掉前导零),其他语言使用英文单词避免冒号
private func formatShortTime(_ date: Date, lang: String) -> String {
let f = DateFormatter()
switch lang {
case "zh": f.dateFormat = "H时m分"; f.locale = Locale(identifier: "zh_CN"); return f.string(from: date)
case "ja": f.dateFormat = "H時m分"; f.locale = Locale(identifier: "ja_JP"); return f.string(from: date)
case "ko": f.dateFormat = "H시 m분"; f.locale = Locale(identifier: "ko_KR"); return f.string(from: date)
default: return formatTimeWestern(date, withSeconds: false)
}
}
private func formatDate(_ date: Date, lang: String) -> String {
let f = DateFormatter()
switch lang {
case "zh": f.dateFormat = "yyyy年M月d日"; f.locale = Locale(identifier: "zh_CN")
case "ja": f.dateFormat = "yyyy年M月d日"; f.locale = Locale(identifier: "ja_JP")
case "ko": f.dateFormat = "yyyy년 M월 d일"; f.locale = Locale(identifier: "ko_KR")
default: f.dateFormat = "MMMM d, yyyy"; f.locale = Locale.current
}
return f.string(from: date)
}
private func formatTime(_ date: Date, lang: String) -> String {
let f = DateFormatter()
switch lang {
case "zh": f.dateFormat = "H时m分s秒"; f.locale = Locale(identifier: "zh_CN"); return f.string(from: date)
case "ja": f.dateFormat = "H時m分s秒"; f.locale = Locale(identifier: "ja_JP"); return f.string(from: date)
case "ko": f.dateFormat = "H시 m분 s초"; f.locale = Locale(identifier: "ko_KR"); return f.string(from: date)
default: return formatTimeWestern(date, withSeconds: true)
}
}
private func formatFull(_ date: Date, lang: String) -> String {
let f = DateFormatter()
switch lang {
case "zh": f.dateFormat = "yyyy年M月d日 EEEE H时m分s秒"; f.locale = Locale(identifier: "zh_CN"); return f.string(from: date)
case "ja": f.dateFormat = "yyyy年M月d日 EEEE H時m分s秒"; f.locale = Locale(identifier: "ja_JP"); return f.string(from: date)
case "ko": f.dateFormat = "yyyy년 M월 d일 EEEE H시 m분 s초"; f.locale = Locale(identifier: "ko_KR"); return f.string(from: date)
default:
f.dateFormat = "MMMM d, yyyy, EEEE, "; f.locale = Locale.current
return f.string(from: date) + formatTimeWestern(date, withSeconds: true)
}
}
private func formatDefault(_ date: Date, lang: String) -> String {
let f = DateFormatter()
switch lang {
case "zh": f.dateFormat = "yyyy年M月d日 H时m分"; f.locale = Locale(identifier: "zh_CN"); return f.string(from: date)
case "ja": f.dateFormat = "yyyy年M月d日 H時m分"; f.locale = Locale(identifier: "ja_JP"); return f.string(from: date)
case "ko": f.dateFormat = "yyyy년 M월 d일 H시 m분"; f.locale = Locale(identifier: "ko_KR"); return f.string(from: date)
default:
f.dateFormat = "MMMM d, yyyy, "; f.locale = Locale.current
return f.string(from: date) + formatTimeWestern(date, withSeconds: false)
}
}
/// 非 CJK 语言的时间格式:避免冒号,使用英文单词,对 TTS 更稳定
private func formatTimeWestern(_ date: Date, withSeconds: Bool) -> String {
let cal = Calendar.current
let hour24 = cal.component(.hour, from: date)
let minute = cal.component(.minute, from: date)
let second = cal.component(.second, from: date)
let h12Raw = hour24 % 12
let h12 = h12Raw == 0 ? 12 : h12Raw
let ampm = hour24 < 12 ? "AM" : "PM"
var parts: [String] = []
parts.append("\(h12) \(h12 == 1 ? "hour" : "hours")")
if minute > 0 || withSeconds {
parts.append("\(minute) \(minute == 1 ? "minute" : "minutes")")
}
if withSeconds {
parts.append("\(second) \(second == 1 ? "second" : "seconds")")
}
parts.append(ampm)
return parts.joined(separator: " ")
}
}
/// 获取当前位置处理器
private class GetCurrentLocationHandler: FunctionHandler {
private weak var systemHandler: SystemFunctionHandler?
init(systemHandler: SystemFunctionHandler) {
self.systemHandler = systemHandler
}
func handle(arguments: [String: Any]) async throws -> String {
guard let systemHandler = systemHandler else {
throw OpenAIException("系统处理器未初始化")
}
let result = await systemHandler.getCurrentLocation()
return "{\"result\": \"\(result.message)\", \"success\": \(result.success)}"
}
}
/// 媒体播放处理器
private class MediaPlayHandler: FunctionHandler {
func handle(arguments: [String: Any]) async throws -> String {
let result = controlMedia(action: "play")
return "{\"result\": \"\(result.message)\"}"
}
private func controlMedia(action: String) -> Result {
let commandCenter = MPRemoteCommandCenter.shared()
switch action {
case "play":
// 发送播放命令通知
NotificationCenter.default.post(name: NSNotification.Name("MediaPlay"), object: nil)
return Result(success: true, message: "已开始播放媒体")
default:
return Result(success: false, message: "未知的媒体操作")
}
}
}
/// 媒体暂停处理器
private class MediaPauseHandler: FunctionHandler {
func handle(arguments: [String: Any]) async throws -> String {
let result = controlMedia(action: "pause")
return "{\"result\": \"\(result.message)\"}"
}
private func controlMedia(action: String) -> Result {
switch action {
case "pause":
// 发送暂停命令通知
NotificationCenter.default.post(name: NSNotification.Name("MediaPause"), object: nil)
return Result(success: true, message: "已暂停媒体播放")
default:
return Result(success: false, message: "未知的媒体操作")
}
}
}
/// 媒体上一首处理器
private class MediaPreviousHandler: FunctionHandler {
func handle(arguments: [String: Any]) async throws -> String {
let result = controlMedia(action: "previous")
return "{\"result\": \"\(result.message)\"}"
}
private func controlMedia(action: String) -> Result {
switch action {
case "previous":
// 发送上一首命令通知
NotificationCenter.default.post(name: NSNotification.Name("MediaPrevious"), object: nil)
return Result(success: true, message: "已切换到上一首")
default:
return Result(success: false, message: "未知的媒体操作")
}
}
}
/// 媒体下一首处理器
private class MediaNextHandler: FunctionHandler {
func handle(arguments: [String: Any]) async throws -> String {
let result = controlMedia(action: "next")
return "{\"result\": \"\(result.message)\"}"
}
private func controlMedia(action: String) -> Result {
switch action {
case "next":
// 发送下一首命令通知
NotificationCenter.default.post(name: NSNotification.Name("MediaNext"), object: nil)
return Result(success: true, message: "已切换到下一首")
default:
return Result(success: false, message: "未知的媒体操作")
}
}
}
/// 打开录音机处理器
private class OpenRecorderHandler: FunctionHandler {
func handle(arguments: [String: Any]) async throws -> String {
let result = openRecorder()
return "{\"result\": \"\(result.message)\"}"
}
private func openRecorder() -> Result {
// 尝试打开语音备忘录应用
if let url = URL(string: "voicememos://") {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url)
return Result(success: true, message: "已打开语音备忘录")
}
}
return Result(success: false, message: "无法打开录音机应用")
}
}