Files
guo zebin bf910fd3f0 feat: MCP 服务器支持开机自启
- 新增 autostart 字段与后端增删改接口
- 启动时后台自动连接所有 autostart=true 的服务器
- 卡片列表展示「开机启动」徽标,添加/编辑弹窗支持勾选
2026-04-17 16:39:31 +08:00

1982 lines
76 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Fay数字人</title>
<link rel="stylesheet" href="{{ url_for('static',filename='css/index.css') }}"/>
<link rel="stylesheet" href="{{ url_for('static',filename='css/setting.css') }}"/>
<link rel="stylesheet" href="{{ url_for('static',filename='css/element/index.css') }}">
<link rel="icon" href="{{ url_for('static',filename='images/favicon.ico') }}" type="image/x-icon">
<script src="{{ url_for('static',filename='js/vue.js') }}"></script>
<!-- 引入样式 -->
<style>
.mcp-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
padding: 20px;
height: 480px;
overflow-y: scroll;
margin-bottom: 20px;
position: relative;
}
.mcp-card {
background-color: #f8f9fa;
border-radius: 8px;
padding: 20px;
/* 维持三列布局:((100%-gap*2)/3) */
width: calc(33.33% - 20px);
min-width: 250px;
height: 220px; /* 固定卡片高度 */
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
position: relative;
cursor: pointer;
transition: all 0.3s;
border: 2px solid transparent;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.mcp-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.mcp-card.selected {
border: 2px solid #409eff;
background-color: #ecf5ff;
}
.mcp-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.mcp-title {
font-weight: bold;
font-size: 16px;
}
.autostart-badge {
margin-left: 8px;
padding: 1px 8px;
border-radius: 10px;
font-size: 12px;
line-height: 1.4;
background-color: #409eff;
color: #fff;
white-space: nowrap;
display: inline-block;
}
.autostart-badge.hidden {
display: none;
}
.status-online {
color: #67c23a;
display: flex;
align-items: center;
}
.status-offline {
color: #909399;
display: flex;
align-items: center;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 5px;
}
.online-dot {
background-color: #67c23a;
}
.offline-dot {
background-color: #909399;
}
.mcp-info {
margin-bottom: 5px;
display: flex;
justify-content: space-between;
}
.mcp-label {
color: #606266;
}
.mcp-value {
color: #303133;
}
.mcp-actions {
display: flex;
justify-content: space-between;
margin-top: auto; /* 将操作按钮推到底部 */
padding-top: 15px;
}
.mcp-btn {
padding: 8px 20px;
border-radius: 4px;
cursor: pointer;
border: none;
}
.disconnect-btn {
background-color: #f5f7fa;
color: #606266;
border: 1px solid #dcdfe6;
}
.connect-btn {
background-color: #409eff;
color: white;
border: 1px solid #409eff;
}
.restart-btn {
background-color: #67c23a;
color: white;
}
.delete-btn {
color: #f56c6c;
font-size: 14px;
cursor: pointer;
padding: 8px 20px;
border-radius: 4px;
background-color: #fff;
border: 1px solid #f56c6c;
}
.add-btn-container {
display: flex;
justify-content: flex-end;
padding: 20px;
}
.add-btn {
width: 32px;
height: 32px;
border-radius: 4px;
background-color: white;
color: #409eff;
border: 1px solid #dcdfe6;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
cursor: pointer;
}
.counter-container {
margin-left: 20px;
margin-bottom: 20px;
display: flex;
align-items: center;
}
.counter {
font-size: 48px;
color: #409eff;
margin: 0 10px;
}
.counter-icon {
width: 64px;
height: 64px;
}
.mcp-dialog {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: none;
justify-content: center;
align-items: center;
z-index: 100;
}
.mcp-dialog-content {
background-color: white;
width: 400px;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.mcp-dialog-header {
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #f0f0f0;
}
.mcp-dialog-title {
font-size: 18px;
font-weight: bold;
}
.mcp-dialog-close {
cursor: pointer;
font-size: 20px;
}
.mcp-dialog-body {
padding: 20px;
}
.mcp-form-item {
margin-bottom: 20px;
display: flex;
align-items: center;
}
.mcp-label {
display: inline-block;
margin-bottom: 0;
width: 100px;
flex-shrink: 0;
}
.mcp-input {
width: 100%;
padding: 10px;
border: 1px solid #dcdfe6;
border-radius: 4px;
}
.mcp-dialog-footer {
padding: 10px 20px 20px;
display: flex;
justify-content: center;
gap: 12px;
}
.add-server-btn {
padding: 8px 20px;
background-color: #409eff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.add-server-btn:hover {
background-color: #66b1ff;
}
.setting_fay_top {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0px 20px;
background-color: #f8f9fa;
border-bottom: 1px solid #ebeef5;
margin-bottom: 10px;
}
/* 添加tooltip样式 */
.tooltip {
position: relative;
display: inline-block;
}
/* 彻底隐藏 tooltip 弹窗,但保留结构供脚本读取数据 */
.tooltip .tooltip-content {
display: none !important;
}
.tooltip-title {
font-weight: bold;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid #ebeef5;
}
.tooltip-item {
margin-bottom: 5px;
display: flex;
}
.tooltip-label {
font-weight: bold;
width: 100px;
}
.mcp-info-panel {
position: absolute;
left: 0;
right: 0;
bottom: 0;
width: 96.8%;
min-height: 80px;
border: 1px solid #363636;
border-radius: 4px;
padding: 15px;
background-color: #fff;
display: none;
}
.mcp-info-panel.active {
display: block;
}
.mcp-info-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #ebeef5;
color: #303133;
}
.mcp-info-content {
display: flex;
flex-wrap: wrap;
}
.mcp-info-item {
width: 50%;
margin-bottom: 10px;
display: flex;
}
/* 让工具列表和资源列表占满整行 */
.mcp-info-item:nth-child(4),
.mcp-info-item:nth-child(5) {
width: 100%;
flex-direction: column;
align-items: flex-start;
}
/* 工具列表容器样式 */
.tools-container {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 5px;
width: 100%;
}
/* 工具按钮样式 */
.tool-btn {
padding: 6px 12px;
border-radius: 20px;
border: 1px solid #dcdfe6;
background-color: #fff;
color: #606266;
cursor: pointer;
transition: all 0.3s;
font-size: 14px;
display: inline-flex;
align-items: center;
gap: 5px;
user-select: none;
}
.tool-btn:hover {
border-color: #409eff;
color: #409eff;
}
/* 工具启用状态 */
.tool-btn.enabled {
background-color: #409eff;
color: #fff;
border-color: #409eff;
}
.tool-btn.enabled:hover {
background-color: #66b1ff;
border-color: #66b1ff;
}
/* 工具禁用状态 */
.tool-btn.disabled {
background-color: #f5f7fa;
color: #c0c4cc;
border-color: #e4e7ed;
cursor: not-allowed;
}
/* 状态指示点 */
.tool-status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background-color: #c0c4cc;
}
.tool-btn.enabled .tool-status-dot {
background-color: #67c23a;
}
/* 预启动工具 - 已启用状态(红色实心背景) */
.tool-btn.prestart.enabled {
border-color: #f56c6c !important;
background-color: #f56c6c !important;
color: #fff !important;
}
.tool-btn.prestart.enabled .tool-status-dot {
background-color: #67c23a !important; /* 绿色点表示已启用 */
}
.tool-btn.prestart.enabled:hover {
background-color: #e64545 !important;
border-color: #e64545 !important;
}
/* 预启动工具 - 未启用状态(红色边框,白色背景) */
.tool-btn.prestart:not(.enabled) {
border: 2px solid #f56c6c !important;
background-color: #fff !important;
color: #f56c6c !important;
}
.tool-btn.prestart:not(.enabled) .tool-status-dot {
background-color: #c0c4cc !important; /* 灰色点表示未启用 */
}
.tool-btn.prestart:not(.enabled):hover {
background-color: #fef0f0 !important;
border-color: #f56c6c !important;
}
.prestart-tag {
margin-left: auto;
padding: 2px 8px;
border-radius: 12px;
background-color: #f56c6c !important;
color: #fff !important;
font-size: 12px;
line-height: 1.2;
cursor: pointer;
}
.prestart-tag.inactive {
background-color: #e4e7ed;
color: #606266;
}
.mcp-textarea {
width: 100%;
min-height: 120px;
padding: 10px;
border: 1px solid #dcdfe6;
border-radius: 4px;
font-family: Consolas, Monaco, monospace;
resize: vertical;
box-sizing: border-box;
}
.prestart-hint {
font-size: 12px;
color: #909399;
margin: 0 0 8px 0;
text-align: left;
}
.prestart-required {
font-size: 12px;
color: #606266;
margin: 4px 0 12px 0;
text-align: left;
width: 90%;
}
/* Resources 容器样式 */
.resources-container {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 5px;
width: 100%;
}
.resource-tag {
padding: 6px 12px;
border-radius: 20px;
border: 1px solid #e6a23c;
background-color: #fdf6ec;
color: #e6a23c;
font-size: 14px;
display: inline-flex;
align-items: center;
gap: 5px;
cursor: pointer;
transition: all 0.3s;
user-select: none;
}
.resource-tag:hover {
border-color: #d48806;
color: #d48806;
}
.resource-tag.enabled {
background-color: #e6a23c;
color: #fff;
border-color: #e6a23c;
}
.resource-tag.enabled:hover {
background-color: #d48806;
border-color: #d48806;
}
.resource-tag .resource-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background-color: #e6a23c;
}
.resource-tag.enabled .resource-dot {
background-color: #67c23a;
}
.mcp-info-label {
font-weight: bold;
width: 100px;
color: #606266;
flex-shrink: 0;
margin-right: 10px;
}
.mcp-info-value {
color: #303133;
flex: 1;
word-break: break-word;
line-height: 1.5;
}
/* 让 main_right 成为定位上下文 */
.main_right {
position: relative;
}
</style>
</head>
<body>
<div class="main_bg" id="app">
<div class="main_left">
<div class="main_left_logo" ><img src="{{ url_for('static',filename='images/Logo.png') }}" alt="">
</div>
<div class="main_left_menu">
<ul>
<li class="changeImg3"><a :href="'http://' + hostname + ':5000/'"><span class="iconimg1">消息</span></a></li>
<li class="changeImg3"><a :href="'http://' + hostname + ':5000/setting'"><span class="iconimg2">人设</span></a></li>
<li class="changeImg3"><a href="/Page3"><span class="iconimg3">MCP</span></a></li>
</ul>
</div>
<div class="main_left_emoji"><img src="{{ url_for('static',filename='images/emoji.png') }}" alt="" style="width: 80%; margin-top: -25px;">
</div>
</div>
<div class="main_right">
<div class="setting_fay_top"><span style="color: #000; font-size: 20px; padding-right: 10px;">MCP服务器</span><span style="color: #666; font-size: 12px; padding-right: 10px;">Fay支持向其它MCP客户端暴露自带及开启的工具: http://127.0.0.1:8765/sse</span><button class="add-server-btn" id="showAddDialogBtn">添加</button></div>
<!-- 以上是即时信息显示 -->
<div class="setting_fay">
<div class="mcp-container">
<!-- 使用Jinja2模板循环渲染MCP服务器卡片 -->
{% for server in mcp_servers %}
<div class="mcp-card tooltip" data-server-id="{{ server.id }}" data-transport="{{ server.transport or 'sse' }}" data-command="{{ server.command or '' }}" data-args="{{ (server.args | join(' ')) if server.args else '' }}" data-cwd="{{ server.cwd or '' }}" data-key="{{ server.key or '' }}" data-env='{{ server.env | tojson }}' data-autostart="{{ '1' if server.autostart else '0' }}">
<div class="mcp-header">
<div style="display:flex; align-items:center;">
<div class="mcp-title">{{ server.name }}</div>
<span class="autostart-badge{% if not server.autostart %} hidden{% endif %}">开机启动</span>
</div>
<div class="status-{{ server.status }}">
<div class="status-dot {{ server.status }}-dot"></div>
{{ '在线' if server.status == 'online' else '离线' }}
</div>
</div>
<div class="mcp-info">
<span class="mcp-label">服务器:</span>
<span class="mcp-value" data-full-ip="{{ server.ip }}" title="{{ server.ip }}">{{ server.ip }}</span>
</div>
<div class="mcp-info">
<span class="mcp-label">延迟:</span>
<span class="mcp-value">{{ server.latency }}</span>
</div>
<div class="mcp-info">
<span class="mcp-label">连接时间:</span>
<span class="mcp-value">{{ server.connection_time }}</span>
</div>
<div class="mcp-actions">
<div>
<button class="delete-btn" onclick="deleteMcpServer('{{ server.id }}')">删除</button>
<button class="mcp-btn" style="margin-left:8px;" onclick="showEditDialog('{{ server.id }}')">管理</button>
</div>
<div>
<button class="mcp-btn {% if server.status == 'online' %}disconnect-btn{% else %}connect-btn{% endif %}" onclick="toggleConnection('{{ server.id }}')">
{% if server.status == 'online' %}断开{% else %}连接{% endif %}
</button>
<button class="mcp-btn restart-btn" onclick="reconnectServer('{{ server.id }}')">刷 新</button>
</div>
</div>
</div>
{% endfor %}
<!-- Add Button -->
<div class="add-btn-container" style="display: none;">
<button class="add-btn" onclick="showAddDialog()">+</button>
</div>
<!-- Add MCP Dialog -->
<div class="mcp-dialog" id="addMcpDialog">
<div class="mcp-dialog-content">
<div class="mcp-dialog-header">
<div class="mcp-dialog-title">添加MCP</div>
<div class="mcp-dialog-close" onclick="hideAddDialog()">×</div>
</div>
<div class="mcp-dialog-body">
<div class="mcp-form-item">
<label class="mcp-label">MCP 名称:</label>
<input type="text" class="mcp-input" id="mcpName" placeholder="请输入名称">
</div>
<div class="mcp-form-item">
<label class="mcp-label">类型:</label>
<select class="mcp-input" id="mcpTransport">
<option value="sse" selected>SSE(远程)</option>
<option value="stdio">STDIO(本地)</option>
</select>
</div>
<!-- SSE 配置 -->
<div id="sseFields">
<div class="mcp-form-item">
<label class="mcp-label">SSE服务器:</label>
<input type="text" class="mcp-input" id="mcpIp" placeholder="请输入SSE地址">
</div>
<div class="mcp-form-item">
<label class="mcp-label">Key:</label>
<input type="text" class="mcp-input" id="mcpKey" placeholder="(可留空)请输入Key">
</div>
</div>
<!-- STDIO 配置 -->
<div id="stdioFields" style="display:none;">
<div class="mcp-form-item">
<label class="mcp-label">命令:</label>
<input type="text" class="mcp-input" id="mcpCommand" placeholder="例如: node">
</div>
<div class="mcp-form-item">
<label class="mcp-label">参数:</label>
<input type="text" class="mcp-input" id="mcpArgs" placeholder='例如: ./servers/fs-server.js --port 0'>
</div>
<div class="mcp-form-item">
<label class="mcp-label">工作目录:</label>
<input type="text" class="mcp-input" id="mcpCwd" placeholder="可留空,默认当前目录">
</div>
<div class="mcp-form-item">
<label class="mcp-label">环境变量(JSON):</label>
<input type="text" class="mcp-input" id="mcpEnv" placeholder='例如: {"MODE":"local"}'>
</div>
</div>
<div class="mcp-form-item">
<label class="mcp-label">开机启动:</label>
<input type="checkbox" id="mcpAutostart">
</div>
</div>
<div class="mcp-dialog-footer">
<button class="add-server-btn" id="addServerBtn">添加</button>
</div>
</div>
</div>
<!-- Edit MCP Dialog -->
<div class="mcp-dialog" id="editMcpDialog">
<div class="mcp-dialog-content">
<div class="mcp-dialog-header">
<div class="mcp-dialog-title">管理MCP</div>
<div class="mcp-dialog-close" onclick="hideEditDialog()">×</div>
</div>
<div class="mcp-dialog-body">
<div class="mcp-form-item">
<label class="mcp-label">MCP 名称:</label>
<input type="text" class="mcp-input" id="editMcpName" placeholder="请输入名称">
</div>
<div class="mcp-form-item">
<label class="mcp-label">类型:</label>
<select class="mcp-input" id="editMcpTransport">
<option value="sse">SSE(远程)</option>
<option value="stdio">STDIO(本地)</option>
</select>
</div>
<!-- SSE 配置 -->
<div id="editSseFields">
<div class="mcp-form-item">
<label class="mcp-label">SSE服务器:</label>
<input type="text" class="mcp-input" id="editMcpIp" placeholder="请输入SSE地址">
</div>
<div class="mcp-form-item">
<label class="mcp-label">Key:</label>
<input type="text" class="mcp-input" id="editMcpKey" placeholder="(可留空)请输入Key">
</div>
</div>
<!-- STDIO 配置 -->
<div id="editStdioFields" style="display:none;">
<div class="mcp-form-item">
<label class="mcp-label">命令:</label>
<input type="text" class="mcp-input" id="editMcpCommand" placeholder="例如: python">
</div>
<div class="mcp-form-item">
<label class="mcp-label">参数:</label>
<input type="text" class="mcp-input" id="editMcpArgs" placeholder='例如: server.py'>
</div>
<div class="mcp-form-item">
<label class="mcp-label">工作目录:</label>
<input type="text" class="mcp-input" id="editMcpCwd" placeholder="可留空,默认当前目录">
</div>
<div class="mcp-form-item">
<label class="mcp-label">环境变量(JSON):</label>
<input type="text" class="mcp-input" id="editMcpEnv" placeholder='例如: {"MODE":"local"}'>
</div>
</div>
<div class="mcp-form-item">
<label class="mcp-label">开机启动:</label>
<input type="checkbox" id="editAutostart">
</div>
<div class="mcp-form-item">
<label class="mcp-label">保存后重连:</label>
<input type="checkbox" id="editAutoReconnect" checked>
</div>
</div>
<div class="mcp-dialog-footer">
<button class="add-server-btn" id="editSaveBtn" onclick="saveEdit()">保存</button>
</div>
</div>
</div>
</div>
</div>
<!-- Prestart Dialog -->
<div class="mcp-dialog" id="prestartDialog">
<div class="mcp-dialog-content">
<div class="mcp-dialog-header">
<div class="mcp-dialog-title">预启动配置</div>
<div class="mcp-dialog-close" onclick="hidePrestartDialog()">×</div>
</div>
<div class="mcp-dialog-body">
<div class="mcp-form-item">
<label class="mcp-label">服务器:</label>
<div class="mcp-info-value" id="prestartServerLabel">--</div>
</div>
<div class="mcp-form-item">
<label class="mcp-label">工具:</label>
<div class="mcp-info-value" id="prestartToolLabel">--</div>
</div>
<div class="prestart-hint">调用参数支持使用 {{question}} 占位符替换为用户问题。</div>
<div class="prestart-required" id="prestartRequired"></div>
<div id="prestartParamsWrapper">
<textarea class="mcp-textarea" id="prestartParamsInput" placeholder='例如: {"query": "{{question}}"}'></textarea>
</div>
<div class="mcp-form-item" style="margin-top: 10px;">
<label class="mcp-label" style="width: auto; margin-right: 10px;">结果保存到记忆:</label>
<input type="checkbox" id="prestartIncludeHistory" checked>
</div>
</div>
<div class="mcp-dialog-footer">
<button class="add-server-btn" onclick="savePrestartConfig(true)">保存预启动</button>
<button class="delete-btn" onclick="savePrestartConfig(false)">取消预启动</button>
</div>
</div>
</div>
<!-- 信息面板 -->
<div class="mcp-info-panel">
<div class="mcp-info-content">
<div class="mcp-info-item">
<span class="mcp-info-label">服务器:</span>
<span class="mcp-info-value">--</span>
</div>
<div class="mcp-info-item">
<span class="mcp-info-label">服务器类型:</span>
<span class="mcp-info-value">--</span>
</div>
<div class="mcp-info-item">
<span class="mcp-info-label">延迟:</span>
<span class="mcp-info-value">--</span>
</div>
<div class="mcp-info-item">
<span class="mcp-info-label">可用工具:</span>
<div class="mcp-info-value">
<div class="tools-container" id="toolsContainer">
<span style="color: #909399;">--</span>
</div>
</div>
</div>
<div class="mcp-info-item" id="resourcesSection" style="display:none;">
<span class="mcp-info-label">Resources:</span>
<div class="mcp-info-value">
<div class="resources-container" id="resourcesContainer">
<span style="color: #909399;">--</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- import Vue before Element -->
<script src="{{ url_for('static',filename='js/vue.js') }}"></script>
<script src="{{ url_for('static',filename='js/element.js') }}"></script>
<script>
function showAddDialog() {
document.getElementById('addMcpDialog').style.display = 'flex';
}
function hideAddDialog() {
document.getElementById('addMcpDialog').style.display = 'none';
}
// 管理弹窗逻辑
let currentEditServerId = null;
function showEditDialog(serverId) {
currentEditServerId = serverId;
const dialog = document.getElementById('editMcpDialog');
if (!dialog) return;
const card = document.querySelector(`.mcp-card[data-server-id="${serverId}"]`);
if (!card) return;
// 读取现有数据
const nameEl = card.querySelector('.mcp-title');
const name = nameEl ? nameEl.textContent.trim() : '';
const transport = card.getAttribute('data-transport') || 'sse';
const ipEl = card.querySelector('.mcp-info .mcp-value');
const ip = ipEl ? (ipEl.getAttribute('data-full-ip') || ipEl.textContent.trim()) : '';
const key = card.getAttribute('data-key') || '';
const command = card.getAttribute('data-command') || '';
const args = card.getAttribute('data-args') || '';
const cwd = card.getAttribute('data-cwd') || '';
const env = card.getAttribute('data-env') || '';
const autostart = card.getAttribute('data-autostart') === '1';
// 填充表单
document.getElementById('editMcpName').value = name;
document.getElementById('editMcpTransport').value = transport;
document.getElementById('editMcpIp').value = ip;
document.getElementById('editMcpKey').value = key;
document.getElementById('editMcpCommand').value = command;
document.getElementById('editMcpArgs').value = args;
document.getElementById('editMcpCwd').value = cwd;
document.getElementById('editMcpEnv').value = env && env !== 'null' ? env : '';
document.getElementById('editAutostart').checked = autostart;
// 切换显示
const editTransportSel = document.getElementById('editMcpTransport');
const editSseFields = document.getElementById('editSseFields');
const editStdioFields = document.getElementById('editStdioFields');
if (transport === 'stdio') {
editSseFields.style.display = 'none';
editStdioFields.style.display = 'block';
} else {
editSseFields.style.display = 'block';
editStdioFields.style.display = 'none';
}
dialog.style.display = 'flex';
}
function hideEditDialog() {
const dialog = document.getElementById('editMcpDialog');
if (dialog) dialog.style.display = 'none';
currentEditServerId = null;
}
function saveEdit() {
if (!currentEditServerId) return;
const serverId = currentEditServerId;
const name = document.getElementById('editMcpName').value.trim();
const transport = document.getElementById('editMcpTransport').value;
const ip = document.getElementById('editMcpIp').value.trim();
const key = document.getElementById('editMcpKey').value.trim();
const command = document.getElementById('editMcpCommand').value.trim();
const args = document.getElementById('editMcpArgs').value.trim();
const cwd = document.getElementById('editMcpCwd').value.trim();
const envText = document.getElementById('editMcpEnv').value.trim();
const autoReconnect = document.getElementById('editAutoReconnect').checked;
const autostart = document.getElementById('editAutostart').checked;
// 获取保存按钮并设置加载状态
const saveBtn = document.getElementById('editSaveBtn');
const originalText = saveBtn.textContent;
saveBtn.textContent = '保存中...';
saveBtn.disabled = true;
let payload = { name, transport, auto_reconnect: autoReconnect, autostart };
if (transport === 'sse') {
payload.ip = ip;
payload.key = key;
} else {
payload.command = command;
payload.args = args; // 后端负责拆分
payload.cwd = cwd;
if (envText) {
payload.env = envText; // 允许字符串,后端解析
} else {
payload.env = {};
}
}
fetch(`/api/mcp/servers/${serverId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(resp => resp.json())
.then(data => {
if (!data.success) throw new Error(data.message || '更新失败');
// 更新卡片显示
const card = document.querySelector(`.mcp-card[data-server-id="${serverId}"]`);
if (card) {
// 标题
const titleEl = card.querySelector('.mcp-title');
if (titleEl) titleEl.textContent = name;
// 传输类型与属性
card.setAttribute('data-transport', transport);
card.setAttribute('data-autostart', autostart ? '1' : '0');
const badgeEl = card.querySelector('.autostart-badge');
if (badgeEl) {
if (autostart) badgeEl.classList.remove('hidden');
else badgeEl.classList.add('hidden');
}
if (transport === 'sse') {
const ipEl = card.querySelector('.mcp-info .mcp-value');
if (ipEl) {
ipEl.textContent = truncateText(ip);
ipEl.setAttribute('data-full-ip', ip);
}
card.setAttribute('data-key', key);
// 清空本地命令属性
card.setAttribute('data-command', '');
card.setAttribute('data-args', '');
card.setAttribute('data-cwd', '');
card.setAttribute('data-env', '');
} else {
card.setAttribute('data-command', command);
card.setAttribute('data-args', args);
card.setAttribute('data-cwd', cwd);
card.setAttribute('data-env', envText || '');
}
// 如果返回连接状态,更新状态显示
if (data.server && data.server.status) {
const statusDiv = card.querySelector('.status-online, .status-offline');
if (statusDiv) {
if (data.server.status === 'online') {
statusDiv.className = 'status-online';
statusDiv.innerHTML = '<div class="status-dot online-dot"></div>在线';
} else {
statusDiv.className = 'status-offline';
statusDiv.innerHTML = '<div class="status-dot offline-dot"></div>离线';
}
}
const latencyValue = card.querySelector('.mcp-info:nth-child(3) .mcp-value');
const connectionTimeValue = card.querySelector('.mcp-info:nth-child(4) .mcp-value');
if (latencyValue && data.server.latency) latencyValue.textContent = data.server.latency;
if (connectionTimeValue && data.server.connection_time) connectionTimeValue.textContent = data.server.connection_time;
}
}
hideEditDialog();
})
.catch(err => {
alert('保存失败: ' + err.message);
})
.finally(() => {
// 恢复按钮状态
saveBtn.textContent = originalText;
saveBtn.disabled = false;
});
}
// 添加MCP服务器函数
function addMcpServer() {
console.log("添加按钮被点击");
// 获取输入值
const name = document.getElementById('mcpName').value.trim();
const transport = document.getElementById('mcpTransport').value;
const autostart = document.getElementById('mcpAutostart').checked;
let data = { name, transport, autostart };
if (transport === 'stdio') {
const command = document.getElementById('mcpCommand').value.trim();
const argsStr = document.getElementById('mcpArgs').value.trim();
const cwd = document.getElementById('mcpCwd').value.trim();
const envStr = document.getElementById('mcpEnv').value.trim();
if (!name || !command) {
alert('请填写名称与命令');
return;
}
let args = [];
if (argsStr) {
// 简单按空格拆分,支持引号可后续优化
args = argsStr.match(/\"[^\"]*\"|'[^']*'|[^\s]+/g) || [];
args = args.map(s => s.replace(/^"|"$/g, '').replace(/^'|'$/g, ''));
}
let env = {};
if (envStr) {
try { env = JSON.parse(envStr); } catch (e) { alert('环境变量需为合法JSON'); return; }
}
data = { ...data, command, args, cwd, env };
} else {
const ip = document.getElementById('mcpIp').value.trim();
const key = document.getElementById('mcpKey').value.trim();
if (!name || !ip) {
alert('请填写名称与SSE地址');
return;
}
data = { ...data, ip, key };
}
console.log("准备发送数据:", JSON.stringify(data));
// 发送请求到后端
fetch('/api/mcp/servers', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(response => {
console.log("收到响应:", response.status);
if (!response.ok) {
throw new Error('添加服务器失败,状态码: ' + response.status);
}
return response.json();
})
.then(data => {
console.log("添加成功,返回数据:", data);
// 添加成功后刷新页面
window.location.reload();
})
.catch(error => {
console.error('Error:', error);
alert('添加服务器失败: ' + error.message);
});
// 隐藏对话框
hideAddDialog();
}
// 使用事件监听器绑定按钮点击事件
document.addEventListener('DOMContentLoaded', function() {
// 绑定显示对话框按钮
const showAddDialogBtn = document.getElementById('showAddDialogBtn');
if (showAddDialogBtn) {
showAddDialogBtn.addEventListener('click', showAddDialog);
console.log('显示对话框按钮事件已绑定');
} else {
console.error('找不到显示对话框按钮元素');
}
// 绑定添加服务器按钮
const addServerBtn = document.getElementById('addServerBtn');
if (addServerBtn) {
addServerBtn.addEventListener('click', addMcpServer);
console.log('添加服务器按钮事件已绑定');
} else {
console.error('找不到添加服务器按钮元素');
}
// 绑定类型切换,显示/隐藏字段
const transportSel = document.getElementById('mcpTransport');
const sseFields = document.getElementById('sseFields');
const stdioFields = document.getElementById('stdioFields');
if (transportSel) {
transportSel.addEventListener('change', function() {
if (this.value === 'stdio') {
sseFields.style.display = 'none';
stdioFields.style.display = 'block';
} else {
sseFields.style.display = 'block';
stdioFields.style.display = 'none';
}
});
}
// 编辑弹窗切换字段显示
const editTransportSel = document.getElementById('editMcpTransport');
const editSseFields = document.getElementById('editSseFields');
const editStdioFields = document.getElementById('editStdioFields');
if (editTransportSel) {
editTransportSel.addEventListener('change', function() {
if (this.value === 'stdio') {
editSseFields.style.display = 'none';
editStdioFields.style.display = 'block';
} else {
editSseFields.style.display = 'block';
editStdioFields.style.display = 'none';
}
});
}
});
// 删除MCP服务器
function deleteMcpServer(serverId) {
console.log("删除服务器:", serverId);
if (confirm("确定要删除此MCP服务器吗?")) {
fetch(`/api/mcp/servers/${serverId}`, {
method: 'DELETE'
})
.then(response => {
if (!response.ok) {
throw new Error('删除服务器失败,状态码: ' + response.status);
}
return response.json();
})
.then(data => {
console.log("删除成功:", data);
window.location.reload();
})
.catch(error => {
console.error('Error:', error);
alert('删除服务器失败: ' + error.message);
});
}
}
// 断开MCP服务器连接
function disconnectServer(serverId) {
console.log("断开服务器连接:", serverId);
fetch(`/api/mcp/servers/${serverId}/disconnect`, {
method: 'POST'
})
.then(response => {
if (!response.ok) {
throw new Error('断开连接失败,状态码: ' + response.status);
}
return response.json();
})
.then(data => {
console.log("断开连接成功:", data);
// 更新UI而不刷新页面
const card = document.querySelector(`.mcp-card[data-server-id="${serverId}"]`);
if (card) {
const statusDiv = card.querySelector('.status-online, .status-offline');
if (statusDiv) {
statusDiv.className = 'status-offline';
statusDiv.innerHTML = '<div class="status-dot offline-dot"></div>离线';
}
}
})
.catch(error => {
console.error('Error:', error);
alert('断开连接失败: ' + error.message);
});
}
// 连接MCP服务器
function connectServer(serverId) {
console.log("连接服务器:", serverId);
const card = document.querySelector(`.mcp-card[data-server-id="${serverId}"]`);
if (!card) return;
const statusDiv = card.querySelector('.status-online, .status-offline');
const connectBtn = card.querySelector('.disconnect-btn, .connect-btn');
// 显示加载状态
if (statusDiv) {
statusDiv.className = 'status-connecting';
statusDiv.innerHTML = '<div class="status-dot connecting-dot"></div>连接中...';
}
// 发送连接请求
fetch(`/api/mcp/servers/${serverId}/connect`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
console.log('连接响应:', data);
const latencyValue = card.querySelector('.mcp-info:nth-child(3) .mcp-value');
const connectionTimeValue = card.querySelector('.mcp-info:nth-child(4) .mcp-value');
if (data.success) {
// 连接成功
if (statusDiv) {
statusDiv.className = 'status-online';
statusDiv.innerHTML = '<div class="status-dot online-dot"></div>在线';
}
if (connectBtn) {
connectBtn.classList.remove('connect-btn');
connectBtn.classList.add('disconnect-btn');
connectBtn.textContent = '断开';
}
// 更新延迟和连接时间
if (latencyValue && data.server && data.server.latency) {
latencyValue.textContent = data.server.latency;
}
if (connectionTimeValue && data.server && data.server.connection_time) {
connectionTimeValue.textContent = data.server.connection_time;
}
// 显示可用工具信息
if (data.tools && data.tools.length > 0) {
const toolNames = data.tools.map(tool => {
if (typeof tool === 'object' && tool !== null && tool.name) {
return tool.name;
} else if (typeof tool === 'string') {
return tool;
} else {
return '未知工具';
}
});
const toolsInfo = `${toolNames.join(', ')}`;
console.log('可用工具:', toolsInfo);
// 更新信息面板
updateInfoPanel(data.server, toolsInfo);
// 选中当前卡片并更新面板
document.querySelectorAll('.mcp-card.selected').forEach(c => c.classList.remove('selected'));
card.classList.add('selected');
onCardSelected(card);
} else {
// 没有工具时也更新信息面板
updateInfoPanel(data.server, '无可用工具');
// 选中当前卡片并更新面板
document.querySelectorAll('.mcp-card.selected').forEach(c => c.classList.remove('selected'));
card.classList.add('selected');
onCardSelected(card);
}
} else {
// 连接失败
if (statusDiv) {
statusDiv.className = 'status-offline';
statusDiv.innerHTML = '<div class="status-dot offline-dot"></div>离线';
}
if (connectBtn) {
connectBtn.classList.remove('disconnect-btn');
connectBtn.classList.add('connect-btn');
connectBtn.textContent = '连接';
}
alert(data.message || '连接失败');
}
})
.catch(error => {
console.error('连接错误:', error);
// 恢复离线状态
if (statusDiv) {
statusDiv.className = 'status-offline';
statusDiv.innerHTML = '<div class="status-dot offline-dot"></div>离线';
}
if (connectBtn) {
connectBtn.classList.remove('disconnect-btn');
connectBtn.classList.add('connect-btn');
connectBtn.textContent = '连接';
}
alert('连接服务器时发生错误');
});
}
// 切换连接状态
function toggleConnection(serverId) {
console.log("切换连接状态:", serverId);
const card = document.querySelector(`.mcp-card[data-server-id="${serverId}"]`);
if (!card) return;
const statusDiv = card.querySelector('.status-online, .status-offline');
const connectBtn = card.querySelector('.disconnect-btn, .connect-btn');
if (!statusDiv || !connectBtn) return;
const isOnline = statusDiv.classList.contains('status-online');
if (isOnline) {
// 如果当前在线,则断开连接
disconnectServer(serverId);
// 更新按钮样式和文本
connectBtn.classList.remove('disconnect-btn');
connectBtn.classList.add('connect-btn');
connectBtn.textContent = '连接';
} else {
// 如果当前离线,则连接
connectServer(serverId);
}
}
// 重新连接服务器
function reconnectServer(serverId) {
console.log("重新连接服务器:", serverId);
const card = document.querySelector(`.mcp-card[data-server-id="${serverId}"]`);
if (!card) return;
const statusDiv = card.querySelector('.status-online, .status-offline');
const connectBtn = card.querySelector('.disconnect-btn, .connect-btn');
// 显示加载状态
if (statusDiv) {
statusDiv.className = 'status-connecting';
statusDiv.innerHTML = '<div class="status-dot connecting-dot"></div>连接中...';
}
// 先断开连接
fetch(`/api/mcp/servers/${serverId}/disconnect`, {
method: 'POST'
})
.then(response => response.json())
.then(data => {
console.log('断开连接响应:', data);
// 然后重新连接
return fetch(`/api/mcp/servers/${serverId}/connect`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
})
.then(response => response.json())
.then(data => {
console.log('重新连接响应:', data);
const latencyValue = card.querySelector('.mcp-info:nth-child(3) .mcp-value');
const connectionTimeValue = card.querySelector('.mcp-info:nth-child(4) .mcp-value');
if (data.success) {
// 连接成功
if (statusDiv) {
statusDiv.className = 'status-online';
statusDiv.innerHTML = '<div class="status-dot online-dot"></div>在线';
}
if (connectBtn) {
connectBtn.classList.remove('connect-btn');
connectBtn.classList.add('disconnect-btn');
connectBtn.textContent = '断开';
}
// 更新延迟和连接时间
if (latencyValue && data.server && data.server.latency) {
latencyValue.textContent = data.server.latency;
}
if (connectionTimeValue && data.server && data.server.connection_time) {
connectionTimeValue.textContent = data.server.connection_time;
}
// 显示可用工具信息
if (data.tools && data.tools.length > 0) {
const toolNames = data.tools.map(tool => {
if (typeof tool === 'object' && tool !== null && tool.name) {
return tool.name;
} else if (typeof tool === 'string') {
return tool;
} else {
return '未知工具';
}
});
const toolsInfo = `${toolNames.join(', ')}`;
console.log('可用工具:', toolsInfo);
// 更新信息面板
updateInfoPanel(data.server, toolsInfo);
// 选中当前卡片并更新面板
document.querySelectorAll('.mcp-card.selected').forEach(c => c.classList.remove('selected'));
card.classList.add('selected');
onCardSelected(card);
} else {
// 没有工具时也更新信息面板
updateInfoPanel(data.server, '无可用工具');
// 选中当前卡片并更新面板
document.querySelectorAll('.mcp-card.selected').forEach(c => c.classList.remove('selected'));
card.classList.add('selected');
onCardSelected(card);
}
} else {
// 连接失败
if (statusDiv) {
statusDiv.className = 'status-offline';
statusDiv.innerHTML = '<div class="status-dot offline-dot"></div>离线';
}
if (connectBtn) {
connectBtn.classList.remove('disconnect-btn');
connectBtn.classList.add('connect-btn');
connectBtn.textContent = '连接';
}
alert(data.message || '重新连接失败');
}
})
.catch(error => {
console.error('重新连接错误:', error);
// 恢复离线状态
if (statusDiv) {
statusDiv.className = 'status-offline';
statusDiv.innerHTML = '<div class="status-dot offline-dot"></div>离线';
}
if (connectBtn) {
connectBtn.classList.remove('disconnect-btn');
connectBtn.classList.add('connect-btn');
connectBtn.textContent = '连接';
}
alert('重新连接服务器时发生错误');
});
}
// 刷新服务器状态
function refreshServer(serverId) {
console.log("刷新服务器状态:", serverId);
}
// 卡片选中功能
const mcpCards = document.querySelectorAll('.mcp-card');
const container = document.querySelector('.mcp-container');
const infoPanel = document.querySelector('.mcp-info-panel');
const infoValues = document.querySelectorAll('.mcp-info-value');
// 处理长文本,超出长度使用省略号替代
function truncateText(text, maxLength = 30) {
if (!text) return '';
if (text.length <= maxLength) return text;
return text.substring(0, maxLength - 3) + '...';
}
// 首次渲染:处理省略显示
mcpCards.forEach((card) => {
const ipNode = card.querySelector('.mcp-info:nth-child(2) .mcp-value');
if (ipNode) {
const full = ipNode.getAttribute('data-full-ip') || ipNode.textContent;
ipNode.textContent = truncateText(full);
ipNode.title = full;
}
});
// 统一的卡片点击处理
function initCardSelection() {
const cards = document.querySelectorAll('.mcp-card');
cards.forEach((card) => {
card.addEventListener('click', function(e) {
// 如果点击的是操作按钮区域,不处理选中逻辑
if (e.target.closest('.mcp-actions')) {
e.stopPropagation();
return;
}
// 移除所有选中状态
document.querySelectorAll('.mcp-card.selected').forEach(c => c.classList.remove('selected'));
// 添加当前卡片选中状态
this.classList.add('selected');
// 调用选中处理函数
onCardSelected(this);
});
});
}
// 页面加载完成后初始化
function initPage() {
initCardSelection();
// 自动选中第一个在线的服务器卡片
const onlineCard = document.querySelector('.mcp-card .status-online')?.closest('.mcp-card');
const firstCard = document.querySelector('.mcp-card');
if (onlineCard) {
onlineCard.classList.add('selected');
onCardSelected(onlineCard);
} else if (firstCard) {
// 如果没有在线的,选中第一个卡片
firstCard.classList.add('selected');
onCardSelected(firstCard);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initPage);
} else {
initPage();
}
function onCardSelected(card) {
const infoPanel = document.querySelector('.mcp-info-panel');
if (!infoPanel) return;
infoPanel.classList.add('active');
const serverId = card.getAttribute('data-server-id');
const nameNode = card.querySelector('.mcp-title');
const ipNode = card.querySelector('.mcp-info:nth-child(2) .mcp-value');
const latencyNode = card.querySelector('.mcp-info:nth-child(3) .mcp-value');
const connectionTimeNode = card.querySelector('.mcp-info:nth-child(4) .mcp-value');
const statusDiv = card.querySelector('.status-online, .status-offline');
const transport = card.getAttribute('data-transport') || 'sse';
const server = {
name: nameNode ? nameNode.textContent : '--',
ip: ipNode ? (ipNode.getAttribute('data-full-ip') || ipNode.textContent) : '--',
latency: latencyNode ? latencyNode.textContent : '--',
connectionTime: connectionTimeNode ? connectionTimeNode.textContent : '--',
transport: transport
};
// 更新面板基础信息
const ipValue = infoPanel.querySelector('.mcp-info-item:nth-child(1) .mcp-info-value');
const typeValue = infoPanel.querySelector('.mcp-info-item:nth-child(2) .mcp-info-value');
const latencyValue = infoPanel.querySelector('.mcp-info-item:nth-child(3) .mcp-info-value');
const toolsValue = infoPanel.querySelector('.mcp-info-item:nth-child(4) .mcp-info-value');
if (ipValue) {
ipValue.textContent = truncateText(server.ip);
ipValue.title = server.ip;
}
if (typeValue) {
typeValue.textContent = transport === 'stdio' ? 'STDIO(本地)' : 'SSE(远程)';
}
if (latencyValue) {
latencyValue.textContent = server.latency;
}
const toolsContainer = document.getElementById('toolsContainer');
if (toolsContainer) {
toolsContainer.innerHTML = '<span style="color: #909399;">加载中...</span>';
}
// 检查是否在线
const isOnline = statusDiv && statusDiv.classList.contains('status-online');
if (!isOnline) {
const toolsContainer = document.getElementById('toolsContainer');
if (toolsContainer) {
toolsContainer.innerHTML = '<span style="color: #909399;">服务器离线(点击"连接"按钮后可查看工具)</span>';
}
const resSection = document.getElementById('resourcesSection');
if (resSection) resSection.style.display = 'none';
return;
}
// 加载工具列表
fetch(`/api/mcp/servers/${serverId}/tools`)
.then(response => {
if (!response.ok) {
throw new Error('Failed to fetch tools');
}
return response.json();
})
.then(data => {
console.log('工具数据:', data); // 调试信息
const toolsContainer = document.getElementById('toolsContainer');
if (toolsContainer) {
toolsContainer.innerHTML = '';
const tools = data.tools || [];
const serverName = server.name || (nameNode ? nameNode.textContent : '');
if (tools.length > 0) {
tools.forEach(tool => {
console.log('工具对象:', tool); // 调试信息
let toolName = '';
let toolEnabled = true; // 默认启用
let prestart = false;
let prestartParams = {};
let includeHistory = true;
if (typeof tool === 'object' && tool !== null) {
toolName = tool.name || '未知工具';
if (typeof tool.enabled !== 'undefined') {
toolEnabled = tool.enabled;
}
prestart = !!tool.prestart;
if (tool.prestart_params && typeof tool.prestart_params === 'object') {
prestartParams = tool.prestart_params;
}
includeHistory = tool.include_history !== false;
} else if (typeof tool === 'string') {
toolName = tool;
} else {
toolName = '未知工具';
}
// 创建工具按钮
const toolBtn = document.createElement('div');
toolBtn.className = `tool-btn ${toolEnabled ? 'enabled' : ''} ${prestart ? 'prestart' : ''}`;
toolBtn.title = `点击${toolEnabled ? '禁用' : '启用'}工具: ${toolName}`;
toolBtn.dataset.toolName = toolName;
toolBtn.dataset.serverId = serverId;
toolBtn.dataset.enabled = toolEnabled;
toolBtn.dataset.prestartParams = JSON.stringify(prestartParams || {});
toolBtn.dataset.includeHistory = includeHistory;
const statusDot = document.createElement('span');
statusDot.className = 'tool-status-dot';
const nameSpan = document.createElement('span');
nameSpan.textContent = toolName;
const prestartTag = document.createElement('span');
prestartTag.className = `prestart-tag ${prestart ? '' : 'inactive'}`;
prestartTag.textContent = prestart ? '预启动' : '预启动?';
prestartTag.title = '配置预启动参数';
prestartTag.addEventListener('click', function(event) {
event.stopPropagation();
showPrestartDialog(serverId, serverName, toolName, prestartParams, tool.inputSchema || {}, includeHistory);
});
// 添加点击事件
toolBtn.addEventListener('click', function() {
toggleTool(this, serverId, toolName);
});
toolBtn.appendChild(statusDot);
toolBtn.appendChild(nameSpan);
toolBtn.appendChild(prestartTag);
toolsContainer.appendChild(toolBtn);
});
} else {
toolsContainer.innerHTML = '<span style="color: #909399;">无可用工具</span>';
}
}
})
.catch(err => {
console.error('获取工具列表失败:', err);
const toolsContainer = document.getElementById('toolsContainer');
if (toolsContainer) {
toolsContainer.innerHTML = '<span style="color: #f56c6c;">获取工具列表失败</span>';
}
});
// 加载 Resources 列表
fetch(`/api/mcp/servers/${serverId}/resources`)
.then(response => response.json())
.then(data => {
const section = document.getElementById('resourcesSection');
const container = document.getElementById('resourcesContainer');
if (!container || !section) return;
const resources = data.resources || [];
if (resources.length > 0) {
section.style.display = '';
container.innerHTML = '';
resources.forEach(res => {
const isEnabled = res.enabled !== false;
const tag = document.createElement('div');
tag.className = `resource-tag ${isEnabled ? 'enabled' : ''}`;
tag.title = `${res.description || res.uri || ''}\n点击${isEnabled ? '禁用' : '启用'}此资源的 prompt 注入`;
tag.dataset.uri = res.uri;
tag.dataset.serverId = serverId;
tag.dataset.enabled = isEnabled;
tag.addEventListener('click', function() {
toggleResource(this, serverId, res.uri);
});
const dot = document.createElement('span');
dot.className = 'resource-dot';
const nameSpan = document.createElement('span');
nameSpan.textContent = res.name || res.uri;
tag.appendChild(dot);
tag.appendChild(nameSpan);
container.appendChild(tag);
});
} else {
section.style.display = 'none';
}
})
.catch(err => {
console.error('获取 Resources 失败:', err);
const section = document.getElementById('resourcesSection');
if (section) section.style.display = 'none';
});
}
let prestartContext = { serverId: null, serverName: '', toolName: '', params: {}, includeHistory: true };
function showPrestartDialog(serverId, serverName, toolName, params, inputSchema, includeHistory) {
console.log('showPrestartDialog called with:', { serverId, serverName, toolName, params, inputSchema, includeHistory });
prestartContext = {
serverId,
serverName: serverName || '',
toolName,
params: params || {},
schema: inputSchema || {},
includeHistory: includeHistory !== false // default true
};
const dialog = document.getElementById('prestartDialog');
if (!dialog) return;
const serverLabel = document.getElementById('prestartServerLabel');
const toolLabel = document.getElementById('prestartToolLabel');
const input = document.getElementById('prestartParamsInput');
const requiredBox = document.getElementById('prestartRequired');
const paramsWrapper = document.getElementById('prestartParamsWrapper');
const includeHistoryCheck = document.getElementById('prestartIncludeHistory');
if (includeHistoryCheck) includeHistoryCheck.checked = prestartContext.includeHistory;
const schema = inputSchema || {};
const requiredList = Array.isArray(schema.required) ? schema.required : [];
const properties = schema.properties || {};
console.log('Schema properties:', properties, 'required:', requiredList);
if (serverLabel) {
serverLabel.textContent = serverName || serverId;
}
if (toolLabel) {
toolLabel.textContent = toolName || '--';
}
if (requiredBox) {
if (requiredList.length === 0) {
requiredBox.textContent = '无必填参数';
} else {
const items = requiredList.map(key => {
const desc = properties[key]?.description || '';
return desc ? `${key}: ${desc}` : key;
});
requiredBox.textContent = `必填参数:${items.join('')}`;
}
}
if (paramsWrapper) {
paramsWrapper.style.display = requiredList.length === 0 ? 'none' : 'block';
}
const mergedParams = Object.assign({}, params || {});
const defaultByType = (t) => {
if (t === 'array') return [];
if (t === 'object') return {};
if (t === 'integer' || t === 'number') return 0;
if (t === 'boolean') return false;
return '';
};
const getExampleValue = (key) => {
const prop = properties[key] || {};
if (mergedParams[key] !== undefined) return mergedParams[key];
if (prop.examples && prop.examples.length > 0) return prop.examples[0];
if (prop.example !== undefined) return prop.example;
if (prop.default !== undefined) return prop.default;
return defaultByType(prop.type);
};
Object.keys(properties).forEach((key) => {
if (mergedParams[key] === undefined) {
mergedParams[key] = getExampleValue(key);
}
});
requiredList.forEach((key) => {
if (mergedParams[key] === undefined) {
mergedParams[key] = getExampleValue(key);
}
});
if (input) {
try {
if (Object.keys(mergedParams).length > 0) {
input.value = JSON.stringify(mergedParams, null, 2);
} else {
input.value = '';
}
} catch (e) {
input.value = '';
}
}
dialog.style.display = 'flex';
}
function hidePrestartDialog() {
const dialog = document.getElementById('prestartDialog');
if (dialog) {
dialog.style.display = 'none';
}
}
function findToolButton(serverId, toolName) {
try {
const escaped = (window.CSS && CSS.escape) ? CSS.escape(toolName) : toolName;
return document.querySelector(`.tool-btn[data-server-id="${serverId}"][data-tool-name="${escaped}"]`);
} catch (e) {
return document.querySelector(`.tool-btn[data-server-id="${serverId}"]`);
}
}
function savePrestartConfig(enable) {
if (!prestartContext.serverId || !prestartContext.toolName) {
hidePrestartDialog();
return;
}
const input = document.getElementById('prestartParamsInput');
let params = {};
const raw = input ? input.value.trim() : '';
if (raw) {
try {
const parsed = JSON.parse(raw);
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
params = parsed;
} else {
throw new Error('参数需要对象');
}
} catch (err) {
alert('参数格式错误,请输入有效的JSON对象');
return;
}
}
if (enable) {
const schema = prestartContext.schema || {};
const requiredList = Array.isArray(schema.required) ? schema.required : [];
if (requiredList.length > 0) {
const missing = requiredList.filter(key => {
const v = params[key];
if (v === undefined || v === null) return true;
if (typeof v === 'string' && v.trim() === '') return true;
if (Array.isArray(v) && v.length === 0) return true;
if (typeof v === 'object' && !Array.isArray(v) && Object.keys(v).length === 0) return true;
return false;
});
if (missing.length > 0) {
alert(`请填写必填参数:${missing.join('')}`);
return;
}
}
}
const includeHistory = document.getElementById('prestartIncludeHistory').checked;
fetch(`/api/mcp/servers/${prestartContext.serverId}/tools/${encodeURIComponent(prestartContext.toolName)}/prestart`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(enable ? { enabled: true, params, include_history: includeHistory } : { enabled: false })
})
.then(resp => resp.json())
.then(data => {
if (!data.success) {
alert(data.message || '更新预启动配置失败');
return;
}
hidePrestartDialog();
const btn = findToolButton(prestartContext.serverId, prestartContext.toolName);
if (btn) {
// Update dataset with new values from response
btn.dataset.prestartParams = JSON.stringify(data.prestart_params || {});
btn.dataset.includeHistory = data.include_history;
const tag = btn.querySelector('.prestart-tag');
if (enable) {
btn.classList.add('prestart');
if (tag) {
tag.textContent = '预启动';
tag.classList.remove('inactive');
}
} else {
btn.classList.remove('prestart');
if (tag) {
tag.textContent = '预启动?';
tag.classList.add('inactive');
}
}
// Update onclick listener to use new values (needs recreation or just rely on dataset/fetching again?
// Recreating the onclick handler on the tag is tricky without removing old one.
// Simplest is to refresh tools for this card, or update the click handler via a closure if possible.
// But here we can just update the tag's click handler property if we stored it, or
// since we used addEventListener with anonymous function, we can't remove it easily.
// However, the `showPrestartDialog` is called with values from `tool` object in `onCardSelected`.
// We need to update that `tool` object reference or reload the tools list.
// Triggering a reload of the current card's tools is best.
const card = document.querySelector(`.mcp-card[data-server-id="${prestartContext.serverId}"]`);
if (card && card.classList.contains('selected')) {
// Re-trigger selection logic to refresh tools list
// onCardSelected(card); // But we don't have access to onCardSelected here easily?
// It is defined in global scope.
if (typeof onCardSelected === 'function') {
onCardSelected(card);
}
}
}
})
.catch(err => {
console.error('更新预启动配置失败:', err);
alert('更新预启动配置失败');
});
}
// 更新信息面板
function updateInfoPanel(server, toolsInfo) {
const infoPanel = document.querySelector('.mcp-info-panel');
if (!infoPanel) return;
const ipValue = infoPanel.querySelector('.mcp-info-item:nth-child(1) .mcp-info-value');
const latencyValue = infoPanel.querySelector('.mcp-info-item:nth-child(3) .mcp-info-value');
const toolsContainer = document.getElementById('toolsContainer');
if (ipValue && server && server.ip) {
ipValue.textContent = truncateText(server.ip);
ipValue.title = server.ip; // 添加完整地址的 title 属性
}
if (latencyValue && server && server.latency) {
latencyValue.textContent = server.latency;
}
if (toolsContainer && toolsInfo) {
// 简单显示工具信息文本,连接成功后会由onCardSelected更新为按钮
toolsContainer.innerHTML = `<span style="color: #303133;">${toolsInfo}</span>`;
}
// 显示信息面板
infoPanel.classList.add('active');
}
// 切换工具状态
function toggleTool(btn, serverId, toolName) {
const isEnabled = btn.dataset.enabled === 'true';
const newState = !isEnabled;
// 立即更新UI
btn.dataset.enabled = newState;
if (newState) {
btn.classList.add('enabled');
btn.title = `点击禁用工具: ${toolName}`;
} else {
btn.classList.remove('enabled');
btn.title = `点击启用工具: ${toolName}`;
}
// 发送API请求更新状态
fetch(`/api/mcp/servers/${serverId}/tools/${encodeURIComponent(toolName)}/toggle`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ enabled: newState })
})
.then(response => {
if (!response.ok) {
throw new Error('Failed to toggle tool state');
}
return response.json();
})
.then(data => {
console.log(`工具 ${toolName}${newState ? '启用' : '禁用'}`);
})
.catch(err => {
console.error('切换工具状态失败:', err);
// 恢复原状态
btn.dataset.enabled = isEnabled;
if (isEnabled) {
btn.classList.add('enabled');
btn.title = `点击禁用工具: ${toolName}`;
} else {
btn.classList.remove('enabled');
btn.title = `点击启用工具: ${toolName}`;
}
alert(`切换工具状态失败: ${err.message}`);
});
}
// 切换 Resource 启用/禁用
function toggleResource(tag, serverId, uri) {
const isEnabled = tag.dataset.enabled === 'true';
const newState = !isEnabled;
tag.dataset.enabled = newState;
if (newState) {
tag.classList.add('enabled');
} else {
tag.classList.remove('enabled');
}
tag.title = tag.title.replace(
isEnabled ? '禁用' : '启用',
newState ? '禁用' : '启用'
);
fetch(`/api/mcp/servers/${serverId}/resources/toggle`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ uri: uri, enabled: newState })
})
.then(response => {
if (!response.ok) throw new Error('Failed to toggle resource');
return response.json();
})
.then(data => {
console.log(`Resource ${uri}${newState ? '启用' : '禁用'}`);
})
.catch(err => {
console.error('切换 Resource 状态失败:', err);
tag.dataset.enabled = isEnabled;
if (isEnabled) {
tag.classList.add('enabled');
} else {
tag.classList.remove('enabled');
}
});
}
// 初始化Vue实例
new Vue({
el: '#app',
delimiters: ['[[', ']]'],
data: {
hostname: window.location.hostname
}
});
</script>
</body>
</html>