重构完成基础设施

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,
componentIndex: -1,
component: undefined,
storage: {},
storage: {
// 默认打开一个 mcp server 的面板
activeNames: [0]
},
};
}

View File

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

View File

@ -1,40 +1,44 @@
<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">
<code>prompts/list</code>
<span
@click="reloadPrompts({ first: false })"
class="iconfont icon-restart"
></span>
<span @click.stop="reloadPrompts(client, { first: false })" class="iconfont icon-restart"></span>
</h3>
</template>
<!-- body -->
<div class="prompt-template-container-scrollbar">
<el-scrollbar height="500px">
<div class="prompt-template-container">
<div
class="item"
<div class="item"
:class="{ 'active': props.tabId >= 0 && tabStorage.currentPromptName === template.name }"
v-for="template of promptsManager.templates"
:key="template.name"
@click="handleClick(template)"
>
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>
</el-collapse-item>
</el-collapse>
</template>
<script setup lang="ts">
import { useMessageBridge } from '@/api/message-bridge';
import type { CasualRestAPI, PromptTemplate, PromptsListResponse } from '@/hook/type';
import { onMounted, onUnmounted, defineProps, defineEmits, reactive } from 'vue';
import type { PromptTemplate } from '@/hook/type';
import { onMounted, defineProps, defineEmits, reactive, ref, type Reactive } from 'vue';
import { useI18n } from 'vue-i18n';
import { promptsManager, type PromptStorage } from './prompts';
import type { PromptStorage } from './prompts';
import { tabs } from '../panel';
import { ElMessage } from 'element-plus';
import { McpClient, mcpClientAdapter } from '@/views/connect/core';
const bridge = useMessageBridge();
const { t } = useI18n();
const props = defineProps({
@ -44,7 +48,7 @@ const props = defineProps({
}
});
const emits = defineEmits([ 'prompt-selected' ]);
const emits = defineEmits(['prompt-selected']);
let tabStorage: PromptStorage;
@ -53,16 +57,15 @@ if (props.tabId >= 0) {
tabStorage = tab.storage as PromptStorage;
} else {
tabStorage = reactive({
activeNames: [0],
currentPromptName: '',
formData: {},
lastPromptGetResponse: undefined
});
}
function reloadPrompts(option: { first: boolean }) {
bridge.postMessage({
command: 'prompts/list'
});
async function reloadPrompts(client: Reactive<McpClient>, option: { first: boolean }) {
await client.getPromptTemplates({ cache: false });
if (!option.first) {
ElMessage({
@ -77,32 +80,21 @@ function reloadPrompts(option: { first: boolean }) {
function handleClick(prompt: PromptTemplate) {
tabStorage.currentPromptName = prompt.name;
tabStorage.lastPromptGetResponse = undefined;
emits('prompt-selected', prompt);
}
let commandCancel: (() => void);
onMounted(() => {
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;
onMounted(async () => {
for (const client of mcpClientAdapter.clients) {
await client.getPromptTemplates();
}
}, { 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>
@ -131,7 +123,7 @@ onUnmounted(() => {
width: 175px;
}
.prompt-template-container > .item {
.prompt-template-container>.item {
margin: 3px;
padding: 5px 10px;
border-radius: .3em;
@ -143,24 +135,24 @@ onUnmounted(() => {
transition: var(--animation-3s);
}
.prompt-template-container > .item:hover {
.prompt-template-container>.item:hover {
background-color: var(--main-light-color);
transition: var(--animation-3s);
}
.prompt-template-container > .item.active {
.prompt-template-container>.item.active {
background-color: var(--main-light-color);
transition: var(--animation-3s);
}
.prompt-template-container > .item > span:first-child {
.prompt-template-container>.item>span:first-child {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.prompt-template-container > .item > span:last-child {
.prompt-template-container>.item>span:last-child {
opacity: 0.6;
font-size: 12.5px;
max-width: 200px;

View File

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

View File

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

View File

@ -1,22 +1,23 @@
<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">
<code>resources/templates/list</code>
<span
class="iconfont icon-restart"
@click="reloadResources({ first: false })"
></span>
<span class="iconfont icon-restart" @click="reloadResources(client, { first: false })"></span>
</h3>
</template>
<!-- body -->
<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="item"
<div class="item"
:class="{ 'active': props.tabId >= 0 && tabStorage.currentType === 'template' && tabStorage.currentResourceName === template.name }"
v-for="template of resourcesManager.templates"
:key="template.name"
@click="handleClick(template)"
>
v-for="template of client.resourceTemplates?.values()" :key="template.name"
@click="handleClick(template)">
<span>{{ template.name }}</span>
<span>{{ template.description || '' }}</span>
</div>
@ -26,16 +27,20 @@
empty
</div>
</div>
</el-collapse-item>
</el-collapse>
</template>
<script setup lang="ts">
import { useMessageBridge } from '@/api/message-bridge';
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 { resourcesManager, type ResourceStorage } from './resources';
import type { ResourceStorage } from './resources';
import { tabs } from '../panel';
import { ElMessage } from 'element-plus';
import { McpClient, mcpClientAdapter } from '@/views/connect/core';
const bridge = useMessageBridge();
const { t } = useI18n();
@ -54,17 +59,18 @@ if (props.tabId >= 0) {
tabStorage = tab.storage as ResourceStorage;
} else {
tabStorage = reactive({
currentType:'template',
activeNames: [0],
templateActiveNames: [0],
currentType: 'template',
currentResourceName: '',
formData: {},
lastResourceReadResponse: undefined
});
}
function reloadResources(option: { first: boolean }) {
bridge.postMessage({
command: 'resources/templates/list'
});
async function reloadResources(client: Reactive<McpClient>, option: { first: boolean }) {
await client.getResourceTemplates({ cache: false });
if (!option.first) {
ElMessage({
@ -82,29 +88,18 @@ function handleClick(template: ResourceTemplate) {
tabStorage.lastResourceReadResponse = undefined;
}
let commandCancel: (() => void);
onMounted(() => {
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;
onMounted(async () => {
for (const client of mcpClientAdapter.clients) {
await client.getResourceTemplates({ cache: false });
}
}
}, { 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>
<style>
@ -146,7 +141,7 @@ h3.resource-template .iconfont.icon-restart:hover {
width: 175px;
}
.resource-template-container > .item {
.resource-template-container>.item {
margin: 3px;
padding: 5px 10px;
border-radius: .3em;
@ -158,24 +153,24 @@ h3.resource-template .iconfont.icon-restart:hover {
transition: var(--animation-3s);
}
.resource-template-container > .item:hover {
.resource-template-container>.item:hover {
background-color: var(--main-light-color);
transition: var(--animation-3s);
}
.resource-template-container > .item.active {
.resource-template-container>.item.active {
background-color: var(--main-light-color);
transition: var(--animation-3s);
}
.resource-template-container > .item > span:first-child {
.resource-template-container>.item>span:first-child {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.resource-template-container > .item > span:last-child {
.resource-template-container>.item>span:last-child {
opacity: 0.6;
font-size: 12.5px;
max-width: 200px;
@ -183,5 +178,4 @@ h3.resource-template .iconfont.icon-restart:hover {
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

View File

@ -1,40 +1,42 @@
<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">
<code>resources/list</code>
<span
class="iconfont icon-restart"
@click="reloadResources({ first: false })"
></span>
<span class="iconfont icon-restart" @click="reloadResources(client, { first: false })"></span>
</h3>
</template>
<!-- body -->
<div class="resource-template-container-scrollbar">
<el-scrollbar height="500px">
<div class="resource-template-container">
<div
class="item"
<div class="item"
:class="{ 'active': props.tabId >= 0 && tabStorage.currentType === 'resource' && tabStorage.currentResourceName === resource.name }"
v-for="resource of resourcesManager.resources"
:key="resource.uri"
@click="handleClick(resource)"
>
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>
</el-collapse-item>
</el-collapse>
</template>
<script setup lang="ts">
import { useMessageBridge } from '@/api/message-bridge';
import type { CasualRestAPI, Resources, ResourcesListResponse } from '@/hook/type';
import { onMounted, onUnmounted, defineProps, defineEmits, reactive } from 'vue';
import type { Resources } from '@/hook/type';
import { onMounted, defineProps, defineEmits, reactive, type Reactive } from 'vue';
import { useI18n } from 'vue-i18n';
import { resourcesManager, type ResourceStorage } from './resources';
import type { ResourceStorage } from './resources';
import { tabs } from '../panel';
import { ElMessage } from 'element-plus';
import { McpClient, mcpClientAdapter } from '@/views/connect/core';
const bridge = useMessageBridge();
const { t } = useI18n();
const props = defineProps({
@ -44,7 +46,7 @@ const props = defineProps({
}
});
const emits = defineEmits([ 'resource-selected' ]);
const emits = defineEmits(['resource-selected']);
let tabStorage: ResourceStorage;
@ -53,17 +55,17 @@ if (props.tabId >= 0) {
tabStorage = tab.storage as ResourceStorage;
} else {
tabStorage = reactive({
currentType:'resource',
activeNames: [0],
templateActiveNames: [0],
currentType: 'resource',
currentResourceName: '',
formData: {},
lastResourceReadResponse: undefined
});
}
function reloadResources(option: { first: boolean }) {
bridge.postMessage({
command: 'resources/list'
});
async function reloadResources(client: Reactive<McpClient>, option: { first: boolean }) {
await client.getResources({ cache: false });
if (!option.first) {
ElMessage({
@ -83,35 +85,23 @@ async function handleClick(resource: Resources) {
//
if (props.tabId >= 0) {
const bridge = useMessageBridge();
const { code, msg } = await bridge.commandRequest('resources/read', { resourceUri: resource.uri });
tabStorage.lastResourceReadResponse = msg;
const res = await mcpClientAdapter.readResource(resource.uri);
tabStorage.lastResourceReadResponse = res;
}
}
let commandCancel: (() => void);
onMounted(() => {
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;
onMounted(async () => {
for (const client of mcpClientAdapter.clients) {
await client.getResources();
}
}
}, { 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>
<style>
@ -153,7 +143,7 @@ h3.resource-template .iconfont.icon-restart:hover {
width: 175px;
}
.resource-template-container > .item {
.resource-template-container>.item {
margin: 3px;
padding: 5px 10px;
border-radius: .3em;
@ -165,24 +155,24 @@ h3.resource-template .iconfont.icon-restart:hover {
transition: var(--animation-3s);
}
.resource-template-container > .item:hover {
.resource-template-container>.item:hover {
background-color: var(--main-light-color);
transition: var(--animation-3s);
}
.resource-template-container > .item.active {
.resource-template-container>.item.active {
background-color: var(--main-light-color);
transition: var(--animation-3s);
}
.resource-template-container > .item > span:first-child {
.resource-template-container>.item>span:first-child {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.resource-template-container > .item > span:last-child {
.resource-template-container>.item>span:last-child {
opacity: 0.6;
font-size: 12.5px;
max-width: 200px;
@ -190,5 +180,4 @@ h3.resource-template .iconfont.icon-restart:hover {
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

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