diff --git a/renderer/src/api/message-bridge.ts b/renderer/src/api/message-bridge.ts index 739d542..ae888b1 100644 --- a/renderer/src/api/message-bridge.ts +++ b/renderer/src/api/message-bridge.ts @@ -1,5 +1,6 @@ import { pinkLog, redLog } from '@/views/setting/util'; import { acquireVsCodeApi, electronApi, getPlatform } from './platform'; +import { isReactive } from 'vue'; export interface VSCodeMessage { command: string; @@ -208,6 +209,21 @@ export class MessageBridge { return () => commandHandlers.delete(wrapperCommandHandler); } + private deserializeReactiveData(data: any) { + if (isReactive(data)) { + return JSON.parse(JSON.stringify(data)); + } + + // 只对第一层进行遍历 + for (const key in data) { + if (isReactive(data[key])) { + data[key] = JSON.parse(JSON.stringify(data[key])); + } + } + + return data; + } + /** * @description do as axios does * @param command @@ -215,6 +231,7 @@ export class MessageBridge { * @returns */ public commandRequest(command: string, data?: ICommandRequestData): Promise> { + return new Promise((resolve, reject) => { this.addCommandListener(command, (data) => { resolve(data as RestFulResponse); @@ -222,7 +239,7 @@ export class MessageBridge { this.postMessage({ command, - data + data: this.deserializeReactiveData(data) }); }); } diff --git a/renderer/src/components/main-panel/chat/chat-box/options/system-prompt.ts b/renderer/src/components/main-panel/chat/chat-box/options/system-prompt.ts index 2a738fc..dee02bd 100644 --- a/renderer/src/components/main-panel/chat/chat-box/options/system-prompt.ts +++ b/renderer/src/components/main-panel/chat/chat-box/options/system-prompt.ts @@ -22,8 +22,7 @@ export function getSystemPrompt(name: string) { export async function saveSystemPrompts() { const bridge = useMessageBridge(); - const payload = JSON.parse(JSON.stringify(systemPrompts.value)); - const res = await bridge.commandRequest('system-prompts/save', { prompts: payload }); + const res = await bridge.commandRequest('system-prompts/save', { prompts: systemPrompts.value }); if (res.code === 200) { pinkLog('system prompt 保存成功'); } diff --git a/renderer/src/components/main-panel/chat/message/user.vue b/renderer/src/components/main-panel/chat/message/user.vue index fecc2e0..9114996 100644 --- a/renderer/src/components/main-panel/chat/message/user.vue +++ b/renderer/src/components/main-panel/chat/message/user.vue @@ -84,7 +84,11 @@ const handleKeydown = (event: KeyboardEvent) => { const copy = async () => { try { if (navigator.clipboard) { - await navigator.clipboard.writeText(userInput.value); + await navigator.clipboard.write([ + new ClipboardItem({ + 'text/plain': new Blob([userInput.value], { type: 'text/plain' }) + }) + ]); } else { const textarea = document.createElement('textarea'); textarea.value = userInput.value; diff --git a/renderer/src/views/connect/connection-panel.vue b/renderer/src/views/connect/connection-panel.vue index 8201618..3b527bb 100644 --- a/renderer/src/views/connect/connection-panel.vue +++ b/renderer/src/views/connect/connection-panel.vue @@ -145,7 +145,6 @@ async function connect() { } .connect-action { - margin-top: 20px; padding: 10px; } \ No newline at end of file diff --git a/renderer/src/views/connect/core.ts b/renderer/src/views/connect/core.ts index cdf17e8..06df6fc 100644 --- a/renderer/src/views/connect/core.ts +++ b/renderer/src/views/connect/core.ts @@ -367,7 +367,9 @@ export class McpClient { */ public async lookupEnvVar(varNames: string[]) { const bridge = useMessageBridge(); - const { code, msg } = await bridge.commandRequest('lookup-env-var', { keys: varNames }); + const { code, msg } = await bridge.commandRequest('lookup-env-var', { + keys: varNames + }); if (code === 200) { this.connectionResult.logString.push({ diff --git a/renderer/src/views/setting/llm.ts b/renderer/src/views/setting/llm.ts index c11ef9f..7807ed2 100644 --- a/renderer/src/views/setting/llm.ts +++ b/renderer/src/views/setting/llm.ts @@ -20,6 +20,7 @@ export function createTest(call: ToolCall) { tab.name = t("tools"); const storage: ToolStorage = { + activeNames: [0], currentToolName: call.function.name, formData: JSON.parse(call.function.arguments) }; diff --git a/service/src/index.ts b/service/src/index.ts index 7e20481..900dcba 100644 --- a/service/src/index.ts +++ b/service/src/index.ts @@ -2,4 +2,4 @@ export { routeMessage } from './common/router'; export { VSCodeWebViewLike, TaskLoopAdapter } from './hook/adapter'; export { setVscodeWorkspace, setRunningCWD } from './hook/setting'; // TODO: 更加规范 -export { client } from './mcp/connect.service'; \ No newline at end of file +export { clientMap } from './mcp/connect.service'; \ No newline at end of file diff --git a/service/src/main.ts b/service/src/main.ts index d4e4de1..0e6b778 100644 --- a/service/src/main.ts +++ b/service/src/main.ts @@ -38,7 +38,7 @@ function refreshConnectionOption(envPath: string) { fs.writeFileSync(envPath, JSON.stringify(defaultOption, null, 4)); - return { data: [ defaultOption ] }; + return { items: [ defaultOption ] }; } function acquireConnectionOption() { @@ -51,16 +51,16 @@ function acquireConnectionOption() { try { const option = JSON.parse(fs.readFileSync(envPath, 'utf-8')); - if (!option.data) { + if (!option.items) { return refreshConnectionOption(envPath); } - if (option.data && option.data.length === 0) { + if (option.items && option.items.length === 0) { return refreshConnectionOption(envPath); } // 按照前端的规范,整理成 commandString 样式 - option.data = option.data.map((item: any) => { + option.items = option.items.map((item: any) => { if (item.connectionType === 'STDIO') { item.commandString = [item.command, ...item.args]?.join(' '); } else { @@ -80,7 +80,7 @@ function acquireConnectionOption() { function updateConnectionOption(data: any) { const envPath = path.join(__dirname, '..', '.env'); - const connection = { data }; + const connection = { items: data }; fs.writeFileSync(envPath, JSON.stringify(connection, null, 4)); } @@ -117,7 +117,7 @@ wss.on('connection', (ws: any) => { case 'web/launch-signature': const launchResult = { code: 200, - msg: option.data + msg: option.items }; webview.postMessage({ diff --git a/service/src/mcp/connect.controller.ts b/service/src/mcp/connect.controller.ts index 2441a48..96df6c4 100644 --- a/service/src/mcp/connect.controller.ts +++ b/service/src/mcp/connect.controller.ts @@ -16,8 +16,22 @@ export class ConnectController { const { keys } = data; const values = keys.map((key: string) => { // TODO: 在 Windows 上测试 - if (process.platform === 'win32' && key.toLowerCase() === 'path') { - key = 'Path'; // 确保正确匹配环境变量的 ke + console.log(key); + console.log(process.env); + + if (process.platform === 'win32') { + switch (key) { + case 'USER': + return process.env.USERNAME || ''; + case 'HOME': + return process.env.USERPROFILE || process.env.HOME; + case 'LOGNAME': + return process.env.USERNAME || ''; + case 'SHELL': + return process.env.SHELL || process.env.COMSPEC; + case 'TERM': + return process.env.TERM || '未设置 (Windows 默认终端)'; + } } return process.env[key] || ''; diff --git a/service/src/mcp/ocr.controller.ts b/service/src/mcp/ocr.controller.ts index 83341ba..6a997bd 100644 --- a/service/src/mcp/ocr.controller.ts +++ b/service/src/mcp/ocr.controller.ts @@ -5,7 +5,7 @@ import { createOcrWorker, saveBase64ImageData } from "./ocr.service"; export class OcrController { @Controller('ocr/get-ocr-image') - async getOcrImage(client: RequestClientType, data: any, webview: PostMessageble) { + async getOcrImage(data: any, webview: PostMessageble) { const { filename } = data; const buffer = diskStorage.getSync(filename); const base64String = buffer ? buffer.toString('base64'): undefined; @@ -18,7 +18,7 @@ export class OcrController { } @Controller('ocr/start-ocr') - async startOcr(client: RequestClientType, data: any, webview: PostMessageble) { + async startOcr(data: any, webview: PostMessageble) { const { base64String, mimeType } = data; const filename = saveBase64ImageData(base64String, mimeType); diff --git a/service/src/server.ts b/service/src/server.ts index cbada75..24f8fd5 100644 --- a/service/src/server.ts +++ b/service/src/server.ts @@ -39,7 +39,7 @@ function refreshConnectionOption(envPath: string) { fs.writeFileSync(envPath, JSON.stringify(defaultOption, null, 4)); - return { data: [defaultOption] }; + return { items: [defaultOption] }; } function acquireConnectionOption() { @@ -52,16 +52,16 @@ function acquireConnectionOption() { try { const option = JSON.parse(fs.readFileSync(envPath, 'utf-8')); - if (!option.data) { + if (!option.items) { return refreshConnectionOption(envPath); } - if (option.data && option.data.length === 0) { + if (option.items && option.items.length === 0) { return refreshConnectionOption(envPath); } // 按照前端的规范,整理成 commandString 样式 - option.data = option.data.map((item: any) => { + option.items = option.items.map((item: any) => { if (item.connectionType === 'STDIO') { item.commandString = [item.command, ...item.args]?.join(' '); } else { @@ -88,7 +88,7 @@ const authPassword = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '.env function updateConnectionOption(data: any) { const envPath = path.join(__dirname, '..', '.env'); - const connection = { data }; + const connection = { items: data }; fs.writeFileSync(envPath, JSON.stringify(connection, null, 4)); } @@ -147,7 +147,7 @@ wss.on('connection', (ws: any) => { case 'web/launch-signature': const launchResult = { code: 200, - msg: option.data + msg: option.items }; webview.postMessage({ diff --git a/src/global.ts b/src/global.ts index 9cef060..f6e1dae 100644 --- a/src/global.ts +++ b/src/global.ts @@ -6,47 +6,43 @@ import * as fs from 'fs'; export type FsPath = string; export const panels = new Map(); -export interface IStdioConnectionItem { - type: 'STDIO'; - name: string; - version?: string; - command: string; - args: string[]; - cwd?: string; - env?: { [key: string]: string }; - filePath?: string; -} - -export interface ISSEConnectionItem { - type: 'SSE'; - name: string; - version: string; - url: string; - oauth?: string; - env?: { [key: string]: string }; - filePath?: string; -} - - -interface IStdioLaunchSignature { - type: 'STDIO'; - commandString: string; - cwd: string; -} - -interface ISSELaunchSignature { - type:'SSE'; - url: string; - oauth: string; -} - -export type IConnectionItem = IStdioConnectionItem | ISSEConnectionItem; -export type ILaunchSigature = IStdioLaunchSignature | ISSELaunchSignature; - export interface IConnectionConfig { - items: IConnectionItem[]; + items: (McpOptions[] | McpOptions)[]; } +export type ConnectionType = 'STDIO' | 'SSE' | 'STREAMABLE_HTTP'; + +export interface McpOptions { + connectionType: ConnectionType; + command?: string; + + // STDIO 特定选项 + args?: string[]; + cwd?: string; + env?: Record; + + // SSE 特定选项 + url?: string; + oauth?: any; + + // 通用客户端选项 + clientName?: string; + clientVersion?: string; + serverInfo?: { + name: string + version: string + } + + // vscode 专用 + filePath?: string; + name?: string; + version?: string; + type?: ConnectionType; + + [key: string]: any; +} + + export const CONNECTION_CONFIG_NAME = 'openmcp_connection.json'; let _connectionConfig: IConnectionConfig | undefined; @@ -71,11 +67,11 @@ export function getConnectionConfig() { const rawConnectionString = fs.readFileSync(connectionConfig, 'utf-8'); let connection; try { - connection = JSON.parse(rawConnectionString) as IConnectionConfig; + connection = JSON.parse(rawConnectionString) as IConnectionConfig; } catch (error) { connection = { items: [] }; } - + _connectionConfig = connection; return connection; } @@ -113,17 +109,20 @@ export function getWorkspaceConnectionConfig() { let connection; try { - connection = JSON.parse(rawConnectionString) as IConnectionConfig; + connection = JSON.parse(rawConnectionString) as IConnectionConfig; } catch (error) { connection = { items: [] }; } const workspacePath = getWorkspacePath(); - for (const item of connection.items) { + for (let item of connection.items) { + item = Array.isArray(item) ? item[0] : item; + const itemType = item.type || item.connectionType; + if (item.filePath && item.filePath.startsWith('{workspace}')) { item.filePath = item.filePath.replace('{workspace}', workspacePath).replace(/\\/g, '/'); } - if (item.type === 'STDIO' && item.cwd && item.cwd.startsWith('{workspace}')) { + if (itemType === 'STDIO' && item.cwd && item.cwd.startsWith('{workspace}')) { item.cwd = item.cwd.replace('{workspace}', workspacePath).replace(/\\/g, '/'); } } @@ -165,42 +164,28 @@ export function saveWorkspaceConnectionConfig(workspace: string) { const connectionConfigPath = fspath.join(configDir, CONNECTION_CONFIG_NAME); const workspacePath = getWorkspacePath(); - for (const item of connectionConfig.items) { + for (let item of connectionConfig.items) { + + item = Array.isArray(item) ? item[0] : item; + const itemType = item.type || item.connectionType; + item.type = undefined; + if (item.filePath && item.filePath.replace(/\\/g, '/').startsWith(workspacePath)) { item.filePath = item.filePath.replace(workspacePath, '{workspace}').replace(/\\/g, '/'); } - if (item.type ==='STDIO' && item.cwd && item.cwd.replace(/\\/g, '/').startsWith(workspacePath)) { + if (item.type === 'STDIO' && item.cwd && item.cwd.replace(/\\/g, '/').startsWith(workspacePath)) { item.cwd = item.cwd.replace(workspacePath, '{workspace}').replace(/\\/g, '/'); } } fs.writeFileSync(connectionConfigPath, JSON.stringify(connectionConfig, null, 2), 'utf-8'); } -interface ClientStdioConnectionItem { - command: string; - args: string[]; - connectionType: 'STDIO'; - cwd: string; - env: { [key: string]: string }; -} - -interface ClientSseConnectionItem { - url: string; - connectionType: 'SSE'; - oauth: string; - env: { [key: string]: string }; -} - -interface ServerInfo { - name: string; - version: string; -} export function updateWorkspaceConnectionConfig( absPath: string, - data: (ClientStdioConnectionItem | ClientSseConnectionItem) & { serverInfo: ServerInfo } + data: McpOptions[] ) { - const connectionItem = getWorkspaceConnectionConfigItemByPath(absPath); + const connectionItem = getWorkspaceConnectionConfigItemByPath(absPath); const workspaceConnectionConfig = getWorkspaceConnectionConfig(); // 如果存在,删除老的 connectionItem @@ -211,48 +196,29 @@ export function updateWorkspaceConnectionConfig( } } - if (data.connectionType === 'STDIO') { - const connectionItem: IStdioConnectionItem = { - type: 'STDIO', - name: data.serverInfo.name, - version: data.serverInfo.version, - command: data.command, - args: data.args, - cwd: data.cwd.replace(/\\/g, '/'), - env: data.env, - filePath: absPath.replace(/\\/g, '/') - }; + // 对于第一个 item 添加 filePath + // 对路径进行标准化 + data.forEach(item => { + item.filePath = absPath.replace(/\\/g, '/'); + item.cwd = item.cwd?.replace(/\\/g, '/'); + item.name = item.serverInfo?.name; + item.version = item.serverInfo?.version; + item.type = undefined; + }); - console.log('get connectionItem: ', connectionItem); - + console.log('get connectionItem: ', data); - // 插入到第一个 - workspaceConnectionConfig.items.unshift(connectionItem); - const workspacePath = getWorkspacePath(); - saveWorkspaceConnectionConfig(workspacePath); - vscode.commands.executeCommand('openmcp.sidebar.workspace-connection.refresh'); + // 插入到第一个 + workspaceConnectionConfig.items.unshift(data); + const workspacePath = getWorkspacePath(); + saveWorkspaceConnectionConfig(workspacePath); + vscode.commands.executeCommand('openmcp.sidebar.workspace-connection.refresh'); - } else { - const connectionItem: ISSEConnectionItem = { - type: 'SSE', - name: data.serverInfo.name, - version: data.serverInfo.version, - url: data.url, - oauth: data.oauth, - filePath: absPath.replace(/\\/g, '/') - }; - - // 插入到第一个 - workspaceConnectionConfig.items.unshift(connectionItem); - const workspacePath = getWorkspacePath(); - saveWorkspaceConnectionConfig(workspacePath); - vscode.commands.executeCommand('openmcp.sidebar.workspace-connection.refresh'); - } } export function updateInstalledConnectionConfig( absPath: string, - data: (ClientStdioConnectionItem | ClientSseConnectionItem) & { serverInfo: ServerInfo } + data: McpOptions[] ) { const connectionItem = getInstalledConnectionConfigItemByPath(absPath); const installedConnectionConfig = getConnectionConfig(); @@ -265,45 +231,26 @@ export function updateInstalledConnectionConfig( } } - if (data.connectionType === 'STDIO') { - const connectionItem: IStdioConnectionItem = { - type: 'STDIO', - name: data.serverInfo.name, - version: data.serverInfo.version, - command: data.command, - args: data.args, - cwd: data.cwd.replace(/\\/g, '/'), - env: data.env, - filePath: absPath.replace(/\\/g, '/') - }; + // 对于第一个 item 添加 filePath + // 对路径进行标准化 + data.forEach(item => { + item.filePath = absPath.replace(/\\/g, '/'); + item.cwd = item.cwd?.replace(/\\/g, '/'); + item.name = item.serverInfo?.name; + item.version = item.serverInfo?.version; + item.type = undefined; + }); - console.log('get connectionItem: ', connectionItem); - + console.log('get connectionItem: ', data); - // 插入到第一个 - installedConnectionConfig.items.unshift(connectionItem); - saveConnectionConfig(); - vscode.commands.executeCommand('openmcp.sidebar.installed-connection.refresh'); - - } else { - const connectionItem: ISSEConnectionItem = { - type: 'SSE', - name: data.serverInfo.name, - version: data.serverInfo.version, - url: data.url, - oauth: data.oauth, - filePath: absPath.replace(/\\/g, '/') - }; - - // 插入到第一个 - installedConnectionConfig.items.unshift(connectionItem); - saveConnectionConfig(); - vscode.commands.executeCommand('openmcp.sidebar.installed-connection.refresh'); - } + // 插入到第一个 + installedConnectionConfig.items.unshift(data); + saveConnectionConfig(); + vscode.commands.executeCommand('openmcp.sidebar.installed-connection.refresh'); } -function normaliseConnectionFilePath(item: IConnectionItem, workspace: string) { +function normaliseConnectionFilePath(item: McpOptions, workspace: string) { if (item.filePath) { if (item.filePath.startsWith('{workspace}')) { return item.filePath.replace('{workspace}', workspace).replace(/\\/g, '/'); @@ -329,7 +276,9 @@ export function getWorkspaceConnectionConfigItemByPath(absPath: string) { const workspaceConnectionConfig = getWorkspaceConnectionConfig(); const normaliseAbsPath = absPath.replace(/\\/g, '/'); - for (const item of workspaceConnectionConfig.items) { + for (let item of workspaceConnectionConfig.items) { + item = Array.isArray(item)? item[0] : item; + const filePath = normaliseConnectionFilePath(item, workspacePath); if (filePath === normaliseAbsPath) { return item; @@ -347,7 +296,9 @@ export function getInstalledConnectionConfigItemByPath(absPath: string) { const installedConnectionConfig = getConnectionConfig(); const normaliseAbsPath = absPath.replace(/\\/g, '/'); - for (const item of installedConnectionConfig.items) { + for (let item of installedConnectionConfig.items) { + item = Array.isArray(item)? item[0] : item; + const filePath = (item.filePath || '').replace(/\\/g, '/'); if (filePath === normaliseAbsPath) { return item; @@ -361,14 +312,14 @@ export function getInstalledConnectionConfigItemByPath(absPath: string) { export async function getFirstValidPathFromCommand(command: string, cwd: string): Promise { // 分割命令字符串 const parts = command.split(' '); - + // 遍历命令部分,寻找第一个可能是路径的部分 for (let i = 1; i < parts.length; i++) { const part = parts[i]; - + // 跳过以 '-' 开头的参数 if (part.startsWith('-')) continue; - + // 处理相对路径 let fullPath = part; if (!fspath.isAbsolute(part)) { @@ -381,6 +332,6 @@ export async function getFirstValidPathFromCommand(command: string, cwd: string) return fullPath; } } - + return undefined; } diff --git a/src/sidebar/common.ts b/src/sidebar/common.ts index f078258..4b78810 100644 --- a/src/sidebar/common.ts +++ b/src/sidebar/common.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode'; -import type { IConnectionItem } from '../global'; +import { McpOptions } from '../global'; export class SidebarItem extends vscode.TreeItem { constructor( @@ -18,7 +18,7 @@ export class ConnectionViewItem extends vscode.TreeItem { constructor( public readonly label: string, public readonly collapsibleState: vscode.TreeItemCollapsibleState, - public readonly item: IConnectionItem, + public readonly item: McpOptions[] | McpOptions, public readonly icon?: string ) { super(label, collapsibleState); diff --git a/src/sidebar/installed.controller.ts b/src/sidebar/installed.controller.ts index 3cd2a43..72c7531 100644 --- a/src/sidebar/installed.controller.ts +++ b/src/sidebar/installed.controller.ts @@ -22,6 +22,7 @@ export class McpInstalledConnectProvider implements vscode.TreeDataProvider { // 连接的名字 + item = Array.isArray(item)? item[0] : item; const itemName = `${item.name} (${item.type})` return new ConnectionViewItem(itemName, vscode.TreeItemCollapsibleState.None, item, 'server'); }) @@ -33,7 +34,9 @@ export class McpInstalledConnectProvider implements vscode.TreeDataProvider { +export async function acquireInstalledConnection(): Promise { // 让用户选择连接类型 - const connectionType = await vscode.window.showQuickPick(['STDIO', 'SSE'], { + const connectionType = await vscode.window.showQuickPick(['STDIO', 'SSE', 'STREAMABLE_HTTP'], { placeHolder: '请选择连接类型', canPickMany: false }); if (!connectionType) { - return; // 用户取消选择 + return []; // 用户取消选择 } if (connectionType === 'STDIO') { @@ -69,7 +71,7 @@ export async function acquireInstalledConnection(): Promise { // 连接的名字 + item = Array.isArray(item) ? item[0] : item; const itemName = `${item.name} (${item.type})` return new ConnectionViewItem(itemName, vscode.TreeItemCollapsibleState.None, item, 'server'); }) @@ -33,7 +34,9 @@ export class McpWorkspaceConnectProvider implements vscode.TreeDataProvider { - // 让用户选择连接类型 - const connectionType = await vscode.window.showQuickPick(['STDIO', 'SSE'], { - placeHolder: '请选择连接类型' - }); - - if (!connectionType) { - return; // 用户取消选择 - } - - if (connectionType === 'STDIO') { - // 获取 command - const commandString = await vscode.window.showInputBox({ - prompt: '请输入连接的 command', - placeHolder: '例如: mcp run main.py' - }); - - if (!commandString) { - return; // 用户取消输入 - } - - // 获取 cwd - const cwd = await vscode.window.showInputBox({ - prompt: '请输入工作目录 (cwd),可选', - placeHolder: '例如: /path/to/project' - }); - - // 校验 command + cwd 是否有效 - try { - const commandPath = await validateAndGetCommandPath(commandString, cwd); - console.log('Command Path:', commandPath); - } catch (error) { - vscode.window.showErrorMessage(`无效的 command: ${error}`); - return; - } - - const commands = commandString.split(' '); - const command = commands[0]; - const args = commands.slice(1); - const filePath = await getFirstValidPathFromCommand(commandString, cwd || ''); - - // 保存连接配置 - return { - type: 'STDIO', - name: `STDIO-${Date.now()}`, - command: command, - args, - cwd: cwd || '', - filePath - }; - - } else if (connectionType === 'SSE') { - // 获取 url - const url = await vscode.window.showInputBox({ - prompt: '请输入连接的 URL', - placeHolder: '例如: https://127.0.0.1:8282' - }); - - if (!url) { - return; // 用户取消输入 - } - - // 获取 oauth - const oauth = await vscode.window.showInputBox({ - prompt: '请输入 OAuth 令牌,可选', - placeHolder: '例如: your-oauth-token' - }); - - // 保存连接配置 - return { - type: 'SSE', - name: `SSE-${Date.now()}`, - version: '1.0', // 假设默认版本为 1.0,可根据实际情况修改 - url: url, - oauth: oauth || '' - } - } -} - -export async function deleteUserConnection(item: IConnectionItem) { +export async function deleteUserConnection(item: McpOptions[] | McpOptions) { // 弹出确认对话框 + const masterNode = Array.isArray(item) ? item[0] : item; + const name = masterNode.name; const confirm = await vscode.window.showWarningMessage( - `确定要删除连接 "${item.name}" 吗?`, + `确定要删除连接 "${name}" 吗?`, { modal: true }, '确定' ); @@ -108,12 +28,12 @@ export async function deleteUserConnection(item: IConnectionItem) { // 刷新侧边栏视图 vscode.commands.executeCommand('openmcp.sidebar.workspace-connection.refresh'); - panels.delete(item.name); + // 如果该连接有对应的webview面板,则关闭它 - if (panels.has(item.filePath || item.name)) { - const panel = panels.get(item.filePath || item.name); - panel?.dispose(); - } + const filePath = masterNode.filePath || ''; + const panel = panels.get(filePath); + panel?.dispose(); + panels.delete(filePath); } } @@ -128,4 +48,110 @@ export async function validateAndGetCommandPath(command: string, cwd?: string): } catch (error) { throw new Error(`无法找到命令: ${command.split(' ')[0]}`); } +} + +export async function acquireUserCustomConnection(): Promise { + // 让用户选择连接类型 + const connectionType = await vscode.window.showQuickPick(['STDIO', 'SSE'], { + placeHolder: '请选择连接类型' + }); + + if (!connectionType) { + return []; // 用户取消选择 + } + + if (connectionType === 'STDIO') { + // 获取 command + const commandString = await vscode.window.showInputBox({ + prompt: '请输入连接的 command', + placeHolder: '例如: mcp run main.py' + }); + + if (!commandString) { + return []; // 用户取消输入 + } + + // 获取 cwd + const cwd = await vscode.window.showInputBox({ + prompt: '请输入工作目录 (cwd),可选', + placeHolder: '例如: /path/to/project' + }); + + // 校验 command + cwd 是否有效 + try { + const commandPath = await validateAndGetCommandPath(commandString, cwd); + console.log('Command Path:', commandPath); + } catch (error) { + vscode.window.showErrorMessage(`无效的 command: ${error}`); + return []; + } + + const commands = commandString.split(' '); + const command = commands[0]; + const args = commands.slice(1); + const filePath = await getFirstValidPathFromCommand(commandString, cwd || ''); + + // 保存连接配置 + return [{ + connectionType: 'STDIO', + name: `STDIO-${Date.now()}`, + command: command, + args, + cwd: cwd || '', + filePath + }]; + + } else if (connectionType === 'SSE') { + // 获取 url + const url = await vscode.window.showInputBox({ + prompt: '请输入连接的 URL', + placeHolder: '例如: https://127.0.0.1:8282/sse' + }); + + if (!url) { + return []; // 用户取消输入 + } + + // 获取 oauth + const oauth = await vscode.window.showInputBox({ + prompt: '请输入 OAuth 令牌,可选', + placeHolder: '例如: your-oauth-token' + }); + + // 保存连接配置 + return [{ + connectionType: 'SSE', + name: `SSE-${Date.now()}`, + version: '1.0', // 假设默认版本为 1.0,可根据实际情况修改 + url: url, + oauth: oauth || '' + }]; + } else if (connectionType === 'STREAMABLE_HTTP') { + // 获取 url + const url = await vscode.window.showInputBox({ + prompt: '请输入连接的 URL', + placeHolder: '例如: https://127.0.0.1:8282/stream' + }); + + if (!url) { + return []; // 用户取消输入 + } + + // 获取 oauth + const oauth = await vscode.window.showInputBox({ + prompt: '请输入 OAuth 令牌,可选', + placeHolder: '例如: your-oauth-token' + }); + + // 保存连接配置 + return [{ + connectionType: 'STREAMABLE_HTTP', + name: `STREAMABLE_HTTP-${Date.now()}`, + version: '1.0', // 假设默认版本为 1.0,可根据实际情况修改 + url: url, + oauth: oauth || '' + }]; + } + + return []; } \ No newline at end of file diff --git a/src/webview/webview.controller.ts b/src/webview/webview.controller.ts index fff6b4f..407b47e 100644 --- a/src/webview/webview.controller.ts +++ b/src/webview/webview.controller.ts @@ -21,7 +21,7 @@ export class WebviewController { } revealOpenMcpWebviewPanel(context, 'workspace', uri.fsPath, { - type: 'STDIO', + connectionType: 'STDIO', name: 'OpenMCP', command: signature.command, args: signature.args, diff --git a/src/webview/webview.service.ts b/src/webview/webview.service.ts index 6bc86b0..c6b5ab4 100644 --- a/src/webview/webview.service.ts +++ b/src/webview/webview.service.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; import * as fspath from 'path'; -import { IConnectionItem, ILaunchSigature, panels, updateInstalledConnectionConfig, updateWorkspaceConnectionConfig } from '../global'; +import { McpOptions, panels, updateInstalledConnectionConfig, updateWorkspaceConnectionConfig } from '../global'; import { routeMessage } from '../../openmcp-sdk/service'; export function getWebviewContent(context: vscode.ExtensionContext, panel: vscode.WebviewPanel): string | undefined { @@ -35,12 +35,7 @@ export function revealOpenMcpWebviewPanel( context: vscode.ExtensionContext, type: 'workspace' | 'installed', panelKey: string, - option: IConnectionItem = { - type: 'STDIO', - name: 'OpenMCP', - command: 'mcp', - args: ['run', 'main.py'] - } + option: McpOptions[] | McpOptions ) { if (panels.has(panelKey)) { const panel = panels.get(panelKey); @@ -75,21 +70,9 @@ export function revealOpenMcpWebviewPanel( // 拦截消息,注入额外信息 switch (command) { case 'vscode/launch-signature': - const launchResultMessage: ILaunchSigature = option.type === 'STDIO' ? - { - type: 'STDIO', - commandString: option.command + ' ' + option.args.join(' '), - cwd: option.cwd || '' - } : - { - type: 'SSE', - url: option.url, - oauth: option.oauth || '' - }; - const launchResult = { code: 200, - msg: launchResultMessage + msg: option }; panel.webview.postMessage({ @@ -118,6 +101,8 @@ export function revealOpenMcpWebviewPanel( // 删除 panels.delete(panelKey); + // TODO: 通过引用计数器关闭后端的 clientMap + // 退出 panel.dispose(); });