支持预设环境变量与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` | 系统配置信息云同步 | `MVP` | 0% | `P1` |
| `all` | 系统提示词管理模块 | `MVP` | 0% | `P1` |
| `service` | 工具 wise 的日志系统 | `MVP` | 0% | `P0` |
## Dev

View File

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

View File

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

View File

@ -143,5 +143,6 @@
"creative": "Kreativität",
"single-dialog": "Einzelrunden-Dialog",
"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",
"single-dialog": "Single-round dialogue",
"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é",
"single-dialog": "Dialogue en un tour",
"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": "創造性",
"single-dialog": "単一ラウンドの対話",
"multi-dialog": "マルチターン会話",
"press-and-run": "テストを開始するには質問を入力してください"
"press-and-run": "テストを開始するには質問を入力してください",
"connect-sigature": "接続署名"
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,23 +1,41 @@
<template>
<!-- STDIO 模式下的命令输入 -->
<div class="connection-option" v-if="connectionMethods.current === 'STDIO'">
<span>{{ t('command') }}</span>
<span>{{ t('connect-sigature') }}</span>
<span style="width: 310px;">
<el-form :model="connectionArgs" :rules="rules" ref="stdioForm">
<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>
</span>
</div>
<!-- 其他模式下的URL输入 -->
<!-- SSE 模式下的URL输入 -->
<div class="connection-option" v-else>
<span>{{ "URL" }}</span>
<span>{{ t('connect-sigature') }}</span>
<span style="width: 310px;">
<el-form :model="connectionArgs" :rules="rules" ref="urlForm">
<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>
</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>
</span>
@ -41,6 +59,12 @@ const rules = reactive<FormRules>({
commandString: [
{ required: true, message: '命令不能为空', trigger: 'blur' }
],
cwd: [
{ required: false, trigger: 'blur' }
],
oauth: [
{ required: false, trigger: 'blur' }
],
urlString: [
{ required: true, message: 'URL不能为空', trigger: 'blur' }
]
@ -62,3 +86,29 @@ const validateForm = async () => {
}
</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>
<div class="connection-option">
<span>{{ t('log') }}</span>
<el-scrollbar height="300px">
<el-scrollbar height="100%">
<div
class="output-content"
contenteditable="false"
@ -24,6 +24,14 @@ const { t } = useI18n();
</script>
<style>
.connection-option {
height: 100%;
}
.connection-option .el-scrollbar__view {
height: 100%;
}
.connection-option .output-content {
border-radius: .5em;
padding: 15px;
@ -37,5 +45,6 @@ const { t } = useI18n();
font-size: 15px;
line-height: 1.5;
background-color: var(--sidebar);
height: 95%;
}
</style>

View File

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

View File

@ -1,6 +1,16 @@
<template>
<div class="connection-option">
<div class="env-switch">
<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">
<span class="input-env-container">
<span>
@ -15,7 +25,6 @@
</div>
</span>
</span>
</div>
<el-scrollbar height="200px" width="350px" class="display-env-container">
<div class="display-env">
@ -33,19 +42,47 @@
<script setup lang="ts">
import { defineComponent } from 'vue';
import { defineComponent, onMounted, ref } from 'vue';
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' });
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 添加环境变量
*/
function addEnvVar() {
function addEnvVar() {
// key
const currentKey = connectionEnv.newKey;
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>
<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>
<div class="connect-action">
<el-button
type="primary"
size="large"
:disabled="!connectionResult"
@click="suitableConnect()"
>
<el-button type="primary" size="large" :loading="isLoading" :disabled="!connectionResult"
@click="suitableConnect()">
<span class="iconfont icon-connect" v-if="!isLoading"></span>
{{ t('connect.appearance.connect') }}
</el-button>
<el-button
type="primary"
size="large"
@click="doReconnect()"
>
{{ t('connect.appearance.reconnect') }}
</el-button>
</div>
</div>
@ -33,7 +22,7 @@
</template>
<script setup lang="ts">
import { defineComponent } from 'vue';
import { defineComponent, ref } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
@ -58,12 +47,19 @@ bridge.addCommandListener('connect', data => {
connectionResult.logString = msg;
}, { once: false });
function suitableConnect() {
const isLoading = ref(false);
async function suitableConnect() {
isLoading.value = true;
connectionResult.logString = '';
if (acquireVsCodeApi === undefined) {
doConnect();
await doConnect();
} else {
launchConnect({ updateCommandString: false });
await launchConnect({ updateCommandString: false });
}
isLoading.value = false;
}
</script>
@ -106,7 +102,7 @@ function suitableConnect() {
padding-bottom: 10px;
}
.input-env-container > span {
.input-env-container>span {
width: 150px;
margin-right: 10px;
display: flex;

View File

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

View File

@ -139,50 +139,6 @@
"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你好"
}
}
]
}
}
}
]
}