From 5bac8f8726746987af8566daf063f512fc063135 Mon Sep 17 00:00:00 2001 From: Kirigaya <1193466151@qq.com> Date: Wed, 23 Apr 2025 03:31:16 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E8=BF=9E=E6=8E=A5=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E7=9A=84=E5=B1=80=E9=83=A8=E4=BF=9D=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 23 ++++++- src/extension.ts | 45 ++++++++----- src/global.ts | 164 ++++++++++++++++++++++++++++++++++++++++++++++- src/sidebar.ts | 59 +++++++++-------- src/webview.ts | 6 +- 5 files changed, 249 insertions(+), 48 deletions(-) diff --git a/package.json b/package.json index e2a0a53..9d9ff9b 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,20 @@ "light": "./icons/light/protocol.svg", "dark": "./icons/dark/protocol.svg" } + }, + { + "command": "openmcp.sidebar.workspace-connection.revealWebviewPanel", + "title": "展示 OpenMCP", + "category": "openmcp", + "icon": { + "light": "./icons/light/protocol.svg", + "dark": "./icons/dark/protocol.svg" + } + }, + { + "command": "openmcp.sidebar.workspace-connection.refresh", + "title": "刷新", + "category": "openmcp" } ], "menus": { @@ -40,6 +54,13 @@ "group": "navigation", "when": "editorLangId == python || editorLangId == javascript || editorLangId == typescript || editorLangId == java || editorLangId == csharp" } + ], + "view/item/context": [ + { + "command": "openmcp.sidebar.workspace-connection.revealWebviewPanel", + "group": "navigation", + "when": "view == openmcp.sidebar-view.workspace-connection" + } ] }, "viewsContainers": { @@ -54,7 +75,7 @@ "views": { "openmcp-sidebar": [ { - "id": "openmcp.sidebar.connect", + "id": "openmcp.sidebar-view.workspace-connection", "icon": "./icons/protocol.svg", "name": "MCP 连接", "type": "tree" diff --git a/src/extension.ts b/src/extension.ts index 44a4fbf..4d3a3e9 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -4,6 +4,7 @@ import * as fspath from 'path'; import * as OpenMCPService from '../resources/service'; import { getLaunchCWD, revealOpenMcpWebviewPanel } from './webview'; import { registerSidebar } from './sidebar'; +import { getWorkspaceConnectionConfigItemByPath, ISSEConnectionItem, IStdioConnectionItem } from './global'; export function activate(context: vscode.ExtensionContext) { console.log('activate openmcp'); @@ -16,25 +17,37 @@ export function activate(context: vscode.ExtensionContext) { registerSidebar(context); - // 注册 showOpenMCP 命令 + context.subscriptions.push( + vscode.commands.registerCommand('openmcp.sidebar.workspace-connection.revealWebviewPanel', (item: IStdioConnectionItem | ISSEConnectionItem) => { + revealOpenMcpWebviewPanel(context, item.name, item); + }) + ); + context.subscriptions.push( vscode.commands.registerCommand('openmcp.showOpenMCP', async (uri: vscode.Uri) => { - const cwd = getLaunchCWD(context, uri); - // 获取 uri 相对于 cwd 的路径 - const relativePath = fspath.relative(cwd, uri.fsPath); - - // TODO: 实现从 connection.json 中读取配置,然后启动对应的 connection - const command = 'mcp'; - const args = ['run', relativePath]; - - revealOpenMcpWebviewPanel(context, uri.fsPath, { - type: 'stdio', - name: 'OpenMCP', - command, - args, - cwd - }); + const connectionItem = getWorkspaceConnectionConfigItemByPath(uri.fsPath); + if (!connectionItem) { + // 项目不存在连接信息 + const cwd = getLaunchCWD(context, uri); + + // 获取 uri 相对于 cwd 的路径 + const relativePath = fspath.relative(cwd, uri.fsPath); + + // TODO: 实现从 connection.json 中读取配置,然后启动对应的 connection + const command = 'mcp'; + const args = ['run', relativePath]; + + revealOpenMcpWebviewPanel(context, uri.fsPath, { + type: 'stdio', + name: 'OpenMCP', + command, + args, + cwd + }); + } else { + revealOpenMcpWebviewPanel(context, uri.fsPath, connectionItem); + } }) ); diff --git a/src/global.ts b/src/global.ts index 3a4e7a7..a176e24 100644 --- a/src/global.ts +++ b/src/global.ts @@ -14,6 +14,7 @@ export interface IStdioConnectionItem { args: string[]; cwd?: string; env?: { [key: string]: string }; + filePath?: string; } export interface ISSEConnectionItem { @@ -22,16 +23,29 @@ export interface ISSEConnectionItem { url: string; oauth?: string; env?: { [key: string]: string }; + filePath?: string; } export interface IConnectionConfig { items: (IStdioConnectionItem | ISSEConnectionItem)[]; } +export const CONNECTION_CONFIG_NAME = 'openmcp_connection.json'; + +let _connectionConfig: IConnectionConfig | undefined; +let _workspaceConnectionConfig: IConnectionConfig | undefined; + +/** + * @description 获取全局的连接信息,全局文件信息都是绝对路径 + * @returns + */ export function getConnectionConfig() { + if (_connectionConfig) { + return _connectionConfig; + } const homeDir = os.homedir(); const configDir = fspath.join(homeDir, '.openmcp'); - const connectionConfig = fspath.join(configDir, 'connection.json'); + const connectionConfig = fspath.join(configDir, CONNECTION_CONFIG_NAME); if (!fs.existsSync(connectionConfig)) { fs.mkdirSync(configDir, { recursive: true }); fs.writeFileSync(connectionConfig, JSON.stringify({ items: [] }), 'utf-8'); @@ -39,5 +53,153 @@ export function getConnectionConfig() { const rawConnectionString = fs.readFileSync(connectionConfig, 'utf-8'); const connection = JSON.parse(rawConnectionString) as IConnectionConfig; + _connectionConfig = connection; return connection; +} + + +/** + * @description 获取工作区的连接信息,工作区的连接文件的路径都是相对路径,以 {workspace} 开头 + * @param workspace + */ +export function getWorkspaceConnectionConfig() { + const workspace = getWorkspacePath(); + + if (_workspaceConnectionConfig) { + return _workspaceConnectionConfig; + } + const configDir = fspath.join(workspace, '.vscode'); + const connectionConfig = fspath.join(configDir, CONNECTION_CONFIG_NAME); + + if (!fs.existsSync(connectionConfig)) { + fs.mkdirSync(configDir, { recursive: true }); + fs.writeFileSync(connectionConfig, JSON.stringify({ items: [] }), 'utf-8'); + } + + const rawConnectionString = fs.readFileSync(connectionConfig, 'utf-8'); + const connection = JSON.parse(rawConnectionString) as IConnectionConfig; + + const workspacePath = getWorkspacePath(); + for (const item of connection.items) { + if (item.filePath && item.filePath.startsWith('{workspace}')) { + item.filePath = item.filePath.replace('{workspace}', workspacePath).replace('\\', '/'); + } + if (item.type === 'stdio' && item.cwd && item.cwd.startsWith('{workspace}')) { + item.cwd = item.cwd.replace('{workspace}', workspacePath).replace('\\', '/'); + } + } + + _workspaceConnectionConfig = connection; + return connection; +} + +export function saveWorkspaceConnectionConfig(workspace: string) { + + if (!_workspaceConnectionConfig) { + return; + } + + const connectionConfig = JSON.parse(JSON.stringify(_workspaceConnectionConfig)) as IConnectionConfig; + + const configDir = fspath.join(workspace, '.vscode'); + const connectionConfigPath = fspath.join(configDir, CONNECTION_CONFIG_NAME); + + const workspacePath = getWorkspacePath(); + for (const item of connectionConfig.items) { + if (item.filePath && item.filePath.startsWith(workspacePath)) { + item.filePath = item.filePath.replace(workspacePath, '{workspace}').replace('\\', '/'); + } + if (item.type ==='stdio' && item.cwd && item.cwd.startsWith(workspacePath)) { + item.cwd = item.cwd.replace(workspacePath, '{workspace}').replace('\\', '/'); + } + } + fs.writeFileSync(connectionConfigPath, JSON.stringify(connectionConfig), 'utf-8'); +} + +interface ClientStdioConnectionItem { + command: string; + args: string[]; + clientName: string; + clientVersion: string; + connectionType: 'STDIO'; + cwd: string; + env: { [key: string]: string }; +} + +interface ClientSseConnectionItem { + url: string; + clientName: string; + clientVersion: string; + connectionType: 'SSE'; + env: { [key: string]: string }; +} + +export function updateWorkspaceConnectionConfig(absPath: string, data: ClientStdioConnectionItem | ClientSseConnectionItem) { + const connectionItem = getWorkspaceConnectionConfigItemByPath(absPath); + const workspaceConnectionConfig = getWorkspaceConnectionConfig(); + + // 如果存在,删除老的 connectionItem + if (connectionItem) { + const index = workspaceConnectionConfig.items.indexOf(connectionItem); + if (index !== -1) { + workspaceConnectionConfig.items.splice(index, 1); + } + } + + if (data.connectionType === 'STDIO') { + const connectionItem: IStdioConnectionItem = { + type:'stdio', + name: data.clientName, + command: data.command, + args: data.args, + cwd: data.cwd.replace('\\', '/'), + env: data.env, + filePath: absPath.replace('\\', '/') + }; + + // 插入到第一个 + workspaceConnectionConfig.items.unshift(connectionItem); + const workspacePath = getWorkspacePath(); + saveWorkspaceConnectionConfig(workspacePath); + vscode.commands.executeCommand('openmcp.sidebar.workspace-connection.refresh'); + + } else { + + } +} + +function normaliseConnectionFilePath(item: IStdioConnectionItem | ISSEConnectionItem, workspace: string) { + if (item.filePath) { + if (item.filePath.startsWith('{workspace}')) { + return item.filePath.replace('{workspace}', workspace).replace('\\', '/'); + } else { + return item.filePath.replace('\\', '/'); + } + } + + return undefined; +} + +export function getWorkspacePath() { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + return (workspaceFolder?.uri.fsPath || '').replace('\\', '/'); +} + +/** + * @description 根据输入的文件路径,获取该文件的 mcp 连接签名 + * @param absPath + */ +export function getWorkspaceConnectionConfigItemByPath(absPath: string) { + const workspacePath = getWorkspacePath(); + const workspaceConnectionConfig = getWorkspaceConnectionConfig(); + + const normaliseAbsPath = absPath.replace('\\', '/'); + for (const item of workspaceConnectionConfig.items) { + const filePath = normaliseConnectionFilePath(item, workspacePath); + if (filePath === normaliseAbsPath) { + return item; + } + } + + return undefined; } \ No newline at end of file diff --git a/src/sidebar.ts b/src/sidebar.ts index cda96d7..a34e9ba 100644 --- a/src/sidebar.ts +++ b/src/sidebar.ts @@ -1,27 +1,9 @@ import * as vscode from 'vscode'; -import { getConnectionConfig, ISSEConnectionItem, IStdioConnectionItem } from './global'; -import { revealOpenMcpWebviewPanel } from './webview'; +import { getConnectionConfig, getWorkspaceConnectionConfig } from './global'; -export function registerSidebar(context: vscode.ExtensionContext) { - - context.subscriptions.push( - vscode.commands.registerCommand('openmcp.sidebar.revealOpenMcpWebviewPanel', (item: IStdioConnectionItem | ISSEConnectionItem) => { - revealOpenMcpWebviewPanel(context, item.name, item); - }) - ) - - // 注册 MCP 连接的 sidebar 视图 - context.subscriptions.push( - vscode.window.registerTreeDataProvider('openmcp.sidebar.connect', new McpConnectProvider(context)) - ); - - // 注册 入门与帮助的 sidebar 视图 - context.subscriptions.push( - vscode.window.registerTreeDataProvider('openmcp.sidebar.help', new HelpProvider(context)) - ); -} - -class McpConnectProvider implements vscode.TreeDataProvider { +class McpWorkspaceConnectProvider implements vscode.TreeDataProvider { + private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; constructor(private context: vscode.ExtensionContext) { } @@ -33,21 +15,42 @@ class McpConnectProvider implements vscode.TreeDataProvider { getChildren(element?: SidebarItem): Thenable { // TODO: 读取 configDir 下的所有文件,作为子节点 - const connection = getConnectionConfig(); + const connection = getWorkspaceConnectionConfig(); const sidebarItems = connection.items.map((item, index) => { - return new SidebarItem(item.name, vscode.TreeItemCollapsibleState.None, { - command: 'openmcp.sidebar.revealOpenMcpWebviewPanel', - title: 'OpenMCP', - arguments: [item] - }, 'server'); + return new SidebarItem(item.name, vscode.TreeItemCollapsibleState.None); }) // 返回子节点 return Promise.resolve(sidebarItems); } + + // 添加 refresh 方法 + public refresh(): void { + this._onDidChangeTreeData.fire(); + } } +// 在 registerSidebar 函数中注册 refresh 命令 +export function registerSidebar(context: vscode.ExtensionContext) { + const workspaceConnectionProvider = new McpWorkspaceConnectProvider(context); + // 注册 refresh 命令 + context.subscriptions.push( + vscode.commands.registerCommand('openmcp.sidebar.workspace-connection.refresh', () => { + workspaceConnectionProvider.refresh(); + }) + ); + + // 注册 MCP 连接的 sidebar 视图 + context.subscriptions.push( + vscode.window.registerTreeDataProvider('openmcp.sidebar-view.workspace-connection', workspaceConnectionProvider) + ); + + // 注册 入门与帮助的 sidebar 视图 + context.subscriptions.push( + vscode.window.registerTreeDataProvider('openmcp.sidebar.help', new HelpProvider(context)) + ); +} class HelpProvider implements vscode.TreeDataProvider { diff --git a/src/webview.ts b/src/webview.ts index 5509931..ea84e8f 100644 --- a/src/webview.ts +++ b/src/webview.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; import * as fspath from 'path'; -import { ISSEConnectionItem, IStdioConnectionItem, panels } from './global'; +import { getWorkspaceConnectionConfigItemByPath, ISSEConnectionItem, IStdioConnectionItem, panels, updateWorkspaceConnectionConfig } from './global'; import * as OpenMCPService from '../resources/service'; @@ -92,7 +92,9 @@ export function revealOpenMcpWebviewPanel( }); break; - + + case 'connect': + updateWorkspaceConnectionConfig(panelKey, data); default: OpenMCPService.messageController(command, data, panel.webview); break;