重构完成基础设施

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,40 +1,44 @@
<template> <template>
<el-collapse :expand-icon-position="'left'" v-model="tabStorage.activeNames">
<el-collapse-item v-for="(client, index) in mcpClientAdapter.clients" :name="index" :class="[]">
<!-- header -->
<template #title>
<h3 class="resource-template"> <h3 class="resource-template">
<code>prompts/list</code> <code>prompts/list</code>
<span <span @click.stop="reloadPrompts(client, { first: false })" class="iconfont icon-restart"></span>
@click="reloadPrompts({ first: false })"
class="iconfont icon-restart"
></span>
</h3> </h3>
</template>
<!-- body -->
<div class="prompt-template-container-scrollbar"> <div class="prompt-template-container-scrollbar">
<el-scrollbar height="500px"> <el-scrollbar height="500px">
<div class="prompt-template-container"> <div class="prompt-template-container">
<div <div class="item"
class="item"
:class="{ 'active': props.tabId >= 0 && tabStorage.currentPromptName === template.name }" :class="{ 'active': props.tabId >= 0 && tabStorage.currentPromptName === template.name }"
v-for="template of promptsManager.templates" v-for="template of client.promptTemplates?.values()" :key="template.name"
:key="template.name" @click="handleClick(template)">
@click="handleClick(template)"
>
<span>{{ template.name }}</span> <span>{{ template.name }}</span>
<span>{{ template.description || '' }}</span> <span>{{ template.description || '' }}</span>
</div> </div>
</div> </div>
</el-scrollbar> </el-scrollbar>
</div> </div>
</el-collapse-item>
</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({
@ -53,16 +57,15 @@ 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({
@ -77,32 +80,21 @@ function reloadPrompts(option: { first: boolean }) {
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) {
onMounted(() => { await client.getPromptTemplates();
commandCancel = bridge.addCommandListener('prompts/list', (data: CasualRestAPI<PromptsListResponse>) => {
promptsManager.templates = data.msg.prompts || [];
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 }); if (tabStorage.currentPromptName === undefined) {
const masterNode = mcpClientAdapter.masterNode;
const prompt = masterNode.promptTemplates?.values().next();
tabStorage.currentPromptName = prompt?.value?.name || '';
}
}); });
onUnmounted(() => {
if (commandCancel){
commandCancel();
}
})
</script> </script>

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 => ({
name: param,
type: 'string',
placeholder: t('enter') + ' ' + param,
required: true
}));
for (const client of mcpClientAdapter.clients) {
const resource = client.resources?.get(tabStorage.currentResourceName);
if (resource) {
return { return {
template, name: resource.name,
params: viewParams, template: resource,
params: [],
// resources fill
fill: () => ''
};
}
const resourceTemplate = client.resourceTemplates?.get(tabStorage.currentResourceName);
if (resourceTemplate) {
const { params, fill } = parseResourceTemplate(resourceTemplate.uriTemplate);
return {
name: resourceTemplate.name,
template: resourceTemplate,
params,
fill 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,22 +1,23 @@
<template> <template>
<el-collapse :expand-icon-position="'left'" v-model="tabStorage.templateActiveNames">
<el-collapse-item v-for="(client, index) in mcpClientAdapter.clients" :name="index" :class="[]">
<!-- header -->
<template #title>
<h3 class="resource-template"> <h3 class="resource-template">
<code>resources/templates/list</code> <code>resources/templates/list</code>
<span <span class="iconfont icon-restart" @click="reloadResources(client, { first: false })"></span>
class="iconfont icon-restart"
@click="reloadResources({ first: false })"
></span>
</h3> </h3>
</template>
<!-- body -->
<div class="resource-template-container-scrollbar"> <div class="resource-template-container-scrollbar">
<el-scrollbar height="500px" v-if="resourcesManager.templates.length > 0"> <el-scrollbar height="500px" v-if="(client.resourceTemplates?.size || 0) > 0">
<div class="resource-template-container"> <div class="resource-template-container">
<div <div class="item"
class="item"
:class="{ 'active': props.tabId >= 0 && tabStorage.currentType === 'template' && tabStorage.currentResourceName === template.name }" :class="{ 'active': props.tabId >= 0 && tabStorage.currentType === 'template' && tabStorage.currentResourceName === template.name }"
v-for="template of resourcesManager.templates" v-for="template of client.resourceTemplates?.values()" :key="template.name"
:key="template.name" @click="handleClick(template)">
@click="handleClick(template)"
>
<span>{{ template.name }}</span> <span>{{ template.name }}</span>
<span>{{ template.description || '' }}</span> <span>{{ template.description || '' }}</span>
</div> </div>
@ -26,16 +27,20 @@
empty empty
</div> </div>
</div> </div>
</el-collapse-item>
</el-collapse>
</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();
@ -54,6 +59,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: 'template', currentType: 'template',
currentResourceName: '', currentResourceName: '',
formData: {}, formData: {},
@ -61,10 +68,9 @@ if (props.tabId >= 0) {
}); });
} }
function reloadResources(option: { first: boolean }) { async function reloadResources(client: Reactive<McpClient>, option: { first: boolean }) {
bridge.postMessage({
command: 'resources/templates/list' await client.getResourceTemplates({ cache: false });
});
if (!option.first) { if (!option.first) {
ElMessage({ ElMessage({
@ -82,29 +88,18 @@ function handleClick(template: ResourceTemplate) {
tabStorage.lastResourceReadResponse = undefined; tabStorage.lastResourceReadResponse = undefined;
} }
let commandCancel: (() => void); onMounted(async () => {
for (const client of mcpClientAdapter.clients) {
onMounted(() => { await client.getResourceTemplates({ cache: false });
commandCancel = bridge.addCommandListener('resources/templates/list', (data: CasualRestAPI<ResourceTemplatesListResponse>) => {
resourcesManager.templates = data.msg.resourceTemplates || [];
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 }); if (tabStorage.currentResourceName === undefined && tabStorage.currentType === 'template') {
const masterNode = mcpClientAdapter.masterNode;
const resourceTemplate = masterNode?.resourceTemplates?.values().next();
tabStorage.currentResourceName = resourceTemplate?.value?.name || '';
}
}); });
onUnmounted(() => {
if (commandCancel){
commandCancel();
}
})
</script> </script>
<style> <style>
@ -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,40 +1,42 @@
<template> <template>
<el-collapse :expand-icon-position="'left'" v-model="tabStorage.activeNames">
<el-collapse-item v-for="(client, index) in mcpClientAdapter.clients" :name="index" :class="[]">
<!-- header -->
<template #title>
<h3 class="resource-template"> <h3 class="resource-template">
<code>resources/list</code> <code>resources/list</code>
<span <span class="iconfont icon-restart" @click="reloadResources(client, { first: false })"></span>
class="iconfont icon-restart"
@click="reloadResources({ first: false })"
></span>
</h3> </h3>
</template>
<!-- body -->
<div class="resource-template-container-scrollbar"> <div class="resource-template-container-scrollbar">
<el-scrollbar height="500px"> <el-scrollbar height="500px">
<div class="resource-template-container"> <div class="resource-template-container">
<div <div class="item"
class="item"
:class="{ 'active': props.tabId >= 0 && tabStorage.currentType === 'resource' && tabStorage.currentResourceName === resource.name }" :class="{ 'active': props.tabId >= 0 && tabStorage.currentType === 'resource' && tabStorage.currentResourceName === resource.name }"
v-for="resource of resourcesManager.resources" v-for="resource of client.resources?.values()" :key="resource.uri"
:key="resource.uri" @click="handleClick(resource)">
@click="handleClick(resource)"
>
<span>{{ resource.name }}</span> <span>{{ resource.name }}</span>
<span>{{ resource.mimeType }}</span> <span>{{ resource.mimeType }}</span>
</div> </div>
</div> </div>
</el-scrollbar> </el-scrollbar>
</div> </div>
</el-collapse-item>
</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({
@ -53,6 +55,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: '', currentResourceName: '',
formData: {}, formData: {},
@ -60,10 +64,8 @@ if (props.tabId >= 0) {
}); });
} }
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({
@ -83,35 +85,23 @@ async function handleClick(resource: Resources) {
// //
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) {
onMounted(() => { await client.getResources();
commandCancel = bridge.addCommandListener('resources/list', (data: CasualRestAPI<ResourcesListResponse>) => {
resourcesManager.resources = data.msg.resources || [];
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 }); if (tabStorage.currentResourceName === undefined && tabStorage.currentType === 'resource') {
const masterNode = mcpClientAdapter.masterNode;
const resource = masterNode.resources?.values().next();
tabStorage.currentResourceName = resource?.value?.name || '';
}
}); });
onUnmounted(() => {
if (commandCancel){
commandCancel();
}
})
</script> </script>
<style> <style>
@ -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;
}