实现连接参数的局部保存

This commit is contained in:
锦恢 2025-04-23 03:31:16 +08:00
parent b2b80c1a3f
commit 5bac8f8726
5 changed files with 249 additions and 48 deletions

View File

@ -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"

View File

@ -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,11 +17,20 @@ 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 connectionItem = getWorkspaceConnectionConfigItemByPath(uri.fsPath);
if (!connectionItem) {
// 项目不存在连接信息
const cwd = getLaunchCWD(context, uri);
// 获取 uri 相对于 cwd 的路径
const relativePath = fspath.relative(cwd, uri.fsPath);
@ -35,6 +45,9 @@ export function activate(context: vscode.ExtensionContext) {
args,
cwd
});
} else {
revealOpenMcpWebviewPanel(context, uri.fsPath, connectionItem);
}
})
);

View File

@ -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;
}

View File

@ -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<SidebarItem> {
class McpWorkspaceConnectProvider implements vscode.TreeDataProvider<SidebarItem> {
private _onDidChangeTreeData: vscode.EventEmitter<SidebarItem | undefined | null | void> = new vscode.EventEmitter<SidebarItem | undefined | null | void>();
readonly onDidChangeTreeData: vscode.Event<SidebarItem | undefined | null | void> = this._onDidChangeTreeData.event;
constructor(private context: vscode.ExtensionContext) {
}
@ -33,21 +15,42 @@ class McpConnectProvider implements vscode.TreeDataProvider<SidebarItem> {
getChildren(element?: SidebarItem): Thenable<SidebarItem[]> {
// 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<SidebarItem> {

View File

@ -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';
@ -93,6 +93,8 @@ export function revealOpenMcpWebviewPanel(
break;
case 'connect':
updateWorkspaceConnectionConfig(panelKey, data);
default:
OpenMCPService.messageController(command, data, panel.webview);
break;