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.
366 lines
12 KiB
366 lines
12 KiB
import Foundation
|
|
|
|
/// 工具类型枚举
|
|
public enum ToolType {
|
|
case localFunction // 本地函数
|
|
case mcpTool // MCP工具
|
|
}
|
|
|
|
/// 函数处理器协议
|
|
public protocol FunctionHandler {
|
|
/// 处理函数调用
|
|
/// - Parameter arguments: 函数参数,字典格式
|
|
/// - Returns: 函数执行结果,字符串
|
|
func handle(arguments: [String: Any]) async throws -> String
|
|
}
|
|
|
|
/// 单个MCP子客户端
|
|
/// 负责连接到一个具体的MCP服务器并管理其工具
|
|
public class MCPSubClient {
|
|
private let tag: String
|
|
private let serverId: String
|
|
private let serverUrl: String
|
|
|
|
// 服务器提供的工具列表
|
|
private var tools: [[String: Any]] = []
|
|
|
|
// 工具Map列表,用于OpenAI请求
|
|
private var toolMaps: [[String: Any]] = []
|
|
|
|
// 是否已连接
|
|
private var isConnectedFlag = false
|
|
|
|
// 工具名称到工具的映射,用于快速查找
|
|
private var toolMapping: [String: [String: Any]] = [:]
|
|
|
|
// URL会话
|
|
private lazy var urlSession: URLSession = {
|
|
let config = URLSessionConfiguration.default
|
|
config.timeoutIntervalForRequest = 30
|
|
config.timeoutIntervalForResource = 30
|
|
return URLSession(configuration: config)
|
|
}()
|
|
|
|
public init(serverId: String, serverUrl: String) {
|
|
self.serverId = serverId
|
|
self.serverUrl = serverUrl
|
|
self.tag = "MCPSubClient[\(serverId)]"
|
|
print("\(tag) 初始化,URL: \(serverUrl)")
|
|
}
|
|
|
|
/// 连接到MCP服务器(通过SSE)
|
|
/// - Returns: 是否连接成功
|
|
public func connect() async -> Bool {
|
|
do {
|
|
print("\(tag) 开始连接服务器: \(serverUrl)")
|
|
|
|
// 这里应该实现实际的SSE连接逻辑
|
|
// 由于iOS没有直接的SSE库,我们可以使用URLSession来模拟
|
|
// 或者使用第三方库如EventSource
|
|
|
|
// 模拟连接成功并获取工具列表
|
|
await processToolsList()
|
|
|
|
isConnectedFlag = true
|
|
print("\(tag) 连接成功,可用工具: \(tools.count)个")
|
|
return true
|
|
} catch {
|
|
print("\(tag) 连接失败: \(error.localizedDescription)")
|
|
return false
|
|
}
|
|
}
|
|
|
|
/// 处理工具列表
|
|
private func processToolsList() async {
|
|
// 这里应该从MCP服务器获取实际的工具列表
|
|
// 目前先模拟一些基本工具
|
|
tools = []
|
|
|
|
// 更新工具映射
|
|
toolMapping.removeAll()
|
|
for tool in tools {
|
|
if let name = tool["name"] as? String {
|
|
toolMapping[name] = tool
|
|
}
|
|
}
|
|
|
|
// 更新工具Map列表
|
|
updateToolMaps()
|
|
|
|
print("\(tag) 成功处理 \(tools.count) 个工具")
|
|
}
|
|
|
|
/// 更新工具Map列表,用于OpenAI请求
|
|
/// 将MCP工具转换为OpenAI函数调用格式
|
|
private func updateToolMaps() {
|
|
toolMaps.removeAll()
|
|
|
|
for tool in tools {
|
|
guard let name = tool["name"] as? String,
|
|
let description = tool["description"] as? String else {
|
|
continue
|
|
}
|
|
|
|
let functionMap: [String: Any] = [
|
|
"name": name,
|
|
"description": description,
|
|
"parameters": [
|
|
"type": "object",
|
|
"properties": [:],
|
|
"required": []
|
|
]
|
|
]
|
|
|
|
toolMaps.append([
|
|
"type": "function",
|
|
"function": functionMap
|
|
])
|
|
}
|
|
|
|
print("\(tag) 更新工具映射完成,共 \(toolMaps.count) 个工具")
|
|
}
|
|
|
|
/// 获取工具Map列表,用于OpenAI请求
|
|
public func getToolMaps() -> [[String: Any]] {
|
|
return toolMaps
|
|
}
|
|
|
|
/// 获取可用的工具列表
|
|
public func getTools() -> [[String: Any]] {
|
|
return tools
|
|
}
|
|
|
|
/// 检查是否包含指定工具
|
|
public func containsTool(name: String) -> Bool {
|
|
return toolMapping.keys.contains(name)
|
|
}
|
|
|
|
/// 调用工具
|
|
public func callTool(name: String, arguments: [String: Any]) async throws -> [String: Any] {
|
|
guard isConnectedFlag else {
|
|
print("\(tag) 未连接到服务器")
|
|
return ["context": "未连接到MCP服务器"]
|
|
}
|
|
|
|
guard containsTool(name: name) else {
|
|
print("\(tag) 此客户端不包含工具: \(name)")
|
|
throw OpenAIException("工具不存在: \(name)")
|
|
}
|
|
|
|
// 这里应该实现实际的工具调用逻辑
|
|
// 目前先返回模拟结果
|
|
return [
|
|
"name": name,
|
|
"context": "工具调用结果: \(name)"
|
|
]
|
|
}
|
|
|
|
/// 检查是否已连接
|
|
public func isConnected() -> Bool {
|
|
return isConnectedFlag
|
|
}
|
|
|
|
/// 关闭连接
|
|
public func close() {
|
|
isConnectedFlag = false
|
|
print("\(tag) 已关闭MCP服务器连接")
|
|
}
|
|
}
|
|
|
|
/// MCP客户端,用于连接Model Context Protocol服务器和管理本地函数
|
|
public class MCPClient {
|
|
private let tag = "MCPClient"
|
|
|
|
// 本地函数Map,函数名 -> 处理器
|
|
private var localFunctions: [String: FunctionHandler] = [:]
|
|
|
|
// 本地函数定义Map,函数名 -> 定义
|
|
private var localFunctionDefs: [String: String] = [:]
|
|
|
|
// 子客户端列表,每个连接一个MCP服务器
|
|
private var subClients: [String: MCPSubClient] = [:]
|
|
|
|
// 是否已连接
|
|
private var isConnectedFlag = false
|
|
|
|
// 系统功能处理器
|
|
private var systemFunctionHandler: SystemFunctionHandler?
|
|
|
|
public init() {
|
|
// 初始化系统功能处理器
|
|
systemFunctionHandler = SystemFunctionHandler()
|
|
initializeVoiceFunctions()
|
|
}
|
|
|
|
/// 初始化语音功能处理器
|
|
private func initializeVoiceFunctions() {
|
|
do {
|
|
// 使用SystemFunctionHandler注册所有功能
|
|
systemFunctionHandler?.registerAllFunctions(client: self)
|
|
print("\(tag) 语音功能初始化完成")
|
|
} catch {
|
|
print("\(tag) 语音功能初始化失败: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
|
|
/// 连接到MCP服务器(通过SSE)
|
|
/// 直接接收完整的JSON配置字符串
|
|
/// - Parameter mcpConfigJson: 包含mcpServers字段的JSON配置字符串
|
|
/// - Returns: 是否连接成功
|
|
public func connectToSSE(mcpConfigJson: String) async throws -> Bool {
|
|
// 清除现有连接
|
|
closeAllConnections()
|
|
|
|
var serverMap: [String: String] = [:]
|
|
var configLoaded = false
|
|
|
|
// 尝试解析传入的JSON配置
|
|
if !mcpConfigJson.isEmpty {
|
|
do {
|
|
guard let data = mcpConfigJson.data(using: .utf8),
|
|
let config = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
let mcpServers = config["mcpServers"] as? [String: [String: String]] else {
|
|
throw OpenAIException("无效的MCP配置格式")
|
|
}
|
|
|
|
// 从服务器配置中提取URL
|
|
for (serverId, serverConfig) in mcpServers {
|
|
if let url = serverConfig["url"], !url.isEmpty {
|
|
serverMap[serverId] = url
|
|
print("\(tag) 从配置中添加服务器 [\(serverId)]: \(url)")
|
|
}
|
|
}
|
|
|
|
configLoaded = !serverMap.isEmpty
|
|
} catch {
|
|
print("\(tag) 解析MCP配置失败: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
|
|
// 如果没有从配置中加载到服务器,使用默认配置
|
|
if !configLoaded {
|
|
print("\(tag) 使用默认MCP服务器配置")
|
|
// 可以在这里添加默认的服务器配置
|
|
}
|
|
|
|
// 连接到所有服务器
|
|
var successCount = 0
|
|
for (serverId, serverUrl) in serverMap {
|
|
let subClient = MCPSubClient(serverId: serverId, serverUrl: serverUrl)
|
|
let connected = await subClient.connect()
|
|
|
|
if connected {
|
|
subClients[serverId] = subClient
|
|
successCount += 1
|
|
print("\(tag) 成功连接到服务器 [\(serverId)]")
|
|
} else {
|
|
print("\(tag) 连接服务器失败 [\(serverId)]")
|
|
}
|
|
}
|
|
|
|
isConnectedFlag = successCount > 0
|
|
print("\(tag) MCP客户端连接完成,成功连接 \(successCount)/\(serverMap.count) 个服务器")
|
|
|
|
return isConnectedFlag
|
|
}
|
|
|
|
/// 关闭所有连接
|
|
private func closeAllConnections() {
|
|
for (_, subClient) in subClients {
|
|
subClient.close()
|
|
}
|
|
subClients.removeAll()
|
|
isConnectedFlag = false
|
|
}
|
|
|
|
/// 获取工具Map列表,用于OpenAI请求
|
|
public func getToolMaps() -> [[String: Any]] {
|
|
var allToolMaps: [[String: Any]] = []
|
|
|
|
// 添加所有子客户端的工具
|
|
for (_, subClient) in subClients {
|
|
allToolMaps.append(contentsOf: subClient.getToolMaps())
|
|
}
|
|
|
|
// 添加本地函数
|
|
for (name, _) in localFunctions {
|
|
if let def = localFunctionDefs[name],
|
|
let data = def.data(using: .utf8),
|
|
let functionDef = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
|
allToolMaps.append([
|
|
"type": "function",
|
|
"function": functionDef
|
|
])
|
|
}
|
|
}
|
|
|
|
return allToolMaps
|
|
}
|
|
|
|
/// 注册本地函数
|
|
public func registerLocalFunction(name: String, description: String, parameters: [String: Any], handler: FunctionHandler) -> Bool {
|
|
do {
|
|
localFunctions[name] = handler
|
|
|
|
let functionDef: [String: Any] = [
|
|
"name": name,
|
|
"description": description,
|
|
"parameters": parameters
|
|
]
|
|
|
|
let jsonData = try JSONSerialization.data(withJSONObject: functionDef)
|
|
let jsonString = String(data: jsonData, encoding: .utf8) ?? ""
|
|
localFunctionDefs[name] = jsonString
|
|
|
|
print("\(tag) 成功注册本地函数: \(name)")
|
|
return true
|
|
} catch {
|
|
print("\(tag) 注册本地函数失败: \(error.localizedDescription)")
|
|
return false
|
|
}
|
|
}
|
|
|
|
/// 调用工具
|
|
public func callTool(name: String, arguments: [String: Any]) async throws -> [String: Any] {
|
|
// 首先检查是否是本地函数
|
|
if let handler = localFunctions[name] {
|
|
do {
|
|
let result = try await handler.handle(arguments: arguments)
|
|
return ["context": result]
|
|
} catch {
|
|
throw OpenAIException("本地函数调用失败: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
|
|
// 然后检查MCP工具
|
|
for (_, subClient) in subClients {
|
|
if subClient.containsTool(name: name) {
|
|
return try await subClient.callTool(name: name, arguments: arguments)
|
|
}
|
|
}
|
|
|
|
throw OpenAIException("未找到工具: \(name)")
|
|
}
|
|
|
|
/// 解析JSON参数
|
|
public func parseJsonArguments(_ argumentsJson: String) throws -> [String: Any] {
|
|
guard let data = argumentsJson.data(using: .utf8),
|
|
let arguments = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
return [:]
|
|
}
|
|
return arguments
|
|
}
|
|
|
|
/// 检查是否已连接
|
|
public func isConnected() -> Bool {
|
|
return isConnectedFlag || !localFunctions.isEmpty
|
|
}
|
|
|
|
/// 关闭连接
|
|
public func close() {
|
|
closeAllConnections()
|
|
localFunctions.removeAll()
|
|
localFunctionDefs.removeAll()
|
|
print("\(tag) 已关闭MCP客户端")
|
|
}
|
|
}
|