This commit is contained in:
锦恢 2025-03-30 23:51:50 +08:00
parent 9ee40df8d2
commit eed67f6eb5
13 changed files with 235 additions and 18 deletions

View File

@ -28,6 +28,7 @@
- [ ] 支持同时调试多个 MCP Server
- [ ] 支持通过大模型进行在线验证
- [ ] 支持 completion/complete 协议字段
- [ ] 支持 对用户对应服务器的调试工作内容进行保存
## Dev

View File

@ -13,7 +13,8 @@ import Sidebar from '@/components/sidebar/index.vue';
import MainPanel from '@/components/main-panel/index.vue';
import { setDefaultCss } from './hook/css';
import { pinkLog } from './views/setting/util';
import { useMessageBridge } from './api/message-bridge';
import { acquireVsCodeApi, useMessageBridge } from './api/message-bridge';
import { connectionArgs, connectionMethods, connectionResult, doConnect } from './views/connect/connection';
const bridge = useMessageBridge();
@ -41,8 +42,25 @@ onMounted(() => {
pinkLog('OpenMCP Client 启动');
sendPing();
})
// debug
if (acquireVsCodeApi === undefined) {
connectionArgs.commandString = 'uv run mcp run ../servers/main.py';
connectionMethods.current = 'STDIO';
let handler: (() => void);
handler = bridge.addCommandListener('connect', data => {
const { code, msg } = data;
connectionResult.success = (code === 200);
connectionResult.logString = msg;
handler();
});
setTimeout(() => {
doConnect();
}, 200);
}
});
</script>

View File

@ -6,9 +6,17 @@
</template>
<script setup lang="ts">
import { defineComponent } from 'vue';
import { defineComponent, defineProps } from 'vue';
defineComponent({ name: 'chat' });
const props = defineProps({
tabId: {
type: Number,
required: true
}
});
</script>
<style scoped>

View File

