181 lines
8.1 KiB
HTML
181 lines
8.1 KiB
HTML
{% extends 'base.html' %}
|
|
{% block title %}MCP 工具实验室 - 天璇{% endblock %}
|
|
{% block content %}
|
|
<style>
|
|
.tool-group { margin-bottom: 2rem; }
|
|
.tool-card {
|
|
border: 1px solid #e0e0e0; border-radius: 8px; padding: 1rem; margin: 0.5rem 0;
|
|
cursor: pointer; transition: box-shadow 0.2s;
|
|
}
|
|
.tool-card:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
|
|
.tool-card.selected { border-color: #4361ee; box-shadow: 0 0 0 2px #4361ee33; }
|
|
.tool-name { font-weight: 700; font-size: 1.05rem; color: #4361ee; }
|
|
.tool-desc { color: #666; font-size: 0.9rem; margin: 0.3rem 0; }
|
|
.tool-badge {
|
|
display: inline-block; padding: 0.15rem 0.5rem; border-radius: 4px;
|
|
font-size: 0.75rem; font-weight: 600; margin-right: 0.5rem;
|
|
}
|
|
.badge-core { background: #4361ee; color: #fff; }
|
|
.badge-diag { background: #f72585; color: #fff; }
|
|
.badge-analysis { background: #43aa8b; color: #fff; }
|
|
.param-row { margin: 0.5rem 0; display: flex; align-items: center; gap: 0.5rem; }
|
|
.param-row label { min-width: 140px; font-size: 0.9rem; color: #555; }
|
|
.param-row input, .param-row select { flex: 1; padding: 0.4rem; border: 1px solid #ccc; border-radius: 4px; }
|
|
#resultArea {
|
|
background: #1a1a2e; color: #e0e0e0; padding: 1rem; border-radius: 6px;
|
|
margin-top: 1rem; max-height: 500px; overflow: auto; font-size: 0.8rem;
|
|
display: none; white-space: pre-wrap;
|
|
}
|
|
.executing { opacity: 0.6; pointer-events: none; }
|
|
</style>
|
|
|
|
<div class="card">
|
|
<h2>🔧 MCP 工具实验室</h2>
|
|
<p style="color:#666;">手动选择并执行分析工具。工具按类别分组,只读工具可安全任意调用。</p>
|
|
</div>
|
|
|
|
<div style="display:flex;gap:1rem;flex-wrap:wrap;">
|
|
<!-- Tool list panel -->
|
|
<div style="flex:1;min-width:350px;">
|
|
<div class="tool-group">
|
|
<h3>⚡ 核心工具</h3>
|
|
{% for t in core_tools %}
|
|
<div class="tool-card" data-tool="{{ t.name }}" onclick="selectTool('{{ t.name }}', this)">
|
|
<div><span class="tool-badge badge-core">CORE</span><span class="tool-name">{{ t.name }}</span></div>
|
|
<div class="tool-desc">{{ t.description }}</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<div class="tool-group">
|
|
<h3>🔍 分析工具(只读)</h3>
|
|
{% for t in analysis_tools %}
|
|
<div class="tool-card" data-tool="{{ t.name }}" onclick="selectTool('{{ t.name }}', this)">
|
|
<div><span class="tool-badge badge-analysis">ANALYSIS</span><span class="tool-name">{{ t.name }}</span></div>
|
|
<div class="tool-desc">{{ t.description }}</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<div class="tool-group">
|
|
<h3>🩺 诊断工具</h3>
|
|
{% for t in diag_tools %}
|
|
<div class="tool-card" data-tool="{{ t.name }}" onclick="selectTool('{{ t.name }}', this)">
|
|
<div><span class="tool-badge badge-diag">DIAG</span><span class="tool-name">{{ t.name }}</span></div>
|
|
<div class="tool-desc">{{ t.description }}</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Execution panel -->
|
|
<div style="flex:1;min-width:350px;">
|
|
<div class="card" id="execPanel">
|
|
<h3 id="selectedToolName">选择工具查看参数</h3>
|
|
<div id="toolParams" style="margin:1rem 0;"></div>
|
|
<button class="btn btn-primary" id="execBtn" onclick="executeTool()" disabled style="display:none;">执行</button>
|
|
<div id="executing" style="display:none;color:#666;margin-top:0.5rem;">执行中...</div>
|
|
</div>
|
|
<div id="resultArea"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const toolsMeta = {{ tools_meta|safe }};
|
|
let currentTool = null;
|
|
|
|
function selectTool(name, el) {
|
|
document.querySelectorAll('.tool-card').forEach(c => c.classList.remove('selected'));
|
|
if (el) el.classList.add('selected');
|
|
currentTool = name;
|
|
document.getElementById('selectedToolName').textContent = '📌 ' + name;
|
|
document.getElementById('execBtn').style.display = 'inline-block';
|
|
document.getElementById('execBtn').disabled = false;
|
|
document.getElementById('resultArea').style.display = 'none';
|
|
|
|
// Build parameter form from tool schema
|
|
const tool = toolsMeta.find(t => t.name === name);
|
|
if (!tool || !tool.inputSchema || !tool.inputSchema.properties) {
|
|
document.getElementById('toolParams').innerHTML = '<p style="color:#888;">无需参数</p>';
|
|
return;
|
|
}
|
|
const props = tool.inputSchema.properties;
|
|
const required = tool.inputSchema.required || [];
|
|
let html = '';
|
|
for (const [key, prop] of Object.entries(props)) {
|
|
const isReq = required.includes(key);
|
|
const label = prop.description || key;
|
|
let input;
|
|
if (prop.type === 'boolean') {
|
|
input = `<select id="param_${key}"><option value="true">true</option><option value="false">false</option></select>`;
|
|
} else if (prop.type === 'array') {
|
|
input = `<input type="text" id="param_${key}" placeholder='["val1","val2"]' value='${prop.default || ""}'>`;
|
|
} else if (prop.type === 'number' || prop.type === 'integer') {
|
|
input = `<input type="number" id="param_${key}" value="${prop.default || ""}" step="${prop.type === 'integer' ? 1 : 0.01}">`;
|
|
} else {
|
|
{% if datasets %}
|
|
if (key === 'dataset_id' || key === 'dataset_id_a' || key === 'dataset_id_b') {
|
|
let opts = '<option value="">-- select --</option>';
|
|
{% for ds in datasets %}
|
|
opts += `<option value="${'{{ ds.id }}'}">${'{{ ds.id }}'}</option>`;
|
|
{% endfor %}
|
|
input = `<select id="param_${key}">${opts}</select>`;
|
|
} else {
|
|
input = `<input type="text" id="param_${key}" placeholder="${label}" value='${prop.default || ""}'>`;
|
|
}
|
|
{% else %}
|
|
input = `<input type="text" id="param_${key}" placeholder="${label}" value='${prop.default || ""}'>`;
|
|
{% endif %}
|
|
}
|
|
html += `<div class="param-row"><label>${key}${isReq ? ' *' : ''}</label>${input}</div>`;
|
|
}
|
|
document.getElementById('toolParams').innerHTML = html;
|
|
}
|
|
|
|
async function executeTool() {
|
|
if (!currentTool) return;
|
|
const tool = toolsMeta.find(t => t.name === currentTool);
|
|
if (!tool || !tool.inputSchema || !tool.inputSchema.properties) {
|
|
// No-param tool
|
|
await doExecute(currentTool, {});
|
|
return;
|
|
}
|
|
const params = {};
|
|
for (const key of Object.keys(tool.inputSchema.properties)) {
|
|
const el = document.getElementById('param_' + key);
|
|
if (!el) continue;
|
|
let val = el.value;
|
|
const prop = tool.inputSchema.properties[key];
|
|
if (prop.type === 'boolean') val = val === 'true';
|
|
else if (prop.type === 'number') val = parseFloat(val);
|
|
else if (prop.type === 'integer') val = parseInt(val);
|
|
else if (prop.type === 'array') { try { val = JSON.parse(val); } catch(e) { val = []; } }
|
|
if (val !== '' && val !== null && val !== undefined) params[key] = val;
|
|
}
|
|
await doExecute(currentTool, params);
|
|
}
|
|
|
|
async function doExecute(name, params) {
|
|
const btn = document.getElementById('execBtn');
|
|
const status = document.getElementById('executing');
|
|
const resultArea = document.getElementById('resultArea');
|
|
btn.disabled = true; status.style.display = 'block';
|
|
resultArea.style.display = 'none';
|
|
|
|
try {
|
|
const resp = await fetch('/tools/run/', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': '{{ csrf_token }}' },
|
|
body: JSON.stringify({ tool: name, params: params }),
|
|
});
|
|
const data = await resp.json();
|
|
resultArea.textContent = JSON.stringify(data, null, 2);
|
|
resultArea.style.display = 'block';
|
|
} catch (err) {
|
|
resultArea.textContent = 'Error: ' + err.message;
|
|
resultArea.style.display = 'block';
|
|
}
|
|
btn.disabled = false; status.style.display = 'none';
|
|
}
|
|
</script>
|
|
{% endblock %} |