支持 resources/list 的调度

This commit is contained in:
锦恢 2025-05-06 20:45:47 +08:00
parent c716cbc6ca
commit 8d549d17e2
39 changed files with 452 additions and 115 deletions

View File

@ -1,5 +1,9 @@
# Change Log # Change Log
## [main] 0.0.7
- 优化页面布局,使得调试内容更加紧凑
- 扩大默认的上下文长度
- 增加「通用选项」用于设置mcp服务器的最大的等待时间
## [main] 0.0.6 ## [main] 0.0.6
- 修复部分因为服务器名称特殊字符而导致的保存实效的错误 - 修复部分因为服务器名称特殊字符而导致的保存实效的错误

View File

@ -1,8 +1,8 @@
@font-face { @font-face {
font-family: "iconfont"; /* Project id 4870215 */ font-family: "iconfont"; /* Project id 4870215 */
src: url('iconfont.woff2?t=1745774700883') format('woff2'), src: url('iconfont.woff2?t=1746529081655') format('woff2'),
url('iconfont.woff?t=1745774700883') format('woff'), url('iconfont.woff?t=1746529081655') format('woff'),
url('iconfont.ttf?t=1745774700883') format('truetype'); url('iconfont.ttf?t=1746529081655') format('truetype');
} }
.iconfont { .iconfont {
@ -13,6 +13,18 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-timeout:before {
content: "\edf5";
}
.icon-dui:before {
content: "\e627";
}
.icon-cuo:before {
content: "\ed1a";
}
.icon-video:before { .icon-video:before {
content: "\e865"; content: "\e865";
} }

Binary file not shown.

View File

@ -94,7 +94,7 @@ onMounted(async () => {
<style> <style>
.main { .main {
height: calc(100vh - 50px); height: calc(100vh - 5px);
display: flex; display: flex;
justify-content: center; justify-content: center;
} }

View File

@ -38,7 +38,7 @@ if (!tabStorage.settings) {
enableTools: [], enableTools: [],
enableWebSearch: false, enableWebSearch: false,
temperature: 0.7, temperature: 0.7,
contextLength: 10, contextLength: 20,
systemPrompt: '' systemPrompt: ''
} as ChatSetting; } as ChatSetting;
} }

View File

@ -70,20 +70,19 @@ function setActiveTab(index: number) {
<style> <style>
.main-panel-container { .main-panel-container {
display: flex;
justify-content: center; justify-content: center;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
min-width: 800px; min-width: 800px;
height: 100%; height: 100%;
margin-left: 20px; margin-left: 5px;
} }
.main-panel { .main-panel {
background-color: var(--sidebar); background-color: var(--sidebar);
border-radius: 1.2em; border-radius: 1.2em;
width: 100%; width: 100%;
height: 90%; height: calc(100% - 35px);
} }
.scroll-tabs-container { .scroll-tabs-container {
@ -92,13 +91,13 @@ function setActiveTab(index: number) {
} }
.tabs-container { .tabs-container {
height: 78px; height: 30px;
width: 90%; width: 90%;
background-color: var(--background); background-color: var(--background);
display: flex; display: flex;
align-items: center; align-items: center;
user-select: none; user-select: none;
padding: 0 10px; margin-bottom: 5px;
} }
.tabs-container .el-scrollbar { .tabs-container .el-scrollbar {
@ -107,12 +106,12 @@ function setActiveTab(index: number) {
.tabs-container .tab { .tabs-container .tab {
white-space: nowrap; white-space: nowrap;
margin: 5px; margin-right: 5px;
font-size: 13px; font-size: 12px;
width: 120px; width: 120px;
border-radius: .5em; border-radius: .5em;
background-color: var(--sidebar); background-color: var(--sidebar);
padding: 10px; padding: 3px 10px;
display: flex; display: flex;
align-items: center; align-items: center;
transition: var(--animation-3s); transition: var(--animation-3s);
@ -160,11 +159,9 @@ function setActiveTab(index: number) {
.tabs-container .add-button { .tabs-container .add-button {
cursor: pointer; cursor: pointer;
font-size: 20px; font-size: 15px;
margin-left: 5px; margin-left: 5px;
border-radius: .5em; border-radius: .5em;
height: 35px;
width: 35px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;

View File

@ -6,11 +6,14 @@
<span class="iconfont icon-file"></span> <span class="iconfont icon-file"></span>
{{ t("resources") + t("module") }} {{ t("resources") + t("module") }}
</h2> </h2>
<h3><code>resources/templates/list</code></h3>
<ResourceListTemplates
<ResourceTemplates
:tab-id="props.tabId" :tab-id="props.tabId"
></ResourceTemplates> ></ResourceListTemplates>
<ResourceList
:tab-id="props.tabId"
></ResourceList>
</div> </div>
<div class="right"> <div class="right">
@ -29,7 +32,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineProps } from 'vue'; import { defineProps } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import ResourceTemplates from './resource-templates.vue';
import ResourceListTemplates from './resource-list-templates.vue';
import ResourceList from './resource-list.vue';
import ResourceReader from './resouce-reader.vue'; import ResourceReader from './resouce-reader.vue';
import ResourceLogger from './resource-logger.vue'; import ResourceLogger from './resource-logger.vue';
@ -42,8 +48,6 @@ const props = defineProps({
} }
}); });
</script> </script>
<style scoped> <style scoped>

View File

@ -122,21 +122,28 @@ const resetForm = () => {
responseData.value = undefined; responseData.value = undefined;
} }
function getUri() {
if (tabStorage.currentType === 'template') {
const fillFn = currentResource.value.fill;
const uri = fillFn(tabStorage.formData);
return uri;
}
const targetResource = resourcesManager.resources.find(resources => resources.name === tabStorage.currentResourceName);
return targetResource?.uri;
}
// //
function handleSubmit() { async function handleSubmit() {
const fillFn = currentResource.value.fill; const uri = getUri();
const uri = fillFn(tabStorage.formData); console.log(uri);
const bridge = useMessageBridge(); const bridge = useMessageBridge();
const { code, msg } = await bridge.commandRequest('resources/read', { resourceUri: uri });
bridge.addCommandListener('resources/read', (data: CasualRestAPI<ResourcesReadResponse>) => {
tabStorage.lastResourceReadResponse = data.msg; if (code === 200) {
}, { once: true }); tabStorage.lastResourceReadResponse = msg;
}
bridge.postMessage({
command: 'resources/read',
data: { resourceUri: uri }
});
} }
// //

View File

@ -1,10 +1,18 @@
<template> <template>
<h3 class="resource-template">
<code>resources/templates/list</code>
<span
class="iconfont icon-restart"
@click="reloadResources({ first: false })"
></span>
</h3>
<div class="resource-template-container-scrollbar"> <div class="resource-template-container-scrollbar">
<el-scrollbar height="500px"> <el-scrollbar height="500px" v-if="resourcesManager.templates.length > 0">
<div class="resource-template-container"> <div class="resource-template-container">
<div <div
class="item" class="item"
:class="{ 'active': tabStorage.currentResourceName === template.name }" :class="{ 'active': tabStorage.currentType === 'template' && tabStorage.currentResourceName === template.name }"
v-for="template of resourcesManager.templates" v-for="template of resourcesManager.templates"
:key="template.name" :key="template.name"
@click="handleClick(template)" @click="handleClick(template)"
@ -14,21 +22,16 @@
</div> </div>
</div> </div>
</el-scrollbar> </el-scrollbar>
<div v-else style="padding: 10px;">
empty
</div>
</div> </div>
<div class="resource-template-function-container">
<el-button
type="primary"
@click="reloadResources({ first: false })"
>
{{ t('refresh') }}
</el-button>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useMessageBridge } from '@/api/message-bridge'; import { useMessageBridge } from '@/api/message-bridge';
import { CasualRestAPI, ResourceTemplate, ResourceTemplatesListResponse } from '@/hook/type'; import { CasualRestAPI, ResourceTemplate, ResourceTemplatesListResponse } from '@/hook/type';
import { onMounted, onUnmounted, defineProps } from 'vue'; import { onMounted, onUnmounted, defineProps, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { resourcesManager, ResourceStorage } from './resources'; import { resourcesManager, ResourceStorage } from './resources';
import { tabs } from '../panel'; import { tabs } from '../panel';
@ -63,6 +66,7 @@ function reloadResources(option: { first: boolean }) {
} }
function handleClick(template: ResourceTemplate) { function handleClick(template: ResourceTemplate) {
tabStorage.currentType = 'template';
tabStorage.currentResourceName = template.name; tabStorage.currentResourceName = template.name;
tabStorage.lastResourceReadResponse = undefined; tabStorage.lastResourceReadResponse = undefined;
} }
@ -73,11 +77,13 @@ onMounted(() => {
commandCancel = bridge.addCommandListener('resources/templates/list', (data: CasualRestAPI<ResourceTemplatesListResponse>) => { commandCancel = bridge.addCommandListener('resources/templates/list', (data: CasualRestAPI<ResourceTemplatesListResponse>) => {
resourcesManager.templates = data.msg.resourceTemplates || []; resourcesManager.templates = data.msg.resourceTemplates || [];
const targetResource = resourcesManager.templates.find(template => template.name === tabStorage.currentResourceName); if (tabStorage.currentType === 'template') {
if (targetResource === undefined) { const targetResource = resourcesManager.templates.find(template => template.name === tabStorage.currentResourceName);
tabStorage.currentResourceName = resourcesManager.templates[0]?.name; if (targetResource === undefined) {
tabStorage.lastResourceReadResponse = undefined; tabStorage.currentResourceName = resourcesManager.templates[0]?.name;
} tabStorage.lastResourceReadResponse = undefined;
}
}
}, { once: false }); }, { once: false });
reloadResources({ first: true }); reloadResources({ first: true });
@ -88,10 +94,23 @@ onUnmounted(() => {
commandCancel(); commandCancel();
} }
}) })
</script> </script>
<style> <style>
h3.resource-template {
display: flex;
align-items: center;
}
h3.resource-template .iconfont.icon-restart {
margin-left: 10px;
cursor: pointer;
}
h3.resource-template .iconfont.icon-restart:hover {
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;

View File

@ -0,0 +1,173 @@
<template>
<h3 class="resource-template">
<code>resources/list</code>
<span
class="iconfont icon-restart"
@click="reloadResources({ first: false })"
></span>
</h3>
<div class="resource-template-container-scrollbar">
<el-scrollbar height="500px">
<div class="resource-template-container">
<div
class="item"
:class="{ 'active': tabStorage.currentType === 'resource' && tabStorage.currentResourceName === resource.name }"
v-for="resource of resourcesManager.resources"
:key="resource.uri"
@click="handleClick(resource)"
>
<span>{{ resource.name }}</span>
<span>{{ resource.mimeType }}</span>
</div>
</div>
</el-scrollbar>
</div>
</template>
<script setup lang="ts">
import { useMessageBridge } from '@/api/message-bridge';
import { CasualRestAPI, Resources, ResourcesListResponse } from '@/hook/type';
import { onMounted, onUnmounted, defineProps, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { resourcesManager, ResourceStorage } from './resources';
import { tabs } from '../panel';
import { ElMessage } from 'element-plus';
const bridge = useMessageBridge();
const { t } = useI18n();
const props = defineProps({
tabId: {
type: Number,
required: true
}
});
const tab = tabs.content[props.tabId];
const tabStorage = tab.storage as ResourceStorage;
function reloadResources(option: { first: boolean }) {
bridge.postMessage({
command: 'resources/list'
});
if (!option.first) {
ElMessage({
message: t('finish-refresh'),
type: 'success',
duration: 3000,
showClose: true,
});
}
}
function handleClick(resource: Resources) {
tabStorage.currentType = 'resource';
tabStorage.currentResourceName = resource.name;
tabStorage.lastResourceReadResponse = undefined;
}
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;
}
}
}, { once: false });
reloadResources({ first: true });
});
onUnmounted(() => {
if (commandCancel){
commandCancel();
}
})
</script>
<style>
h3.resource-template {
display: flex;
align-items: center;
}
h3.resource-template .iconfont.icon-restart {
margin-left: 10px;
cursor: pointer;
}
h3.resource-template .iconfont.icon-restart:hover {
color: var(--main-color);
}
.resource-template-container-scrollbar {
background-color: var(--background);
margin-bottom: 10px;
border-radius: .5em;
}
.resource-template-container {
height: fit-content;
display: flex;
flex-direction: column;
padding: 10px;
}
.resource-template-function-container {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.resource-template-function-container button {
width: 175px;
}
.resource-template-container > .item {
margin: 3px;
padding: 5px 10px;
border-radius: .3em;
user-select: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
transition: var(--animation-3s);
}
.resource-template-container > .item:hover {
background-color: var(--main-light-color);
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;
text-overflow: ellipsis;
white-space: nowrap;
}
.resource-template-container > .item > span:last-child {
opacity: 0.6;
font-size: 12.5px;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

View File

@ -20,7 +20,20 @@
> >
<template v-if="!showRawJson"> <template v-if="!showRawJson">
<span v-for="(content, index) of tabStorage.lastResourceReadResponse?.contents || []" :key="index"> <span v-for="(content, index) of tabStorage.lastResourceReadResponse?.contents || []" :key="index">
{{ content.text }} <span v-if="content.mimeType === 'image/png'">
<img
class="resource-list-image"
:src="getImageBlobUrlByBase64(content.blob || '', content.mimeType)"
:alt="content.text"
style="max-width: 100%; max-height: 300px;"
/>
</span>
<span v-if="content.mimeType === 'image/jpeg'">
</span>
<span v-else>
{{ content.text }}
</span>
</span> </span>
</template> </template>
<template v-else> <template v-else>
@ -36,6 +49,7 @@ import { defineComponent, defineProps, computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { tabs } from '../panel'; import { tabs } from '../panel';
import { ResourceStorage } from './resources'; import { ResourceStorage } from './resources';
import { getImageBlobUrlByBase64 } from '@/hook/util';
defineComponent({ name: 'resource-logger' }); defineComponent({ name: 'resource-logger' });
const { t } = useI18n(); const { t } = useI18n();
@ -101,4 +115,8 @@ const formattedJson = computed(() => {
line-height: 1.5; line-height: 1.5;
background-color: var(--sidebar); background-color: var(--sidebar);
} }
.resource-list-image {
cursor: unset;
}
</style> </style>

View File

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

View File

@ -142,9 +142,15 @@ const resetForm = () => {
}; };
async function handleExecute() { async function handleExecute() {
if (!currentTool.value) return; if (!currentTool.value) return;
const toolResponse = await callTool(tabStorage.currentToolName, tabStorage.formData); loading.value = true;
tabStorage.lastToolCallResponse = toolResponse; try {
tabStorage.lastToolCallResponse = undefined;
const toolResponse = await callTool(tabStorage.currentToolName, tabStorage.formData);
tabStorage.lastToolCallResponse = toolResponse;
} finally {
loading.value = false;
}
} }
watch(() => tabStorage.currentToolName, () => { watch(() => tabStorage.currentToolName, () => {

View File

@ -15,6 +15,11 @@
</div> </div>
</el-scrollbar> </el-scrollbar>
</div> </div>
<div>
<!-- resources/list -->
</div>
<div class="tool-list-function-container"> <div class="tool-list-function-container">
<el-button <el-button
type="primary" type="primary"

View File

@ -40,7 +40,7 @@ import { defineComponent, defineProps, computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { tabs } from '../panel'; import { tabs } from '../panel';
import { ToolStorage } from './tools'; import { ToolStorage } from './tools';
import { markdownToHtml } from '../chat/markdown/markdown'; import { useMessageBridge } from '@/api/message-bridge';
defineComponent({ name: 'tool-logger' }); defineComponent({ name: 'tool-logger' });
const { t } = useI18n(); const { t } = useI18n();

View File

@ -1,4 +1,5 @@
import { useMessageBridge } from '@/api/message-bridge'; import { useMessageBridge } from '@/api/message-bridge';
import { mcpSetting } from '@/hook/mcp';
import { ToolsListResponse, ToolCallResponse, CasualRestAPI } from '@/hook/type'; import { ToolsListResponse, ToolCallResponse, CasualRestAPI } from '@/hook/type';
import { pinkLog } from '@/views/setting/util'; import { pinkLog } from '@/views/setting/util';
import { reactive } from 'vue'; import { reactive } from 'vue';
@ -36,7 +37,10 @@ export function callTool(toolName: string, toolArgs: Record<string, any>) {
command: 'tools/call', command: 'tools/call',
data: { data: {
toolName, toolName,
toolArgs: JSON.parse(JSON.stringify(toolArgs)) toolArgs: JSON.parse(JSON.stringify(toolArgs)),
callToolOption: {
timeout: mcpSetting.timeout * 1000
}
} }
}); });
}); });

