支持预设环境变量与stdio启动的 cwd 自定

This commit is contained in:
锦恢 2025-04-24 16:00:54 +08:00
parent ddb4dfb565
commit f484688a4b
18 changed files with 255 additions and 117 deletions

View File

@ -50,6 +50,7 @@
| `service` | 对于连接的 mcp server 进行热更新 | `MVP` | 0% | `P1` | | `service` | 对于连接的 mcp server 进行热更新 | `MVP` | 0% | `P1` |
| `service` | 系统配置信息云同步 | `MVP` | 0% | `P1` | | `service` | 系统配置信息云同步 | `MVP` | 0% | `P1` |
| `all` | 系统提示词管理模块 | `MVP` | 0% | `P1` | | `all` | 系统提示词管理模块 | `MVP` | 0% | `P1` |
| `service` | 工具 wise 的日志系统 | `MVP` | 0% | `P0` |
## Dev ## Dev

View File

@ -39,15 +39,18 @@ export class TaskLoop {
const toolName = toolCall.function.name; const toolName = toolCall.function.name;
const toolArgs = JSON.parse(toolCall.function.arguments); const toolArgs = JSON.parse(toolCall.function.arguments);
const toolResponse = await callTool(toolName, toolArgs); const toolResponse = await callTool(toolName, toolArgs);
if (!toolResponse.isError) { if (!toolResponse.isError) {
const content = JSON.stringify(toolResponse.content); const content = JSON.stringify(toolResponse.content);
return content; return content;
} else { } else {
this.onError(`工具调用失败: ${toolResponse.content}`); this.onError(`工具调用失败: ${toolResponse.content}`);
console.error(toolResponse.content);
} }
} catch (error) { } catch (error) {
this.onError(`工具调用失败: ${(error as Error).message}`); this.onError(`工具调用失败: ${(error as Error).message}`);
console.error(error);
} }
} }

View File

@ -143,5 +143,6 @@
"creative": "إبداع", "creative": "إبداع",
"single-dialog": "محادثة من جولة واحدة", "single-dialog": "محادثة من جولة واحدة",
"multi-dialog": "محادثة متعددة الجولات", "multi-dialog": "محادثة متعددة الجولات",
"press-and-run": "اكتب سؤالاً لبدء الاختبار" "press-and-run": "اكتب سؤالاً لبدء الاختبار",
"connect-sigature": "توقيع الاتصال"
} }

View File

@ -143,5 +143,6 @@
"creative": "Kreativität", "creative": "Kreativität",
"single-dialog": "Einzelrunden-Dialog", "single-dialog": "Einzelrunden-Dialog",
"multi-dialog": "Mehrrundengespräch", "multi-dialog": "Mehrrundengespräch",
"press-and-run": "Geben Sie eine Frage ein, um den Test zu starten" "press-and-run": "Geben Sie eine Frage ein, um den Test zu starten",
"connect-sigature": "Verbindungssignatur"
} }

View File

@ -143,5 +143,6 @@
"creative": "Creativity", "creative": "Creativity",
"single-dialog": "Single-round dialogue", "single-dialog": "Single-round dialogue",
"multi-dialog": "Multi-turn conversation", "multi-dialog": "Multi-turn conversation",
"press-and-run": "Type a question to start the test" "press-and-run": "Type a question to start the test",
"connect-sigature": "Connection signature"
} }

View File

@ -143,5 +143,6 @@
"creative": "Créativité", "creative": "Créativité",
"single-dialog": "Dialogue en un tour", "single-dialog": "Dialogue en un tour",
"multi-dialog": "Conversation multi-tours", "multi-dialog": "Conversation multi-tours",
"press-and-run": "Tapez une question pour commencer le test" "press-and-run": "Tapez une question pour commencer le test",
"connect-sigature": "Signature de connexion"
} }

View File

@ -143,5 +143,6 @@
"creative": "創造性", "creative": "創造性",
"single-dialog": "単一ラウンドの対話", "single-dialog": "単一ラウンドの対話",
"multi-dialog": "マルチターン会話", "multi-dialog": "マルチターン会話",
"press-and-run": "テストを開始するには質問を入力してください" "press-and-run": "テストを開始するには質問を入力してください",
"connect-sigature": "接続署名"
} }

View File

