187 lines
9.3 KiB
Markdown
187 lines
9.3 KiB
Markdown
# 天璇 v4 修复计划
|
||
|
||
## TL;DR (For humans)
|
||
|
||
7个问题,24个todo。核心改动:IP子网实体聚合、多列实体、地球手动缩放、异步聚类修复、列类型值优先检测。
|
||
|
||
---
|
||
|
||
## Problem & Approach
|
||
|
||
Intent: **CLEAR** — 用户列出7个具体问题,方案可从前端/后端代码推导。无需高精度审查。
|
||
|
||
### Issue 1 — 聚类选非数值列报错
|
||
**根因**: `_run_analysis_worker` (views.py:502) 默认取前10非_开头的列;手动向导步骤2未过滤非数值列。`_handle_run_clustering` (tool_registry.py:787-791) 虽有数值过滤,但无列给用户时返回"无可用维度"且不提示哪些可用。
|
||
**方案**:
|
||
- manual.html 步骤2特征列只展示数值列(标注dtype)
|
||
- `_handle_run_clustering` 报错时返回可用数值列名清单
|
||
- views.py 默认特征列选择排除实体列自身
|
||
|
||
### Issue 2 — 地球问题 (4个子问题)
|
||
**根因**:
|
||
- TLS流不显示 → globe_view 只用GeoIP,未用数据列经纬度
|
||
- 经纬线 → Three.js未画辅助线
|
||
- 滚轮缩放 → 无wheel事件处理
|
||
- 复选框状态丢失 → GET表单→刷新页面→选中状态丢失
|
||
**方案**:
|
||
- globe_view 优先用数据列`src_latitude/src_longitude/dst_latitude/dst_longitude`,回退GeoIP
|
||
- 增强现有手动拖拽JS + 添加滚轮事件(不引入OrbitControls——three.min.js是UMD构建,OrbitControls需要ESM,不兼容)
|
||
- 用 LineLoop 绘10度间隔经纬线
|
||
- 模板端将 `?runs=1,2` 参数回填到checkbox checked属性
|
||
|
||
### Issue 3 — Django异步聚类失败
|
||
**根因**: `_run_analysis_worker` (views.py:503) 在线程中调 `asyncio.run()`。`_handle_run_clustering` 本质是同步代码(polars/numpy/sklearn,无await),`_handle_extract_features` 内有 `sync_to_async` 用于ORM。
|
||
**方案**:
|
||
- `_handle_run_clustering` 提取同步核心函数 `_cluster_sync()`,worker直接调同步版
|
||
- `_handle_extract_features` 用 `asgiref.sync.async_to_sync` 包装
|
||
|
||
### Issue 4 — 列类型靠列名而非数值
|
||
**根因**: `type_classifier.py` 优先级: config→名称匹配→值检测。名称匹配(如 _ip→IPv4)过于宽泛。
|
||
**方案**: 改为 config→值检测→名称匹配(仅值检测返回STRING时作为二次提示)。增加MAC地址、端口值模式。
|
||
|
||
### Issue 5 — 数据列初步分析
|
||
**方案**: `scripts/column_survey.py`——standalone脚本,输出每列名称/dtype/唯一值数/空值率/前5非空样本。
|
||
|
||
### Issue 6 — src_ip和dst_ip都作为实体
|
||
**根因**: `entity_detector.py` 只返回单 `recommended`。`_background_process` (views.py:367) 只用单列。
|
||
**实际状态**: manual.html line 46 `<select multiple>` 已支持多选!`_run_analysis_worker` (views.py:464) 已处理 `entity_columns` 列表。gap仅在于 `_background_process` 和 `entity_detector`。
|
||
**方案**:
|
||
- `entity_detector` 增加 `recommended_multi` (前N候选)
|
||
- `_background_process` 支持多列实体聚合
|
||
- manual.html 确认页(步骤3)修复多选显示
|
||
|
||
### Issue 7 — IP子网实体聚合 (/24 + /28)
|
||
**根因**: 无子网处理。IP直接作为实体标识。
|
||
**方案**:
|
||
- Config新增 `Entity` 类,含 `subnet_masks` 和 `ip_columns`
|
||
- `entity_aggregator.py` 新增 `ip_to_subnet()` 和 `apply_subnet_aggregation()`
|
||
- 在 group_by 之前将IP转换为子网前缀
|
||
- 支持 /24 (0-255) 和 /28 (240-255)
|
||
|
||
---
|
||
|
||
## Architecture & Design Decisions
|
||
|
||
### Decision 1: IP子网聚合设计
|
||
- 位置: entity_aggregator.py,`aggregate_by_entity` 之前
|
||
- mask从 config 读取,支持 [24, 28]
|
||
- 转换后的子网前缀替代原始IP:
|
||
- `ip_to_subnet('10.0.1.15', 24)` → `'10.0.1.0/24'`
|
||
- `ip_to_subnet('10.0.1.250', 28)` → `'10.0.1.240/28'`
|
||
- 无效IP/None → 原值
|
||
|
||
```python
|
||
# IP → subnet prefix
|
||
def ip_to_subnet(ip_str: str, mask: int) -> str:
|
||
"""Convert IP to CIDR subnet prefix."""
|
||
if not ip_str or not isinstance(ip_str, str):
|
||
return ip_str
|
||
try:
|
||
parts = [int(o) for o in ip_str.strip().split('.')]
|
||
if len(parts) != 4:
|
||
return ip_str
|
||
if any(p < 0 or p > 255 for p in parts):
|
||
return ip_str
|
||
ip_int = (parts[0]<<24) + (parts[1]<<16) + (parts[2]<<8) + parts[3]
|
||
subnet_int = ip_int & (0xFFFFFFFF << (32 - mask))
|
||
return f"{(subnet_int>>24)&255}.{(subnet_int>>16)&255}.{(subnet_int>>8)&255}.{subnet_int&255}/{mask}"
|
||
except (ValueError, IndexError, TypeError):
|
||
return ip_str
|
||
```
|
||
|
||
### Decision 2: 多列实体
|
||
- `EntityProfile.entity_value` (max_length=512) 存储多列值: `"src_ip=10.0.0.1||dst_ip=203.0.113.5"`
|
||
- `AnalysisRun.entity_column` (max_length=256) 存储逗号分隔: `"src_ip,dst_ip"`
|
||
- `unique_together = ['run', 'entity_value']` 自然支持多列唯一性
|
||
|
||
### Decision 3: 值优先类型检测
|
||
优先级:
|
||
1. Config override → 立即返回
|
||
2. 值采样 → 按顺序检测: IPv4→MAC→HEX→URL→Port→BOOL_ENUM→ENUM→Float→STRING
|
||
3. 名称匹配 → 仅值检测返回STRING时作为二次提示(如 `_ip` 后缀提示IPv4)
|
||
4. STRING作为最终兜底
|
||
|
||
新增模式:
|
||
- MAC: `r'^([0-9A-Fa-f]{2}[:.-]){5}[0-9A-Fa-f]{2}$'`
|
||
- Port: 非空值全部在 1-65535 且全为整数 → ENUM
|
||
- Float: >80% 样本可 `float()` 解析 → FLOAT
|
||
|
||
### Decision 4: 地球缩放+旋转 (不引入OrbitControls)
|
||
**重要**: three.min.js (603KB) 是 UMD 独立构建,OrbitControls 是 three/examples/jsm/ 的 ESM 格式,
|
||
无法直接加载。两个可行方案已评估:
|
||
|
||
a) ❌ 下载 OrbitControls 到 static/ - 需要处理模块依赖链(Controls→Euler→...),不适用于离线场景
|
||
b) ✅ 增强现有手动拖拽JS + 添加滚轮缩放事件 (globe.html lines 101-110 已有拖拽骨架)
|
||
|
||
实施:
|
||
- 保留并增强现有 mousedown/mousemove/mouseup 逻辑(已验证可用)
|
||
- 添加 `wheel` 事件: `camera.position.z += event.deltaY * 0.01` + clamp(3, 30)
|
||
- 鼠标中键重置视角
|
||
|
||
---
|
||
|
||
## Dependency Graph
|
||
|
||
```
|
||
独立阶段(可并行):
|
||
Todo 1-2: 列分析脚本 ──── (无依赖)
|
||
Todo 3-4: 值类型检测 ──── (无依赖)
|
||
Todo 5-8: 地球修复 ────── (无依赖)
|
||
Todo 9-10: 异步修复 ───── (无依赖)
|
||
|
||
顺序阶段:
|
||
Todo 11-13: 子网聚合 ──── (无依赖,但避免与类型检测冲突,需确保IP列识别的正确性)
|
||
Todo 14-17: 多列实体 ──── (依赖子网聚合的 IP→subnet 转换,因为多列中的IP列需要被子网处理)
|
||
Todo 18-20: 聚类列过滤 ── (依赖多列实体,因为 entity 列需要从 feature 列中排除)
|
||
|
||
最终:
|
||
Todo 21-24: 测试+文档 ─── (依赖所有)
|
||
F1-F3: 最终验证 ───────── (依赖所有)
|
||
```
|
||
|
||
---
|
||
|
||
## Todos
|
||
|
||
- [x] 1. 创建 `scripts/column_survey.py`: 对每列输出名称/dtype/唯一值数/空值率/前5样本
|
||
- [x] 2. QA column_survey: 在现有测试数据上有合理输出
|
||
- [x] 3. `type_classifier.py`: 值优先检测(IPv4→MAC→HEX→URL→Port→BOOL→ENUM→Float→STRING);名称降级为二次提示
|
||
- [x] 4. QA type_classifier: `pytest tests/test_type_classifier.py`
|
||
- [x] 5. `globe.html`: 添加wheel滚轮缩放事件 + 增强现有mousedown/move/up拖拽(不引入OrbitControls)
|
||
- [x] 6. `globe.html`: 添加经纬线网格(10度间隔 LineLoop 绘制纬线圆+经线弧)
|
||
- [x] 7. `globe.html` + `globe_view` 修复: (a) 复选框提交后保留选中状态 (b) globe_view 优先用数据列经纬度
|
||
- [x] 8. QA globe: 打开 `/globe/` 交互验证滚轮缩放+经纬线+复选框
|
||
- [x] 9. `views.py` + `tool_registry.py`: 提取 `_cluster_sync()` 同步核心 + `async_to_sync(_handle_extract_features)`
|
||
- [x] 10. QA async: 手动分析流程聚类步骤不报 `asyncio.run()` 错误
|
||
- [x] 11. `config/loader.py`: 新增 `Config.Entity` 类含 `subnet_masks`(list[int]) 和 `ip_columns`
|
||
- [x] 12. `entity_aggregator.py`: 新增 `ip_to_subnet()` 函数(含空值/无效IP处理)+ `apply_subnet_aggregation()`
|
||
- [x] 13. QA subnet: `ip_to_subnet('10.0.1.15', 24) == '10.0.1.0/24'` + `ip_to_subnet(None, 24) == None`
|
||
- [x] 14. `entity_detector.py`: 返回 `recommended_multi`(前3候选list);对IP列自动推荐多列
|
||
- [x] 15. `views.py`: `_background_process` 支持多列实体聚合;`AnalysisRun.entity_column` 存逗号分隔
|
||
- [x] 16. `manual.html`: 步骤3确认页展示多选实体列(`entity_columns|join:", "`)
|
||
- [x] 17. QA multi-col: 选 src_ip+dst_ip → 确认页显示两个 → 聚合 → 聚类 → 成功
|
||
- [x] 18. `manual.html`: 步骤2特征列只展示数值列(标注dtype如"bytes_sent (Int64)");排除entity列
|
||
- [x] 19. `views.py` + `tool_registry.py`: 聚类报错时返回可用数值列清单;views.py默认排除entity列
|
||
- [x] 20. QA cluster-col: 手动向导验证特征列仅数值列
|
||
- [x] 21. `tests/test_type_classifier.py`: 添加 MAC/端口/值优先测试
|
||
- [x] 22. `tests/test_entity.py`: 添加多列实体+IP子网测试
|
||
- [x] 23. `tests/test_e2e.py`: 添加多列实体→聚类端到端
|
||
- [x] 24. 更新 AGENTS.md 和 docs/工作流.md: 新增架构决策+已知问题表
|
||
|
||
## Final Verification Wave
|
||
|
||
- [x] F1. 全量测试: `pytest tests -q` → 全部通过
|
||
- [x] F2. 解除占用: 清理临时文件
|
||
- [x] F3. 更新所有 MD 文件到最终状态
|
||
|
||
---
|
||
|
||
## Approval Gate
|
||
|
||
状态: 等待批准
|
||
待执行: 24个todo,预计500-800行变更
|
||
关键变更: 10个文件(views.py/tool_registry.py/type_classifier.py/entity_aggregator.py/
|
||
entity_detector.py/globe.html/manual.html/config.yaml/loader.py/models.py)
|
||
|
||
同意后开始执行?
|