feat: MCP tool lab page + workflow docs + nav link
This commit is contained in:
@@ -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'),
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user