This commit is contained in:
锦恢 2025-03-31 02:05:26 +08:00
parent eed67f6eb5
commit 37d0194fab
18 changed files with 233 additions and 71 deletions

View File

@ -22,7 +22,7 @@ const bridge = useMessageBridge();
bridge.addCommandListener('hello', data => {
pinkLog(`${data.name} 上线`);
pinkLog(`version: ${data.version}`);
});
}, { once: true });
//
@ -47,14 +47,11 @@ onMounted(() => {
connectionArgs.commandString = 'uv run mcp run ../servers/main.py';
connectionMethods.current = 'STDIO';
let handler: (() => void);
handler = bridge.addCommandListener('connect', data => {
bridge.addCommandListener('connect', data => {
const { code, msg } = data;
connectionResult.success = (code === 200);
connectionResult.logString = msg;
handler();
});
}, { once: true });
setTimeout(() => {
doConnect();

View File

@ -12,6 +12,10 @@ export type CommandHandler = (data: any) => void;
export const acquireVsCodeApi = (window as any)['acquireVsCodeApi'];
interface AddCommandListenerOption {
once: boolean // 只调用一次就销毁
}
class MessageBridge {
private ws: WebSocket | null = null;
private handlers = new Map<string, Set<CommandHandler>>();
@ -94,17 +98,21 @@ class MessageBridge {
/**
* @description
* @param handler
* @returns
*/
public addCommandListener(command: string, commandHandler: CommandHandler) {
public addCommandListener(command: string, commandHandler: CommandHandler, option: AddCommandListenerOption) {
if (!this.handlers.has(command)) {
this.handlers.set(command, new Set<CommandHandler>());
}
const commandHandlers = this.handlers.get(command)!;
commandHandlers.add(commandHandler);
return () => commandHandlers.delete(commandHandler);
const wrapperCommandHandler = option.once ? (data: any) => {
commandHandler(data);
commandHandlers.delete(wrapperCommandHandler);
} : commandHandler;
commandHandlers.add(wrapperCommandHandler);
return () => commandHandlers.delete(wrapperCommandHandler);
}
public destroy() {

View File

@ -16,6 +16,10 @@
<ResourceReader
:tab-id="props.tabId"
></ResourceReader>
<ResourceLogger
:tab-id="props.tabId"
></ResourceLogger>
</div>
</div>
</template>
@ -24,6 +28,7 @@
import { defineProps } from 'vue';
import ResourceTemplates from './resource-templates.vue';
import ResourceReader from './resouce-reader.vue';
import ResourceLogger from './resource-logger.vue';
const props = defineProps({
tabId: {
@ -40,6 +45,8 @@ const props = defineProps({
.resource-module {
padding: 20px;
height: 100%;
display: flex;
justify-content: space-between;
}
.resource-module .left {

View File

@ -1,24 +1,27 @@
<template>
<div>
<h3>{{ currentResource.template?.name }}</h3>
</div>
<div class="resource-reader-container">
<el-form :model="formData" :rules="formRules" ref="formRef" label-position="top">
<el-form-item v-for="param in currentResource?.params" :key="param.name"
:label="`${param.name}${param.required ? '*' : ''}`" :prop="param.name" :rules="getParamRules(param)">
:label="param.name" :prop="param.name">
<!-- 根据不同类型渲染不同输入组件 -->
<el-input v-if="param.type === 'string'" v-model="formData[param.name]"
:placeholder="param.description || `请输入${param.name}`" />
:placeholder="param.placeholder || `请输入${param.name}`" />
<el-input-number v-else-if="param.type === 'number'" v-model="formData[param.name]"
:placeholder="param.description || `请输入${param.name}`" />
:placeholder="param.placeholder || `请输入${param.name}`" />
<el-switch v-else-if="param.type === 'boolean'" v-model="formData[param.name]" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="handleSubmit">
submit
{{ t('read-resource') }}
</el-button>
<el-button @click="resetForm">
reset
{{ t('reset') }}
</el-button>
</el-form-item>
</el-form>
@ -27,14 +30,17 @@
<script setup lang="ts">
import { defineComponent, defineProps, watch, ref, computed } from 'vue';
import { ElMessage } from 'element-plus';
import { useI18n } from 'vue-i18n';
import type { FormInstance, FormRules } from 'element-plus';
import { tabs } from '../panel';
import { ResourceStorage } from './resources';
import { ResourcesReadResponse } from '@/hook/type';
import { parseResourceTemplate, resourcesManager, ResourceStorage } from './resources';
import { CasualRestAPI, ResourcesReadResponse } from '@/hook/type';
import { useMessageBridge } from '@/api/message-bridge';
defineComponent({ name: 'resource-reader' });
const { t } = useI18n();
const props = defineProps({
tabId: {
type: Number,
@ -51,8 +57,24 @@ const formData = ref<Record<string, any>>({});
const loading = ref(false);
const responseData = ref<ResourcesReadResponse>();
//
const currentResource = computed(() => props.resource);
// resource
const currentResource = computed(() => {
const template = resourcesManager.templates.find(template => template.name === tabStorage.currentResourceName);
const { params, fill } = parseResourceTemplate(template?.uriTemplate || '');
const viewParams = params.map(param => ({
name: param,
type: 'string',
placeholder: t('enter') +' ' + param,
required: true
}));
return {
template,
params: viewParams,
fill
};
});
//
const formRules = computed<FormRules>(() => {
@ -60,7 +82,6 @@ const formRules = computed<FormRules>(() => {
currentResource.value?.params.forEach(param => {
rules[param.name] = [
{
required: param.required,
message: `${param.name} 是必填字段`,
trigger: 'blur'
}
@ -68,30 +89,8 @@ const formRules = computed<FormRules>(() => {
});
return rules;
})
});
//
const getParamRules = (param: ResourceProtocol['params'][0]) => {
const rules: FormRules[number] = []
if (param.required) {
rules.push({
required: true,
message: `${param.name}是必填字段`,
trigger: 'blur'
})
}
if (param.type === 'number') {
rules.push({
type: 'number',
message: `${param.name}必须是数字`,
trigger: 'blur'
})
}
return rules
}
//
const initFormData = () => {
@ -109,8 +108,20 @@ const resetForm = () => {
}
//
const handleSubmit = async () => {
console.log('submit');
function handleSubmit() {
const fillFn = currentResource.value.fill;
const uri = fillFn(formData.value);
const bridge = useMessageBridge();
bridge.addCommandListener('resources/read', (data: CasualRestAPI<ResourcesReadResponse>) => {
tabStorage.lastResourceReadResponse = data.msg;
}, { once: true });
bridge.postMessage({
command: 'resources/read',
data: { resourceUri: uri }
});
}
//
@ -121,4 +132,11 @@ watch(() => tabStorage.currentResourceName, () => {
</script>
<style></style>
<style>
.resource-reader-container {
background-color: var(--background);
padding: 10px 12px;
border-radius: .5em;
margin-bottom: 15px;
}
</style>

View File

@ -0,0 +1,61 @@
<template>
<div class="resource-logger">
<span>{{ "Response" }}</span>
<el-scrollbar height="300px">
<div
class="output-content"
contenteditable="false"
>
<span v-for="(content, index) of tabStorage.lastResourceReadResponse?.contents || []" :key="index">
{{ content.text }}
</span>
</div>
</el-scrollbar>
</div>
</template>
<script setup lang="ts">
import { defineComponent, defineProps } from 'vue';
import { tabs } from '../panel';
import { ResourceStorage } from './resources';
defineComponent({ name: 'resource-logger' });
const props = defineProps({
tabId: {
type: Number,
required: true
}
});
const tab = tabs.content[props.tabId];
const tabStorage = tab.storage as ResourceStorage;
</script>
<style>
.resource-logger {
border-radius: .5em;
background-color: var(--background);
padding: 10px;
}
.resource-logger .output-content {
border-radius: .5em;
padding: 15px;
min-height: 300px;
height: fit-content;
font-family: var(--code-font-family);
white-space: pre-wrap;
word-break: break-all;
user-select: text;
cursor: text;
font-size: 15px;
line-height: 1.5;
background-color: var(--sidebar);
}
</style>

View File

@ -25,7 +25,6 @@ import { resourcesManager, ResourceStorage } from './resources';
import { tabs } from '../panel';
const bridge = useMessageBridge();
let cancelListener: undefined | (() => void) = undefined;
const props = defineProps({
tabId: {
@ -45,24 +44,24 @@ function reloadResources() {
function handleClick(template: ResourceTemplate) {
tabStorage.currentResourceName = template.name;
// TODO:
tabStorage.lastResourceReadResponse = undefined;
}
onMounted(() => {
cancelListener = bridge.addCommandListener('resources/templates/list', (data: CasualRestAPI<ResourceTemplatesListResponse>) => {
bridge.addCommandListener('resources/templates/list', (data: CasualRestAPI<ResourceTemplatesListResponse>) => {
resourcesManager.templates = data.msg.resourceTemplates || [];
if (resourcesManager.templates.length > 0) {
tabStorage.currentResourceName = resourcesManager.templates[0].name;
// TODO:
tabStorage.lastResourceReadResponse = undefined;
}
});
}, { once: true });
reloadResources();
});
onUnmounted(() => {
if (cancelListener) {
cancelListener();
}
});
</script>

View File

@ -1,15 +1,61 @@
import { ResourceTemplate, ResourceTemplatesListResponse } from '@/hook/type';
import { ResourcesReadResponse, ResourceTemplate, ResourceTemplatesListResponse } from '@/hook/type';
import { reactive } from 'vue';
export const resourcesManager = reactive<{
current: ResourceTemplate | undefined
templates: ResourceTemplate[]
current: ResourceTemplate | undefined
templates: ResourceTemplate[]
}>({
current: undefined,
templates: []
current: undefined,
templates: []
});
export interface ResourceStorage {
currentResourceName: string;
lastResourceReadResponse?: ResourcesReadResponse;
}
/**
* @description
* @param template "greeting://{name}"
* @returns { params: 参数名数组, fill: 填充函数 }
*/
export function parseResourceTemplate(template: string): {
params: string[],
fill: (params: Record<string, string>) => string
} {
// 1. 提取所有参数名
const paramRegex = /\{([^}]+)\}/g;
const params = new Set<string>();
let match;
while ((match = paramRegex.exec(template)) !== null) {
params.add(match[1]);
}
const paramList = Array.from(params);
// 2. 创建填充函数
const fill = (values: Record<string, string>): string => {
let result = template;
// 验证所有必填参数
for (const param of paramList) {
if (values[param] === undefined) {
throw new Error(`缺少必要参数: ${param}`);
}
}
// 替换所有参数
for (const param of paramList) {
result = result.replace(new RegExp(`\\{${param}\\}`, 'g'), values[param]);
}
return result;
};
return {
params: paramList,
fill
};
}

View File

@ -105,5 +105,8 @@
"command": "أمر",
"env-var": "متغيرات البيئة",
"log": "سجلات",
"warning.click-to-connect": "يرجى النقر أولاً على $1 على اليسار للاتصال"
"warning.click-to-connect": "يرجى النقر أولاً على $1 على اليسار للاتصال",
"reset": "إعادة تعيين",
"read-resource": "قراءة الموارد",
"enter": "إدخال"
}

View File

@ -105,5 +105,8 @@
"command": "Befehl",
"env-var": "Umgebungsvariablen",
"log": "Protokolle",
"warning.click-to-connect": "Bitte klicken Sie zuerst auf $1 links, um eine Verbindung herzustellen"
"warning.click-to-connect": "Bitte klicken Sie zuerst auf $1 links, um eine Verbindung herzustellen",
"reset": "Zurücksetzen",
"read-resource": "Ressourcen lesen",
"enter": "Eingabe"
}

View File

@ -105,5 +105,8 @@
"command": "Command",
"env-var": "Environment variables",
"log": "Logs",
"warning.click-to-connect": "Please first click on $1 on the left to connect"
"warning.click-to-connect": "Please first click on $1 on the left to connect",
"reset": "Reset",
"read-resource": "Read resources",
"enter": "Input"
}

View File

@ -105,5 +105,8 @@
"command": "Commande",
"env-var": "Variables d'environnement",
"log": "Journaux",
"warning.click-to-connect": "Veuillez d'abord cliquer sur $1 à gauche pour vous connecter"
"warning.click-to-connect": "Veuillez d'abord cliquer sur $1 à gauche pour vous connecter",
"reset": "Réinitialiser",
"read-resource": "Lire les ressources",
"enter": "Entrée"
}

View File

@ -105,5 +105,8 @@
"command": "コマンド",
"env-var": "環境変数",
"log": "ログ",
"warning.click-to-connect": "まず左側の$1をクリックして接続してください"
"warning.click-to-connect": "まず左側の$1をクリックして接続してください",
"reset": "リセット",
"read-resource": "リソースを読み込む",
"enter": "入力"
}

View File

@ -105,5 +105,8 @@
"command": "명령",
"env-var": "환경 변수",
"log": "로그",
"warning.click-to-connect": "먼저 왼쪽의 $1을 클릭하여 연결하십시오"
"warning.click-to-connect": "먼저 왼쪽의 $1을 클릭하여 연결하십시오",
"reset": "재설정",
"read-resource": "리소스 읽기",
"enter": "입력"
}

View File

@ -105,5 +105,8 @@
"command": "Команда",
"env-var": "Переменные среды",
"log": "Логи",
"warning.click-to-connect": "Пожалуйста, сначала нажмите на $1 слева для подключения"
"warning.click-to-connect": "Пожалуйста, сначала нажмите на $1 слева для подключения",
"reset": "Сброс",
"read-resource": "Чтение ресурсов",
"enter": "Ввод"
}

View File

@ -105,5 +105,8 @@
"command": "命令",
"env-var": "环境变量",
"log": "日志",
"warning.click-to-connect": "请先点击左侧的 $1 进行连接"
"warning.click-to-connect": "请先点击左侧的 $1 进行连接",
"reset": "重置",
"read-resource": "读取资源",
"enter": "输入"
}

View File

@ -105,5 +105,8 @@
"command": "命令",
"env-var": "環境變數",
"log": "日誌",
"warning.click-to-connect": "請先點擊左側的 $1 進行連接"
"warning.click-to-connect": "請先點擊左側的 $1 進行連接",
"reset": "重置",
"read-resource": "讀取資源",
"enter": "輸入"
}

View File

@ -24,7 +24,6 @@ const { t } = useI18n();
</script>
<style>
.connection-option .output-content {
border-radius: .5em;
padding: 15px;

View File

@ -52,7 +52,7 @@ bridge.addCommandListener('connect', data => {
const { code, msg } = data;
connectionResult.success = (code === 200);
connectionResult.logString = msg;
});
}, { once: false });
</script>