@ -143,5 +143,6 @@
"creative": "창의성", "creative": "창의성",
"single-dialog": "단일 라운드 대화", "single-dialog": "단일 라운드 대화",
"multi-dialog": "다중 턴 대화", "multi-dialog": "다중 턴 대화",
"press-and-run": "테스트를 시작하려면 질문을 입력하세요" "press-and-run": "테스트를 시작하려면 질문을 입력하세요",
"connect-sigature": "연결 서명"
} }

View File

@ -143,5 +143,6 @@
"creative": "Творчество", "creative": "Творчество",
"single-dialog": "Однораундовый диалог", "single-dialog": "Однораундовый диалог",
"multi-dialog": "Многораундовый разговор", "multi-dialog": "Многораундовый разговор",
"press-and-run": "Введите вопрос, чтобы начать тест" "press-and-run": "Введите вопрос, чтобы начать тест",
"connect-sigature": "Подпись соединения"
} }

View File

@ -143,5 +143,6 @@
"creative": "创意", "creative": "创意",
"single-dialog": "单轮对话", "single-dialog": "单轮对话",
"multi-dialog": "多轮对话", "multi-dialog": "多轮对话",
"press-and-run": "键入问题以开始测试" "press-and-run": "键入问题以开始测试",
"connect-sigature": "连接签名"
} }

View File

@ -143,5 +143,6 @@
"creative": "創意", "creative": "創意",
"single-dialog": "單輪對話", "single-dialog": "單輪對話",
"multi-dialog": "多輪對話", "multi-dialog": "多輪對話",
"press-and-run": "輸入問題以開始測試" "press-and-run": "輸入問題以開始測試",
"connect-sigature": "連接簽名"
} }

View File

