统一多端模态的消息桥接层
This commit is contained in:
parent
0bea084c35
commit
354380cf23
@ -14,10 +14,11 @@ import Sidebar from '@/components/sidebar/index.vue';
|
|||||||
import MainPanel from '@/components/main-panel/index.vue';
|
import MainPanel from '@/components/main-panel/index.vue';
|
||||||
import { setDefaultCss } from './hook/css';
|
import { setDefaultCss } from './hook/css';
|
||||||
import { greenLog, pinkLog } from './views/setting/util';
|
import { greenLog, pinkLog } from './views/setting/util';
|
||||||
import { acquireVsCodeApi, useMessageBridge } from './api/message-bridge';
|
import { useMessageBridge } from './api/message-bridge';
|
||||||
import { connectionArgs, connectionMethods, doWebConnect, doVscodeConnect, loadEnvVar } from './views/connect/connection';
|
import { connectionArgs, connectionMethods, doConnect, loadEnvVar } from './views/connect/connection';
|
||||||
import { loadSetting } from './hook/setting';
|
import { loadSetting } from './hook/setting';
|
||||||
import { loadPanels } from './hook/panel';
|
import { loadPanels } from './hook/panel';
|
||||||
|
import { getPlatform } from './api/platform';
|
||||||
|
|
||||||
const bridge = useMessageBridge();
|
const bridge = useMessageBridge();
|
||||||
|
|
||||||
@ -28,51 +29,10 @@ bridge.addCommandListener('hello', data => {
|
|||||||
}, { once: true });
|
}, { once: true });
|
||||||
|
|
||||||
|
|
||||||
function initDebug() {
|
|
||||||
|
|
||||||
setTimeout(async () => {
|
|
||||||
// 初始化 设置
|
|
||||||
loadSetting();
|
|
||||||
|
|
||||||
// 初始化环境变量
|
|
||||||
loadEnvVar();
|
|
||||||
|
|
||||||
// 尝试连接
|
|
||||||
await doWebConnect();
|
|
||||||
|
|
||||||
// 初始化 tab
|
|
||||||
loadPanels();
|
|
||||||
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
async function initProduce() {
|
onMounted(async () => {
|
||||||
// TODO: get from vscode
|
|
||||||
connectionArgs.commandString = 'mcp run ../servers/main.py';
|
|
||||||
connectionMethods.current = 'STDIO';
|
|
||||||
|
|
||||||
// 初始化 设置
|
|
||||||
loadSetting();
|
|
||||||
|
|
||||||
// 初始化环境变量
|
|
||||||
loadEnvVar();
|
|
||||||
|
|
||||||
// 尝试连接
|
|
||||||
await doVscodeConnect();
|
|
||||||
|
|
||||||
// 初始化 tab
|
|
||||||
await loadPanels();
|
|
||||||
|
|
||||||
if (route.name !== 'debug') {
|
|
||||||
router.replace('/debug');
|
|
||||||
router.push('/debug');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
// 初始化 css
|
// 初始化 css
|
||||||
setDefaultCss();
|
setDefaultCss();
|
||||||
|
|
||||||
@ -82,11 +42,36 @@ onMounted(() => {
|
|||||||
|
|
||||||
pinkLog('OpenMCP Client 启动');
|
pinkLog('OpenMCP Client 启动');
|
||||||
|
|
||||||
if (acquireVsCodeApi === undefined) {
|
const platform = getPlatform();
|
||||||
initDebug();
|
|
||||||
} else {
|
// 跳转到首页
|
||||||
initProduce();
|
if (platform !== 'web') {
|
||||||
|
if (route.name !== 'debug') {
|
||||||
|
router.replace('/debug');
|
||||||
|
router.push('/debug');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 进行桥接
|
||||||
|
await bridge.awaitForWebsockt();
|
||||||
|
|
||||||
|
pinkLog('准备请求设置');
|
||||||
|
|
||||||
|
// 加载全局设置
|
||||||
|
loadSetting();
|
||||||
|
|
||||||
|
// 设置环境变量
|
||||||
|
loadEnvVar();
|
||||||
|
|
||||||
|
// 尝试进行初始化连接
|
||||||
|
await doConnect({
|
||||||
|
namespace: platform,
|
||||||
|
updateCommandString: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// loading panels
|
||||||
|
await loadPanels();
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { pinkLog } from '@/views/setting/util';
|
import { pinkLog, redLog } from '@/views/setting/util';
|
||||||
import { onUnmounted, ref } from 'vue';
|
import { acquireVsCodeApi, electronApi, getPlatform } from './platform';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
export interface VSCodeMessage {
|
export interface VSCodeMessage {
|
||||||
command: string;
|
command: string;
|
||||||
@ -15,8 +16,6 @@ export interface RestFulResponse {
|
|||||||
export type MessageHandler = (message: VSCodeMessage) => void;
|
export type MessageHandler = (message: VSCodeMessage) => void;
|
||||||
export type CommandHandler = (data: any) => void;
|
export type CommandHandler = (data: any) => void;
|
||||||
|
|
||||||
export const acquireVsCodeApi = (window as any)['acquireVsCodeApi'];
|
|
||||||
|
|
||||||
interface AddCommandListenerOption {
|
interface AddCommandListenerOption {
|
||||||
once: boolean // 只调用一次就销毁
|
once: boolean // 只调用一次就销毁
|
||||||
}
|
}
|
||||||
@ -24,27 +23,36 @@ interface AddCommandListenerOption {
|
|||||||
class MessageBridge {
|
class MessageBridge {
|
||||||
private ws: WebSocket | null = null;
|
private ws: WebSocket | null = null;
|
||||||
private handlers = new Map<string, Set<CommandHandler>>();
|
private handlers = new Map<string, Set<CommandHandler>>();
|
||||||
public isConnected = ref(false);
|
private isConnected: Promise<boolean> | null = null;
|
||||||
|
|
||||||
constructor(private wsUrl: string = 'ws://localhost:8080') {
|
constructor(private wsUrl: string = 'ws://localhost:8080') {
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
private init() {
|
|
||||||
// 环境检测优先级:
|
// 环境检测优先级:
|
||||||
// 1. VS Code WebView 环境
|
// 1. VS Code WebView 环境
|
||||||
// 2. 浏览器 WebSocket 环境
|
// 2. 浏览器 WebSocket 环境
|
||||||
if (typeof acquireVsCodeApi !== 'undefined') {
|
|
||||||
this.setupVSCodeListener();
|
const platform = getPlatform();
|
||||||
pinkLog('当前模式:release');
|
|
||||||
} else {
|
switch (platform) {
|
||||||
|
case 'vscode':
|
||||||
|
this.setupVsCodeListener();
|
||||||
|
pinkLog('当前模式: vscode');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'electron':
|
||||||
|
this.setupElectronListener();
|
||||||
|
pinkLog('当前模式: electron');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'web':
|
||||||
this.setupWebSocket();
|
this.setupWebSocket();
|
||||||
pinkLog('当前模式:debug');
|
pinkLog('当前模式: web');
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// VS Code 环境监听
|
// VS Code 环境监听
|
||||||
private setupVSCodeListener() {
|
private setupVsCodeListener() {
|
||||||
const vscode = acquireVsCodeApi();
|
const vscode = acquireVsCodeApi();
|
||||||
|
|
||||||
window.addEventListener('message', (event: MessageEvent<VSCodeMessage>) => {
|
window.addEventListener('message', (event: MessageEvent<VSCodeMessage>) => {
|
||||||
@ -52,17 +60,12 @@ class MessageBridge {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.postMessage = (message) => vscode.postMessage(message);
|
this.postMessage = (message) => vscode.postMessage(message);
|
||||||
this.isConnected.value = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebSocket 环境连接
|
// WebSocket 环境连接
|
||||||
private setupWebSocket() {
|
private setupWebSocket() {
|
||||||
this.ws = new WebSocket(this.wsUrl);
|
this.ws = new WebSocket(this.wsUrl);
|
||||||
|
|
||||||
this.ws.onopen = () => {
|
|
||||||
this.isConnected.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.ws.onmessage = (event) => {
|
this.ws.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
const message = JSON.parse(event.data) as VSCodeMessage;
|
const message = JSON.parse(event.data) as VSCodeMessage;
|
||||||
@ -74,16 +77,41 @@ class MessageBridge {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.ws.onclose = () => {
|
this.ws.onclose = () => {
|
||||||
this.isConnected.value = false;
|
redLog('WebSocket connection closed');
|
||||||
};
|
};
|
||||||
|
|
||||||
this.postMessage = (message) => {
|
this.postMessage = (message) => {
|
||||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||||
console.log(message);
|
console.log('send', { command: message.command });
|
||||||
|
|
||||||
this.ws.send(JSON.stringify(message));
|
this.ws.send(JSON.stringify(message));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ws = this.ws;
|
||||||
|
|
||||||
|
this.isConnected = new Promise<boolean>((resolve, reject) => {
|
||||||
|
ws.onopen = () => {
|
||||||
|
resolve(true);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async awaitForWebsockt() {
|
||||||
|
if (this.isConnected) {
|
||||||
|
await this.isConnected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupElectronListener() {
|
||||||
|
electronApi.onReply((event: MessageEvent<VSCodeMessage>) => {
|
||||||
|
console.log(event);
|
||||||
|
this.dispatchMessage(event.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.postMessage = (message) => {
|
||||||
|
console.log(message);
|
||||||
|
electronApi.sendToMain(message);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -176,6 +204,6 @@ export function useMessageBridge() {
|
|||||||
postMessage: bridge.postMessage.bind(bridge),
|
postMessage: bridge.postMessage.bind(bridge),
|
||||||
addCommandListener: bridge.addCommandListener.bind(bridge),
|
addCommandListener: bridge.addCommandListener.bind(bridge),
|
||||||
commandRequest: bridge.commandRequest.bind(bridge),
|
commandRequest: bridge.commandRequest.bind(bridge),
|
||||||
isConnected: bridge.isConnected
|
awaitForWebsockt: bridge.awaitForWebsockt.bind(bridge)
|
||||||
};
|
};
|
||||||
}
|
}
|
15
renderer/src/api/platform.ts
Normal file
15
renderer/src/api/platform.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export type OpenMcpSupportPlatform = 'web' | 'vscode' | 'electron';
|
||||||
|
|
||||||
|
export const acquireVsCodeApi = (window as any)['acquireVsCodeApi'];
|
||||||
|
|
||||||
|
export const electronApi = (window as any)['electronApi'];
|
||||||
|
|
||||||
|
export function getPlatform(): OpenMcpSupportPlatform {
|
||||||
|
if (typeof acquireVsCodeApi !== 'undefined') {
|
||||||
|
return 'vscode';
|
||||||
|
} else if (typeof electronApi !== 'undefined') {
|
||||||
|
return 'electron';
|
||||||
|
} else {
|
||||||
|
return 'web';
|
||||||
|
}
|
||||||
|
}
|
@ -3,10 +3,10 @@ import { llmManager, llms } from "@/views/setting/llm";
|
|||||||
import { pinkLog } from "@/views/setting/util";
|
import { pinkLog } from "@/views/setting/util";
|
||||||
import I18n from '@/i18n/index';
|
import I18n from '@/i18n/index';
|
||||||
|
|
||||||
export function loadSetting() {
|
export async function loadSetting() {
|
||||||
const bridge = useMessageBridge();
|
const bridge = useMessageBridge();
|
||||||
|
|
||||||
bridge.addCommandListener('setting/load', data => {
|
const data = await bridge.commandRequest('setting/load');
|
||||||
if (data.code !== 200) {
|
if (data.code !== 200) {
|
||||||
pinkLog('配置加载失败');
|
pinkLog('配置加载失败');
|
||||||
console.log(data.msg);
|
console.log(data.msg);
|
||||||
@ -22,12 +22,6 @@ export function loadSetting() {
|
|||||||
llms.push(element);
|
llms.push(element);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}, { once: true });
|
|
||||||
|
|
||||||
bridge.postMessage({
|
|
||||||
command: 'setting/load'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveSetting(saveHandler?: () => void) {
|
export function saveSetting(saveHandler?: () => void) {
|
||||||
|
@ -3,6 +3,7 @@ import { reactive } from 'vue';
|
|||||||
import { pinkLog } from '../setting/util';
|
import { pinkLog } from '../setting/util';
|
||||||
import { arrowMiddleware, ElMessage } from 'element-plus';
|
import { arrowMiddleware, ElMessage } from 'element-plus';
|
||||||
import { ILaunchSigature } from '@/hook/type';
|
import { ILaunchSigature } from '@/hook/type';
|
||||||
|
import { OpenMcpSupportPlatform } from '@/api/platform';
|
||||||
|
|
||||||
export const connectionMethods = reactive({
|
export const connectionMethods = reactive({
|
||||||
current: 'STDIO',
|
current: 'STDIO',
|
||||||
@ -69,15 +70,21 @@ export interface McpOptions {
|
|||||||
clientVersion?: string;
|
clientVersion?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function doWebConnect(option: { updateCommandString?: boolean } = {}) {
|
export async function doConnect(
|
||||||
|
option: {
|
||||||
|
namespace: OpenMcpSupportPlatform
|
||||||
|
updateCommandString?: boolean
|
||||||
|
}
|
||||||
|
) {
|
||||||
const {
|
const {
|
||||||
// updateCommandString 为 true 代表是初始化阶段
|
// updateCommandString 为 true 代表是初始化阶段
|
||||||
|
namespace,
|
||||||
updateCommandString = true
|
updateCommandString = true
|
||||||
} = option;
|
} = option;
|
||||||
|
|
||||||
if (updateCommandString) {
|
if (updateCommandString) {
|
||||||
pinkLog('请求启动参数');
|
pinkLog('请求启动参数');
|
||||||
const connectionItem = await getLaunchSignature('web/launch-signature');
|
const connectionItem = await getLaunchSignature(namespace + '/launch-signature');
|
||||||
|
|
||||||
if (connectionItem.type ==='stdio') {
|
if (connectionItem.type ==='stdio') {
|
||||||
connectionMethods.current = 'STDIO';
|
connectionMethods.current = 'STDIO';
|
||||||
@ -98,54 +105,13 @@ export async function doWebConnect(option: { updateCommandString?: boolean } = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (connectionMethods.current === 'STDIO') {
|
if (connectionMethods.current === 'STDIO') {
|
||||||
await launchStdio();
|
await launchStdio(namespace);
|
||||||
} else {
|
} else {
|
||||||
await launchSSE();
|
await launchSSE(namespace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
async function launchStdio(namespace: string) {
|
||||||
* @description vscode 中初始化启动
|
|
||||||
*/
|
|
||||||
export async function doVscodeConnect(option: { updateCommandString?: boolean } = {}) {
|
|
||||||
// 本地开发只用 IPC 进行启动
|
|
||||||
// 后续需要考虑到不同的连接方式
|
|
||||||
|
|
||||||
const {
|
|
||||||
// updateCommandString 为 true 代表是初始化阶段
|
|
||||||
updateCommandString = true
|
|
||||||
} = option;
|
|
||||||
|
|
||||||
if (updateCommandString) {
|
|
||||||
pinkLog('请求启动参数');
|
|
||||||
const connectionItem = await getLaunchSignature('vscode/launch-signature');
|
|
||||||
|
|
||||||
if (connectionItem.type ==='stdio') {
|
|
||||||
connectionMethods.current = 'STDIO';
|
|
||||||
connectionArgs.commandString = connectionItem.commandString;
|
|
||||||
connectionArgs.cwd = connectionItem.cwd;
|
|
||||||
|
|
||||||
if (connectionArgs.commandString.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
connectionMethods.current = 'SSE';
|
|
||||||
connectionArgs.urlString = connectionItem.url;
|
|
||||||
|
|
||||||
if (connectionArgs.urlString.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (connectionMethods.current === 'STDIO') {
|
|
||||||
await launchStdio();
|
|
||||||
} else {
|
|
||||||
await launchSSE();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function launchStdio() {
|
|
||||||
const bridge = useMessageBridge();
|
const bridge = useMessageBridge();
|
||||||
const env = makeEnv();
|
const env = makeEnv();
|
||||||
|
|
||||||
@ -193,7 +159,7 @@ async function launchStdio() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
bridge.postMessage({
|
bridge.postMessage({
|
||||||
command: 'vscode/update-connection-sigature',
|
command: namespace + '/update-connection-sigature',
|
||||||
data: JSON.parse(JSON.stringify(clientStdioConnectionItem))
|
data: JSON.parse(JSON.stringify(clientStdioConnectionItem))
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -210,7 +176,7 @@ async function launchStdio() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function launchSSE() {
|
async function launchSSE(namespace: string) {
|
||||||
const bridge = useMessageBridge();
|
const bridge = useMessageBridge();
|
||||||
const env = makeEnv();
|
const env = makeEnv();
|
||||||
|
|
||||||
@ -247,7 +213,7 @@ async function launchSSE() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
bridge.postMessage({
|
bridge.postMessage({
|
||||||
command: 'vscode/update-connection-sigature',
|
command: namespace + '/update-connection-sigature',
|
||||||
data: JSON.parse(JSON.stringify(clientSseConnectionItem))
|
data: JSON.parse(JSON.stringify(clientSseConnectionItem))
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -265,23 +231,11 @@ async function launchSSE() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function getLaunchSignature(signatureName: string) {
|
||||||
function getLaunchSignature(signatureName: string) {
|
|
||||||
return new Promise<ILaunchSigature>((resolve, reject) => {
|
|
||||||
// 与 vscode 进行同步
|
|
||||||
const bridge = useMessageBridge();
|
const bridge = useMessageBridge();
|
||||||
|
const { code, msg } = await bridge.commandRequest(signatureName);
|
||||||
|
|
||||||
bridge.addCommandListener(signatureName, data => {
|
return msg;
|
||||||
pinkLog('收到启动参数');
|
|
||||||
resolve(data.msg);
|
|
||||||
|
|
||||||
}, { once: true });
|
|
||||||
|
|
||||||
bridge.postMessage({
|
|
||||||
command: signatureName,
|
|
||||||
data: {}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doReconnect() {
|
export function doReconnect() {
|
||||||
|
@ -29,15 +29,14 @@ import { useI18n } from 'vue-i18n';
|
|||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
import { connectionResult, doWebConnect, doVscodeConnect } from './connection';
|
import { connectionResult, doConnect } from './connection';
|
||||||
|
|
||||||
import ConnectionMethod from './connection-method.vue';
|
import ConnectionMethod from './connection-method.vue';
|
||||||
import ConnectionArgs from './connection-args.vue';
|
import ConnectionArgs from './connection-args.vue';
|
||||||
import EnvVar from './env-var.vue';
|
import EnvVar from './env-var.vue';
|
||||||
|
|
||||||
import ConnectionLog from './connection-log.vue';
|
import ConnectionLog from './connection-log.vue';
|
||||||
|
import { getPlatform } from '@/api/platform';
|
||||||
import { acquireVsCodeApi } from '@/api/message-bridge';
|
|
||||||
|
|
||||||
defineComponent({ name: 'connect' });
|
defineComponent({ name: 'connect' });
|
||||||
|
|
||||||
@ -46,11 +45,9 @@ const isLoading = ref(false);
|
|||||||
async function suitableConnect() {
|
async function suitableConnect() {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
if (acquireVsCodeApi === undefined) {
|
const plaform = getPlatform();
|
||||||
await doWebConnect({ updateCommandString: false });
|
|
||||||
} else {
|
await doConnect({ namespace: plaform, updateCommandString: false })
|
||||||
await doVscodeConnect({ updateCommandString: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "ts-node-dev --respawn --transpile-only src/main.ts",
|
"serve": "ts-node-dev --respawn --transpile-only src/main.ts",
|
||||||
"build": "tsc && webpack --config webpack.config.js",
|
"build": "tsc",
|
||||||
"build:watch": "tsc --watch",
|
"build:watch": "tsc --watch",
|
||||||
"start": "node dist/main.js",
|
"start": "node dist/main.js",
|
||||||
"start:prod": "NODE_ENV=production node dist/main.js",
|
"start:prod": "NODE_ENV=production node dist/main.js",
|
||||||
|
@ -15,7 +15,6 @@ export interface WebSocketResponse {
|
|||||||
|
|
||||||
export interface PostMessageble {
|
export interface PostMessageble {
|
||||||
postMessage(message: any): void;
|
postMessage(message: any): void;
|
||||||
onDidReceiveMessage(callback: MessageHandler): { dispose: () => void };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听器回调类型
|
// 监听器回调类型
|
||||||
|
@ -108,7 +108,6 @@ wss.on('connection', ws => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const option = getInitConnectionOption();
|
const option = getInitConnectionOption();
|
||||||
|
|
||||||
// 注册消息接受的管线
|
// 注册消息接受的管线
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { app, BrowserWindow } from 'electron';
|
import { app, BrowserWindow, ipcMain } from 'electron';
|
||||||
import WebSocket from 'ws';
|
|
||||||
import * as OpenMCPService from '../resources/service';
|
import * as OpenMCPService from '../resources/service';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { ElectronIPCLike, getInitConnectionOption, ILaunchSigature, updateConnectionOption } from './util';
|
||||||
|
|
||||||
let mainWindow: BrowserWindow
|
let mainWindow: BrowserWindow
|
||||||
|
|
||||||
@ -11,20 +12,14 @@ function createWindow(): void {
|
|||||||
width: 1200,
|
width: 1200,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true,
|
nodeIntegration: true,
|
||||||
contextIsolation: false
|
contextIsolation: true,
|
||||||
|
preload: path.join(__dirname, 'preload.js')
|
||||||
},
|
},
|
||||||
autoHideMenuBar: true
|
autoHideMenuBar: true,
|
||||||
})
|
icon: path.join(__dirname, '..', 'icons', 'icon.png')
|
||||||
|
});
|
||||||
|
|
||||||
mainWindow.loadFile('resources/renderer/index.html')
|
const webview = new ElectronIPCLike(mainWindow.webContents);
|
||||||
}
|
|
||||||
|
|
||||||
const wss = new (WebSocket as any).Server({ port: 8080 });
|
|
||||||
|
|
||||||
wss.on('connection', (ws: any) => {
|
|
||||||
|
|
||||||
// 仿造 webview 进行统一接口访问
|
|
||||||
const webview = new OpenMCPService.VSCodeWebViewLike(ws);
|
|
||||||
|
|
||||||
// 先发送成功建立的消息
|
// 先发送成功建立的消息
|
||||||
webview.postMessage({
|
webview.postMessage({
|
||||||
@ -35,17 +30,58 @@ wss.on('connection', (ws: any) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const option = getInitConnectionOption();
|
||||||
|
|
||||||
// 注册消息接受的管线
|
// 注册消息接受的管线
|
||||||
webview.onDidReceiveMessage((message: any) => {
|
webview.onDidReceiveMessage((message: any) => {
|
||||||
console.info(`command: [${message.command || 'No Command'}]`);
|
console.info(`command: [${message.command || 'No Command'}]`);
|
||||||
|
|
||||||
const { command, data } = message;
|
const { command, data } = message;
|
||||||
OpenMCPService.routeMessage(command, data, webview);
|
|
||||||
|
switch (command) {
|
||||||
|
case 'electron/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
|
||||||
|
};
|
||||||
|
|
||||||
|
webview.postMessage({
|
||||||
|
command: 'electron/launch-signature',
|
||||||
|
data: launchResult
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'electron/update-connection-sigature':
|
||||||
|
updateConnectionOption(data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
OpenMCPService.routeMessage(command, data, webview);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const indexPath = path.join(__dirname, '..', 'resources/renderer/index.html');
|
||||||
|
mainWindow.loadFile(indexPath);
|
||||||
|
}
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
createWindow()
|
|
||||||
|
createWindow();
|
||||||
|
|
||||||
app.on('activate', function () {
|
app.on('activate', function () {
|
||||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||||
|
12
software/src/preload.ts
Normal file
12
software/src/preload.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { contextBridge, ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('electronApi', {
|
||||||
|
onReply: (callback: (event: MessageEvent<any>) => void) => {
|
||||||
|
ipcRenderer.on('message', (event, data) => {
|
||||||
|
callback({ data } as MessageEvent<any>);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
sendToMain: (message: any) => {
|
||||||
|
ipcRenderer.send('message', message);
|
||||||
|
}
|
||||||
|
});
|
89
software/src/util.ts
Normal file
89
software/src/util.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
|
||||||
|
import { ipcMain } from 'electron';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
export class ElectronIPCLike {
|
||||||
|
private webContents: Electron.WebContents;
|
||||||
|
|
||||||
|
constructor(webContents: Electron.WebContents) {
|
||||||
|
this.webContents = webContents;
|
||||||
|
}
|
||||||
|
|
||||||
|
postMessage(message: { command: string; data: any }): void {
|
||||||
|
this.webContents.send('message', message);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDidReceiveMessage(callback: (message: { command: string; data: any }) => void): void {
|
||||||
|
ipcMain.on('message', (event, message) => {
|
||||||
|
callback(message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface IStdioLaunchSignature {
|
||||||
|
type: 'stdio';
|
||||||
|
commandString: string;
|
||||||
|
cwd: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ISSELaunchSignature {
|
||||||
|
type:'sse';
|
||||||
|
url: string;
|
||||||
|
oauth: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ILaunchSigature = IStdioLaunchSignature | ISSELaunchSignature;
|
||||||
|
|
||||||
|
export function refreshConnectionOption(envPath: string) {
|
||||||
|
const defaultOption = {
|
||||||
|
type:'stdio',
|
||||||
|
command: 'mcp',
|
||||||
|
args: ['run', 'main.py'],
|
||||||
|
cwd: '../server'
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.writeFileSync(envPath, JSON.stringify(defaultOption, null, 4));
|
||||||
|
|
||||||
|
return defaultOption;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getInitConnectionOption() {
|
||||||
|
const envPath = path.join(__dirname, '..', '.env');
|
||||||
|
|
||||||
|
if (!fs.existsSync(envPath)) {
|
||||||
|
return refreshConnectionOption(envPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const option = JSON.parse(fs.readFileSync(envPath, 'utf-8'));
|
||||||
|
return option;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
return refreshConnectionOption(envPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateConnectionOption(data: any) {
|
||||||
|
const envPath = path.join(__dirname, '..', '.env');
|
||||||
|
|
||||||
|
if (data.connectionType === 'STDIO') {
|
||||||
|
const connectionItem = {
|
||||||
|
type: 'stdio',
|
||||||
|
command: data.command,
|
||||||
|
args: data.args,
|
||||||
|
cwd: data.cwd.replace(/\\/g, '/')
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.writeFileSync(envPath, JSON.stringify(connectionItem, null, 4));
|
||||||
|
} else {
|
||||||
|
const connectionItem = {
|
||||||
|
type: 'sse',
|
||||||
|
url: data.url,
|
||||||
|
oauth: data.oauth
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.writeFileSync(envPath, JSON.stringify(connectionItem, null, 4));
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user