From 23d0ce1d93c4e57fb9763853b636e526994a38c6 Mon Sep 17 00:00:00 2001 From: Kirigaya <1193466151@qq.com> Date: Mon, 19 May 2025 04:38:36 +0800 Subject: [PATCH] update --- .vscode/tabs.example-servers_puppeteer.json | 28 ++ package.json | 2 +- renderer/src/api/message-bridge.ts | 7 +- renderer/src/hook/panel.ts | 94 ++--- .../src/views/connect/connection-args.vue | 6 +- renderer/src/views/connect/connection-item.ts | 278 +++++++++++-- renderer/src/views/connect/connection.ts | 386 ------------------ renderer/src/views/connect/env-var.vue | 1 - renderer/src/views/connect/index.ts | 30 ++ renderer/src/views/connect/type.ts | 80 ++++ 10 files changed, 444 insertions(+), 468 deletions(-) delete mode 100644 renderer/src/views/connect/connection.ts create mode 100644 renderer/src/views/connect/index.ts create mode 100644 renderer/src/views/connect/type.ts diff --git a/.vscode/tabs.example-servers_puppeteer.json b/.vscode/tabs.example-servers_puppeteer.json index e8d179d..48644ce 100644 --- a/.vscode/tabs.example-servers_puppeteer.json +++ b/.vscode/tabs.example-servers_puppeteer.json @@ -331,6 +331,34 @@ "state": "success", "serverName": "Huoshan DeepSeek" } + }, + { + "role": "user", + "content": "请将下面的更新日志翻译成 GitHub release 风格的英文说明,请只返回翻译后的结果,不要出现任何多余的前缀: ", + "extraInfo": { + "created": 1747598248965, + "state": "success", + "serverName": "deepseek" + } + }, + { + "role": "assistant", + "content": "Here’s the translation in GitHub release style:\n\n---\n\n### **New Features**\n- Added support for real-time data synchronization.\n- Introduced a new dashboard for analytics.\n\n### **Improvements**\n- Optimized query performance for large datasets.\n- Enhanced error handling for API requests.\n\n### **Bug Fixes**\n- Fixed an issue where the system crashed on invalid input.\n- Resolved a memory leak in the background service.\n\n### **Breaking Changes**\n- Removed deprecated endpoints (`/v1/old`).\n- Updated the authentication mechanism to use OAuth 2.0.\n\n### **Documentation**\n- Added detailed guides for API integration.\n- Improved inline code comments.", + "extraInfo": { + "created": 1747598260799, + "state": "success", + "serverName": "deepseek", + "usage": { + "prompt_tokens": 7946, + "completion_tokens": 136, + "total_tokens": 8082, + "prompt_tokens_details": { + "cached_tokens": 896 + }, + "prompt_cache_hit_tokens": 896, + "prompt_cache_miss_tokens": 7050 + } + } } ], "settings": { diff --git a/package.json b/package.json index 7bfea14..23a83f8 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "openmcp", "displayName": "OpenMCP", "description": "An all in one MCP Client/TestTool", - "version": "0.0.8", + "version": "0.1.0", "publisher": "kirigaya", "author": { "name": "kirigaya", diff --git a/renderer/src/api/message-bridge.ts b/renderer/src/api/message-bridge.ts index 1229688..f652321 100644 --- a/renderer/src/api/message-bridge.ts +++ b/renderer/src/api/message-bridge.ts @@ -19,6 +19,11 @@ interface AddCommandListenerOption { once: boolean // 只调用一次就销毁 } +export interface ICommandRequestData { + clientId?: string; + [key: string]: any; +} + export class MessageBridge { private ws: WebSocket | null = null; private handlers = new Map>(); @@ -206,7 +211,7 @@ export class MessageBridge { * @param data * @returns */ - public commandRequest(command: string, data?: any): Promise> { + public commandRequest(command: string, data?: ICommandRequestData): Promise> { return new Promise((resolve, reject) => { this.addCommandListener(command, (data) => { resolve(data as RestFulResponse); diff --git a/renderer/src/hook/panel.ts b/renderer/src/hook/panel.ts index 0713523..4fae94c 100644 --- a/renderer/src/hook/panel.ts +++ b/renderer/src/hook/panel.ts @@ -3,6 +3,7 @@ import { pinkLog } from "@/views/setting/util"; import { debugModes, tabs } from "@/components/main-panel/panel"; import { markRaw, ref, nextTick } from "vue"; import { v4 as uuidv4 } from 'uuid'; +import type { McpClient } from "@/views/connect/connection-item"; interface SaveTabItem { name: string; @@ -12,64 +13,55 @@ interface SaveTabItem { storage: Record; } -interface SaveTab { +export interface SaveTab { tabs: SaveTabItem[] currentIndex: number } export const panelLoaded = ref(false); -export function loadPanels() { - - return new Promise((resolve, reject) => { - const bridge = useMessageBridge(); - - bridge.addCommandListener('panel/load', data => { - if (data.code !== 200) { - pinkLog('tabs 加载失败'); - console.log(data.msg); - - } else { - const persistTab = data.msg as SaveTab; - - pinkLog('tabs 加载成功'); - - if (persistTab.tabs.length === 0) { - // 空的,直接返回不需要管 - panelLoaded.value = true; - resolve(void 0); - return; - } - - tabs.activeIndex = 0; - tabs.content = []; - - for (const tab of persistTab.tabs || []) { - - const component = tab.componentIndex >= 0? markRaw(debugModes[tab.componentIndex]) : undefined; - - tabs.content.push({ - id: uuidv4(), - name: tab.name, - icon: tab.icon, - type: tab.type, - componentIndex: tab.componentIndex, - component: component, - storage: tab.storage - }); - } - - tabs.activeIndex = persistTab.currentIndex; - } - - panelLoaded.value = true; - resolve(void 0); - }, { once: true }); - - bridge.postMessage({ - command: 'panel/load' - }); +export async function loadPanels(client: McpClient) { + const bridge = useMessageBridge(); + const { code, msg } = await bridge.commandRequest('panel/load', { + clientId: client.clientId }); + if (code !== 200) { + pinkLog('tabs 加载失败'); + console.log(msg); + + } else { + const persistTab = msg; + + pinkLog('tabs 加载成功'); + + if (persistTab.tabs.length === 0) { + // 空的,直接返回不需要管 + panelLoaded.value = true; + return; + } + + tabs.activeIndex = 0; + tabs.content = []; + + for (const tab of persistTab.tabs || []) { + + const component = tab.componentIndex >= 0? markRaw(debugModes[tab.componentIndex]) : undefined; + + tabs.content.push({ + id: uuidv4(), + name: tab.name, + icon: tab.icon, + type: tab.type, + componentIndex: tab.componentIndex, + component: component, + storage: tab.storage + }); + } + + tabs.activeIndex = persistTab.currentIndex; + } + + panelLoaded.value = true; } let debounceHandler: NodeJS.Timeout; diff --git a/renderer/src/views/connect/connection-args.vue b/renderer/src/views/connect/connection-args.vue index 1632b0e..148bae5 100644 --- a/renderer/src/views/connect/connection-args.vue +++ b/renderer/src/views/connect/connection-args.vue @@ -25,10 +25,10 @@ {{ t('connect-sigature') }} - +
URL - +
@@ -65,7 +65,7 @@ const rules = reactive({ oauth: [ { required: false, trigger: 'blur' } ], - urlString: [ + url: [ { required: true, message: 'URL不能为空', trigger: 'blur' } ] }) diff --git a/renderer/src/views/connect/connection-item.ts b/renderer/src/views/connect/connection-item.ts index 891b733..8745b03 100644 --- a/renderer/src/views/connect/connection-item.ts +++ b/renderer/src/views/connect/connection-item.ts @@ -1,11 +1,10 @@ +import { useMessageBridge } from "@/api/message-bridge"; import { reactive, type Reactive } from "vue"; +import type { IConnectionResult, ConnectionTypeOptionItem, IConnectionArgs, IConnectionEnvironment, McpOptions } from "./type"; +import { ElMessage } from "element-plus"; +import { loadPanels, type SaveTab } from "@/hook/panel"; +import { getPlatform } from "@/api/platform"; -export type ConnectionType = 'STDIO' | 'SSE' | 'STREAMABLE_HTTP'; - -export interface ConnectionTypeOptionItem { - value: ConnectionType; - label: string; -} export const connectionSelectDataViewOption: ConnectionTypeOptionItem[] = [ { @@ -22,37 +21,266 @@ export const connectionSelectDataViewOption: ConnectionTypeOptionItem[] = [ } ] -export interface IConnectionArgs { - type: ConnectionType; - commandString?: string; - cwd?: string; - urlString?: string; +export async function getLaunchSignature(platform: string): Promise { + const bridge = useMessageBridge(); + const { code, msg } = await bridge.commandRequest(platform + '/launch-signature'); + + if (code !== 200) { + const message = msg.toString(); + ElMessage.error(message); + return []; + } + + // 判断一下版本,新版本的 msg 应该是数组,老版本是对象 + // 返回的数组的第一个为主节点,其余为从节点 + if (Array.isArray(msg)) { + return msg; + } + return [msg]; } -export class McpClient { - public clientId?: string; - public name?: string; - public version?: string; - public connectionArgs: Reactive; - constructor() { + +export class McpClient { + + public connectionArgs: Reactive; + public connectionResult: Reactive; + + public presetsEnvironment: string[] = ['HOME', 'LOGNAME', 'PATH', 'SHELL', 'TERM', 'USER']; + public connectionEnvironment: Reactive; + + constructor( + public clientVersion: string = '0.0.1', + public clientNamePrefix: string = 'openmcp.connect' + ) { + // 连接入参 this.connectionArgs = reactive({ type: 'STDIO', commandString: '', cwd: '', - urlString: '' + url: '', + oauth: '' + }); + + // 连接出参 + this.connectionResult = reactive({ + success: false, + status: 'disconnected', + clientId: '', + name: '', + version: '', + logString: [] + }); + + // 环境变量 + this.connectionEnvironment = reactive({ + data: [], + newKey: '', + newValue: '' }); } - async connect() { + async acquireConnectionSignature(args: IConnectionArgs) { + this.connectionArgs.type = args.type; + this.connectionArgs.commandString = args.commandString || ''; + this.connectionArgs.cwd = args.cwd || ''; + this.connectionArgs.url = args.url || ''; + this.connectionArgs.oauth = args.oauth || ''; + } + get clientId() { + return this.connectionResult.clientId; + } + + get name() { + return this.connectionResult.name; + } + + get version() { + return this.connectionResult.version; + } + + get status() { + return this.connectionResult.status; + } + + get connected() { + return this.connectionResult.success; + } + + get env() { + const env = {} as Record; + this.connectionEnvironment.data.forEach(item => { + env[item.key] = item.value; + }); + return env; + } + + private get commandAndArgs() { + const commandString = this.connectionArgs.commandString; + + if (!commandString) { + return { command: '', args: [] }; + } + + const args = commandString.split(' '); + const command = args.shift() || ''; + + return { command, args }; + } + + get connectOption() { + const { command, args } = this.commandAndArgs; + const env = this.env; + const url = this.connectionArgs.url; + const oauth = this.connectionArgs.oauth; + const connectionType = this.connectionArgs.type; + + const clientName = this.clientNamePrefix + '.' + this.connectionArgs.type; + const clientVersion = this.clientVersion; + + const option: McpOptions = { + connectionType, + command, + args, + url, + oauth, + clientName, + clientVersion, + env, + serverInfo: { + name: this.connectionResult.name, + version: this.connectionResult.version + } + }; + + return option; + } + + public async connect(platform: string) { + const bridge = useMessageBridge(); + const { code, msg } = await bridge.commandRequest('connect', this.connectOption); + + this.connectionResult.success = (code === 200); + + if (code !== 200) { + const message = msg.toString(); + this.connectionResult.logString.push({ + type: 'error', + message + }); + + ElMessage.error(message); + return; + } + + this.connectionResult.status = msg.status; + this.connectionResult.clientId = msg.clientId; + this.connectionResult.name = msg.name; + this.connectionResult.version = msg.version; + + // 同步成功的连接参数到后端,更新 vscode treeview 中的列表 + const deserializeOption = JSON.parse(JSON.stringify(this.connectOption)); + + bridge.postMessage({ + command: platform + '/update-connection-sigature', + data: deserializeOption + }); + } + + /** + * @description 处理环境变量开关 + * - 开启时,刷新预设环境变量的数值 + * - 关闭时,清空预设环境变量的数值 + * @param enabled + */ + public async handleEnvSwitch(enabled: boolean) { + const presetVars = this.presetsEnvironment; + if (enabled) { + const values = await this.lookupEnvVar(presetVars); + + if (values) { + // 将 key values 合并进 connectionEnv.data 中 + // 若已有相同的 key, 则替换 value + for (let i = 0; i < presetVars.length; i++) { + const key = presetVars[i]; + const value = values[i]; + const sameNameItems = this.connectionEnvironment.data.filter(item => item.key === key); + if (sameNameItems.length > 0) { + const conflictItem = sameNameItems[0]; + conflictItem.value = value; + } else { + this.connectionEnvironment.data.push({ + key: key, value: value + }); + } + } + } + } else { + // 清空 connectionEnv.data 中所有 key 为 presetVars 的项 + const reserveItems = this.connectionEnvironment.data.filter(item => !presetVars.includes(item.key)); + this.connectionEnvironment.data = reserveItems; + } + } + + + /** + * @description 查询环境变量 + * @param varNames + * @returns + */ + public async lookupEnvVar(varNames: string[]) { + const bridge = useMessageBridge(); + const { code, msg } = await bridge.commandRequest('lookup-env-var', { keys: varNames }); + + if (code === 200) { + this.connectionResult.logString.push({ + type: 'info', + message: '预设环境变量同步完成' + }); + + return msg; + } else { + this.connectionResult.logString.push({ + type: 'error', + message: '预设环境变量同步失败: ' + msg + }); + } } } -// 用于描述一个连接的数据结构 -export interface McpServer { - type: ConnectionType; - clientId: string; - name: string; -} \ No newline at end of file +class McpClientAdapter { + public clients: McpClient[] = []; + + constructor( + public platform: string + ) {} + + public async launch() { + const launchSignature = await getLaunchSignature(this.platform); + + for (const item of launchSignature) { + const client = new McpClient(); + + // 同步连接参数 + await client.acquireConnectionSignature(item); + + // 同步环境变量 + await client.handleEnvSwitch(true); + + // 连接 + await client.connect(this.platform); + + this.clients.push(client); + } + } + + public async loadPanels() { + const masterNode = this.clients[0]; + await loadPanels(masterNode); + } +} + +const platform = getPlatform(); +export const mcpClientAdapter = new McpClientAdapter(platform); \ No newline at end of file diff --git a/renderer/src/views/connect/connection.ts b/renderer/src/views/connect/connection.ts deleted file mode 100644 index 8715b99..0000000 --- a/renderer/src/views/connect/connection.ts +++ /dev/null @@ -1,386 +0,0 @@ -import { useMessageBridge } from '@/api/message-bridge'; -import { reactive, ref } from 'vue'; -import { pinkLog } from '../setting/util'; -import { ElLoading, ElMessage } from 'element-plus'; -import { getPlatform, type OpenMcpSupportPlatform } from '@/api/platform'; -import { getTour, loadSetting } from '@/hook/setting'; -import { loadPanels } from '@/hook/panel'; -import type { ConnectionType } from './connection-item'; - -export const connectionMethods = reactive<{ - current: ConnectionType, - data: { - value: ConnectionType, - label: string - }[] -}>({ - current: 'STDIO', - data: [ - { - value: 'STDIO', - label: 'STDIO' - }, - { - value: 'SSE', - label: 'SSE' - }, - { - value: 'STREAMABLE_HTTP', - label: 'STREAMABLE_HTTP' - } - ] -}); - -export const connectionSettingRef = ref(null); -export const connectionLogRef = ref(null); - -// 主 mcp 服务器的连接参数 -export const connectionArgs = reactive({ - commandString: '', - cwd: '', - oauth: '', - urlString: '' -}); - -export interface EnvItem { - key: string - value: string -} - -export interface IConnectionEnv { - data: EnvItem[] - newKey: string - newValue: string -} - -export interface ConnectionResult { - status: string - clientId: string - name: string - version: string -} - -export const connectionEnv = reactive({ - data: [], - newKey: '', - newValue: '' -}); - -export function makeEnv() { - const env = {} as Record; - connectionEnv.data.forEach(item => { - env[item.key] = item.value; - }); - return env; -} - - -// 定义命令行参数接口 -export interface McpOptions { - connectionType: ConnectionType; - // STDIO 特定选项 - command?: string; - args?: string[]; - cwd?: string; - env?: Record; - // SSE 特定选项 - url?: string; - // 通用客户端选项 - clientName?: string; - clientVersion?: string; -} - -/** - * @description 试图启动 mcp 服务器,它会 - * 1. 请求启动参数 - * 2. 启动 mcp 服务器 - * 3. 将本次的启动参数同步到本地 - * @param option - * @returns - */ -export async function doConnect( - option: { - namespace: OpenMcpSupportPlatform - updateCommandString?: boolean - } -) { - const { - // updateCommandString 为 true 代表是初始化阶段 - namespace, - updateCommandString = true - } = option; - - // 如果是初始化,则需要请求启动参数 - if (updateCommandString) { - pinkLog('请求启动参数'); - const connectionItem = await getLaunchSignature(namespace + '/launch-signature'); - connectionMethods.current = connectionItem.type; - connectionArgs.commandString = connectionItem.commandString || ''; - connectionArgs.cwd = connectionItem.cwd || ''; - connectionArgs.oauth = connectionItem.oauth || ''; - connectionArgs.urlString = connectionItem.url || ''; - } - - if (connectionMethods.current === 'STDIO') { - return await launchStdio(namespace); - } else { - return await launchRemote(namespace); - } -} - -async function launchStdio(namespace: string) { - const bridge = useMessageBridge(); - const env = makeEnv(); - - const commandComponents = connectionArgs.commandString.split(/\s+/g); - const command = commandComponents[0]; - commandComponents.shift(); - - const connectOption = { - connectionType: 'STDIO', - command: command, - args: commandComponents, - cwd: connectionArgs.cwd, - clientName: 'openmcp.connect.STDIO', - clientVersion: '0.0.1', - env - }; - - const { code, msg } = await bridge.commandRequest('connect', connectOption); - - connectionResult.success = (code === 200); - - if (code === 200) { - - const message = `connect to ${msg.name} ${msg.version} success, clientId: ${msg.clientId}`; - connectionResult.logString.push({ type: 'info', message }); - - connectionResult.serverInfo.name = msg.name || ''; - connectionResult.serverInfo.version = msg.version || ''; - connectionResult.clientId = msg.clientId || ''; - - // 同步信息到 后端 - const commandComponents = connectionArgs.commandString.split(/\s+/g); - const command = commandComponents[0]; - commandComponents.shift(); - - const clientStdioConnectionItem = { - serverInfo: connectionResult.serverInfo, - connectionType: 'STDIO', - name: 'openmcp.connect.STDIO', - command: command, - args: commandComponents, - cwd: connectionArgs.cwd, - env - }; - - bridge.postMessage({ - command: namespace + '/update-connection-sigature', - data: JSON.parse(JSON.stringify(clientStdioConnectionItem)) - }); - - } else { - const messaage = msg.toString(); - connectionResult.logString.push({ - type: 'error', - message: messaage - }); - - ElMessage.error(messaage); - } -} - -async function launchRemote(namespace: string) { - const bridge = useMessageBridge(); - const env = makeEnv(); - - const connectOption: McpOptions = { - connectionType: connectionMethods.current, - url: connectionArgs.urlString, - clientName: 'openmcp.connect.' + connectionMethods.current, - clientVersion: '0.0.1', - env - }; - - const { code, msg } = await bridge.commandRequest('connect', connectOption); - - connectionResult.success = (code === 200); - - if (code === 200) { - const message = `connect to ${msg.name} ${msg.version} success, clientId: ${msg.clientId}`; - - connectionResult.logString.push({ - type: 'info', - message: message - }); - - connectionResult.serverInfo.name = msg.name || ''; - connectionResult.serverInfo.version = msg.version || ''; - connectionResult.clientId = msg.clientId || ''; - - // 同步信息到 vscode - const clientSseConnectionItem = { - serverInfo: connectionResult.serverInfo, - connectionType: connectionMethods.current, - name: 'openmcp.connect.' + connectionMethods.current, - url: connectionArgs.urlString, - oauth: connectionArgs.oauth, - env: env - }; - - bridge.postMessage({ - command: namespace + '/update-connection-sigature', - data: JSON.parse(JSON.stringify(clientSseConnectionItem)) - }); - - } else { - const message = msg.toString(); - connectionResult.logString.push({ - type: 'error', - message: message - }); - - ElMessage.error(message); - } -} - - -async function getLaunchSignature(signatureName: string) { - const bridge = useMessageBridge(); - const { code, msg } = await bridge.commandRequest(signatureName); - - return msg; -} - -export function doReconnect() { - // TODO: finish this - console.log(); -} - -export const connectionResult = reactive<{ - success: boolean, - logString: { type: 'info' | 'error' | 'warning', message: string }[], - serverInfo: { - name: string, - version: string - }, - clientId: string -}>({ - success: false, - logString: [], - serverInfo: { - name: '', - version: '' - }, - clientId: '' -}); - - - -export const envVarStatus = { - launched: false -}; - -function lookupEnvVar(varNames: string[]) { - const bridge = useMessageBridge(); - - return new Promise((resolve, reject) => { - bridge.addCommandListener('lookup-env-var', data => { - const { code, msg } = data; - - if (code === 200) { - connectionResult.logString.push({ - type: 'info', - message: '预设环境变量同步完成' - }); - - resolve(msg); - } else { - connectionResult.logString.push({ - type: 'error', - message: '预设环境变量同步失败: ' + msg - }); - - resolve(undefined); - } - }, { once: true }); - - console.log(varNames); - - - bridge.postMessage({ - command: 'lookup-env-var', - data: { - keys: varNames - } - }) - }); -} - - -export async function handleEnvSwitch(enabled: boolean) { - const presetVars = ['HOME', 'LOGNAME', 'PATH', 'SHELL', 'TERM', 'USER']; - - if (enabled) { - const values = await lookupEnvVar(presetVars); - - if (values) { - // 将 key values 合并进 connectionEnv.data 中 - // 若已有相同的 key, 则替换 value - for (let i = 0; i < presetVars.length; i++) { - const key = presetVars[i]; - const value = values[i]; - const sameNameItems = connectionEnv.data.filter(item => item.key === key); - if (sameNameItems.length > 0) { - const conflictItem = sameNameItems[0]; - conflictItem.value = value; - } else { - connectionEnv.data.push({ - key: key, value: value - }); - } - } - } - } else { - // 清空 connectionEnv.data 中所有 key 为 presetVars 的项 - const reserveItems = connectionEnv.data.filter(item => !presetVars.includes(item.key)); - connectionEnv.data = reserveItems; - } -} - -export async function loadEnvVar() { - return await handleEnvSwitch(true); -} - -export async function initialise() { - - pinkLog('准备请求设置'); - - const loading = ElLoading.service({ - fullscreen: true, - lock: true, - text: 'Loading', - background: 'rgba(0, 0, 0, 0.7)' - }); - const platform = getPlatform(); - - // 加载全局设置 - loadSetting(); - - // 设置环境变量 - await loadEnvVar(); - - // 获取引导状态 - await getTour(); - - // 尝试进行初始化连接 - await doConnect({ - namespace: platform, - updateCommandString: true - }); - - // loading panels - await loadPanels(); - - loading.close(); -} \ No newline at end of file diff --git a/renderer/src/views/connect/env-var.vue b/renderer/src/views/connect/env-var.vue index f56f284..73024de 100644 --- a/renderer/src/views/connect/env-var.vue +++ b/renderer/src/views/connect/env-var.vue @@ -44,7 +44,6 @@