feat: MCP tool lab page + workflow docs + nav link

This commit is contained in:
PM-pinou
2026-07-19 23:09:13 +08:00
parent 7fbf5e2720
commit 06db287c0e
5 changed files with 423 additions and 0 deletions
+2
View File
@@ -22,4 +22,6 @@ urlpatterns = [
path('runs/<int:run_id>/delete/', views.delete_upload, name='delete_upload'),
path('logs/', views.log_viewer, name='log_viewer'),
path('globe/', views.globe_view, name='globe'),
path('tools/', views.tool_lab, name='tool_lab'),
path('tools/run/', views.tool_lab_run, name='tool_lab_run'),
]
+80
View File
@@ -1183,3 +1183,83 @@ def globe_view(request):
resp['Expires'] = '0'
return resp
# ── MCP Tool Lab — manual tool execution ─────────────────────────────
def tool_lab(request):
"""List all available MCP tools with descriptions and input schemas."""
from analysis.tool_registry import get_tools_meta
tools = get_tools_meta()
# Group tools by category
core_names = {'profile_data', 'build_entity_profiles', 'compute_scores',
'run_clustering', 'extract_features', 'detect_anomalies',
'visualize_anomalies'}
diag_names = {'validate_data', 'explore_distributions', 'find_outliers',
'diagnose_clustering', 'compare_datasets', 'export_debug_sample',
'repair_schema'}
analysis_names = {'analyze_patterns', 'analyze_temporal', 'analyze_tls_health',
'analyze_geo_distribution', 'analyze_entity_detail'}
core_tools, diag_tools, analysis_tools, other_tools = [], [], [], []
for t in tools:
if t.name in core_names:
core_tools.append(t)
elif t.name in diag_names:
diag_tools.append(t)
elif t.name in analysis_names:
analysis_tools.append(t)
else:
other_tools.append(t)
# Get list of datasets for parameter autofill
from analysis.session_store import SessionStore
store = SessionStore()
ds_list = store.list_datasets() if hasattr(store, 'list_datasets') else []
import json as _json
# Serialize tools metadata for JS
meta_list = []
for t in tools:
meta_list.append({
'name': t.name,
'description': t.description,
'inputSchema': t.inputSchema,
})
return render(request, 'analysis/tool_lab.html', {
'core_tools': core_tools,
'diag_tools': diag_tools,
'analysis_tools': analysis_tools,
'other_tools': other_tools,
'datasets': ds_list,
'tools_meta': _json.dumps(meta_list, ensure_ascii=False),
})
@csrf_exempt
def tool_lab_run(request):
"""Execute a single MCP tool via AJAX POST."""
if request.method != 'POST':
return JsonResponse({'error': 'POST required'}, status=405)
import json as _json
try:
body = _json.loads(request.body)
except Exception:
return JsonResponse({'error': 'Invalid JSON body'}, status=400)
tool_name = body.get('tool')
params = body.get('params', {})
if not tool_name:
return JsonResponse({'error': 'Missing tool name'}, status=400)
from analysis.tool_registry import handle_call
import asyncio
try:
result = asyncio.run(handle_call(tool_name, params))
return JsonResponse({'status': 'ok', 'result': result})
except ValueError as e:
return JsonResponse({'error': str(e)}, status=400)
except Exception as e:
import traceback
return JsonResponse({'error': str(e), 'traceback': traceback.format_exc()}, status=500)
+159
View File
@@ -0,0 +1,159 @@
# 天璇 TLS 流量分析工作流
## 工具总览(27 个 MCP 工具)
### ⚡ 核心工具(7个)— 执行实际分析,可修改数据
| 工具 | 输入 | 输出 | 何时调用 |
|------|------|------|---------|
| `profile_data` | dataset_id | 列类型/统计/空值率 | 第一步,了解数据 |
| `build_entity_profiles` | dataset_id, [entity_column] | 实体画像数据集 | 确定实体列后 |
| `compute_scores` | dataset_id | proxy_score/risk_level/threat_score | 实体画像完成后 |
| `run_clustering` | dataset_id, cluster_columns, algorithm | 聚类结果 | 需要行为分组时 |
| `extract_features` | dataset_id, cluster_result_id | 各组区分特征 | 聚类完成后 |
| `detect_anomalies` | dataset_id, [contamination] | 异常实体列表 | 评分完成后 |
| `visualize_anomalies` | dataset_id | PCA散点数据 | 评分/检测完成后 |
### 🔍 分析工具(5个)— 只读,安全可随时调用
| 工具 | 功能 | 典型场景 |
|------|------|---------|
| `analyze_patterns` | 热门IP/端口/TLS版本分布 | 快速了解流量结构 |
| `analyze_tls_health` | TLS安全评估(旧版比例/弱加密) | 安全审计 |
| `analyze_geo_distribution` | 地理分布/不可能旅行 | 检测跨国代理 |
| `analyze_entity_detail` | 单实体深度调查 | 可疑实体深入分析 |
| `analyze_temporal` | 时间维度流量模式 | 理解流量周期性 |
### 🩺 诊断工具(7个)— 排查问题
| 工具 | 解决什么问题 |
|------|-------------|
| `validate_data` | 数据质量:空列、常量列、schema不一致 |
| `explore_distributions` | 查看列分布统计,理解数据 |
| `find_outliers` | 发现极端值 |
| `diagnose_clustering` | 聚类效果差时诊断原因 |
| `compare_datasets` | 对比处理前后的数据差异 |
| `export_debug_sample` | 导出JSON供人工检查 |
| `repair_schema` | 修复列名不一致 |
### 其他(8个)
`load_data`, `filter_data`, `preprocess_data`, `evaluate_clustering`, `export_results`, `list_datasets`, `drop_dataset`, `clone_dataset`
---
## 标准分析流程
```
load_data → profile_data → build_entity_profiles → compute_scores → run_clustering → extract_features
```
### 完整步骤
#### 1. 加载数据
```
工具: load_data
参数: csv_glob="data/*.csv"
说明: 支持glob模式,自动合并同结构多文件
```
#### 2. 了解数据
```
工具: profile_data
参数: dataset_id="..."
说明: 查看列数、类型、空值率、统计摘要
```
> **可选**: 调用 `analyze_patterns` 查看更多流量结构信息
#### 3. 构建实体画像
```
工具: build_entity_profiles
参数: dataset_id="...", auto_detect=true
说明: 自动检测实体列(IP/SNI等),聚合流数据为实体特征
```
> 如果自动检测失败 → 调用 `explore_distributions` 查看候选列
#### 4. 计算异常分数
```
工具: compute_scores
参数: dataset_id="<entity_dataset_id>"
说明: PCA自动学习权重,输出 proxy_score / risk_level / threat_score
```
#### 5. 检测异常(可选)
```
工具: detect_anomalies
参数: dataset_id="...", contamination=0.05
说明: Isolation Forest 找出异常实体
```
#### 6. 聚类分析
```
工具: run_clustering
参数: dataset_id="...", cluster_columns=[...], algorithm="hdbscan"
说明: 实体行为分组
```
> 如果聚类效果差 → 调用 `diagnose_clustering` 诊断
#### 7. 提取特征
```
工具: extract_features
参数: dataset_id="...", cluster_result_id="...", method="zscore"
说明: 找出每组区分特征
```
#### 8. 可视化
```
工具: visualize_anomalies
参数: dataset_id="..."
说明: PCA降维,散点图数据
```
---
## 异常排查流程
### 数据加载失败
```
load_data 报错
→ validate_data: 检查 schema 问题
→ repair_schema: 修复列名不一致
→ 重试 load_data
```
### 实体检测失败
```
build_entity_profiles 返回无实体列
→ explore_distributions: 检查各列unique值
→ 手动指定 entity_column 重试
```
### 聚类效果差
```
run_clustering 返回 1 个簇或全噪声
→ diagnose_clustering: 诊断原因
→ 根据诊断调整参数重试
→ 或用 analyze_tls_health 检查 TLS 质量
```
### 分数异常
```
compute_scores 后多数实体分数极高/低
→ explore_distributions: 检查各特征分布
→ find_outliers: 找出极端值
→ analyze_entity_detail: 深入调查可疑实体
```
---
## LLM自动模式
在配置页面设置 LLM 后,`LLM分析` 页面会自动编排工具链。LLM 的策略:
1.`profile_data` 了解数据
2. 根据 profile 决定下一步
3. 失败时自动调用诊断工具
4. 分析过程中随时调用只读分析工具深入了解
5. 中文总结分析结果
> 可通过配置文件调整 LLM 参数 (config.yaml → llm)
+181
View File
@@ -0,0 +1,181 @@
{% 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 %}
+1
View File
@@ -47,6 +47,7 @@
<a href="{% url 'analysis:auto' %}">LLM分析</a>
<a href="{% url 'analysis:run_list' %}">运行记录</a>
<a href="{% url 'analysis:globe' %}">地球</a>
<a href="{% url 'analysis:tool_lab' %}">工具</a>
</nav>
<div class="container">
{% block content %}{% endblock %}