重构完成基础设施

This commit is contained in:
锦恢 2025-05-21 16:55:51 +08:00
parent 86c218ab5e
commit 4017fc3290
12 changed files with 352 additions and 386 deletions

View File

@ -70,7 +70,10 @@ export function createTab(type: string, index: number): Tab {
id, id,
componentIndex: -1, componentIndex: -1,
component: undefined, component: undefined,
storage: {}, storage: {
// 默认打开一个 mcp server 的面板
activeNames: [0]
},
}; };
} }

View File

@ -1,22 +1,15 @@
<template> <template>
<div> <div>
<h3>{{ currentPrompt.name }}</h3> <h3>{{ currentPrompt?.name }}</h3>
</div> </div>
<div class="prompt-reader-container"> <div class="prompt-reader-container">
<el-form :model="tabStorage.formData" :rules="formRules" ref="formRef" label-position="top"> <el-form :model="tabStorage.formData" :rules="formRules" ref="formRef" label-position="top">
<el-form-item v-for="param in currentPrompt?.params" :key="param.name" <el-form-item v-for="param in currentPrompt?.arguments" :key="param.name"
:label="param.name" :prop="param.name"> :label="param.name" :prop="param.name">
<el-input v-if="param.type === 'string'" v-model="tabStorage.formData[param.name]" <el-input v-model="tabStorage.formData[param.name]"
:placeholder="param.placeholder || `请输入${param.name}`" :placeholder="t('enter') +' ' + param.name"
@keydown.enter.prevent="handleSubmit" @keydown.enter.prevent="handleSubmit"
/> />
<el-input-number v-else-if="param.type === 'number'" v-model="tabStorage.formData[param.name]"
:placeholder="param.placeholder || `请输入${param.name}`"
@keydown.enter.prevent="handleSubmit"
/>
<el-switch v-else-if="param.type === 'boolean'" v-model="tabStorage.formData[param.name]" />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
@ -36,10 +29,8 @@ import { defineComponent, defineProps, defineEmits, watch, ref, computed, reacti
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import type { FormInstance, FormRules } from 'element-plus'; import type { FormInstance, FormRules } from 'element-plus';
import { tabs } from '../panel'; import { tabs } from '../panel';
import { promptsManager, type PromptStorage } from './prompts'; import type { PromptStorage } from './prompts';
import type { PromptsGetResponse } from '@/hook/type'; import type { PromptsGetResponse } from '@/hook/type';
import { useMessageBridge } from '@/api/message-bridge';
import { getDefaultValue, normaliseJavascriptType } from '@/hook/mcp';
import { mcpClientAdapter } from '@/views/connect/core'; import { mcpClientAdapter } from '@/views/connect/core';
defineComponent({ name: 'prompt-reader' }); defineComponent({ name: 'prompt-reader' });
@ -65,6 +56,7 @@ if (props.tabId >= 0) {
tabStorage = tabs.content[props.tabId].storage as PromptStorage; tabStorage = tabs.content[props.tabId].storage as PromptStorage;
} else { } else {
tabStorage = reactive({ tabStorage = reactive({
activeNames: [0],
currentPromptName: props.currentPromptName || '', currentPromptName: props.currentPromptName || '',
formData: {}, formData: {},
lastPromptGetResponse: undefined lastPromptGetResponse: undefined
@ -80,26 +72,18 @@ const loading = ref(false);
const responseData = ref<PromptsGetResponse>(); const responseData = ref<PromptsGetResponse>();
const currentPrompt = computed(() => { const currentPrompt = computed(() => {
const template = promptsManager.templates.find(template => template.name === tabStorage.currentPromptName);
const name = template?.name || '';
const params = template?.arguments || [];
const viewParams = params.map(param => ({ for (const client of mcpClientAdapter.clients) {
name: param.name, const prompt = client.promptTemplates?.get(tabStorage.currentPromptName);
type: 'string', if (prompt) {
placeholder: t('enter') +' ' + param.name, return prompt;
required: param.required }
})); }
return {
name,
params: viewParams
};
}); });
const formRules = computed<FormRules>(() => { const formRules = computed<FormRules>(() => {
const rules: FormRules = {} const rules: FormRules = {}
currentPrompt.value?.params.forEach(param => { currentPrompt.value?.arguments.forEach(param => {
rules[param.name] = [ rules[param.name] = [
{ {
message: `${param.name} 是必填字段`, message: `${param.name} 是必填字段`,
@ -112,15 +96,12 @@ const formRules = computed<FormRules>(() => {
}); });
const initFormData = () => { const initFormData = () => {
if (!currentPrompt.value?.arguments) return;
if (!currentPrompt.value?.params) return;
const newSchemaDataForm: Record<string, number | boolean | string> = {}; const newSchemaDataForm: Record<string, number | boolean | string> = {};
currentPrompt.value.params.forEach(param => { currentPrompt.value.arguments.forEach(param => {
newSchemaDataForm[param.name] = getDefaultValue(param); newSchemaDataForm[param.name] = '';
const originType = normaliseJavascriptType(typeof tabStorage.formData[param.name]); if (tabStorage.formData[param.name]!== undefined) {
if (tabStorage.formData[param.name]!== undefined && originType === param.type) {
newSchemaDataForm[param.name] = tabStorage.formData[param.name]; newSchemaDataForm[param.name] = tabStorage.formData[param.name];
} }
}); });
@ -134,7 +115,7 @@ const resetForm = () => {
async function handleSubmit() { async function handleSubmit() {
const res = await mcpClientAdapter.readPromptTemplate( const res = await mcpClientAdapter.readPromptTemplate(
currentPrompt.value.name, currentPrompt.value?.name || '',
JSON.parse(JSON.stringify(tabStorage.formData)) JSON.parse(JSON.stringify(tabStorage.formData))
); );

View File

@ -1,50 +1,54 @@
<template> <template>
<h3 class="resource-template"> <el-collapse :expand-icon-position="'left'" v-model="tabStorage.activeNames">
<code>prompts/list</code> <el-collapse-item v-for="(client, index) in mcpClientAdapter.clients" :name="index" :class="[]">
<span
@click="reloadPrompts({ first: false })"
class="iconfont icon-restart"
></span>
</h3>
<div class="prompt-template-container-scrollbar"> <!-- header -->
<el-scrollbar height="500px"> <template #title>
<div class="prompt-template-container"> <h3 class="resource-template">
<div <code>prompts/list</code>
class="item" <span @click.stop="reloadPrompts(client, { first: false })" class="iconfont icon-restart"></span>
:class="{ 'active': props.tabId >= 0 && tabStorage.currentPromptName === template.name }" </h3>
v-for="template of promptsManager.templates"
:key="template.name" </template>
@click="handleClick(template)"
> <!-- body -->
<span>{{ template.name }}</span>
<span>{{ template.description || '' }}</span> <div class="prompt-template-container-scrollbar">
</div> <el-scrollbar height="500px">
<div class="prompt-template-container">
<div class="item"
:class="{ 'active': props.tabId >= 0 && tabStorage.currentPromptName === template.name }"
v-for="template of client.promptTemplates?.values()" :key="template.name"
@click="handleClick(template)">
<span>{{ template.name }}</span>
<span>{{ template.description || '' }}</span>
</div>
</div>
</el-scrollbar>
</div> </div>
</el-scrollbar> </el-collapse-item>
</div> </el-collapse>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useMessageBridge } from '@/api/message-bridge'; import type { PromptTemplate } from '@/hook/type';
import type { CasualRestAPI, PromptTemplate, PromptsListResponse } from '@/hook/type'; import { onMounted, defineProps, defineEmits, reactive, ref, type Reactive } from 'vue';
import { onMounted, onUnmounted, defineProps, defineEmits, reactive } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { promptsManager, type PromptStorage } from './prompts'; import type { PromptStorage } from './prompts';
import { tabs } from '../panel'; import { tabs } from '../panel';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { McpClient, mcpClientAdapter } from '@/views/connect/core';
const bridge = useMessageBridge();
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps({ const props = defineProps({
tabId: { tabId: {
type: Number, type: Number,
required: true required: true
} }
}); });
const emits = defineEmits([ 'prompt-selected' ]); const emits = defineEmits(['prompt-selected']);
let tabStorage: PromptStorage; let tabStorage: PromptStorage;
@ -53,63 +57,51 @@ if (props.tabId >= 0) {
tabStorage = tab.storage as PromptStorage; tabStorage = tab.storage as PromptStorage;
} else { } else {
tabStorage = reactive({ tabStorage = reactive({
activeNames: [0],
currentPromptName: '', currentPromptName: '',
formData: {}, formData: {},
lastPromptGetResponse: undefined lastPromptGetResponse: undefined
}); });
} }
function reloadPrompts(option: { first: boolean }) { async function reloadPrompts(client: Reactive<McpClient>, option: { first: boolean }) {
bridge.postMessage({ await client.getPromptTemplates({ cache: false });
command: 'prompts/list'
});
if (!option.first) { if (!option.first) {
ElMessage({ ElMessage({
message: t('finish-refresh'), message: t('finish-refresh'),
type: 'success', type: 'success',
duration: 3000, duration: 3000,
showClose: true, showClose: true,
}); });
} }
} }
function handleClick(prompt: PromptTemplate) { function handleClick(prompt: PromptTemplate) {
tabStorage.currentPromptName = prompt.name; tabStorage.currentPromptName = prompt.name;
tabStorage.lastPromptGetResponse = undefined; tabStorage.lastPromptGetResponse = undefined;
emits('prompt-selected', prompt);
emits('prompt-selected', prompt);
} }
let commandCancel: (() => void); onMounted(async () => {
for (const client of mcpClientAdapter.clients) {
await client.getPromptTemplates();
}
onMounted(() => { if (tabStorage.currentPromptName === undefined) {
commandCancel = bridge.addCommandListener('prompts/list', (data: CasualRestAPI<PromptsListResponse>) => { const masterNode = mcpClientAdapter.masterNode;
promptsManager.templates = data.msg.prompts || []; const prompt = masterNode.promptTemplates?.values().next();
tabStorage.currentPromptName = prompt?.value?.name || '';
const targetPrompt = promptsManager.templates.find(template => template.name === tabStorage.currentPromptName); }
if (targetPrompt === undefined) {
tabStorage.currentPromptName = promptsManager.templates[0].name;
tabStorage.lastPromptGetResponse = undefined;
}
}, { once: false });
reloadPrompts({ first: true });
}); });
onUnmounted(() => {
if (commandCancel){
commandCancel();
}
})
</script> </script>
<style> <style>
.prompt-template-container-scrollbar { .prompt-template-container-scrollbar {
background-color: var(--background); background-color: var(--background);
margin-bottom: 10px; margin-bottom: 10px;
border-radius: .5em; border-radius: .5em;
} }
@ -121,17 +113,17 @@ onUnmounted(() => {
} }
.prompt-template-function-container { .prompt-template-function-container {
width: 100%; width: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.prompt-template-function-container button { .prompt-template-function-container button {
width: 175px; width: 175px;
} }
.prompt-template-container > .item { .prompt-template-container>.item {
margin: 3px; margin: 3px;
padding: 5px 10px; padding: 5px 10px;
border-radius: .3em; border-radius: .3em;
@ -143,24 +135,24 @@ onUnmounted(() => {
transition: var(--animation-3s); transition: var(--animation-3s);
} }
.prompt-template-container > .item:hover { .prompt-template-container>.item:hover {
background-color: var(--main-light-color); background-color: var(--main-light-color);
transition: var(--animation-3s); transition: var(--animation-3s);
} }
.prompt-template-container > .item.active { .prompt-template-container>.item.active {
background-color: var(--main-light-color); background-color: var(--main-light-color);
transition: var(--animation-3s); transition: var(--animation-3s);
} }
.prompt-template-container > .item > span:first-child { .prompt-template-container>.item>span:first-child {
max-width: 200px; max-width: 200px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.prompt-template-container > .item > span:last-child { .prompt-template-container>.item>span:last-child {
opacity: 0.6; opacity: 0.6;
font-size: 12.5px; font-size: 12.5px;
max-width: 200px; max-width: 200px;

View File

@ -1,15 +1,7 @@
import type { PromptsGetResponse, PromptTemplate } from '@/hook/type'; import type { PromptsGetResponse } from '@/hook/type';
import { reactive } from 'vue';
export const promptsManager = reactive<{
current: PromptTemplate | undefined
templates: PromptTemplate[]
}>({
current: undefined,
templates: []
});
export interface PromptStorage { export interface PromptStorage {
activeNames: any[];
currentPromptName: string; currentPromptName: string;
lastPromptGetResponse?: PromptsGetResponse; lastPromptGetResponse?: PromptsGetResponse;
formData: Record<string, any>; formData: Record<string, any>;

View File

@ -1,19 +1,15 @@
<template> <template>
<div> <div>
<h3>{{ currentResource.template?.name }}</h3> <h3>{{ currentResource?.name }}</h3>
</div> </div>
<div class="resource-reader-container"> <div class="resource-reader-container">
<el-form :model="tabStorage.formData" :rules="formRules" ref="formRef" label-position="top"> <el-form :model="tabStorage.formData" :rules="formRules" ref="formRef" label-position="top">
<el-form-item v-for="param in currentResource?.params" :key="param.name" :label="param.name" <el-form-item v-for="param in currentResource?.params" :key="param" :label="param"
:prop="param.name"> :prop="param">
<!-- 根据不同类型渲染不同输入组件 --> <el-input v-model="tabStorage.formData[param]"
<el-input v-if="param.type === 'string'" v-model="tabStorage.formData[param.name]" :placeholder="t('enter') + ' ' + param"
:placeholder="param.placeholder || `请输入${param.name}`" @keydown.enter.prevent="handleSubmit" /> @keydown.enter.prevent="handleSubmit"
/>
<el-input-number v-else-if="param.type === 'number'" v-model="tabStorage.formData[param.name]"
:placeholder="param.placeholder || `请输入${param.name}`" @keydown.enter.prevent="handleSubmit" />
<el-switch v-else-if="param.type === 'boolean'" v-model="tabStorage.formData[param.name]" />
</el-form-item> </el-form-item>
<el-form-item v-if="tabStorage.currentType === 'template'"> <el-form-item v-if="tabStorage.currentType === 'template'">
@ -38,9 +34,8 @@ import { defineComponent, defineProps, watch, ref, computed, reactive, defineEmi
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import type { FormInstance, FormRules } from 'element-plus'; import type { FormInstance, FormRules } from 'element-plus';
import { tabs } from '../panel'; import { tabs } from '../panel';
import { parseResourceTemplate, resourcesManager, type ResourceStorage } from './resources'; import { parseResourceTemplate, type ResourceStorage } from './resources';
import type{ ResourcesReadResponse } from '@/hook/type'; import type{ ResourcesReadResponse } from '@/hook/type';
import { useMessageBridge } from '@/api/message-bridge';
import { getDefaultValue, normaliseJavascriptType } from '@/hook/mcp'; import { getDefaultValue, normaliseJavascriptType } from '@/hook/mcp';
import { mcpClientAdapter } from '@/views/connect/core'; import { mcpClientAdapter } from '@/views/connect/core';
@ -68,6 +63,8 @@ if (props.tabId >= 0) {
tabStorage = tab.storage as ResourceStorage; tabStorage = tab.storage as ResourceStorage;
} else { } else {
tabStorage = reactive({ tabStorage = reactive({
activeNames: [0],
templateActiveNames: [0],
currentType: 'resource', currentType: 'resource',
currentResourceName: props.currentResourceName || '', currentResourceName: props.currentResourceName || '',
formData: {}, formData: {},
@ -86,30 +83,39 @@ const responseData = ref<ResourcesReadResponse>();
// resource // resource
const currentResource = computed(() => { const currentResource = computed(() => {
const template = resourcesManager.templates.find(template => template.name === tabStorage.currentResourceName);
const { params, fill } = parseResourceTemplate(template?.uriTemplate || '');
const viewParams = params.map(param => ({ for (const client of mcpClientAdapter.clients) {
name: param, const resource = client.resources?.get(tabStorage.currentResourceName);
type: 'string', if (resource) {
placeholder: t('enter') + ' ' + param, return {
required: true name: resource.name,
})); template: resource,
params: [],
// resources fill
fill: () => ''
};
}
return { const resourceTemplate = client.resourceTemplates?.get(tabStorage.currentResourceName);
template, if (resourceTemplate) {
params: viewParams, const { params, fill } = parseResourceTemplate(resourceTemplate.uriTemplate);
fill return {
}; name: resourceTemplate.name,
template: resourceTemplate,
params,
fill
};
}
}
}); });
// //
const formRules = computed<FormRules>(() => { const formRules = computed<FormRules>(() => {
const rules: FormRules = {} const rules: FormRules = {}
currentResource.value?.params.forEach(param => { currentResource.value?.params.forEach(param => {
rules[param.name] = [ rules[param] = [
{ {
message: `${param.name} 是必填字段`, message: `${param} 是必填字段`,
trigger: 'blur' trigger: 'blur'
} }
] ]
@ -121,15 +127,11 @@ const formRules = computed<FormRules>(() => {
// //
const initFormData = () => { const initFormData = () => {
if (!currentResource.value?.params) return; if (!currentResource.value?.params) return;
const newSchemaDataForm: Record<string, number | boolean | string> = {}; const newSchemaDataForm: Record<string, number | boolean | string> = {};
currentResource.value.params.forEach(param => { currentResource.value.params.forEach(param => {
newSchemaDataForm[param.name] = getDefaultValue(param); newSchemaDataForm[param] = '';
const originType = normaliseJavascriptType(typeof tabStorage.formData[param.name]); if (tabStorage.formData[param] !== undefined) {
newSchemaDataForm[param] = tabStorage.formData[param];
if (tabStorage.formData[param.name] !== undefined && originType === param.type) {
newSchemaDataForm[param.name] = tabStorage.formData[param.name];
} }
}) })
} }
@ -142,21 +144,23 @@ const resetForm = () => {
function getUri() { function getUri() {
if (tabStorage.currentType === 'template') { if (tabStorage.currentType === 'template') {
const fillFn = currentResource.value.fill; const fillFn = currentResource.value?.fill || ((str: any) => str);
const uri = fillFn(tabStorage.formData); const uri = fillFn(tabStorage.formData);
return uri; return uri;
} }
const currentResourceName = props.tabId >= 0 ? tabStorage.currentResourceName : props.currentResourceName; for (const client of mcpClientAdapter.clients) {
const targetResource = resourcesManager.resources.find(resources => resources.name === currentResourceName); const resource = client.resources?.get(tabStorage.currentResourceName);
return targetResource?.uri; if (resource) {
return resource.uri;
}
}
} }
// //
async function handleSubmit() { async function handleSubmit() {
const uri = getUri(); const uri = getUri();
const res = await mcpClientAdapter.readResource(uri); const res = await mcpClientAdapter.readResource(uri);
tabStorage.lastResourceReadResponse = res; tabStorage.lastResourceReadResponse = res;
emits('resource-get-response', res); emits('resource-get-response', res);
} }

View File

@ -1,130 +1,125 @@
<template> <template>
<h3 class="resource-template"> <el-collapse :expand-icon-position="'left'" v-model="tabStorage.templateActiveNames">
<code>resources/templates/list</code> <el-collapse-item v-for="(client, index) in mcpClientAdapter.clients" :name="index" :class="[]">
<span
class="iconfont icon-restart"
@click="reloadResources({ first: false })"
></span>
</h3>
<div class="resource-template-container-scrollbar"> <!-- header -->
<el-scrollbar height="500px" v-if="resourcesManager.templates.length > 0"> <template #title>
<div class="resource-template-container"> <h3 class="resource-template">
<div <code>resources/templates/list</code>
class="item" <span class="iconfont icon-restart" @click="reloadResources(client, { first: false })"></span>
:class="{ 'active': props.tabId >= 0 && tabStorage.currentType === 'template' && tabStorage.currentResourceName === template.name }" </h3>
v-for="template of resourcesManager.templates" </template>
:key="template.name"
@click="handleClick(template)" <!-- body -->
> <div class="resource-template-container-scrollbar">
<span>{{ template.name }}</span> <el-scrollbar height="500px" v-if="(client.resourceTemplates?.size || 0) > 0">
<span>{{ template.description || '' }}</span> <div class="resource-template-container">
<div class="item"
:class="{ 'active': props.tabId >= 0 && tabStorage.currentType === 'template' && tabStorage.currentResourceName === template.name }"
v-for="template of client.resourceTemplates?.values()" :key="template.name"
@click="handleClick(template)">
<span>{{ template.name }}</span>
<span>{{ template.description || '' }}</span>
</div>
</div>
</el-scrollbar>
<div v-else style="padding: 10px;">
empty
</div> </div>
</div> </div>
</el-scrollbar> </el-collapse-item>
<div v-else style="padding: 10px;"> </el-collapse>
empty
</div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useMessageBridge } from '@/api/message-bridge'; import { useMessageBridge } from '@/api/message-bridge';
import type { CasualRestAPI, ResourceTemplate, ResourceTemplatesListResponse } from '@/hook/type'; import type { CasualRestAPI, ResourceTemplate, ResourceTemplatesListResponse } from '@/hook/type';
import { onMounted, onUnmounted, defineProps, ref, reactive } from 'vue'; import { onMounted, onUnmounted, defineProps, ref, reactive, type Reactive } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { resourcesManager, type ResourceStorage } from './resources'; import type { ResourceStorage } from './resources';
import { tabs } from '../panel'; import { tabs } from '../panel';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { McpClient, mcpClientAdapter } from '@/views/connect/core';
const bridge = useMessageBridge(); const bridge = useMessageBridge();
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps({ const props = defineProps({
tabId: { tabId: {
type: Number, type: Number,
required: true required: true
} }
}); });
let tabStorage: ResourceStorage; let tabStorage: ResourceStorage;
if (props.tabId >= 0) { if (props.tabId >= 0) {
const tab = tabs.content[props.tabId]; const tab = tabs.content[props.tabId];
tabStorage = tab.storage as ResourceStorage; tabStorage = tab.storage as ResourceStorage;
} else { } else {
tabStorage = reactive({ tabStorage = reactive({
currentType:'template', activeNames: [0],
currentResourceName: '', templateActiveNames: [0],
formData: {}, currentType: 'template',
lastResourceReadResponse: undefined currentResourceName: '',
}); formData: {},
lastResourceReadResponse: undefined
});
} }
function reloadResources(option: { first: boolean }) { async function reloadResources(client: Reactive<McpClient>, option: { first: boolean }) {
bridge.postMessage({
command: 'resources/templates/list'
});
if (!option.first) { await client.getResourceTemplates({ cache: false });
ElMessage({
message: t('finish-refresh'), if (!option.first) {
type: 'success', ElMessage({
message: t('finish-refresh'),
type: 'success',
duration: 3000, duration: 3000,
showClose: true, showClose: true,
}); });
} }
} }
function handleClick(template: ResourceTemplate) { function handleClick(template: ResourceTemplate) {
tabStorage.currentType = 'template'; tabStorage.currentType = 'template';
tabStorage.currentResourceName = template.name; tabStorage.currentResourceName = template.name;
tabStorage.lastResourceReadResponse = undefined; tabStorage.lastResourceReadResponse = undefined;
} }
let commandCancel: (() => void); onMounted(async () => {
for (const client of mcpClientAdapter.clients) {
await client.getResourceTemplates({ cache: false });
}
onMounted(() => { if (tabStorage.currentResourceName === undefined && tabStorage.currentType === 'template') {
commandCancel = bridge.addCommandListener('resources/templates/list', (data: CasualRestAPI<ResourceTemplatesListResponse>) => { const masterNode = mcpClientAdapter.masterNode;
resourcesManager.templates = data.msg.resourceTemplates || []; const resourceTemplate = masterNode?.resourceTemplates?.values().next();
tabStorage.currentResourceName = resourceTemplate?.value?.name || '';
if (tabStorage.currentType === 'template') { }
const targetResource = resourcesManager.templates.find(template => template.name === tabStorage.currentResourceName);
if (targetResource === undefined) {
tabStorage.currentResourceName = resourcesManager.templates[0]?.name;
tabStorage.lastResourceReadResponse = undefined;
}
}
}, { once: false });
reloadResources({ first: true });
}); });
onUnmounted(() => {
if (commandCancel){
commandCancel();
}
})
</script> </script>
<style> <style>
h3.resource-template { h3.resource-template {
display: flex; display: flex;
align-items: center; align-items: center;
} }
h3.resource-template .iconfont.icon-restart { h3.resource-template .iconfont.icon-restart {
margin-left: 10px; margin-left: 10px;
cursor: pointer; cursor: pointer;
} }
h3.resource-template .iconfont.icon-restart:hover { h3.resource-template .iconfont.icon-restart:hover {
color: var(--main-color); color: var(--main-color);
} }
.resource-template-container-scrollbar { .resource-template-container-scrollbar {
background-color: var(--background); background-color: var(--background);
margin-bottom: 10px; margin-bottom: 10px;
border-radius: .5em; border-radius: .5em;
} }
@ -136,17 +131,17 @@ h3.resource-template .iconfont.icon-restart:hover {
} }
.resource-template-function-container { .resource-template-function-container {
width: 100%; width: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.resource-template-function-container button { .resource-template-function-container button {
width: 175px; width: 175px;
} }
.resource-template-container > .item { .resource-template-container>.item {
margin: 3px; margin: 3px;
padding: 5px 10px; padding: 5px 10px;
border-radius: .3em; border-radius: .3em;
@ -158,24 +153,24 @@ h3.resource-template .iconfont.icon-restart:hover {
transition: var(--animation-3s); transition: var(--animation-3s);
} }
.resource-template-container > .item:hover { .resource-template-container>.item:hover {
background-color: var(--main-light-color); background-color: var(--main-light-color);
transition: var(--animation-3s); transition: var(--animation-3s);
} }
.resource-template-container > .item.active { .resource-template-container>.item.active {
background-color: var(--main-light-color); background-color: var(--main-light-color);
transition: var(--animation-3s); transition: var(--animation-3s);
} }
.resource-template-container > .item > span:first-child { .resource-template-container>.item>span:first-child {
max-width: 200px; max-width: 200px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.resource-template-container > .item > span:last-child { .resource-template-container>.item>span:last-child {
opacity: 0.6; opacity: 0.6;
font-size: 12.5px; font-size: 12.5px;
max-width: 200px; max-width: 200px;
@ -183,5 +178,4 @@ h3.resource-template .iconfont.icon-restart:hover {
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
</style> </style>

View File

@ -1,137 +1,127 @@
<template> <template>
<h3 class="resource-template"> <el-collapse :expand-icon-position="'left'" v-model="tabStorage.activeNames">
<code>resources/list</code> <el-collapse-item v-for="(client, index) in mcpClientAdapter.clients" :name="index" :class="[]">
<span
class="iconfont icon-restart"
@click="reloadResources({ first: false })"
></span>
</h3>
<div class="resource-template-container-scrollbar"> <!-- header -->
<el-scrollbar height="500px"> <template #title>
<div class="resource-template-container"> <h3 class="resource-template">
<div <code>resources/list</code>
class="item" <span class="iconfont icon-restart" @click="reloadResources(client, { first: false })"></span>
:class="{ 'active': props.tabId >= 0 && tabStorage.currentType === 'resource' && tabStorage.currentResourceName === resource.name }" </h3>
v-for="resource of resourcesManager.resources" </template>
:key="resource.uri"
@click="handleClick(resource)" <!-- body -->
> <div class="resource-template-container-scrollbar">
<span>{{ resource.name }}</span> <el-scrollbar height="500px">
<span>{{ resource.mimeType }}</span> <div class="resource-template-container">
</div> <div class="item"
:class="{ 'active': props.tabId >= 0 && tabStorage.currentType === 'resource' && tabStorage.currentResourceName === resource.name }"
v-for="resource of client.resources?.values()" :key="resource.uri"
@click="handleClick(resource)">
<span>{{ resource.name }}</span>
<span>{{ resource.mimeType }}</span>
</div>
</div>
</el-scrollbar>
</div> </div>
</el-scrollbar> </el-collapse-item>
</div> </el-collapse>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useMessageBridge } from '@/api/message-bridge'; import type { Resources } from '@/hook/type';
import type { CasualRestAPI, Resources, ResourcesListResponse } from '@/hook/type'; import { onMounted, defineProps, defineEmits, reactive, type Reactive } from 'vue';
import { onMounted, onUnmounted, defineProps, defineEmits, reactive } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { resourcesManager, type ResourceStorage } from './resources'; import type { ResourceStorage } from './resources';
import { tabs } from '../panel'; import { tabs } from '../panel';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { McpClient, mcpClientAdapter } from '@/views/connect/core';
const bridge = useMessageBridge();
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps({ const props = defineProps({
tabId: { tabId: {
type: Number, type: Number,
required: true required: true
} }
}); });
const emits = defineEmits([ 'resource-selected' ]); const emits = defineEmits(['resource-selected']);
let tabStorage: ResourceStorage; let tabStorage: ResourceStorage;
if (props.tabId >= 0) { if (props.tabId >= 0) {
const tab = tabs.content[props.tabId]; const tab = tabs.content[props.tabId];
tabStorage = tab.storage as ResourceStorage; tabStorage = tab.storage as ResourceStorage;
} else { } else {
tabStorage = reactive({ tabStorage = reactive({
currentType:'resource', activeNames: [0],
currentResourceName: '', templateActiveNames: [0],
formData: {}, currentType: 'resource',
lastResourceReadResponse: undefined currentResourceName: '',
}); formData: {},
lastResourceReadResponse: undefined
});
} }
function reloadResources(option: { first: boolean }) { async function reloadResources(client: Reactive<McpClient>, option: { first: boolean }) {
bridge.postMessage({ await client.getResources({ cache: false });
command: 'resources/list'
});
if (!option.first) { if (!option.first) {
ElMessage({ ElMessage({
message: t('finish-refresh'), message: t('finish-refresh'),
type: 'success', type: 'success',
duration: 3000, duration: 3000,
showClose: true, showClose: true,
}); });
} }
} }
async function handleClick(resource: Resources) { async function handleClick(resource: Resources) {
tabStorage.currentType = 'resource'; tabStorage.currentType = 'resource';
tabStorage.currentResourceName = resource.name; tabStorage.currentResourceName = resource.name;
tabStorage.lastResourceReadResponse = undefined; tabStorage.lastResourceReadResponse = undefined;
emits('resource-selected', resource); emits('resource-selected', resource);
// //
if (props.tabId >= 0) { if (props.tabId >= 0) {
const bridge = useMessageBridge(); const res = await mcpClientAdapter.readResource(resource.uri);
const { code, msg } = await bridge.commandRequest('resources/read', { resourceUri: resource.uri }); tabStorage.lastResourceReadResponse = res;
tabStorage.lastResourceReadResponse = msg;
} }
} }
let commandCancel: (() => void); onMounted(async () => {
for (const client of mcpClientAdapter.clients) {
await client.getResources();
}
onMounted(() => { if (tabStorage.currentResourceName === undefined && tabStorage.currentType === 'resource') {
commandCancel = bridge.addCommandListener('resources/list', (data: CasualRestAPI<ResourcesListResponse>) => { const masterNode = mcpClientAdapter.masterNode;
resourcesManager.resources = data.msg.resources || []; const resource = masterNode.resources?.values().next();
tabStorage.currentResourceName = resource?.value?.name || '';
if (tabStorage.currentType === 'resource') { }
const targetResource = resourcesManager.resources.find(resources => resources.name === tabStorage.currentResourceName);
if (targetResource === undefined) {
tabStorage.currentResourceName = resourcesManager.templates[0]?.name;
tabStorage.lastResourceReadResponse = undefined;
}
}
}, { once: false });
reloadResources({ first: true });
}); });
onUnmounted(() => {
if (commandCancel){
commandCancel();
}
})
</script> </script>
<style> <style>
h3.resource-template { h3.resource-template {
display: flex; display: flex;
align-items: center; align-items: center;
} }
h3.resource-template .iconfont.icon-restart { h3.resource-template .iconfont.icon-restart {
margin-left: 10px; margin-left: 10px;
cursor: pointer; cursor: pointer;
} }
h3.resource-template .iconfont.icon-restart:hover { h3.resource-template .iconfont.icon-restart:hover {
color: var(--main-color); color: var(--main-color);
} }
.resource-template-container-scrollbar { .resource-template-container-scrollbar {
background-color: var(--background); background-color: var(--background);
margin-bottom: 10px; margin-bottom: 10px;
border-radius: .5em; border-radius: .5em;
} }
@ -143,17 +133,17 @@ h3.resource-template .iconfont.icon-restart:hover {
} }
.resource-template-function-container { .resource-template-function-container {
width: 100%; width: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.resource-template-function-container button { .resource-template-function-container button {
width: 175px; width: 175px;
} }
.resource-template-container > .item { .resource-template-container>.item {
margin: 3px; margin: 3px;
padding: 5px 10px; padding: 5px 10px;
border-radius: .3em; border-radius: .3em;
@ -165,24 +155,24 @@ h3.resource-template .iconfont.icon-restart:hover {
transition: var(--animation-3s); transition: var(--animation-3s);
} }
.resource-template-container > .item:hover { .resource-template-container>.item:hover {
background-color: var(--main-light-color); background-color: var(--main-light-color);
transition: var(--animation-3s); transition: var(--animation-3s);
} }
.resource-template-container > .item.active { .resource-template-container>.item.active {
background-color: var(--main-light-color); background-color: var(--main-light-color);
transition: var(--animation-3s); transition: var(--animation-3s);
} }
.resource-template-container > .item > span:first-child { .resource-template-container>.item>span:first-child {
max-width: 200px; max-width: 200px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.resource-template-container > .item > span:last-child { .resource-template-container>.item>span:last-child {
opacity: 0.6; opacity: 0.6;
font-size: 12.5px; font-size: 12.5px;
max-width: 200px; max-width: 200px;
@ -190,5 +180,4 @@ h3.resource-template .iconfont.icon-restart:hover {
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
</style> </style>

View File

@ -1,18 +1,8 @@
import type { ResourcesReadResponse, ResourceTemplate, Resources } from '@/hook/type'; import type { ResourcesReadResponse } from '@/hook/type';
import { reactive } from 'vue';
export const resourcesManager = reactive<{
current: ResourceTemplate | undefined
templates: ResourceTemplate[],
resources: Resources[]
}>({
current: undefined,
templates: [],
resources: []
});
export interface ResourceStorage { export interface ResourceStorage {
activeNames: any[];
templateActiveNames: any[];
currentType: 'resource' | 'template'; currentType: 'resource' | 'template';
currentResourceName: string; currentResourceName: string;
lastResourceReadResponse?: ResourcesReadResponse; lastResourceReadResponse?: ResourcesReadResponse;

View File

@ -1,18 +1,16 @@
<template> <template>
<el-collapse :expand-icon-position="'left'" v-model="tabStorage.activeNames">
<el-collapse :expand-icon-position="'left'" v-model="activeNames">
<el-collapse-item v-for="(client, index) in mcpClientAdapter.clients" :name="index" :class="[]"> <el-collapse-item v-for="(client, index) in mcpClientAdapter.clients" :name="index" :class="[]">
<!-- header --> <!-- header -->
<template #title> <template #title>
<h3 class="resource-template"> <h3 class="resource-template">
<code>tools/list</code> <code>tools/list</code>
<span class="iconfont icon-restart" @click="reloadTools({ first: false })"></span> <span class="iconfont icon-restart" @click.stop="reloadTools(client, { first: false })"></span>
</h3> </h3>
</template> </template>
<!-- body --> <!-- body -->
<div class="tool-list-container-scrollbar"> <div class="tool-list-container-scrollbar">
<el-scrollbar height="500px"> <el-scrollbar height="500px">
<div class="tool-list-container"> <div class="tool-list-container">
@ -29,16 +27,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useMessageBridge } from '@/api/message-bridge'; import { onMounted, defineProps, ref, type Reactive } from 'vue';
import type { CasualRestAPI, ToolsListResponse } from '@/hook/type';
import { onMounted, onUnmounted, defineProps, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import type { ToolStorage } from './tools'; import type { ToolStorage } from './tools';
import { tabs } from '../panel'; import { tabs } from '../panel';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { mcpClientAdapter } from '@/views/connect/core'; import { McpClient, mcpClientAdapter } from '@/views/connect/core';
const bridge = useMessageBridge();
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps({ const props = defineProps({
@ -51,12 +46,8 @@ const props = defineProps({
const tab = tabs.content[props.tabId]; const tab = tabs.content[props.tabId];
const tabStorage = tab.storage as ToolStorage; const tabStorage = tab.storage as ToolStorage;
const activeNames = ref<any[]>([0]); async function reloadTools(client: Reactive<McpClient>, option: { first: boolean }) {
await client.getTools({ cache: false });
function reloadTools(option: { first: boolean }) {
bridge.postMessage({
command: 'tools/list'
});
if (!option.first) { if (!option.first) {
ElMessage({ ElMessage({
@ -69,8 +60,6 @@ function reloadTools(option: { first: boolean }) {
} }
function handleClick(tool: { name: string }) { function handleClick(tool: { name: string }) {
console.log('enter');
tabStorage.currentToolName = tool.name; tabStorage.currentToolName = tool.name;
tabStorage.lastToolCallResponse = undefined; tabStorage.lastToolCallResponse = undefined;
} }
@ -79,6 +68,12 @@ onMounted(async () => {
for (const client of mcpClientAdapter.clients) { for (const client of mcpClientAdapter.clients) {
await client.getTools(); await client.getTools();
} }
if (tabStorage.currentToolName === undefined) {
const masterNode = mcpClientAdapter.masterNode;
const tool = masterNode.tools?.values().next();
tabStorage.currentToolName = tool?.value?.name || '';
}
}); });
</script> </script>

View File

@ -4,6 +4,7 @@ import type { ToolsListResponse, ToolCallResponse, CasualRestAPI } from '@/hook/
import { mcpClientAdapter } from '@/views/connect/core'; import { mcpClientAdapter } from '@/views/connect/core';
export interface ToolStorage { export interface ToolStorage {
activeNames: any[];
currentToolName: string; currentToolName: string;
lastToolCallResponse?: ToolCallResponse | string; lastToolCallResponse?: ToolCallResponse | string;
formData: Record<string, any>; formData: Record<string, any>;

View File

@ -1,6 +1,6 @@
import { useMessageBridge } from "@/api/message-bridge"; import { useMessageBridge } from "@/api/message-bridge";
import { reactive, type Reactive } from "vue"; import { reactive, type Reactive } from "vue";
import type { IConnectionResult, ConnectionTypeOptionItem, IConnectionArgs, IConnectionEnvironment, McpOptions } from "./type"; import type { IConnectionResult, ConnectionTypeOptionItem, IConnectionArgs, IConnectionEnvironment, McpOptions, McpClientGetCommonOption } from "./type";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { loadPanels } from "@/hook/panel"; import { loadPanels } from "@/hook/panel";
import { getPlatform } from "@/api/platform"; import { getPlatform } from "@/api/platform";
@ -111,8 +111,13 @@ export class McpClient {
return env; return env;
} }
public async getTools() { public async getTools(option?: McpClientGetCommonOption) {
if (this.tools) {
const {
cache = true
} = option || {};
if (cache && this.tools) {
return this.tools; return this.tools;
} }
@ -131,14 +136,20 @@ export class McpClient {
return this.tools; return this.tools;
} }
public async getPromptTemplates() { public async getPromptTemplates(option?: McpClientGetCommonOption) {
if (this.promptTemplates) {
const {
cache = true
} = option || {};
if (cache && this.promptTemplates) {
return this.promptTemplates; return this.promptTemplates;
} }
const bridge = useMessageBridge(); const bridge = useMessageBridge();
const { code, msg } = await bridge.commandRequest<PromptsListResponse>('prompts/list', { clientId: this.clientId }); const { code, msg } = await bridge.commandRequest<PromptsListResponse>('prompts/list', { clientId: this.clientId });
if (code!== 200) { if (code!== 200) {
return new Map<string, PromptTemplate>(); return new Map<string, PromptTemplate>();
} }
@ -151,8 +162,13 @@ export class McpClient {
return this.promptTemplates; return this.promptTemplates;
} }
public async getResources() { public async getResources(option?: McpClientGetCommonOption) {
if (this.resources) {
const {
cache = true
} = option || {};
if (cache && this.resources) {
return this.resources; return this.resources;
} }
@ -170,8 +186,13 @@ export class McpClient {
return this.resources; return this.resources;
} }
public async getResourceTemplates() { public async getResourceTemplates(option?: McpClientGetCommonOption) {
if (this.resourceTemplates) {
const {
cache = true
} = option || {};
if (cache && this.resourceTemplates) {
return this.resourceTemplates; return this.resourceTemplates;
} }
@ -184,6 +205,7 @@ export class McpClient {
this.resourceTemplates = new Map<string, ResourceTemplate>(); this.resourceTemplates = new Map<string, ResourceTemplate>();
msg.resourceTemplates.forEach(template => { msg.resourceTemplates.forEach(template => {
this.resourceTemplates!.set(template.name, template); this.resourceTemplates!.set(template.name, template);
}); });
return this.resourceTemplates; return this.resourceTemplates;
} }

View File

@ -71,3 +71,6 @@ export interface ConnectionResult {
version: string version: string
} }
export interface McpClientGetCommonOption {
cache: boolean;
}