@ -10,19 +10,22 @@ interface Tab {
icon: string;
type: string;
component: any;
storage: Record<string, any>;
}
export const debugModes = [
Resource, Prompt, Tool, Chat
]
// TODO: 实现对于 tabs 这个数据的可持久化
export const tabs = reactive({
content: [
{
name: '空白测试 1',
icon: 'icon-blank',
type: 'blank',
component: undefined
component: undefined,
storage: {}
}
] as Tab[],
activeIndex: 0,
@ -38,7 +41,8 @@ export function addNewTab() {
name: `空白测试 ${++ tabCounter}`,
icon: 'icon-blank',
type: 'blank',
component: undefined
component: undefined,
storage: {}
};
tabs.content.push(newTab);
tabs.activeIndex = tabs.content.length - 1;

View File

@ -6,9 +6,17 @@
</template>
<script setup lang="ts">
import { defineComponent } from 'vue';
import { defineComponent, defineProps } from 'vue';
defineComponent({ name: 'prompt' });
const props = defineProps({
tabId: {
type: Number,
required: true
}
});
</script>
<style scoped>

View File

@ -7,16 +7,32 @@
</h2>
<h3><code>resources/templates/list</code></h3>
<ResourceTemplates></ResourceTemplates>
<ResourceTemplates
:tab-id="props.tabId"
></ResourceTemplates>
</div>
<div class="right">
<ResourceReader
:tab-id="props.tabId"
></ResourceReader>
</div>
</div>
</template>
<script setup lang="ts">
import { defineProps } from 'vue';
import ResourceTemplates from './resource-templates.vue';
import ResourceReader from './resouce-reader.vue';
const props = defineProps({
tabId: {
type: Number,
required: true
}
});
</script>

View File

@ -0,0 +1,124 @@
<template>
<div class="resource-reader-container">
<el-form :model="formData" :rules="formRules" ref="formRef" label-position="top">
<el-form-item v-for="param in currentResource?.params" :key="param.name"
:label="`${param.name}${param.required ? '*' : ''}`" :prop="param.name" :rules="getParamRules(param)">
<!-- 根据不同类型渲染不同输入组件 -->
<el-input v-if="param.type === 'string'" v-model="formData[param.name]"
:placeholder="param.description || `请输入${param.name}`" />
<el-input-number v-else-if="param.type === 'number'" v-model="formData[param.name]"
:placeholder="param.description || `请输入${param.name}`" />
<el-switch v-else-if="param.type === 'boolean'" v-model="formData[param.name]" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="handleSubmit">
submit
</el-button>
<el-button @click="resetForm">
reset
</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { defineComponent, defineProps, watch, ref, computed } from 'vue';
import { ElMessage } from 'element-plus';
import type { FormInstance, FormRules } from 'element-plus';
import { tabs } from '../panel';
import { ResourceStorage } from './resources';
import { ResourcesReadResponse } from '@/hook/type';
defineComponent({ name: 'resource-reader' });
const props = defineProps({
tabId: {
type: Number,
required: true
}
});
const tab = tabs.content[props.tabId];
const tabStorage = tab.storage as ResourceStorage;
//
const formRef = ref<FormInstance>();
const formData = ref<Record<string, any>>({});
const loading = ref(false);
const responseData = ref<ResourcesReadResponse>();
//
const currentResource = computed(() => props.resource);
//
const formRules = computed<FormRules>(() => {
const rules: FormRules = {}
currentResource.value?.params.forEach(param => {
rules[param.name] = [
{
required: param.required,
message: `${param.name} 是必填字段`,
trigger: 'blur'
}
]
});
return rules;
})
//
const getParamRules = (param: ResourceProtocol['params'][0]) => {
const rules: FormRules[number] = []
if (param.required) {
rules.push({
required: true,
message: `${param.name}是必填字段`,
trigger: 'blur'
})
}
if (param.type === 'number') {
rules.push({
type: 'number',
message: `${param.name}必须是数字`,
trigger: 'blur'
})
}
return rules
}
//
const initFormData = () => {
formData.value = {}
currentResource.value?.params.forEach(param => {
formData.value[param.name] = param.type === 'number' ? 0 :
param.type === 'boolean' ? false : ''
})
}
//
const resetForm = () => {
formRef.value?.resetFields();
responseData.value = undefined;
}
//
const handleSubmit = async () => {
console.log('submit');
}
//
watch(() => tabStorage.currentResourceName, () => {
initFormData();
resetForm();
}, { immediate: true });
</script>
<style></style>

View File

@ -4,8 +4,10 @@
<div class="resource-template-container">
<div
class="item"
:class="{ 'active': tabStorage.currentResourceName === template.name }"
v-for="template of resourcesManager.templates"
:key="template.name"
@click="handleClick(template)"
>
<span>{{ template.name }}</span>
<span>{{ template.description || '' }}</span>
@ -17,22 +19,41 @@
<script setup lang="ts">
import { useMessageBridge } from '@/api/message-bridge';
import { CasualRestAPI, ResourceTemplatesListResponse } from '@/hook/type';
import { onMounted, onUnmounted } from 'vue';
import { resourcesManager } from './resources';
import { CasualRestAPI, ResourceTemplate, ResourceTemplatesListResponse } from '@/hook/type';
import { onMounted, onUnmounted, defineProps } from 'vue';
import { resourcesManager, ResourceStorage } from './resources';
import { tabs } from '../panel';
const bridge = useMessageBridge();
let cancelListener: undefined | (() => void) = undefined;
const props = defineProps({
tabId: {
type: Number,
required: true
}
});
const tab = tabs.content[props.tabId];
const tabStorage = tab.storage as ResourceStorage;
function reloadResources() {
bridge.postMessage({
command: 'resources/templates/list'
});
}
function handleClick(template: ResourceTemplate) {
tabStorage.currentResourceName = template.name;
}
onMounted(() => {
cancelListener = bridge.addCommandListener('resources/templates/list', (data: CasualRestAPI<ResourceTemplatesListResponse>) => {
resourcesManager.templates = data.msg.resourceTemplates;
resourcesManager.templates = data.msg.resourceTemplates || [];
if (resourcesManager.templates.length > 0) {
tabStorage.currentResourceName = resourcesManager.templates[0].name;
}
});
reloadResources();
});
@ -75,6 +96,11 @@ onUnmounted(() => {
transition: var(--animation-3s);
}
.resource-template-container > .item.active {
background-color: var(--main-light-color);
transition: var(--animation-3s);
}
.resource-template-container > .item > span:first-child {
max-width: 200px;
overflow: hidden;

View File

@ -9,3 +9,7 @@ export const resourcesManager = reactive<{
current: undefined,
templates: []
});
export interface ResourceStorage {
currentResourceName: string;
}

View File

@ -6,9 +6,17 @@
</template>
<script setup lang="ts">
import { defineComponent } from 'vue';
import { defineComponent, defineProps } from 'vue';
defineComponent({ name: 'tool' });
const props = defineProps({
storage: {
type: Object,
required: true
}
});
</script>
<style scoped>

View File

@ -50,7 +50,6 @@ const bridge = useMessageBridge();
bridge.addCommandListener('connect', data => {
const { code, msg } = data;
connectionResult.success = (code === 200);
connectionResult.logString = msg;
});

View File

@ -5,10 +5,11 @@
<!-- 如果存在激活标签页则根据标签页进行渲染 -->
<div v-show="tabs.activeTab.component">
<component
v-for="(tab, index) of tabs.content"
v-show="tab === tabs.activeTab"
v-show="tab === tabs.activeTab"
v-for="(tab, index) of tabs.content"
:key="index"
:is="tab.component"
:tab-id="index"
/>
</div>
</div>

View File

@ -16,7 +16,7 @@ const logger = pino({
options: {
colorize: true, // 开启颜色
levelFirst: true, // 先打印日志级别
translateTime: 'yyyy-mm-dd HH:MM:ss', // 格式化时间
translateTime: 'SYS:yyyy-mm-dd HH:MM:ss', // 格式化时间
ignore: 'pid,hostname', // 忽略部分字段
}
}