2025-05-12 14:10:05 +08:00
|
|
|
|
<template>
|
2025-06-12 23:07:42 +08:00
|
|
|
|
<el-drawer :visible.sync="dialogVisible" direction="rtl" size="80%" :wrapperClosable="false" :withHeader="false">
|
2025-05-12 14:10:05 +08:00
|
|
|
|
<!-- 自定义标题区域 -->
|
|
|
|
|
|
<div class="custom-header">
|
|
|
|
|
|
<div class="header-left">
|
|
|
|
|
|
<h3 class="bold-title">功能管理</h3>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button class="custom-close-btn" @click="closeDialog">×</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="function-manager">
|
|
|
|
|
|
<!-- 左侧:未选功能 -->
|
|
|
|
|
|
<div class="function-column">
|
|
|
|
|
|
<div class="column-header">
|
|
|
|
|
|
<h4 class="column-title">未选功能</h4>
|
|
|
|
|
|
<el-button type="text" @click="selectAll" class="select-all-btn">全选</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="function-list">
|
2025-05-29 00:58:20 +08:00
|
|
|
|
<div v-if="unselected.length">
|
|
|
|
|
|
<div v-for="func in unselected" :key="func.name" class="function-item">
|
|
|
|
|
|
<el-checkbox :label="func.name" v-model="selectedNames" @change="(val) => handleCheckboxChange(func, val)"
|
2025-06-09 11:39:24 +08:00
|
|
|
|
@click.native.stop></el-checkbox>
|
2025-05-29 00:58:20 +08:00
|
|
|
|
<div class="func-tag" @click="handleFunctionClick(func)">
|
2025-06-09 11:39:24 +08:00
|
|
|
|
<div class="color-dot" :style="{ backgroundColor: getFunctionColor(func.name) }"></div>
|
2025-05-29 00:58:20 +08:00
|
|
|
|
<span>{{ func.name }}</span>
|
|
|
|
|
|
</div>
|
2025-05-12 16:48:34 +08:00
|
|
|
|
</div>
|
2025-05-29 00:58:20 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else style="display: flex; justify-content: center; align-items: center;">
|
2025-06-09 11:39:24 +08:00
|
|
|
|
<el-empty description="没有更多的插件了" />
|
2025-05-12 14:10:05 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 中间:已选功能 -->
|
|
|
|
|
|
<div class="function-column">
|
|
|
|
|
|
<div class="column-header">
|
|
|
|
|
|
<h4 class="column-title">已选功能</h4>
|
|
|
|
|
|
<el-button type="text" @click="deselectAll" class="select-all-btn">全选</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="function-list">
|
2025-05-29 00:58:20 +08:00
|
|
|
|
<div v-if="selectedList.length > 0">
|
|
|
|
|
|
<div v-for="func in selectedList" :key="func.name" class="function-item">
|
|
|
|
|
|
<el-checkbox :label="func.name" v-model="selectedNames" @change="(val) => handleCheckboxChange(func, val)"
|
2025-06-09 11:39:24 +08:00
|
|
|
|
@click.native.stop></el-checkbox>
|
2025-05-29 00:58:20 +08:00
|
|
|
|
<div class="func-tag" @click="handleFunctionClick(func)">
|
2025-06-09 11:39:24 +08:00
|
|
|
|
<div class="color-dot" :style="{ backgroundColor: getFunctionColor(func.name) }"></div>
|
2025-05-29 00:58:20 +08:00
|
|
|
|
<span>{{ func.name }}</span>
|
|
|
|
|
|
</div>
|
2025-05-12 16:48:34 +08:00
|
|
|
|
</div>
|
2025-05-12 14:10:05 +08:00
|
|
|
|
</div>
|
2025-05-29 00:58:20 +08:00
|
|
|
|
<div v-else style="display: flex; justify-content: center; align-items: center;">
|
2025-06-09 11:39:24 +08:00
|
|
|
|
<el-empty description="请选择插件功能" />
|
2025-05-29 00:58:20 +08:00
|
|
|
|
</div>
|
2025-05-12 14:10:05 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 右侧:参数配置 -->
|
|
|
|
|
|
<div class="params-column">
|
|
|
|
|
|
<h4 v-if="currentFunction" class="column-title">参数配置 - {{ currentFunction.name }}</h4>
|
2025-05-29 00:58:20 +08:00
|
|
|
|
<div v-if="currentFunction" class="params-container">
|
2025-06-12 23:07:42 +08:00
|
|
|
|
<el-form :model="currentFunction" class="param-form">
|
2025-05-29 00:58:20 +08:00
|
|
|
|
<!-- 遍历 fieldsMeta,而不是 params 的 keys -->
|
2025-06-11 23:05:38 +08:00
|
|
|
|
<div v-if="currentFunction.fieldsMeta.length == 0">
|
|
|
|
|
|
<el-empty :description="currentFunction.name + ' 无需配置参数'" />
|
|
|
|
|
|
</div>
|
2025-06-09 11:39:24 +08:00
|
|
|
|
<el-form-item v-for="field in currentFunction.fieldsMeta" :key="field.key" :label="field.label"
|
2025-06-12 23:07:42 +08:00
|
|
|
|
class="param-item" :class="{ 'textarea-field': field.type === 'array' || field.type === 'json' }">
|
2025-05-29 00:58:20 +08:00
|
|
|
|
<template #label>
|
2025-06-11 23:05:38 +08:00
|
|
|
|
<span style="font-size: 16px; margin-right: 6px;">{{ field.label }}</span>
|
2025-05-29 00:58:20 +08:00
|
|
|
|
<el-tooltip effect="dark" :content="fieldRemark(field)" placement="top">
|
|
|
|
|
|
<img src="@/assets/home/info.png" alt="" class="info-icon">
|
|
|
|
|
|
</el-tooltip>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<!-- ARRAY -->
|
2025-06-12 23:07:42 +08:00
|
|
|
|
<el-input v-if="field.type === 'array'" type="textarea" v-model="currentFunction.params[field.key]"
|
|
|
|
|
|
@change="val => handleParamChange(currentFunction, field.key, val)" />
|
2025-05-29 00:58:20 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- JSON -->
|
2025-06-09 11:39:24 +08:00
|
|
|
|
<el-input v-else-if="field.type === 'json'" type="textarea" :rows="6" placeholder="请输入合法的 JSON"
|
|
|
|
|
|
v-model="textCache[field.key]" @blur="flushJson(field)" />
|
2025-05-29 00:58:20 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- number -->
|
2025-06-09 11:39:24 +08:00
|
|
|
|
<el-input-number v-else-if="field.type === 'number'" :value="currentFunction.params[field.key]"
|
|
|
|
|
|
@change="val => handleParamChange(currentFunction, field.key, val)" />
|
2025-05-29 00:58:20 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- boolean -->
|
2025-06-09 11:39:24 +08:00
|
|
|
|
<el-switch v-else-if="field.type === 'boolean' || field.type === 'bool'"
|
|
|
|
|
|
:value="currentFunction.params[field.key]"
|
|
|
|
|
|
@change="val => handleParamChange(currentFunction, field.key, val)" />
|
2025-05-29 00:58:20 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- string or fallback -->
|
2025-06-09 11:39:24 +08:00
|
|
|
|
<el-input v-else v-model="currentFunction.params[field.key]"
|
|
|
|
|
|
@change="val => handleParamChange(currentFunction, field.key, val)" />
|
2025-05-29 00:58:20 +08:00
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
</div>
|
2025-05-12 14:10:05 +08:00
|
|
|
|
<div v-else class="empty-tip">请选择已配置的功能进行参数设置</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-06-24 14:39:18 +08:00
|
|
|
|
<!-- MCP区域 -->
|
|
|
|
|
|
<div class="mcp-access-point">
|
|
|
|
|
|
<div class="mcp-container">
|
|
|
|
|
|
<!-- 左侧区域 -->
|
2025-06-27 11:33:12 +08:00
|
|
|
|
<div class="mcp-left">
|
|
|
|
|
|
<div class="mcp-header">
|
|
|
|
|
|
<h3 class="bold-title">MCP接入点</h3>
|
2025-06-24 14:39:18 +08:00
|
|
|
|
</div>
|
2025-06-27 11:33:12 +08:00
|
|
|
|
<div class="url-header">
|
|
|
|
|
|
<div class="address-desc">
|
|
|
|
|
|
<span>以下是智能体的MCP接入点地址。</span>
|
|
|
|
|
|
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/mcp-endpoint-integration.md"
|
2025-06-27 15:30:18 +08:00
|
|
|
|
target="_blank" class="doc-link">查看接入点使用文档</a>
|
2025-06-27 11:33:12 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-input v-model="mcpUrl" readonly class="url-input">
|
|
|
|
|
|
<template #suffix>
|
|
|
|
|
|
<el-button @click="copyUrl" class="inner-copy-btn" icon="el-icon-document-copy">
|
|
|
|
|
|
复制
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-input>
|
2025-06-24 14:39:18 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 右侧区域 -->
|
|
|
|
|
|
<div class="mcp-right">
|
2025-06-27 11:33:12 +08:00
|
|
|
|
<div class="mcp-header">
|
|
|
|
|
|
<h3 class="bold-title">接入点状态</h3>
|
|
|
|
|
|
</div>
|
2025-06-24 14:39:18 +08:00
|
|
|
|
<div class="status-container">
|
|
|
|
|
|
<span class="status-indicator" :class="mcpStatus"></span>
|
2025-06-27 11:33:12 +08:00
|
|
|
|
<span class="status-text">{{
|
|
|
|
|
|
mcpStatus === 'connected' ? '已连接' :
|
|
|
|
|
|
mcpStatus === 'loading' ? '加载中...' : '未连接'
|
|
|
|
|
|
}}</span>
|
2025-06-24 14:39:18 +08:00
|
|
|
|
<button class="refresh-btn" @click="refreshStatus">
|
|
|
|
|
|
<span class="refresh-icon">↻</span>
|
|
|
|
|
|
<span>刷新</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2025-06-27 11:33:12 +08:00
|
|
|
|
<div class="mcp-tools-list">
|
|
|
|
|
|
<div v-if="mcpTools.length > 0" class="tools-grid">
|
|
|
|
|
|
<el-button v-for="tool in mcpTools" :key="tool" size="small" class="tool-btn" plain>
|
|
|
|
|
|
{{ tool }}
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="no-tools">
|
|
|
|
|
|
<span>暂无可用工具</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-06-24 14:39:18 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-05-12 14:10:05 +08:00
|
|
|
|
<div class="drawer-footer">
|
|
|
|
|
|
<el-button @click="closeDialog">取消</el-button>
|
|
|
|
|
|
<el-button type="primary" @click="saveSelection">保存配置</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-drawer>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
2025-06-27 11:33:12 +08:00
|
|
|
|
import Api from '@/apis/api';
|
|
|
|
|
|
|
2025-05-12 14:10:05 +08:00
|
|
|
|
export default {
|
|
|
|
|
|
props: {
|
|
|
|
|
|
value: Boolean,
|
|
|
|
|
|
functions: {
|
|
|
|
|
|
type: Array,
|
|
|
|
|
|
default: () => []
|
2025-05-29 00:58:20 +08:00
|
|
|
|
},
|
|
|
|
|
|
allFunctions: {
|
|
|
|
|
|
type: Array,
|
|
|
|
|
|
default: () => []
|
2025-06-27 11:33:12 +08:00
|
|
|
|
},
|
|
|
|
|
|
agentId: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
required: true
|
2025-05-12 14:10:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
2025-05-29 00:58:20 +08:00
|
|
|
|
textCache: {},
|
2025-05-12 14:10:05 +08:00
|
|
|
|
dialogVisible: this.value,
|
|
|
|
|
|
selectedNames: [],
|
|
|
|
|
|
currentFunction: null,
|
|
|
|
|
|
modifiedFunctions: {},
|
|
|
|
|
|
functionColorMap: [
|
|
|
|
|
|
'#FF6B6B', '#4ECDC4', '#45B7D1',
|
|
|
|
|
|
'#96CEB4', '#FFEEAD', '#D4A5A5', '#A2836E'
|
|
|
|
|
|
],
|
|
|
|
|
|
tempFunctions: {},
|
|
|
|
|
|
// 添加一个标志位来跟踪是否已经保存
|
|
|
|
|
|
hasSaved: false,
|
2025-05-12 16:48:34 +08:00
|
|
|
|
loading: false,
|
2025-06-27 11:33:12 +08:00
|
|
|
|
|
|
|
|
|
|
mcpUrl: "",
|
2025-06-24 14:39:18 +08:00
|
|
|
|
mcpStatus: "disconnected",
|
2025-06-27 11:33:12 +08:00
|
|
|
|
mcpTools: [],
|
2025-05-12 14:10:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
computed: {
|
|
|
|
|
|
selectedList() {
|
|
|
|
|
|
return this.allFunctions.filter(f => this.selectedNames.includes(f.name));
|
|
|
|
|
|
},
|
|
|
|
|
|
unselected() {
|
|
|
|
|
|
return this.allFunctions.filter(f => !this.selectedNames.includes(f.name));
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
watch: {
|
2025-05-29 00:58:20 +08:00
|
|
|
|
currentFunction(newFn) {
|
|
|
|
|
|
if (!newFn) return;
|
|
|
|
|
|
// 对每个字段,如果是 array 或 json,就在 textCache 里生成初始字符串
|
|
|
|
|
|
newFn.fieldsMeta.forEach(f => {
|
|
|
|
|
|
const v = newFn.params[f.key];
|
|
|
|
|
|
if (f.type === 'array') {
|
|
|
|
|
|
this.$set(this.textCache, f.key, Array.isArray(v) ? v.join('\n') : '');
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (f.type === 'json') {
|
|
|
|
|
|
try {
|
|
|
|
|
|
this.$set(this.textCache, f.key, JSON.stringify(v ?? {}, null, 2));
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
this.$set(this.textCache, f.key, '');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
value(v) {
|
|
|
|
|
|
this.dialogVisible = v;
|
|
|
|
|
|
if (v) {
|
|
|
|
|
|
// 对话框打开时,初始化选中态
|
2025-05-12 14:10:05 +08:00
|
|
|
|
this.selectedNames = this.functions.map(f => f.name);
|
2025-05-29 00:58:20 +08:00
|
|
|
|
// 把后端传来的 this.functions(带 params)merge 到 allFunctions 上
|
|
|
|
|
|
this.functions.forEach(saved => {
|
|
|
|
|
|
const idx = this.allFunctions.findIndex(f => f.name === saved.name);
|
|
|
|
|
|
if (idx >= 0) {
|
|
|
|
|
|
// 保留用户之前在 saved.params 上的改动
|
2025-06-09 11:39:24 +08:00
|
|
|
|
this.allFunctions[idx].params = { ...saved.params };
|
2025-05-29 00:58:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
// 右侧默认指向第一个
|
2025-05-12 14:10:05 +08:00
|
|
|
|
this.currentFunction = this.selectedList[0] || null;
|
2025-06-27 11:33:12 +08:00
|
|
|
|
|
|
|
|
|
|
// 加载MCP数据
|
|
|
|
|
|
this.loadMcpAddress();
|
|
|
|
|
|
this.loadMcpTools();
|
2025-05-12 14:10:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
dialogVisible(newVal) {
|
|
|
|
|
|
this.$emit('input', newVal);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
methods: {
|
2025-06-24 14:39:18 +08:00
|
|
|
|
copyUrl() {
|
|
|
|
|
|
const textarea = document.createElement('textarea');
|
|
|
|
|
|
textarea.value = this.mcpUrl;
|
|
|
|
|
|
textarea.style.position = 'fixed'; // 防止页面滚动
|
|
|
|
|
|
document.body.appendChild(textarea);
|
|
|
|
|
|
textarea.select();
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const successful = document.execCommand('copy');
|
|
|
|
|
|
if (successful) {
|
|
|
|
|
|
this.$message.success('已复制到剪贴板');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.$message.error('复制失败,请手动复制');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
this.$message.error('复制失败,请手动复制');
|
|
|
|
|
|
console.error('复制失败:', err);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
document.body.removeChild(textarea);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
refreshStatus() {
|
|
|
|
|
|
this.mcpStatus = "loading";
|
2025-06-27 11:33:12 +08:00
|
|
|
|
this.loadMcpTools();
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 加载MCP接入点地址
|
|
|
|
|
|
loadMcpAddress() {
|
|
|
|
|
|
Api.agent.getAgentMcpAccessAddress(this.agentId, (res) => {
|
|
|
|
|
|
if (res.data.code === 0) {
|
|
|
|
|
|
this.mcpUrl = res.data.data || "";
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.mcpUrl = "";
|
|
|
|
|
|
console.error('获取MCP地址失败:', res.data.msg);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 加载MCP工具列表
|
|
|
|
|
|
loadMcpTools() {
|
|
|
|
|
|
Api.agent.getAgentMcpToolsList(this.agentId, (res) => {
|
|
|
|
|
|
if (res.data.code === 0) {
|
|
|
|
|
|
this.mcpTools = res.data.data || [];
|
|
|
|
|
|
// 根据工具列表更新状态
|
|
|
|
|
|
this.mcpStatus = this.mcpTools.length > 0 ? "connected" : "disconnected";
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.mcpTools = [];
|
|
|
|
|
|
this.mcpStatus = "disconnected";
|
|
|
|
|
|
console.error('获取MCP工具列表失败:', res.data.msg);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-06-24 14:39:18 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
2025-05-29 00:58:20 +08:00
|
|
|
|
flushArray(key) {
|
|
|
|
|
|
const text = this.textCache[key] || '';
|
|
|
|
|
|
const arr = text
|
2025-06-09 11:39:24 +08:00
|
|
|
|
.split('\n')
|
|
|
|
|
|
.map(s => s.trim())
|
|
|
|
|
|
.filter(Boolean);
|
2025-05-29 00:58:20 +08:00
|
|
|
|
this.handleParamChange(this.currentFunction, key, arr);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
flushJson(field) {
|
|
|
|
|
|
const key = field.key;
|
|
|
|
|
|
if (!key) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const text = this.textCache[key] || '';
|
|
|
|
|
|
try {
|
|
|
|
|
|
const obj = JSON.parse(text);
|
|
|
|
|
|
this.handleParamChange(this.currentFunction, key, obj);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
this.$message.error(`${this.currentFunction.name}的${key}字段格式错误:JSON格式有误`);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-05-12 14:10:05 +08:00
|
|
|
|
handleFunctionClick(func) {
|
|
|
|
|
|
if (this.selectedNames.includes(func.name)) {
|
2025-05-29 00:58:20 +08:00
|
|
|
|
const tempFunc = this.tempFunctions[func.name];
|
|
|
|
|
|
this.currentFunction = tempFunc ? tempFunc : func;
|
2025-05-12 14:10:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
handleParamChange(func, key, value) {
|
|
|
|
|
|
if (!this.tempFunctions[func.name]) {
|
|
|
|
|
|
this.tempFunctions[func.name] = JSON.parse(JSON.stringify(func));
|
|
|
|
|
|
}
|
|
|
|
|
|
this.tempFunctions[func.name].params[key] = value;
|
|
|
|
|
|
},
|
|
|
|
|
|
handleCheckboxChange(func, checked) {
|
|
|
|
|
|
if (checked) {
|
|
|
|
|
|
if (!this.selectedNames.includes(func.name)) {
|
|
|
|
|
|
this.selectedNames = [...this.selectedNames, func.name];
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.selectedNames = this.selectedNames.filter(name => name !== func.name);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-12 16:48:34 +08:00
|
|
|
|
if (this.selectedList.length > 0) {
|
|
|
|
|
|
this.currentFunction = this.selectedList[0];
|
|
|
|
|
|
} else {
|
2025-05-12 14:10:05 +08:00
|
|
|
|
this.currentFunction = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
selectAll() {
|
|
|
|
|
|
this.selectedNames = [...this.allFunctions.map(f => f.name)];
|
|
|
|
|
|
if (this.selectedList.length > 0) {
|
|
|
|
|
|
this.currentFunction = JSON.parse(JSON.stringify(this.selectedList[0]));
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
deselectAll() {
|
|
|
|
|
|
this.selectedNames = [];
|
|
|
|
|
|
this.currentFunction = null;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
closeDialog() {
|
|
|
|
|
|
this.tempFunctions = {};
|
|
|
|
|
|
this.selectedNames = this.functions.map(f => f.name);
|
|
|
|
|
|
this.currentFunction = null;
|
|
|
|
|
|
this.dialogVisible = false;
|
|
|
|
|
|
this.$emit('input', false);
|
|
|
|
|
|
this.$emit('dialog-closed', false);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
saveSelection() {
|
|
|
|
|
|
Object.keys(this.tempFunctions).forEach(name => {
|
|
|
|
|
|
this.modifiedFunctions[name] = JSON.parse(JSON.stringify(this.tempFunctions[name]));
|
|
|
|
|
|
});
|
|
|
|
|
|
this.tempFunctions = {};
|
|
|
|
|
|
this.hasSaved = true;
|
|
|
|
|
|
|
|
|
|
|
|
const selected = this.selectedList.map(f => {
|
|
|
|
|
|
const modified = this.modifiedFunctions[f.name];
|
2025-05-29 00:58:20 +08:00
|
|
|
|
return {
|
|
|
|
|
|
id: f.id,
|
|
|
|
|
|
name: f.name,
|
|
|
|
|
|
params: modified
|
2025-06-09 11:39:24 +08:00
|
|
|
|
? { ...modified.params }
|
|
|
|
|
|
: { ...f.params }
|
2025-05-29 00:58:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-05-12 14:10:05 +08:00
|
|
|
|
|
|
|
|
|
|
this.$emit('update-functions', selected);
|
|
|
|
|
|
this.dialogVisible = false;
|
|
|
|
|
|
// 通知父组件对话框已关闭且已保存
|
|
|
|
|
|
this.$emit('dialog-closed', true);
|
|
|
|
|
|
},
|
|
|
|
|
|
getFunctionColor(name) {
|
|
|
|
|
|
const hash = [...name].reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
2025-05-29 00:58:20 +08:00
|
|
|
|
return this.functionColorMap[hash % this.functionColorMap.length];
|
|
|
|
|
|
},
|
|
|
|
|
|
fieldRemark(field) {
|
|
|
|
|
|
let description = (field && field.label) ? field.label : '';
|
|
|
|
|
|
if (field.default) {
|
|
|
|
|
|
description += `(默认值:${field.default})`;
|
|
|
|
|
|
}
|
|
|
|
|
|
return description;
|
|
|
|
|
|
},
|
2025-05-12 14:10:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
|
.function-manager {
|
|
|
|
|
|
display: grid;
|
2025-05-29 00:58:20 +08:00
|
|
|
|
grid-template-columns: max-content max-content 1fr;
|
2025-05-12 14:10:05 +08:00
|
|
|
|
gap: 12px;
|
2025-06-24 14:39:18 +08:00
|
|
|
|
height: calc(58vh);
|
2025-05-12 14:10:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.custom-header {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding: 20px 24px;
|
|
|
|
|
|
border-bottom: 1px solid #EBEEF5;
|
|
|
|
|
|
|
|
|
|
|
|
.header-left {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bold-title {
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.select-all-btn {
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
height: auto;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.function-column {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
width: auto;
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
border-right: 1px solid #EBEEF5;
|
|
|
|
|
|
scrollbar-width: none;
|
2025-05-29 00:58:20 +08:00
|
|
|
|
overflow-x: hidden;
|
2025-05-12 14:10:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.function-column::-webkit-scrollbar {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.function-list {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2025-05-29 00:58:20 +08:00
|
|
|
|
gap: 8px;
|
2025-05-12 14:10:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.function-item {
|
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
margin: 4px 0;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
transition: background-color 0.2s;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
background-color: #f5f7fa;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.params-column {
|
|
|
|
|
|
min-width: 280px;
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
scrollbar-width: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.params-column::-webkit-scrollbar {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.column-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.column-title {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.func-tag {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2025-05-12 16:48:34 +08:00
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
flex-grow: 1;
|
|
|
|
|
|
margin-left: 8px;
|
2025-05-12 14:10:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.color-dot {
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
width: 8px;
|
|
|
|
|
|
height: 8px;
|
|
|
|
|
|
margin-right: 8px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.param-form {
|
2025-05-29 00:58:20 +08:00
|
|
|
|
.param-item {
|
|
|
|
|
|
font-size: 16px;
|
2025-06-12 23:07:42 +08:00
|
|
|
|
|
|
|
|
|
|
&.textarea-field {
|
|
|
|
|
|
::v-deep .el-form-item__content {
|
|
|
|
|
|
margin-left: 0 !important;
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
::v-deep .el-form-item__label {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
width: 100% !important;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-05-29 00:58:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.param-input {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-12 14:10:05 +08:00
|
|
|
|
::v-deep .el-form-item {
|
|
|
|
|
|
display: flex;
|
2025-06-12 23:07:42 +08:00
|
|
|
|
flex-direction: column;
|
2025-05-12 14:10:05 +08:00
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
|
|
|
|
|
|
.el-form-item__label {
|
|
|
|
|
|
font-size: 14px !important;
|
|
|
|
|
|
color: #606266;
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
padding-right: 10px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
width: auto !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-form-item__content {
|
|
|
|
|
|
margin-left: 0 !important;
|
|
|
|
|
|
flex-grow: 1;
|
|
|
|
|
|
|
|
|
|
|
|
.el-input__inner {
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
padding-left: 8px;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.params-container {
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
min-width: 280px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.empty-tip {
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.drawer-footer {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
bottom: 0;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
border-top: 1px solid #e8e8e8;
|
|
|
|
|
|
padding: 10px 16px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-icon {
|
|
|
|
|
|
width: 16px;
|
|
|
|
|
|
height: 16px;
|
|
|
|
|
|
margin-right: 1vh;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.custom-close-btn {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
right: 10px;
|
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
|
width: 35px;
|
|
|
|
|
|
height: 35px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
border: 2px solid #cfcfcf;
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
font-size: 30px;
|
|
|
|
|
|
font-weight: lighter;
|
|
|
|
|
|
color: #cfcfcf;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
z-index: 1;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.custom-close-btn:hover {
|
|
|
|
|
|
color: #409EFF;
|
|
|
|
|
|
border-color: #409EFF;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-12 16:48:34 +08:00
|
|
|
|
::v-deep .el-checkbox__label {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
2025-06-24 14:39:18 +08:00
|
|
|
|
|
|
|
|
|
|
.mcp-access-point {
|
|
|
|
|
|
border-top: 1px solid #EBEEF5;
|
|
|
|
|
|
padding: 20px 24px;
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.mcp-header {
|
|
|
|
|
|
.bold-title {
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
margin: 5px 0 30px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.mcp-container {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 30px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-27 11:33:12 +08:00
|
|
|
|
.mcp-left,
|
|
|
|
|
|
.mcp-right {
|
2025-06-24 14:39:18 +08:00
|
|
|
|
flex: 1;
|
2025-07-01 17:31:58 +08:00
|
|
|
|
padding-bottom: 50px;
|
2025-06-24 14:39:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.url-header {
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
color: black;
|
2025-06-27 11:33:12 +08:00
|
|
|
|
|
2025-06-24 14:39:18 +08:00
|
|
|
|
h4 {
|
|
|
|
|
|
margin: 0 0 15px 0;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: normal;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.address-desc {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
|
|
|
|
|
|
.doc-link {
|
|
|
|
|
|
color: #1677ff;
|
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
|
margin-left: 4px;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
text-decoration: underline;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.url-input {
|
|
|
|
|
|
border-radius: 4px 0 0 4px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
height: 36px;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
background-color: #f5f5f5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-27 11:33:12 +08:00
|
|
|
|
::v-deep .el-input__inner {
|
2025-06-24 14:39:18 +08:00
|
|
|
|
background-color: #f5f5f5;
|
2025-06-27 11:33:12 +08:00
|
|
|
|
padding-right: 80px;
|
2025-06-24 14:39:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.url-input {
|
|
|
|
|
|
|
|
|
|
|
|
::v-deep .el-input__suffix {
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2025-06-27 11:33:12 +08:00
|
|
|
|
padding-right: 10px;
|
2025-06-24 14:39:18 +08:00
|
|
|
|
|
|
|
|
|
|
.inner-copy-btn {
|
|
|
|
|
|
pointer-events: auto;
|
|
|
|
|
|
border: none;
|
2025-06-27 11:33:12 +08:00
|
|
|
|
background: #1677ff;
|
|
|
|
|
|
color: white;
|
2025-06-24 14:39:18 +08:00
|
|
|
|
padding: 6px;
|
2025-06-27 11:33:12 +08:00
|
|
|
|
margin-top: 4px;
|
2025-06-24 14:39:18 +08:00
|
|
|
|
margin-left: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.mcp-right {
|
|
|
|
|
|
h4 {
|
|
|
|
|
|
margin: 0 0 10px 0;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: normal;
|
|
|
|
|
|
color: black;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.status-container {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
|
|
|
|
.status-indicator {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
width: 8px;
|
|
|
|
|
|
height: 8px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
margin-right: 8px;
|
|
|
|
|
|
|
|
|
|
|
|
&.disconnected {
|
2025-06-27 11:33:12 +08:00
|
|
|
|
background-color: #909399;
|
|
|
|
|
|
/* 灰色 - 未连接 */
|
2025-06-24 14:39:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.connected {
|
2025-06-27 11:33:12 +08:00
|
|
|
|
background-color: #67C23A;
|
|
|
|
|
|
/* 绿色 - 已连接 */
|
2025-06-24 14:39:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.loading {
|
2025-06-27 11:33:12 +08:00
|
|
|
|
background-color: #E6A23C;
|
|
|
|
|
|
/* 橙色 - 加载中 */
|
2025-06-24 14:39:18 +08:00
|
|
|
|
animation: pulse 1.5s infinite;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.status-text {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
margin-right: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.refresh-btn {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding: 2px 10px;
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
color: black;
|
|
|
|
|
|
border: 1px solid #DCDFE6;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
background: #1677ff;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border-color: #1677ff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.refresh-icon {
|
|
|
|
|
|
margin-right: 6px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes pulse {
|
2025-06-27 11:33:12 +08:00
|
|
|
|
0% {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
50% {
|
|
|
|
|
|
opacity: 0.4;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
100% {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.mcp-tools-list {
|
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
|
|
|
|
|
|
|
.tools-grid {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tool-btn {
|
|
|
|
|
|
padding: 6px 12px;
|
|
|
|
|
|
border-color: #1677ff;
|
|
|
|
|
|
color: #1677ff;
|
|
|
|
|
|
background-color: white;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
background-color: #1677ff;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border-color: #1677ff;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.no-tools {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
padding: 10px 0;
|
|
|
|
|
|
}
|
2025-06-24 14:39:18 +08:00
|
|
|
|
}
|
2025-05-12 14:10:05 +08:00
|
|
|
|
</style>
|