View File

@ -2,6 +2,7 @@
<div class="connected-status-container" <div class="connected-status-container"
id="connected-status-container" id="connected-status-container"
@click.stop="toggleConnectionPanel()" @click.stop="toggleConnectionPanel()"
:class="{ 'connected': connectionResult.success }"
> >
<span class="mcp-server-info"> <span class="mcp-server-info">
<el-tooltip <el-tooltip
@ -14,12 +15,14 @@
</el-tooltip> </el-tooltip>
</span> </span>
<span class="connect-status"> <span class="connect-status">
<span <span v-if="connectionResult.success">
class="status-circle" <span class="iconfont icon-connect"></span>
:class="statusColorStyle" <span class="iconfont icon-dui"></span>
> </span>
<span v-else>
<span class="iconfont icon-connect"></span>
<span class="iconfont icon-cuo"></span>
</span> </span>
<span class="status-string">{{ statusString }}</span>
</span> </span>
</div> </div>
@ -35,22 +38,6 @@ defineComponent({ name: 'connected' });
const { t } = useI18n(); const { t } = useI18n();
const statusString = computed(() => {
if (connectionResult.success) {
return t('connected');
} else {
return t('disconnected');
}
});
const statusColorStyle = computed(() => {
if (connectionResult.success) {
return 'connected-color';
} else {
return 'disconnected-color';
}
});
const fullDisplayServerName = computed(() => { const fullDisplayServerName = computed(() => {
return connectionResult.serverInfo.name + '/' + connectionResult.serverInfo.version; return connectionResult.serverInfo.name + '/' + connectionResult.serverInfo.version;
}); });
@ -88,9 +75,13 @@ function toggleConnectionPanel() {
</script> </script>
<style> <style>
.connected .status-circle {
background-color: var(--el-color-success) !important;
}
.connected-color { .connected .connect-status {
background-color: #21DA49; border: 1px solid var(--el-color-success) !important;
color: var(--el-color-success) !important;
} }
.disconnected-color { .disconnected-color {
@ -100,8 +91,8 @@ function toggleConnectionPanel() {
.status-circle { .status-circle {
height: 12px; height: 12px;
width: 12px; width: 12px;
margin-right: 8px;
border-radius: 99%; border-radius: 99%;
background-color: var(--main-color);
box-shadow: 0 0 4px rgba(0, 0, 0, 0.1); box-shadow: 0 0 4px rgba(0, 0, 0, 0.1);
} }
@ -123,7 +114,13 @@ function toggleConnectionPanel() {
.connected-status-container .connect-status { .connected-status-container .connect-status {
display: flex; display: flex;
align-items: center; align-items: center;
margin-top: 20px; justify-content: space-between;
margin-top: 10px;
border-radius: .5em;
padding: 5px 10px;
width: 30px;
border: 1px solid var(--main-color);
color: var(--main-color);
} }
.connected-status-container:hover { .connected-status-container:hover {
@ -148,7 +145,10 @@ function toggleConnectionPanel() {
.mcp-server-info .name { .mcp-server-info .name {
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
max-width: 60px; width: 30px;
display: flex;
align-items: center;
justify-content: center;
white-space: wrap; white-space: wrap;
background-color: #f39a6d; background-color: #f39a6d;
padding: 5px 12px; padding: 5px 12px;

View File

@ -35,6 +35,8 @@ function clickLogo() {
.simple-logo { .simple-logo {
height: 73px; height: 73px;
display: flex; display: flex;
align-items: center;
flex-direction: column; flex-direction: column;
user-select: none; user-select: none;
-webkit-user-drag: none; -webkit-user-drag: none;
@ -47,7 +49,7 @@ function clickLogo() {
} }
.simple-logo .iconfont { .simple-logo .iconfont {
font-size: 48px; font-size: 38px;
} }
</style> </style>

View File

@ -1,3 +1,5 @@
import { reactive } from "vue";
interface TypeAble { interface TypeAble {
type: string; type: string;
} }
@ -27,4 +29,8 @@ export function normaliseJavascriptType(type: string) {
default: default:
return 'string'; return 'string';
} }
} }
export const mcpSetting = reactive({
timeout: 60,
});

View File

@ -3,6 +3,7 @@ import { llmManager, llms } from "@/views/setting/llm";
import { pinkLog } from "@/views/setting/util"; import { pinkLog } from "@/views/setting/util";
import I18n from '@/i18n/index'; import I18n from '@/i18n/index';
import { userHasReadGuide } from "@/components/guide/tour"; import { userHasReadGuide } from "@/components/guide/tour";
import { mcpSetting } from "./mcp";
export async function loadSetting() { export async function loadSetting() {
const bridge = useMessageBridge(); const bridge = useMessageBridge();
@ -14,10 +15,11 @@ export async function loadSetting() {
} else { } else {
const persistConfig = data.msg; const persistConfig = data.msg;
pinkLog('配置加载成功'); pinkLog('配置加载成功');
llmManager.currentModelIndex = persistConfig.MODEL_INDEX; llmManager.currentModelIndex = persistConfig.MODEL_INDEX || 0;
I18n.global.locale.value = persistConfig.LANG; I18n.global.locale.value = persistConfig.LANG || 'zh';
mcpSetting.timeout = persistConfig.MCP_TIMEOUT_SEC || 60;
persistConfig.LLM_INFO.forEach((element: any) => { persistConfig.LLM_INFO.forEach((element: any) => {
llms.push(element); llms.push(element);
@ -48,7 +50,8 @@ export function saveSetting(saveHandler?: () => void) {
const saveConfig = { const saveConfig = {
MODEL_INDEX: llmManager.currentModelIndex, MODEL_INDEX: llmManager.currentModelIndex,
LLM_INFO: JSON.parse(JSON.stringify(llms)), LLM_INFO: JSON.parse(JSON.stringify(llms)),
LANG: I18n.global.locale.value LANG: I18n.global.locale.value,
MCP_TIMEOUT_SEC: mcpSetting.timeout
}; };
bridge.addCommandListener('setting/save', data => { bridge.addCommandListener('setting/save', data => {

View File

@ -21,6 +21,8 @@ export interface Content {
uri: string; uri: string;
mimeType: string; mimeType: string;
text: string; text: string;
blob?: string;
[key: string]: any;
} }
export interface MessageContent { export interface MessageContent {
@ -59,12 +61,18 @@ export interface ResourceTemplate {
description: string; description: string;
} }
export interface Resources {
uri: string;
name: string;
mimeType: string;
}
export interface ResourceTemplatesListResponse { export interface ResourceTemplatesListResponse {
resourceTemplates: ResourceTemplate[] resourceTemplates: ResourceTemplate[]
} }
export interface ResourcesListResponse { export interface ResourcesListResponse {
resources: any[]; // 根据示例返回空数组,可进一步定义具体类型 resources: Resources[]
} }
export interface ResourcesReadResponse { export interface ResourcesReadResponse {

View File

@ -64,6 +64,12 @@ export async function getBlobUrlByFilename(filename: string) {
mimeType = 'image/svg+xml'; mimeType = 'image/svg+xml';
break; break;
} }
const blobUrl = getImageBlobUrlByBase64(base64String, mimeType, filename);
return blobUrl;
}
export function getImageBlobUrlByBase64(base64String: string, mimeType: string, cacheKey?: string) {
const byteCharacters = atob(base64String); const byteCharacters = atob(base64String);
const byteNumbers = new Array(byteCharacters.length); const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) { for (let i = 0; i < byteCharacters.length; i++) {
@ -73,6 +79,8 @@ export async function getBlobUrlByFilename(filename: string) {
const blob = new Blob([byteArray], { type: mimeType }); const blob = new Blob([byteArray], { type: mimeType });
const blobUrl = URL.createObjectURL(blob); const blobUrl = URL.createObjectURL(blob);
// 将结果存入缓存 // 将结果存入缓存
blobUrlCache.set(filename, blobUrl); if (cacheKey) {
blobUrlCache.set(cacheKey, blobUrl);
}
return blobUrl; return blobUrl;
} }

View File

@ -149,5 +149,7 @@
"add-system-prompt.name-placeholder": "عنوان prompt المخصص", "add-system-prompt.name-placeholder": "عنوان prompt المخصص",
"enter-message-dot": "أدخل الرسالة...", "enter-message-dot": "أدخل الرسالة...",
"generate-answer": "جارٍ إنشاء الإجابة", "generate-answer": "جارٍ إنشاء الإجابة",
"choose-presetting": "اختر الإعداد المسبق" "choose-presetting": "اختر الإعداد المسبق",
"cwd": "دليل التنفيذ",
"mcp-server-timeout": "أطول وقت لاستدعاء أداة MCP"
} }

View File

@ -149,5 +149,7 @@
"add-system-prompt.name-placeholder": "Titel für benutzerdefinierte Eingabeaufforderung", "add-system-prompt.name-placeholder": "Titel für benutzerdefinierte Eingabeaufforderung",
"enter-message-dot": "Nachricht eingeben...", "enter-message-dot": "Nachricht eingeben...",
"generate-answer": "Antwort wird generiert", "generate-answer": "Antwort wird generiert",
"choose-presetting": "Voreinstellung auswählen" "choose-presetting": "Voreinstellung auswählen",
"cwd": "Ausführungsverzeichnis",
"mcp-server-timeout": "Maximale Aufrufzeit des MCP-Tools"
} }

View File

@ -149,5 +149,7 @@
"add-system-prompt.name-placeholder": "Title for custom prompt", "add-system-prompt.name-placeholder": "Title for custom prompt",
"enter-message-dot": "Enter message...", "enter-message-dot": "Enter message...",
"generate-answer": "Generating answer", "generate-answer": "Generating answer",
"choose-presetting": "Select preset" "choose-presetting": "Select preset",
"cwd": "Execution directory",
"mcp-server-timeout": "Maximum call time of MCP tool"
} }

View File

@ -149,5 +149,7 @@
"add-system-prompt.name-placeholder": "Titre de l'invite personnalisée", "add-system-prompt.name-placeholder": "Titre de l'invite personnalisée",
"enter-message-dot": "Entrez un message...", "enter-message-dot": "Entrez un message...",
"generate-answer": "Génération de la réponse", "generate-answer": "Génération de la réponse",
"choose-presetting": "Sélectionner un préréglage" "choose-presetting": "Sélectionner un préréglage",
"cwd": "Répertoire d'exécution",
"mcp-server-timeout": "Temps d'appel maximum de l'outil MCP"
} }

View File

@ -149,5 +149,7 @@
"add-system-prompt.name-placeholder": "カスタムプロンプトのタイトル", "add-system-prompt.name-placeholder": "カスタムプロンプトのタイトル",
"enter-message-dot": "メッセージを入力...", "enter-message-dot": "メッセージを入力...",
"generate-answer": "回答を生成中", "generate-answer": "回答を生成中",
"choose-presetting": "プリセットを選択" "choose-presetting": "プリセットを選択",
"cwd": "実行ディレクトリ",
"mcp-server-timeout": "MCPツールの最大呼び出し時間"
} }

View File

@ -149,5 +149,7 @@
"add-system-prompt.name-placeholder": "사용자 지정 프롬프트 제목", "add-system-prompt.name-placeholder": "사용자 지정 프롬프트 제목",
"enter-message-dot": "메시지를 입력하세요...", "enter-message-dot": "메시지를 입력하세요...",
"generate-answer": "답변 생성 중", "generate-answer": "답변 생성 중",
"choose-presetting": "프리셋 선택" "choose-presetting": "프리셋 선택",
"cwd": "실행 디렉터리",
"mcp-server-timeout": "MCP 도구 최대 호출 시간"
} }

View File

@ -149,5 +149,7 @@
"add-system-prompt.name-placeholder": "Заголовок пользовательского prompt", "add-system-prompt.name-placeholder": "Заголовок пользовательского prompt",
"enter-message-dot": "Введите сообщение...", "enter-message-dot": "Введите сообщение...",
"generate-answer": "Генерация ответа", "generate-answer": "Генерация ответа",
"choose-presetting": "Выбрать预设" "choose-presetting": "Выбрать预设",
"cwd": "Каталог выполнения",
"mcp-server-timeout": "Максимальное время вызова инструмента MCP"
} }

View File

@ -149,5 +149,7 @@
"add-system-prompt.name-placeholder": "输入自定义 prompt 的标题", "add-system-prompt.name-placeholder": "输入自定义 prompt 的标题",
"enter-message-dot": "输入消息...", "enter-message-dot": "输入消息...",
"generate-answer": "正在生成答案", "generate-answer": "正在生成答案",
"choose-presetting": "选择预设" "choose-presetting": "选择预设",
"cwd": "执行目录",
"mcp-server-timeout": "MCP工具最长调用时间"
} }

View File

@ -149,5 +149,7 @@
"add-system-prompt.name-placeholder": "自定義提示的標題", "add-system-prompt.name-placeholder": "自定義提示的標題",
"enter-message-dot": "輸入訊息...", "enter-message-dot": "輸入訊息...",
"generate-answer": "正在生成答案", "generate-answer": "正在生成答案",
"choose-presetting": "選擇預設" "choose-presetting": "選擇預設",
"cwd": "執行目錄",
"mcp-server-timeout": "MCP工具最長調用時間"
} }

View File

@ -6,7 +6,7 @@
</span> </span>
<p> <p>
OpenMCP Client 0.0.5 OpenMCP@<a href="https://www.zhihu.com/people/can-meng-zhong-de-che-xian">锦恢</a> 开发 OpenMCP Client 0.0.7 OpenMCP@<a href="https://www.zhihu.com/people/can-meng-zhong-de-che-xian">锦恢</a> 开发
</p> </p>
<p> <p>

View File

@ -6,13 +6,13 @@
<el-form :model="connectionArgs" :rules="rules" ref="stdioForm"> <el-form :model="connectionArgs" :rules="rules" ref="stdioForm">
<el-form-item prop="commandString"> <el-form-item prop="commandString">
<div class="input-with-label"> <div class="input-with-label">
<span class="input-label">命令</span> <span class="input-label">{{ t("command") }}</span>
<el-input v-model="connectionArgs.commandString" placeholder="mcp run <your script>"></el-input> <el-input v-model="connectionArgs.commandString" placeholder="mcp run <your script>"></el-input>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item prop="cwd"> <el-form-item prop="cwd">
<div class="input-with-label"> <div class="input-with-label">
<span class="input-label">执行目录</span> <span class="input-label">{{ t('cwd') }}</span>
<el-input v-model="connectionArgs.cwd" placeholder="cwd, 可为空"></el-input> <el-input v-model="connectionArgs.cwd" placeholder="cwd, 可为空"></el-input>
</div> </div>
</el-form-item> </el-form-item>

View File

@ -139,6 +139,7 @@ function chooseDebugMode(index: number) {
.debug-welcome { .debug-welcome {
user-select: none; user-select: none;
height: 80%;
} }
.debug-option.disable { .debug-option.disable {

View File

@ -14,6 +14,19 @@
</el-select> </el-select>
</div> </div>
</div> </div>
<div class="setting-option">
<span>
<span class="iconfont icon-timeout"></span>
<span class="option-title">{{ t('mcp-server-timeout') }} (sec)</span>
</span>
<div style="width: 200px;">
<el-slider
v-model="mcpSetting.timeout"
:min="10" :max="10000" :step="1"
@change="safeSaveSetting" />
</div>
</div>
</div> </div>
</template> </template>
@ -21,6 +34,8 @@
import { defineComponent, ref } from 'vue'; import { defineComponent, ref } from 'vue';
import { languageSetting } from './language'; import { languageSetting } from './language';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { mcpSetting } from '@/hook/mcp';
import { debounce } from 'lodash';
import { saveSetting } from '@/hook/setting'; import { saveSetting } from '@/hook/setting';
defineComponent({ name: 'appearance' }); defineComponent({ name: 'appearance' });
@ -29,15 +44,25 @@ const { t, locale } = useI18n();
const currentLanguage = ref('简体中文'); const currentLanguage = ref('简体中文');
function onlanguagechange(code: string) { function onlanguagechange(code: string) {
console.log('enter lang change');
const option = languageSetting.options.find(item => item.value === code); const option = languageSetting.options.find(item => item.value === code);
if (option) { if (option) {
currentLanguage.value = option.text; currentLanguage.value = option.text;
} }
// languageDialogShow.value = true; // languageDialogShow.value = true;
saveSetting(); saveSetting();
} }
const safeSaveSetting = debounce(() => {
saveSetting();
}, 10);
</script> </script>
<style></style> <style></style>

View File

@ -97,6 +97,8 @@ export class ClientController {
} }
const resource = await client.readResource(option.resourceUri); const resource = await client.readResource(option.resourceUri);
console.log(resource);
return { return {
code: 200, code: 200,
msg: resource msg: resource
@ -130,7 +132,8 @@ export class ClientController {
const toolResult = await client.callTool({ const toolResult = await client.callTool({
name: option.toolName, name: option.toolName,
arguments: option.toolArgs arguments: option.toolArgs,
callToolOption: option.callToolOption
}); });
// console.log(JSON.stringify(toolResult, null, 2)); // console.log(JSON.stringify(toolResult, null, 2));

View File

@ -41,7 +41,8 @@ export class McpClient {
this.transport = new StdioClientTransport({ this.transport = new StdioClientTransport({
command: this.options.command || '', command: this.options.command || '',
args: this.options.args || [], args: this.options.args || [],
cwd: this.options.cwd || process.cwd() cwd: this.options.cwd || process.cwd(),
stderr: 'pipe'
}); });
break; break;
@ -117,8 +118,10 @@ export class McpClient {
} }
// 调用工具 // 调用工具
public async callTool(options: { name: string; arguments: Record<string, any> }) { public async callTool(options: { name: string; arguments: Record<string, any>, callToolOption?: any }) {
return await this.client.callTool(options);
const { callToolOption, ...methodArgs } = options;
return await this.client.callTool(methodArgs, undefined, callToolOption);
} }
} }

View File

@ -25,7 +25,7 @@ function getDefaultLanguage() {
const DEFAULT_CONFIG: IConfig = { const DEFAULT_CONFIG: IConfig = {
MODEL_INDEX: 0, MODEL_INDEX: 0,
LLM_INFO: llms, LLM_INFO: llms,
LANG: getDefaultLanguage() LANG: getDefaultLanguage(), MCP_TIMEOUT_SEC: 60
}; };

View File

@ -1,4 +1,12 @@
{ {
"currentIndex": 0, "currentIndex": 0,
"tabs": [] "tabs": [
{
"name": "Blank test 1",
"icon": "icon-blank",
"type": "blank",
"componentIndex": -1,
"storage": {}
}
]
} }