@ -1,23 +1,41 @@
<template> <template>
<!-- STDIO 模式下的命令输入 --> <!-- STDIO 模式下的命令输入 -->
<div class="connection-option" v-if="connectionMethods.current === 'STDIO'"> <div class="connection-option" v-if="connectionMethods.current === 'STDIO'">
<span>{{ t('command') }}</span> <span>{{ t('connect-sigature') }}</span>
<span style="width: 310px;"> <span style="width: 310px;">
<el-form :model="connectionArgs" :rules="rules" ref="stdioForm"> <el-form :model="connectionArgs" :rules="rules" ref="stdioForm">
<el-form-item prop="commandString"> <el-form-item prop="commandString">
<el-input v-model="connectionArgs.commandString"></el-input> <div class="input-with-label">
<span class="input-label">命令</span>
<el-input v-model="connectionArgs.commandString" placeholder="mcp run <your script>"></el-input>
</div>
</el-form-item>
<el-form-item prop="cwd">
<div class="input-with-label">
<span class="input-label">执行目录</span>
<el-input v-model="connectionArgs.cwd" placeholder="cwd, 可为空"></el-input>
</div>
</el-form-item> </el-form-item>
</el-form> </el-form>
</span> </span>
</div> </div>
<!-- 其他模式下的URL输入 --> <!-- SSE 模式下的URL输入 -->
<div class="connection-option" v-else> <div class="connection-option" v-else>
<span>{{ "URL" }}</span> <span>{{ t('connect-sigature') }}</span>
<span style="width: 310px;"> <span style="width: 310px;">
<el-form :model="connectionArgs" :rules="rules" ref="urlForm"> <el-form :model="connectionArgs" :rules="rules" ref="urlForm">
<el-form-item prop="urlString"> <el-form-item prop="urlString">
<div class="input-with-label">
<span class="input-label">URL</span>
<el-input v-model="connectionArgs.urlString" placeholder="http://"></el-input> <el-input v-model="connectionArgs.urlString" placeholder="http://"></el-input>
</div>
</el-form-item>
<el-form-item prop="oauth">
<div class="input-with-label">
<span class="input-label">OAuth</span>
<el-input v-model="connectionArgs.oauth" placeholder="认证签名, 可为空"></el-input>
</div>
</el-form-item> </el-form-item>
</el-form> </el-form>
</span> </span>
@ -41,6 +59,12 @@ const rules = reactive<FormRules>({
commandString: [ commandString: [
{ required: true, message: '命令不能为空', trigger: 'blur' } { required: true, message: '命令不能为空', trigger: 'blur' }
], ],
cwd: [
{ required: false, trigger: 'blur' }
],
oauth: [
{ required: false, trigger: 'blur' }
],
urlString: [ urlString: [
{ required: true, message: 'URL不能为空', trigger: 'blur' } { required: true, message: 'URL不能为空', trigger: 'blur' }
] ]
@ -62,3 +86,29 @@ const validateForm = async () => {
} }
</script> </script>
<style scoped>
.input-with-label {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
width: 100%;
}
.input-label {
width: 80px;
font-size: 14px;
color: var(--el-text-color-regular);
}
.connection-option {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px;
background-color: var(--el-bg-color);
border-radius: 4px;
margin-bottom: 16px;
}
</style>

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="connection-option"> <div class="connection-option">
<span>{{ t('log') }}</span> <span>{{ t('log') }}</span>
<el-scrollbar height="300px"> <el-scrollbar height="100%">
<div <div
class="output-content" class="output-content"
contenteditable="false" contenteditable="false"
@ -24,6 +24,14 @@ const { t } = useI18n();
</script> </script>
<style> <style>
.connection-option {
height: 100%;
}
.connection-option .el-scrollbar__view {
height: 100%;
}
.connection-option .output-content { .connection-option .output-content {
border-radius: .5em; border-radius: .5em;
padding: 15px; padding: 15px;
@ -37,5 +45,6 @@ const { t } = useI18n();
font-size: 15px; font-size: 15px;
line-height: 1.5; line-height: 1.5;
background-color: var(--sidebar); background-color: var(--sidebar);
height: 95%;
} }
</style> </style>

View File

@ -22,7 +22,6 @@ export const connectionMethods = reactive({
export const connectionArgs = reactive({ export const connectionArgs = reactive({
commandString: '', commandString: '',
cwd: '', cwd: '',
env: {},
oauth: '', oauth: '',
urlString: '' urlString: ''
}); });
@ -44,6 +43,14 @@ export const connectionEnv = reactive<IConnectionEnv>({
newValue: '' newValue: ''
}); });
export function makeEnv() {
const env = {} as Record<string, string>;
connectionEnv.data.forEach(item => {
env[item.key] = item.value;
});
return env;
}
// 定义连接类型 // 定义连接类型
type ConnectionType = 'STDIO' | 'SSE'; type ConnectionType = 'STDIO' | 'SSE';
@ -54,6 +61,8 @@ export interface MCPOptions {
// STDIO 特定选项 // STDIO 特定选项
command?: string; command?: string;
args?: string[]; args?: string[];
cwd?: string;
env?: Record<string, string>;
// SSE 特定选项 // SSE 特定选项
url?: string; url?: string;
// 通用客户端选项 // 通用客户端选项
@ -64,6 +73,7 @@ export interface MCPOptions {
export function doConnect() { export function doConnect() {
let connectOption: MCPOptions; let connectOption: MCPOptions;
const bridge = useMessageBridge(); const bridge = useMessageBridge();
const env = makeEnv();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// 监听 connect // 监听 connect
@ -86,7 +96,6 @@ export function doConnect() {
resolve(void 0); resolve(void 0);
}, { once: true }); }, { once: true });
// TODO: 增加判断,获取 cwd
if (connectionMethods.current === 'STDIO') { if (connectionMethods.current === 'STDIO') {
if (connectionArgs.commandString.length === 0) { if (connectionArgs.commandString.length === 0) {
@ -100,7 +109,9 @@ export function doConnect() {
connectOption = { connectOption = {
connectionType: 'STDIO', connectionType: 'STDIO',
command: command, command: command,
cwd: connectionArgs.cwd,
args: commandComponents, args: commandComponents,
env: env,
clientName: 'openmcp.connect.stdio', clientName: 'openmcp.connect.stdio',
clientVersion: '0.0.1' clientVersion: '0.0.1'
} }
@ -115,6 +126,7 @@ export function doConnect() {
connectOption = { connectOption = {
connectionType: 'SSE', connectionType: 'SSE',
url: url, url: url,
env: env,
clientName: 'openmcp.connect.sse', clientName: 'openmcp.connect.sse',
clientVersion: '0.0.1' clientVersion: '0.0.1'
} }
@ -147,7 +159,6 @@ export async function launchConnect(option: { updateCommandString?: boolean } =
if (updateCommandString) { if (updateCommandString) {
connectionArgs.commandString = connectionItem.commandString; connectionArgs.commandString = connectionItem.commandString;
connectionArgs.cwd = connectionItem.cwd; connectionArgs.cwd = connectionItem.cwd;
connectionArgs.env = {};
if (connectionArgs.commandString.length === 0) { if (connectionArgs.commandString.length === 0) {
return; return;
@ -171,6 +182,7 @@ export async function launchConnect(option: { updateCommandString?: boolean } =
async function launchStdio() { async function launchStdio() {
const bridge = useMessageBridge(); const bridge = useMessageBridge();
const env = makeEnv();
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
// 监听 connect // 监听 connect
@ -196,7 +208,7 @@ async function launchStdio() {
command: command, command: command,
args: commandComponents, args: commandComponents,
cwd: connectionArgs.cwd, cwd: connectionArgs.cwd,
env: connectionArgs.env, env
}; };
bridge.postMessage({ bridge.postMessage({
@ -225,7 +237,8 @@ async function launchStdio() {
args: commandComponents, args: commandComponents,
cwd: connectionArgs.cwd, cwd: connectionArgs.cwd,
clientName: 'openmcp.connect.stdio', clientName: 'openmcp.connect.stdio',
clientVersion: '0.0.1' clientVersion: '0.0.1',
env
}; };
bridge.postMessage({ bridge.postMessage({
@ -237,6 +250,7 @@ async function launchStdio() {
async function launchSSE() { async function launchSSE() {
const bridge = useMessageBridge(); const bridge = useMessageBridge();
const env = makeEnv();
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
// 监听 connect // 监听 connect
@ -257,7 +271,7 @@ async function launchSSE() {
name: 'openmcp.connect.sse', name: 'openmcp.connect.sse',
url: connectionArgs.urlString, url: connectionArgs.urlString,
oauth: connectionArgs.oauth, oauth: connectionArgs.oauth,
env: connectionArgs.env env: env
}; };
bridge.postMessage({ bridge.postMessage({
command: 'vscode/update-connection-sigature', command: 'vscode/update-connection-sigature',
@ -278,7 +292,8 @@ async function launchSSE() {
connectionType: 'SSE', connectionType: 'SSE',
url: connectionArgs.urlString, url: connectionArgs.urlString,
clientName: 'openmcp.connect.sse', clientName: 'openmcp.connect.sse',
clientVersion: '0.0.1' clientVersion: '0.0.1',
env
}; };
bridge.postMessage({ bridge.postMessage({

View File

@ -1,6 +1,16 @@
<template> <template>
<div class="connection-option"> <div class="connection-option">
<div class="env-switch">
<span>{{ t('env-var') }}</span> <span>{{ t('env-var') }}</span>
<el-switch
v-model="envEnabled"
@change="handleEnvSwitch"
inline-prompt
active-text="预设"
inactive-text="预设"
></el-switch>
</div>
<div class="input-env"> <div class="input-env">
<span class="input-env-container"> <span class="input-env-container">
<span> <span>
@ -15,7 +25,6 @@
</div> </div>
</span> </span>
</span> </span>
</div> </div>
<el-scrollbar height="200px" width="350px" class="display-env-container"> <el-scrollbar height="200px" width="350px" class="display-env-container">
<div class="display-env"> <div class="display-env">
@ -33,19 +42,47 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue'; import { defineComponent, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { connectionEnv, EnvItem } from './connection'; import { connectionEnv, connectionResult, EnvItem } from './connection';
import { useMessageBridge } from '@/api/message-bridge';
defineComponent({ name: 'env-var' }); defineComponent({ name: 'env-var' });
const { t } = useI18n(); const { t } = useI18n();
const bridge = useMessageBridge();
function lookupEnvVar(varNames: string[]) {
console.log('enter');
return new Promise<string[] | undefined>((resolve, reject) => {
bridge.addCommandListener('lookup-env-var', data => {
const { code, msg } = data;
if (code === 200) {
resolve(msg);
} else {
connectionResult.logString += '\n' + msg;
resolve(undefined);
}
}, { once: true });
console.log(varNames);
bridge.postMessage({
command: 'lookup-env-var',
data: {
keys: varNames
}
})
});
}
/** /**
* @description 添加环境变量 * @description 添加环境变量
*/ */
function addEnvVar() { function addEnvVar() {
// key // key
const currentKey = connectionEnv.newKey; const currentKey = connectionEnv.newKey;
const currentValue = connectionEnv.newValue; const currentValue = connectionEnv.newValue;
@ -78,6 +115,63 @@ function deleteEnvVar(option: EnvItem) {
} }
const envEnabled = ref(true);
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;
}
}
onMounted(() => {
setTimeout(() => {
handleEnvSwitch(envEnabled.value);
}, 200);
});
</script> </script>
<style></style> <style>
.env-switch {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.env-switch .el-switch .el-switch__action {
background-color: var(--main-color);
}
.env-switch .el-switch.is-checked .el-switch__action {
background-color: var(--sidebar);
}
.env-switch .el-switch__core {
border: 1px solid var(--main-color) !important;
}
</style>

View File

@ -6,22 +6,11 @@
<EnvVar></EnvVar> <EnvVar></EnvVar>
<div class="connect-action"> <div class="connect-action">
<el-button <el-button type="primary" size="large" :loading="isLoading" :disabled="!connectionResult"
type="primary" @click="suitableConnect()">
size="large" <span class="iconfont icon-connect" v-if="!isLoading"></span>
:disabled="!connectionResult"
@click="suitableConnect()"
>
{{ t('connect.appearance.connect') }} {{ t('connect.appearance.connect') }}
</el-button> </el-button>
<el-button
type="primary"
size="large"
@click="doReconnect()"
>
{{ t('connect.appearance.reconnect') }}
</el-button>
</div> </div>
</div> </div>
@ -33,7 +22,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue'; import { defineComponent, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
const { t } = useI18n(); const { t } = useI18n();
@ -58,12 +47,19 @@ bridge.addCommandListener('connect', data => {
connectionResult.logString = msg; connectionResult.logString = msg;
}, { once: false }); }, { once: false });
function suitableConnect() { const isLoading = ref(false);
async function suitableConnect() {
isLoading.value = true;
connectionResult.logString = '';
if (acquireVsCodeApi === undefined) { if (acquireVsCodeApi === undefined) {
doConnect(); await doConnect();
} else { } else {
launchConnect({ updateCommandString: false }); await launchConnect({ updateCommandString: false });
} }
isLoading.value = false;
} }
</script> </script>
@ -106,7 +102,7 @@ function suitableConnect() {
padding-bottom: 10px; padding-bottom: 10px;
} }
.input-env-container > span { .input-env-container>span {
width: 150px; width: 150px;
margin-right: 10px; margin-right: 10px;
display: flex; display: flex;

View File

@ -1,6 +1,7 @@
import { PostMessageble } from '../adapter'; import { PostMessageble } from '../adapter';
import { connect, MCPClient, type MCPOptions } from './connect'; import { connect, MCPClient, type MCPOptions } from './connect';
import { lookupEnvVarHandler } from './env-var';
import { callTool, getPrompt, getServerVersion, listPrompts, listResources, listResourceTemplates, listTools, readResource } from './handler'; import { callTool, getPrompt, getServerVersion, listPrompts, listResources, listResourceTemplates, listTools, readResource } from './handler';
import { chatCompletionHandler } from './llm'; import { chatCompletionHandler } from './llm';
import { panelLoadHandler, panelSaveHandler } from './panel'; import { panelLoadHandler, panelSaveHandler } from './panel';
@ -132,6 +133,10 @@ export function messageController(command: string, data: any, webview: PostMessa
chatCompletionHandler(client, data, webview); chatCompletionHandler(client, data, webview);
break; break;
case 'lookup-env-var':
lookupEnvVarHandler(client, data, webview);
break;
default: default:
break; break;
} }

View File

@ -139,50 +139,6 @@
"isError": false "isError": false
} }
} }
},
{
"name": "资源",
"icon": "icon-file",
"type": "blank",
"componentIndex": 0,
"storage": {
"currentResourceName": "greeting",
"formData": {
"name": "kirigaya"
},
"lastResourceReadResponse": {
"contents": [
{
"uri": "greeting://kirigaya",
"mimeType": "text/plain",
"text": "Hello, kirigaya!"
}
]
}
}
},
{
"name": "提词",
"icon": "icon-chat",
"type": "blank",
"componentIndex": 1,
"storage": {
"formData": {
"message": "你好"
},
"currentPromptName": "translate",
"lastPromptGetResponse": {
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "请将下面的话语翻译成中文:\n\n你好"
}
}
]
}
}
} }
] ]
} }