mirror of
https://github.com/xszyou/Fay.git
synced 2026-08-30 17:33:39 +08:00
bf910fd3f0
- 新增 autostart 字段与后端增删改接口 - 启动时后台自动连接所有 autostart=true 的服务器 - 卡片列表展示「开机启动」徽标,添加/编辑弹窗支持勾选
1982 lines
76 KiB
HTML
1982 lines
76 KiB
HTML
<!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>
|