fix: nl_describe variance awareness + timestamp formatting + AGENTS.md update

This commit is contained in:
PM-pinou
2026-07-24 12:38:00 +08:00
parent 1463e56a42
commit f683a084f3
16 changed files with 1114 additions and 258 deletions
+14 -6
View File
@@ -354,9 +354,17 @@ runtime\python\python.exe scripts\column_survey.py --csv "data/*.csv"
## 近期变更 (beta-clean 分支)
- views.py (2000+ 行单体) → `analysis/views/` 包 (12 模块)
- tool_registry.py → `analysis/tools/` 包 (17 模块)
- 删除 `simple_analysis/` 模块 (已移至 master 分支独立维护)
- 工具数量从 27 升至 30 (新增 export_entities_json, diagnostic toolbox 扩展)
- 模板目录重组: 6 个分析页面在 `analysis/`, 6 个功能页面在 `tianxuan/`
- 新增 `analysis/distance.py` (距离计算) 和 `analysis/nl_describe.py` (自然语言描述)
- views.py (2000+ 行单体) → `analysis/views/` 包 (13 模块)
- tool_registry.py (3000+ 行)`analysis/tools/` 包 (18 模块)
- 默认聚类算法改为 AgglomerativeClustering
- 移除实体概念,直接对原始行聚类 (entity_value = row_N)
- cluster_detail 显示原始数据行而非实体画像
- run_detail 新增全域 SVD 特征分析 + LLM 工作流时间线
- 3D 地球侧栏复用 `/globe/?embed=1`iframe + postMessage 联动)
- UI 全面中文化
- 非阻塞启动 (run.bat 打印 PID 后退出)
## 待办
- [ ] **前端 UI 验证**: 打开 `prototype_cluster_ui.html` 查看交互设计是否符合要求
- [ ] **更新前端原型**后,将验证通过的交互逻辑合并到正式模板
+1 -1
View File
@@ -69,7 +69,7 @@ class Command(BaseCommand):
run.save(update_fields=['total_flows'])
from analysis.views import _run_clustering_pipeline
_run_clustering_pipeline(
run=run, store=store, entity_ds_id=ds_id,
run=run, store=store, ds_id=ds_id,
feature_columns=feature_cols,
algorithm=algo, min_cluster_size=5,
run_umap=True,
+16 -6
View File
@@ -285,6 +285,7 @@ def describe_feature(
feature_name: str,
value: float,
std_dev: float | None = None,
cluster_std: float | None = None,
) -> str:
"""Generate a Chinese natural-language description for a single feature.
@@ -296,14 +297,15 @@ def describe_feature(
The feature's mean value in the current context (cluster mean).
std_dev : float | None
Optional Z-score magnitude (``abs(distinguishing_score)``). When
provided and >= 1.0, appends a deviation suffix like
"比全局均值高2.1个标准差".
provided and >= 1.0, appends a deviation suffix.
cluster_std : float | None
Optional within-cluster standard deviation. Describes how spread
the values are within this cluster (high = scattered, low = tight).
Returns
-------
str
Natural-language description string, e.g.
``"中等数据包 (720 bytes),比全局均值高2.1个标准差"``.
Natural-language description string.
"""
meaning = _lookup_meaning(feature_name)
category = _resolve_category(feature_name)
@@ -322,7 +324,6 @@ def describe_feature(
meaning=meaning)
break
except Exception:
# Fall through to next template on format errors
continue
if not text:
@@ -332,6 +333,13 @@ def describe_feature(
if std_dev is not None and std_dev >= 1.0:
text += f",比全局均值高{std_dev:.1f}个标准差"
# Append within-cluster variance context
if cluster_std is not None and cluster_std > 0:
if cluster_std > 2.0:
text += "(簇内离散度高)"
elif cluster_std < 0.3:
text += "(簇内高度集中)"
return text
@@ -380,11 +388,13 @@ def describe_cluster(
name = f.get("feature_name", "")
mean = f.get("mean")
score = abs(f.get("distinguishing_score", 0.0))
cstd = f.get("std")
if mean is None or name == "":
continue
desc = describe_feature(name, float(mean), std_dev=score if score >= 1.0 else None)
desc = describe_feature(name, float(mean), std_dev=score if score >= 1.0 else None,
cluster_std=float(cstd) if cstd is not None else None)
sentences.append(desc)
if not sentences:
+5
View File
@@ -4,6 +4,7 @@ import logging
import traceback
from django.shortcuts import render
from django.views.decorators.clickjacking import xframe_options_exempt
from analysis.constants import GLOBE_MAX_ROWS
from analysis.models import AnalysisRun
@@ -112,6 +113,10 @@ def _extract_flows_from_df(df, MAX_ROWS):
return flows
from django.views.decorators.clickjacking import xframe_options_exempt
@xframe_options_exempt
def globe_view(request):
from analysis.geoip import lookup as geo_lookup
from analysis.data_loader import load_csv_directory, load_from_db
+634
View File
@@ -45,5 +45,639 @@
"lon": 77.0,
"city": "",
"country": "印度"
},
"anon, hosting": null,
"-108.418446": null,
"-63.303819": null,
"156.494253": {
"lat": 34.053,
"lon": -118.2642,
"city": "洛杉矶",
"country": "美国"
},
"174.324906": {
"lat": 43.6425,
"lon": -79.3872,
"city": "",
"country": "加拿大"
},
"91.251045": {
"lat": 53.55,
"lon": 10.0,
"city": "汉堡市",
"country": "德国"
},
"136.838644": {
"lat": 42.3223,
"lon": -83.1763,
"city": "迪尔伯恩",
"country": "美国"
},
"-135.232683": null,
"134.708535": {
"lat": 45.4763,
"lon": -122.6408,
"city": "Portland",
"country": "美国"
},
"172.779207": {
"lat": 38.0,
"lon": -97.0,
"city": "",
"country": "美国"
},
"76.375007": {
"lat": 35.6507,
"lon": -78.4564,
"city": "Clayton",
"country": "美国"
},
"-37.054377": null,
"58.368656": {
"lat": 35.685,
"lon": 139.7514,
"city": "东京",
"country": "日本"
},
"-128.312338": null,
"-86.295682": null,
"-83.710197": null,
"-117.654327": null,
"-94.931072": null,
"-31.118326": null,
"41.94697": {
"lat": -29.0,
"lon": 24.0,
"city": "",
"country": "南非"
},
"-175.557379": null,
"None": null,
"90.484378": {
"lat": 50.6846,
"lon": 1.7610999999999999,
"city": "维尔维涅",
"country": "法国"
},
"91.136808": {
"lat": 50.8934,
"lon": 7.8856,
"city": "Freudenberg",
"country": "德国"
},
"98.398423": {
"lat": 38.0,
"lon": -97.0,
"city": "",
"country": "美国"
},
"-37.351993": null,
"108.8728": {
"lat": 33.5363,
"lon": -117.0439,
"city": "特曼库拉",
"country": "美国"
},
"-40.490238": null,
"-108.320482": null,
"-42.599081": null,
"60.847895": {
"lat": 30.2936,
"lon": 120.1614,
"city": "杭州",
"country": "中国"
},
"-70.344735": null,
"-17.065978": null,
"-17.594761": null,
"3.748318": {
"lat": 41.1412,
"lon": -73.2637,
"city": "Fairfield",
"country": "美国"
},
"117.90784": {
"lat": 21.0333,
"lon": 105.85,
"city": "Hanoi",
"country": "越南"
},
"-178.325477": null,
"47.034714": {
"lat": 45.3155,
"lon": -75.837,
"city": "Ottawa",
"country": "加拿大"
},
"12.6356": {
"lat": 38.0,
"lon": -97.0,
"city": "",
"country": "美国"
},
"-4.376563": null,
"117.415791": {
"lat": 21.0333,
"lon": 105.85,
"city": "Hanoi",
"country": "越南"
},
"-95.943745": null,
"115.811466": {
"lat": 37.57,
"lon": 126.98,
"city": "",
"country": "大韩民国"
},
"82.858229": {
"lat": 54.5842,
"lon": -1.5631,
"city": "Darlington",
"country": "英国"
},
"120.177108": {
"lat": 39.8897,
"lon": 115.275,
"city": "Hebei",
"country": "中国"
},
"12.054574": {
"lat": 38.0,
"lon": -97.0,
"city": "",
"country": "美国"
},
"-112.238338": null,
"-161.091649": null,
"24.207799": {
"lat": 40.3698,
"lon": -80.634,
"city": "斯托本维尔",
"country": "美国"
},
"-148.31237": null,
"-132.223679": null,
"76.252382": {
"lat": 35.6513,
"lon": -78.8336,
"city": "Holly Springs",
"country": "美国"
},
"104.839037": {
"lat": 38.0,
"lon": -97.0,
"city": "",
"country": "美国"
},
"-147.319447": null,
"-137.111721": null,
"-107.76671": null,
"-167.802595": null,
"168.500812": {
"lat": 29.7176,
"lon": -95.4188,
"city": "休斯敦",
"country": "美国"
},
"-19.640731": null,
"-36.115066": null,
"-31.172609": null,
"4.746775": {
"lat": 38.0,
"lon": -97.0,
"city": "",
"country": "美国"
},
"-128.714202": null,
"-95.66714": null,
"-19.935247": null,
"52.225763": {
"lat": 39.5645,
"lon": -75.597,
"city": "威尔明顿",
"country": "美国"
},
"-39.370987": null,
"162.006633": {
"lat": 43.7807,
"lon": -79.2855,
"city": "士嘉堡",
"country": "加拿大"
},
"51.696746": {
"lat": 51.5,
"lon": -0.13,
"city": "",
"country": "英国"
},
"6.673336": {
"lat": 31.5273,
"lon": -110.3607,
"city": "谢拉维斯塔",
"country": "美国"
},
"102.5503": null,
"-144.964956": null,
"-104.457778": null,
"83.087052": null,
"-125.101539": null,
"90.262005": {
"lat": 48.9018,
"lon": 2.4893,
"city": "邦迪",
"country": "法国"
},
"-83.715609": null,
"29.378547": {
"lat": 38.0,
"lon": -97.0,
"city": "",
"country": "美国"
},
"-57.090496": null,
"142.184372": {
"lat": 44.7314,
"lon": -63.6482,
"city": "Dartmouth",
"country": "加拿大"
},
"165.297813": {
"lat": -33.9167,
"lon": 18.4167,
"city": "Cape Town",
"country": "南非"
},
"105.875829": {
"lat": -29.0,
"lon": 24.0,
"city": "",
"country": "南非"
},
"160.97215": {
"lat": 43.286,
"lon": -77.6843,
"city": "Rochester",
"country": "美国"
},
"-76.327692": null,
"-179.768984": null,
"-135.649249": null,
"-129.397466": null,
"-159.604348": null,
"123.957119": {
"lat": 34.6836,
"lon": 113.5325,
"city": "郑州",
"country": "中国"
},
"106.508142": {
"lat": 28.55,
"lon": 115.9333,
"city": "南昌",
"country": "中国"
},
"60.737607": {
"lat": 45.75,
"lon": 126.65,
"city": "哈尔滨",
"country": "中国"
},
"12.851572": {
"lat": 38.0,
"lon": -97.0,
"city": "",
"country": "美国"
},
"133.109702": {
"lat": 35.69,
"lon": 139.69,
"city": "",
"country": "日本"
},
"97.468122": {
"lat": 38.0,
"lon": -97.0,
"city": "",
"country": "美国"
},
"-49.099249": null,
"31.114424": {
"lat": 52.25,
"lon": 21.0,
"city": "华沙",
"country": "波兰"
},
"49.613214": {
"lat": 37.5985,
"lon": 126.9783,
"city": "首尔特别市",
"country": "大韩民国"
},
"-76.937967": null,
"24.350375": {
"lat": 38.0,
"lon": -97.0,
"city": "",
"country": "美国"
},
"-57.188778": null,
"-129.431676": null,
"-136.186649": null,
"-61.694727": null,
"-90.513611": null,
"91.194896": {
"lat": 49.5333,
"lon": 11.8,
"city": "哈恩巴赫",
"country": "德国"
},
"132.327773": {
"lat": 32.404,
"lon": -86.2539,
"city": "蒙哥马利",
"country": "美国"
},
"-112.197533": null,
"155.022955": null,
"162.854691": {
"lat": 51.5,
"lon": -0.13,
"city": "",
"country": "英国"
},
"-52.303734": null,
"-37.91953": null,
"144.662392": {
"lat": 37.3762,
"lon": -122.1826,
"city": "Palo Alto",
"country": "美国"
},
"17.315107": {
"lat": 37.323,
"lon": -122.0322,
"city": "Cupertino",
"country": "美国"
},
"97.359727": {
"lat": 38.0,
"lon": -97.0,
"city": "",
"country": "美国"
},
"-37.814213": null,
"-57.066252": null,
"170.670964": {
"lat": 47.6689,
"lon": -117.4369,
"city": "斯波坎",
"country": "美国"
},
"19.237916": {
"lat": 42.3223,
"lon": -83.1763,
"city": "迪尔伯恩",
"country": "美国"
},
"-77.426219": null,
"35.273579": {
"lat": 42.2776,
"lon": -83.7409,
"city": "安娜堡",
"country": "美国"
},
"-29.56673": null,
"-173.438083": null,
"-164.691055": null,
"-140.463216": null,
"-168.663276": null,
"-138.543279": null,
"-47.142859": null,
"-102.389184": null,
"24.754033": {
"lat": 32.981,
"lon": -80.0326,
"city": "Goose Creek",
"country": "美国"
},
"-109.476174": null,
"92.836244": {
"lat": 51.5,
"lon": -0.13,
"city": "",
"country": "英国"
},
"2.089979": null,
"82.41569": {
"lat": 51.6578,
"lon": -0.3954,
"city": "沃特福德",
"country": "英国"
},
"-117.765392": null,
"-112.753899": null,
"-147.563834": null,
"-27.4301": null,
"66.656057": {
"lat": 38.0,
"lon": -97.0,
"city": "",
"country": "美国"
},
"-52.329152": null,
"16.783619": {
"lat": 37.3762,
"lon": -122.1826,
"city": "Palo Alto",
"country": "美国"
},
"-160.668335": null,
"-155.782242": null,
"-43.415128": null,
"-151.682652": null,
"33.713437": {
"lat": 38.0,
"lon": -97.0,
"city": "",
"country": "美国"
},
"151.170164": {
"lat": 42.8333,
"lon": 12.8333,
"city": "",
"country": "意大利"
},
"-2.437103": null,
"17.096003": null,
"4.372196": {
"lat": 38.0,
"lon": -97.0,
"city": "",
"country": "美国"
},
"12.775114": {
"lat": 38.0,
"lon": -97.0,
"city": "",
"country": "美国"
},
"-26.847855": null,
"-2.96053": null,
"-136.421502": null,
"156.152711": {
"lat": 46.2857,
"lon": -119.2845,
"city": "里奇兰",
"country": "美国"
},
"-121.186972": null,
"-126.924167": null,
"-101.388131": null,
"-101.03754": null,
"77.828572": {
"lat": 52.5167,
"lon": 13.4,
"city": "柏林",
"country": "德国"
},
"-34.813058": null,
"123.011334": {
"lat": 23.7,
"lon": 90.375,
"city": "",
"country": "孟加拉"
},
"11.265017": {
"lat": 39.9612,
"lon": -82.9988,
"city": "哥伦布",
"country": "美国"
},
"11.202764": {
"lat": 39.9612,
"lon": -82.9988,
"city": "哥伦布",
"country": "美国"
},
"70.262109": {
"lat": 38.0,
"lon": -97.0,
"city": "",
"country": "美国"
},
"-70.027646": null,
"26.990087": {
"lat": 38.0,
"lon": -97.0,
"city": "",
"country": "美国"
},
"147.506751": {
"lat": 47.5,
"lon": 19.0833,
"city": "布达佩斯",
"country": "匈牙利"
},
"-107.81862": null,
"141.75262": {
"lat": 51.0,
"lon": 9.0,
"city": "",
"country": "德国"
},
"30.674809": {
"lat": 38.0,
"lon": -97.0,
"city": "",
"country": "美国"
},
"120.327798": {
"lat": 39.8897,
"lon": 115.275,
"city": "Hebei",
"country": "中国"
},
"-158.088393": null,
"-121.790773": null,
"77.908193": {
"lat": 50.3167,
"lon": 7.8,
"city": "Nassau",
"country": "德国"
},
"57.252522": {
"lat": 47.0,
"lon": 8.0,
"city": "",
"country": ""
},
"-15.466886": null,
"-136.800708": null,
"-139.560022": null,
"155.309838": {
"lat": 59.3294,
"lon": 18.0686,
"city": "",
"country": "瑞典"
},
"158.175545": {
"lat": 31.5273,
"lon": -110.3607,
"city": "谢拉维斯塔",
"country": "美国"
},
"-38.283752": null,
"146.597747": {
"lat": 42.3755,
"lon": -83.0772,
"city": "底特律",
"country": "美国"
},
"-146.199812": null,
"-60.696207": null,
"141.547786": {
"lat": 35.8869,
"lon": 14.4025,
"city": "Mdina",
"country": "马耳他"
},
"10.542121": null,
"169.586656": {
"lat": 45.0059,
"lon": -93.1059,
"city": "圣保罗",
"country": "美国"
},
"53.588162": {
"lat": 51.0,
"lon": 9.0,
"city": "",
"country": "德国"
},
"-48.221466": null,
"-104.847562": null,
"-97.617615": null,
"145.767865": {
"lat": 51.8425,
"lon": 5.8528,
"city": "奈梅亨",
"country": "荷兰"
},
"-173.124365": null,
"-158.431061": null,
"-132.023148": null,
"-171.873785": null,
"-23.369992": null,
"53.281077": {
"lat": 51.0,
"lon": 9.0,
"city": "",
"country": "德国"
},
"28.294006": {
"lat": 38.0,
"lon": -97.0,
"city": "",
"country": "美国"
}
}
+356
View File
@@ -0,0 +1,356 @@
<!DOCTYPE html>
<html lang="zh-hans">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>原型 — 聚类UI交互验证</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; background:#f5f7fa; color:#1a1a2e; }
.header { background:#1a1a2e; color:#fff; padding:0.8rem 1.5rem; display:flex; justify-content:space-between; align-items:center; }
.header h1 { font-size:1.1rem; }
.header span { font-size:0.8rem; color:#aaa; }
.main-layout { display:flex; position:relative; transition:all 0.3s; min-height:calc(100vh - 48px); }
.left-panel { flex:1; min-width:0; transition:flex 0.3s; padding:1rem; overflow-y:auto; }
.left-panel.globe-open { flex:0 0 55%; }
.right-panel { flex:0 0 0; overflow:hidden; transition:flex 0.3s; }
.right-panel.globe-open { flex:0 0 45%; }
/* toggle handle */
.toggle-handle { position:absolute; z-index:10; top:50%; width:20px; height:44px; background:#1a1a2e; color:#fff; border:none; border-radius:4px 0 0 4px; cursor:pointer; display:flex; align-items:center; justify-content:center; font-size:12px; transition:left 0.3s ease; }
.toggle-handle:hover { background:#4361ee; }
/* scatter */
.scatter-wrap { position:relative; background:#fff; border-radius:8px; padding:0.75rem; margin-bottom:1rem; box-shadow:0 1px 3px rgba(0,0,0,0.1); }
.scatter-wrap h3 { font-size:0.95rem; margin-bottom:0.5rem; }
.scatter-wrap canvas { width:100%; height:360px; display:block; cursor:crosshair; border-radius:4px; background:#fafbfc; }
/* pills */
.pill-row { display:flex; flex-wrap:wrap; gap:4px; margin-bottom:0.75rem; }
.pill { display:inline-flex; align-items:center; gap:3px; padding:2px 10px; border-radius:12px; font-size:0.75rem; cursor:pointer; border:2px solid transparent; transition:all 0.15s; color:#fff; white-space:nowrap; }
.pill.all { background:#e8e8e8; color:#333; }
.pill.active { border-color:#333; font-weight:700; }
/* cluster cards */
.card-grid { display:grid; grid-template-columns:1fr 1fr; gap:0.75rem; margin-bottom:1rem; }
.cluster-card { background:#fff; border:1px solid #e0e0e0; border-radius:8px; padding:0.8rem; cursor:pointer; transition:box-shadow 0.15s; }
.cluster-card:hover { box-shadow:0 2px 8px rgba(0,0,0,0.1); }
.cluster-card.selected { border-color:#4361ee; box-shadow:0 0 0 2px rgba(67,97,238,0.2); }
.cluster-card .label { font-weight:700; font-size:1rem; }
.cluster-card .meta { font-size:0.78rem; color:#666; margin-top:0.2rem; }
.cluster-card .desc { font-size:0.8rem; color:#333; margin-top:0.4rem; padding:0.4rem; background:#f8f9fa; border-left:3px solid #4361ee; border-radius:0 4px 4px 0; line-height:1.5; }
/* detail panel */
.detail-panel { position:fixed; bottom:0; left:0; right:0; z-index:100; transform:translateY(100%); transition:transform 0.35s cubic-bezier(.4,0,.2,1); }
.detail-panel.open { transform:translateY(0); }
.detail-body { background:#fff; border-radius:16px 16px 0 0; box-shadow:0 -4px 24px rgba(0,0,0,0.15); max-height:40vh; overflow-y:auto; padding:0.5rem 1.5rem 1.5rem; }
.detail-handle { width:36px; height:4px; background:#ccc; border-radius:2px; margin:8px auto; cursor:pointer; }
.detail-close { float:right; cursor:pointer; font-size:1.3rem; color:#999; line-height:1; border:none; background:none; }
.detail-close:hover { color:#333; }
.detail-body h3 { font-size:1rem; margin-bottom:0.3rem; }
.detail-body p { font-size:0.85rem; color:#555; margin:0.2rem 0; }
.detail-body table { width:100%; border-collapse:collapse; font-size:0.8rem; margin-top:0.5rem; }
.detail-body th { background:#f5f5f5; padding:0.25rem 0.4rem; text-align:left; font-weight:600; border-bottom:2px solid #ddd; }
.detail-body td { padding:0.2rem 0.4rem; border-bottom:1px solid #eee; }
.detail-body td code { font-size:0.72rem; }
/* 3D地球容器 */
#globeContainer { width:100%; height:calc(100vh - 48px); background:#0a0a1a; position:relative; }
#globeContainer canvas { display:block; }
</style>
</head>
<body>
<div class="header">
<h1>聚类概览 — 运行 #9</h1>
<span>200 条流 · 3 个簇 · 12 个噪声点</span>
</div>
<div class="main-layout" id="mainLayout">
<!-- Left: cluster content -->
<div class="left-panel" id="leftPanel">
<!-- Scatter -->
<div class="scatter-wrap">
<h3>UMAP 嵌入 <span style="font-weight:400;font-size:0.8rem;color:#888;" id="ptCount">加载中...</span></h3>
<canvas id="scatterChart"></canvas>
</div>
<!-- Cluster pills -->
<div class="pill-row" id="pillRow">
<span class="pill all active" data-lbl="all" onclick="clearSel()">全部</span>
</div>
<!-- Cluster cards -->
<div class="card-grid" id="cardGrid"></div>
</div>
<!-- Right: 3D Globe -->
<div class="right-panel" id="rightPanel">
<div id="globeContainer"></div>
</div>
<!-- Toggle handle -->
<button class="toggle-handle" id="globeHandle" onclick="toggleGlobe()"></button>
</div>
<!-- Bottom detail panel -->
<div class="detail-panel" id="detailPanel">
<div class="detail-body" id="detailBody">
<button class="detail-close" onclick="closeDetail()"></button>
<div class="detail-handle" onclick="closeDetail()"></div>
<div id="detailContent"><p style="color:#999;">选择一个簇或数据点查看详情</p></div>
</div>
</div>
<script src="static/tianxuan/three.min.js"></script>
<script>
// ═══════════════════════════════════════════════
// MOCK DATA
// ═══════════════════════════════════════════════
const MOCK_CLUSTERS = [
{ label:0, size:78, prop:0.39, silh:0.72, desc:'低延迟 (23ms),比全局均值高3.1σ;大数据包 (1520 bytes),高2.3σ;系统端口 (443)' },
{ label:1, size:65, prop:0.33, silh:0.68, desc:'高TLS1.3比例 (92%),比均值高1.8σ;短会话 (12s),低1.5σ;ECDHE曲线 X25519' },
{ label:2, size:45, prop:0.23, silh:0.54, desc:'高连接频率 (85次/min),比均值高4.2σ;非标准端口 (8080/8443),目标多域名' },
{ label:-1, size:12, prop:0.06, silh:null, desc:'噪声点 — 异常TLS指纹、零散目标、低流量' },
];
const COLORS = ['#4361ee','#f72585','#7209b7','#4cc9f0','#f8961e','#43aa8b','#f94144','#577590','#e9c46a','#9b5de5'];
function colorFor(lbl) { return lbl===-1?'rgba(150,150,150,0.7)':COLORS[Math.abs(lbl)%COLORS.length]; }
// Generate mock UMAP scatter data (200 points)
const SCATTER = [];
const RNG = (seed) => { let s=seed%2147483647; return ()=> { s=s*16807%2147483647; return (s-1)/2147483646; }; };
const rng = RNG(42);
const centers = {0:[2,1.5],1:[-1.8,0.5],2:[0.5,-2], '-1':[3.5,-1.5]};
const spreads = {0:[0.6,0.5],1:[0.7,0.6],2:[0.5,0.7], '-1':[1.2,0.8]};
for (const c of MOCK_CLUSTERS) {
for (let i=0;i<c.size;i++) {
const cx=centers[c.label][0], cy=centers[c.label][1];
const sx=spreads[c.label][0], sy=spreads[c.label][1];
SCATTER.push({
x:cx+(rng()-0.5)*2*sx, y:cy+(rng()-0.5)*2*sy,
label:c.label, entity:`模拟主机 ${1000+i}`
});
}
}
// ═══════════════════════════════════════════════
// SCATTER 2D CANVAS
// ═══════════════════════════════════════════════
let selCluster = null, selPoint = null;
let globeOpen = false;
function drawScatter(highlight) {
const c=document.getElementById('scatterChart');
if(!c)return;
const W=c.parentElement.clientWidth||700, H=360;
c.width=W*2; c.height=H*2; c.style.width=W+'px'; c.style.height=H+'px';
const ctx=c.getContext('2d'); ctx.scale(2,2);
const pad={top:25,bottom:30,left:50,right:20};
const pw=W-pad.left-pad.right, ph=H-pad.top-pad.bottom;
const xs=SCATTER.map(d=>d.x), ys=SCATTER.map(d=>d.y);
const xMin=Math.min(...xs), xMax=Math.max(...xs), yMin=Math.min(...ys), yMax=Math.max(...ys);
const xR=xMax-xMin||1, yR=yMax-yMin||1;
const toX=v=>pad.left+(v-xMin)/xR*pw, toY=v=>pad.top+ph-(v-yMin)/yR*ph;
ctx.clearRect(0,0,W,H);
// grid
ctx.strokeStyle='#eee'; ctx.lineWidth=1;
for(let i=0;i<=5;i++){ctx.beginPath();ctx.moveTo(pad.left,pad.top+i*ph/5);ctx.lineTo(W-pad.right,pad.top+i*ph/5);ctx.stroke();}
// points
SCATTER.forEach(d=>{
const cx=toX(d.x),cy=toY(d.y);
let radius=3,color=colorFor(d.label);
if(highlight!==null&&highlight!==undefined){
if(d.label===highlight){color=colorFor(d.label);radius=5;}
else{color='rgba(200,200,200,0.15)';radius=2;}
}
ctx.beginPath();ctx.arc(cx,cy,radius,0,Math.PI*2);ctx.fillStyle=color;ctx.fill();
});
// labels
ctx.fillStyle='#999'; ctx.font='11px sans-serif'; ctx.textAlign='center';
ctx.fillText('UMAP-1',W/2,H-3);
ctx.save();ctx.translate(14,H/2);ctx.rotate(-Math.PI/2);ctx.fillText('UMAP-2',0,0);ctx.restore();
c.onclick=function(e){
const r=c.getBoundingClientRect();
const mx=e.clientX-r.left, my=e.clientY-r.top;
const sx=mx/W*pw, sy=my/H*ph;
let best=null,bestD=10;
for(const d of SCATTER){
const dx=toX(d.x)-mx, dy=toY(d.y)-my;
const dist=Math.sqrt(dx*dx+dy*dy);
if(dist<bestD){bestD=dist;best=d;}
}
if(best){selCluster=MOCK_CLUSTERS.find(c=>c.label===best.label);selPoint=best;showDetail(selCluster,selPoint);}
};
document.getElementById('ptCount').textContent=SCATTER.length+' 个点';
}
// ═══════════════════════════════════════════════
// CLUSTER SELECTION
// ═══════════════════════════════════════════════
function selectCluster(lbl){
selCluster=MOCK_CLUSTERS.find(c=>c.label===lbl);
selPoint=null;
drawScatter(lbl);
document.querySelectorAll('.pill').forEach(p=>p.classList.toggle('active',parseInt(p.dataset.lbl)===lbl));
document.querySelectorAll('.cluster-card').forEach(p=>p.classList.toggle('selected',parseInt(p.dataset.label)===lbl));
if(globeOpen)globeFilter(lbl);
showDetail(selCluster,null);
}
function clearSel(){
selCluster=null;selPoint=null;
drawScatter(null);
document.querySelectorAll('.pill,.cluster-card').forEach(p=>p.classList.remove('active','selected'));
closeDetail();
if(globeOpen)globeFilter(null);
}
// ═══════════════════════════════════════════════
// DETAIL PANEL
// ═══════════════════════════════════════════════
function showDetail(cluster,point){
const dc=document.getElementById('detailContent');
let html='';
if(point){
html+='<h3>数据点详情</h3><table><thead><tr><th>字段</th><th>值</th></tr></thead><tbody>'+
'<tr><td>标识</td><td><code>'+point.entity+'</code></td></tr>'+
'<tr><td>所属簇</td><td>#'+(point.label===-1?'噪声':point.label)+'</td></tr>'+
'<tr><td>UMAP-x</td><td>'+point.x.toFixed(4)+'</td></tr>'+
'<tr><td>UMAP-y</td><td>'+point.y.toFixed(4)+'</td></tr>'+
'</tbody></table>';
}else if(cluster){
html+='<h3>簇 #'+(cluster.label===-1?'噪声':cluster.label)+'</h3>'+
'<p>大小: '+cluster.size+' | 占比: '+(cluster.prop*100).toFixed(0)+'%'+
(cluster.silh!==null?' | 轮廓系数: '+cluster.silh.toFixed(4):'')+'</p>'+
'<p style="background:#f8f9fa;border-left:3px solid #4361ee;padding:0.5rem;margin:0.5rem 0;border-radius:0 4px 4px 0;font-size:0.85rem;line-height:1.6;">'+cluster.desc+'</p>'+
'<table><thead><tr><th>特征</th><th>区分度</th><th>均值</th><th>标准差</th></tr></thead><tbody>'+
'<tr><td>延迟 (4dur)</td><td style="color:#2e7d32;">+3.2</td><td>23ms</td><td>5.1</td></tr>'+
'<tr><td>数据包大小 (8ppk)</td><td style="color:#2e7d32;">+2.3</td><td>1520B</td><td>128</td></tr>'+
'<tr><td>TLS 版本 (0ver)</td><td style="color:#c62828;">-1.8</td><td>1.2</td><td>0.3</td></tr>'+
'</tbody></table>';
}
dc.innerHTML=html;
document.getElementById('detailPanel').classList.add('open');
}
function closeDetail(){document.getElementById('detailPanel').classList.remove('open');}
// ═══════════════════════════════════════════════
// GLOBE SIDEBAR
// ═══════════════════════════════════════════════
function toggleGlobe(){
globeOpen=!globeOpen;
document.getElementById('leftPanel').classList.toggle('globe-open',globeOpen);
document.getElementById('rightPanel').classList.toggle('globe-open',globeOpen);
const h=document.getElementById('globeHandle');
h.innerHTML=globeOpen?'▶':'◀';
h.style.left=globeOpen?'calc(55% - 20px)':'calc(100% - 20px)';
if(globeOpen&&!window._globeInit)initGlobe3D();
}
function globeFilter(label){
if(!window._arcMeshes)return;
window._arcMeshes.forEach(m=>{
if(label===null){m.material.opacity=0.5;m.material.color.setHex(parseInt(m.userData.color,16));}
else{m.material.opacity=0.06;m.material.color.setHex(0x666666);}
});
}
// ═══════════════════════════════════════════════
// 3D GLOBE (Three.js, simplified)
// ═══════════════════════════════════════════════
function initGlobe3D(){
if(window._globeInit)return;
const cont=document.getElementById('globeContainer');
if(!cont)return;
const W=cont.clientWidth||450, H=cont.clientHeight||500;
const scene=new THREE.Scene();scene.background=new THREE.Color(0x0a0a1a);
const cam=new THREE.PerspectiveCamera(45,W/H,0.1,100);cam.position.set(0,0,6);
const ren=new THREE.WebGLRenderer({antialias:true});ren.setSize(W,H);ren.setPixelRatio(Math.min(window.devicePixelRatio,2));
cont.appendChild(ren.domElement);
// earth
const earth=new THREE.Mesh(new THREE.SphereGeometry(2,48,48),new THREE.MeshPhongMaterial({color:0x1a3a5c,emissive:0x0a1a2a}));
scene.add(earth);
// glow
scene.add(new THREE.Mesh(new THREE.SphereGeometry(2.06,48,48),new THREE.MeshBasicMaterial({color:0x224488,transparent:true,opacity:0.12})));
// lights
scene.add(new THREE.AmbientLight(0x222244));
const dl=new THREE.DirectionalLight(0xffffff,1);dl.position.set(5,10,7);scene.add(dl);
scene.add(new THREE.DirectionalLight(0x4488ff,0.3));
// stars
const s=new THREE.BufferGeometry();const p=new Float32Array(600*3);
for(let i=0;i<600*3;i++)p[i]=(Math.random()-0.5)*100;
s.setAttribute('position',new THREE.BufferAttribute(p,3));
scene.add(new THREE.Points(s,new THREE.PointsMaterial({color:0xffffff,size:0.15})));
// mock flows → arcs
const tlsC={'TLSv1.3':0x4cc9f0,'TLSv1.2':0x43aa8b,'TLSv1.1':0xf8961e,'TLSv1.0':0xf94144};
const arcGrp=new THREE.Group();scene.add(arcGrp);
const arcs=[];
function llz(lat,lon,r){const p=(90-lat)*Math.PI/180,t=(lon+180)*Math.PI/180;return new THREE.Vector3(-r*Math.sin(p)*Math.cos(t),r*Math.cos(p),r*Math.sin(p)*Math.sin(t));}
// generate ~30 mock flows
const cities=[{lat:31.2,lon:121.5},{lat:39.9,lon:116.4},{lat:35.7,lon:139.7},{lat:37.6,lon:127.0},{lat:51.5,lon:-0.13},{lat:48.9,lon:2.35},{lat:40.7,lon:-74.0},{lat:37.8,lon:-122.4},{lat:1.35,lon:103.8},{lat:-33.9,lon:151.2}];
const tlsOpts=['TLSv1.3','TLSv1.2','TLSv1.3','TLSv1.2','TLSv1.1'];
for(let i=0;i<30;i++){
const src=cities[i%cities.length],dst=cities[(i+3)%cities.length];
const f=llz(src.lat,src.lon,2),t=llz(dst.lat,dst.lon,2);
const mid=new THREE.Vector3().addVectors(f,t).multiplyScalar(0.5).normalize().multiplyScalar(3.2);
const curve=new THREE.QuadraticBezierCurve3(f,mid,t);
const c=tlsOpts[i%tlsOpts.length];
const mat=new THREE.MeshBasicMaterial({color:tlsC[c],transparent:true,opacity:0.5});
const mesh=new THREE.Mesh(new THREE.TubeGeometry(curve,16,0.015,4,false),mat);
mesh.userData={color:tlsC[c].toString(16)};
arcGrp.add(mesh);arcs.push(mesh);
}
window._arcMeshes=arcs;
// drag
let isD=false,pm={x:0,y:0};
ren.domElement.onmousedown=e=>{isD=true;pm={x:e.clientX,y:e.clientY};};
window.onmouseup=()=>isD=false;
window.onmousemove=e=>{if(!isD)return;const dx=e.clientX-pm.x,dy=e.clientY-pm.y;earth.rotation.y+=dx*0.005;earth.rotation.x+=dy*0.005;arcGrp.rotation.y=earth.rotation.y;arcGrp.rotation.x=earth.rotation.x;pm={x:e.clientX,y:e.clientY};};
!function anim(){requestAnimationFrame(anim);if(!isD){earth.rotation.y+=0.003;arcGrp.rotation.y=earth.rotation.y;}ren.render(scene,cam);}();
window._globeInit=true;
}
// ═══════════════════════════════════════════════
// RENDER PILLS & CARDS
// ═══════════════════════════════════════════════
(function init(){
const pr=document.getElementById('pillRow');
MOCK_CLUSTERS.forEach(c=>{
if(c.label===-1)return;
const p=document.createElement('span');p.className='pill';p.style.background=colorFor(c.label);
p.dataset.lbl=c.label;p.textContent='#'+c.label+' ('+c.size+')';p.onclick=()=>selectCluster(c.label);
pr.appendChild(p);
});
// noise pill
const np=document.createElement('span');np.className='pill';np.style.background='#999';
np.dataset.lbl='-1';np.textContent='噪声 ('+MOCK_CLUSTERS.find(c=>c.label===-1).size+')';np.onclick=()=>selectCluster(-1);
pr.appendChild(np);
// cards
const cg=document.getElementById('cardGrid');
MOCK_CLUSTERS.forEach(c=>{
if(c.label===-1)return;
const card=document.createElement('div');card.className='cluster-card';card.dataset.label=c.label;
card.innerHTML='<div class="label">簇 #'+c.label+'</div>'+
'<div class="meta">'+c.size+' 个实体 · '+(c.prop*100).toFixed(0)+'%'+(c.silh!==null?' · 轮廓系数: '+c.silh.toFixed(4):'')+'</div>'+
'<div class="desc">'+c.desc+'</div>';
card.onclick=()=>selectCluster(c.label);
cg.appendChild(card);
});
drawScatter(null);
// set initial handle pos
document.getElementById('globeHandle').style.left='calc(100% - 20px)';
})();
</script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

+60 -178
View File
@@ -1,178 +1,60 @@
Restored 2 datasets from disk
[2026-07-23 13:06:51,899] INFO django: Restored 2 datasets from disk
[2026-07-23 13:06:52,038] INFO analysis.data_loader: [LOAD_FROM_DB_LAZY] table=_data_64 rows=9990 cols=57
[2026-07-23 13:06:52,043] INFO analysis.data_loader: [LOAD_FROM_DB_LAZY] table=_data_66 rows=2000 cols=9
[23/Jul/2026 13:21:24] "GET / HTTP/1.1" 200 10790
[23/Jul/2026 13:21:24,471] - Broken pipe from ('127.0.0.1', 64227)
[2026-07-23 13:27:21,369] INFO analysis.views: [UPLOAD] Received 1998 files
[23/Jul/2026 13:27:22] "POST /upload/csv/ HTTP/1.1" 200 178755
[23/Jul/2026 13:27:22] "POST /runs/5/finalize/ HTTP/1.1" 200 34
[23/Jul/2026 13:27:22] "GET /runs/5/status/ HTTP/1.1" 200 188
[2026-07-23 13:27:22,672] INFO analysis.views: [BACKGROUND] Forcing lat/lon column ':ips.latd' to Utf8 for safe parsing
[2026-07-23 13:27:22,672] INFO analysis.views: [BACKGROUND] Forcing lat/lon column ':ips.lond' to Utf8 for safe parsing
[2026-07-23 13:27:22,672] INFO analysis.views: [BACKGROUND] Forcing lat/lon column ':ipd.latd' to Utf8 for safe parsing
[2026-07-23 13:27:22,672] INFO analysis.views: [BACKGROUND] Forcing lat/lon column ':ipd.lond' to Utf8 for safe parsing
[2026-07-23 13:27:22,672] INFO analysis.views: [BACKGROUND] Found 1998 CSV files in C:\Users\25044\AppData\Roaming\TianXuan\data\uploads\20260723_052721
[2026-07-23 13:27:23,325] INFO analysis.data_loader: [SAVE_TO_DB] table=_data_68 rows=495 mode=append
[2026-07-23 13:27:23,609] INFO analysis.data_loader: [SAVE_TO_DB] table=_data_68 rows=495 mode=append
[2026-07-23 13:27:23,895] INFO analysis.data_loader: [SAVE_TO_DB] table=_data_68 rows=495 mode=append
[2026-07-23 13:27:24,170] INFO analysis.data_loader: [SAVE_TO_DB] table=_data_68 rows=495 mode=append
[2026-07-23 13:27:24,447] INFO analysis.data_loader: [SAVE_TO_DB] table=_data_68 rows=495 mode=append
[2026-07-23 13:27:24,733] INFO analysis.data_loader: [SAVE_TO_DB] table=_data_68 rows=495 mode=append
[2026-07-23 13:27:25,000] INFO analysis.data_loader: [SAVE_TO_DB] table=_data_68 rows=495 mode=append
[2026-07-23 13:27:25,268] INFO analysis.data_loader: [SAVE_TO_DB] table=_data_68 rows=495 mode=append
[2026-07-23 13:27:25,553] INFO analysis.data_loader: [SAVE_TO_DB] table=_data_68 rows=495 mode=append
[23/Jul/2026 13:27:25] "GET /runs/5/status/ HTTP/1.1" 200 242
[2026-07-23 13:27:25,838] INFO analysis.data_loader: [SAVE_TO_DB] table=_data_68 rows=495 mode=append
[2026-07-23 13:27:26,116] INFO analysis.data_loader: [SAVE_TO_DB] table=_data_68 rows=495 mode=append
[2026-07-23 13:27:26,392] INFO analysis.data_loader: [SAVE_TO_DB] table=_data_68 rows=495 mode=append
[2026-07-23 13:27:26,668] INFO analysis.data_loader: [SAVE_TO_DB] table=_data_68 rows=495 mode=append
[2026-07-23 13:27:26,946] INFO analysis.data_loader: [SAVE_TO_DB] table=_data_68 rows=495 mode=append
[2026-07-23 13:27:27,231] INFO analysis.data_loader: [SAVE_TO_DB] table=_data_68 rows=495 mode=append
[2026-07-23 13:27:27,497] INFO analysis.data_loader: [SAVE_TO_DB] table=_data_68 rows=495 mode=append
[2026-07-23 13:27:27,782] INFO analysis.data_loader: [SAVE_TO_DB] table=_data_68 rows=495 mode=append
[2026-07-23 13:27:28,071] INFO analysis.data_loader: [SAVE_TO_DB] table=_data_68 rows=495 mode=append
[2026-07-23 13:27:28,349] INFO analysis.data_loader: [SAVE_TO_DB] table=_data_68 rows=495 mode=append
[2026-07-23 13:27:28,621] INFO analysis.data_loader: [SAVE_TO_DB] table=_data_68 rows=495 mode=append
[23/Jul/2026 13:27:28] "GET /runs/5/status/ HTTP/1.1" 200 243
[2026-07-23 13:27:28,713] INFO analysis.data_loader: [SAVE_TO_DB] table=_data_68 rows=90 mode=append
E:\hjq\澶╃拠\analysis\views.py:584: PerformanceWarning: Determining the column names of a LazyFrame requires resolving its schema, which is a potentially expensive operation. Use `LazyFrame.collect_schema().names()` to get the column names without this warning.
if lat_lon_col in lf.columns:
[2026-07-23 13:27:29,071] INFO analysis.views: [BACKGROUND] Restored numeric types for 16 columns: ['1ipp', '2tmo', '4dbn', '4dur', '4srs', '8ack', '8byt', '8dbd', '8did', '8pak']
[2026-07-23 13:27:29,150] INFO analysis.views: [BACKGROUND] SVD: 15 components from 16 numeric columns (explained variance ratio sum=1.0000)
[23/Jul/2026 13:27:31] "GET /runs/5/status/ HTTP/1.1" 200 223
[23/Jul/2026 13:27:56] "POST /analyze/run/ HTTP/1.1" 200 45
[23/Jul/2026 13:27:56] "GET /runs/5/status/ HTTP/1.1" 200 223
[2026-07-23 13:27:57,344] INFO analysis.tool_registry: [CLUSTER_INPUT] rows=9990 cols=10 feature_cols=['1ipp', '2tmo', '4dbn', '4dur', '4srs', '8ack', '8byt', '8dbd', '8did', '8pak']
[2026-07-23 13:27:57,344] INFO analysis.tool_registry: [CLUSTER_SCALE] Starting StandardScaler...
[2026-07-23 13:27:57,344] INFO analysis.tool_registry: [CLUSTER_SCALE] Done. shape=(9990, 10)
E:\hjq\澶╃拠\runtime\python\Lib\site-packages\sklearn\cluster\_hdbscan\hdbscan.py:722: FutureWarning: The default value of `copy` will change from False to True in 1.10. Explicitly set a value for `copy` to silence this warning.
warn(
[23/Jul/2026 13:27:59] "GET /runs/5/status/ HTTP/1.1" 200 240
[23/Jul/2026 13:28:02] "GET /runs/5/status/ HTTP/1.1" 200 240
[23/Jul/2026 13:28:06] "GET /runs/5/status/ HTTP/1.1" 200 240
E:\hjq\澶╃拠\runtime\python\Lib\site-packages\umap\umap_.py:1952: UserWarning: n_jobs value 1 overridden to 1 by setting random_state. Use no seed for parallelism.
warn(
[23/Jul/2026 13:28:09] "GET /runs/5/status/ HTTP/1.1" 200 240
[23/Jul/2026 13:28:12] "GET /runs/5/status/ HTTP/1.1" 200 240
[23/Jul/2026 13:28:15] "GET /runs/5/status/ HTTP/1.1" 200 240
[23/Jul/2026 13:28:18] "GET /runs/5/status/ HTTP/1.1" 200 240
[23/Jul/2026 13:28:21] "GET /runs/5/status/ HTTP/1.1" 200 240
[23/Jul/2026 13:28:24] "GET /runs/5/status/ HTTP/1.1" 200 240
[23/Jul/2026 13:28:27] "GET /runs/5/status/ HTTP/1.1" 200 240
[23/Jul/2026 13:28:30] "GET /runs/5/status/ HTTP/1.1" 200 240
[23/Jul/2026 13:28:33] "GET /runs/5/status/ HTTP/1.1" 200 240
[23/Jul/2026 13:28:37] "GET /runs/5/status/ HTTP/1.1" 200 240
[2026-07-23 13:28:38,183] INFO analysis.geoip: [GEOIP] loaded 803 ranges from E:\hjq\澶╃拠\data\geoip_data.txt (0 skipped)
E:\hjq\澶╃拠\runtime\python\Lib\site-packages\umap\umap_.py:1952: UserWarning: n_jobs value 1 overridden to 1 by setting random_state. Use no seed for parallelism.
warn(
[23/Jul/2026 13:28:40] "GET /runs/5/status/ HTTP/1.1" 200 254
Restored 3 datasets from disk
[2026-07-23 21:16:22,571] INFO django: Restored 3 datasets from disk
[2026-07-23 21:16:22,802] INFO analysis.data_loader: [LOAD_FROM_DB_LAZY] table=_data_64 rows=9990 cols=57
[2026-07-23 21:16:22,804] INFO analysis.data_loader: [LOAD_FROM_DB_LAZY] table=_data_66 rows=2000 cols=9
[2026-07-23 21:16:22,992] INFO analysis.data_loader: [LOAD_FROM_DB_LAZY] table=_data_68 rows=9990 cols=57
[23/Jul/2026 21:16:44] "GET / HTTP/1.1" 200 12728
[23/Jul/2026 21:16:44] "GET /static/tianxuan/favicon.svg HTTP/1.1" 304 0
[23/Jul/2026 21:17:14] "HEAD / HTTP/1.1" 200 0
[23/Jul/2026 21:17:20] "HEAD / HTTP/1.1" 200 0
Restored 3 datasets from disk
[2026-07-23 21:22:58,596] INFO django: Restored 3 datasets from disk
[2026-07-23 21:22:58,783] INFO analysis.data_loader: [LOAD_FROM_DB_LAZY] table=_data_64 rows=9990 cols=57
[2026-07-23 21:22:58,801] INFO analysis.data_loader: [LOAD_FROM_DB_LAZY] table=_data_66 rows=2000 cols=9
[2026-07-23 21:22:59,009] INFO analysis.data_loader: [LOAD_FROM_DB_LAZY] table=_data_68 rows=9990 cols=57
[23/Jul/2026 21:22:59] "GET / HTTP/1.1" 200 12728
Restored 3 datasets from disk
[2026-07-23 21:37:07[23/Jul/2026 21:37:07] "GET / HTTP/1.1" 200 12728
[[23/Jul/2026 21:37:38] "HEAD / HTTP/1.1" 200 0
[23/Jul/2026 21:37:49] "HEAD / HTTP/1.1" 200 0
[23/Jul/2026 21:38:08] "HEAD / HTTP/1.1" 200 0
[23/Jul/2026 21:38:38] "HEAD / HTTP/1.1" 200 0
[23/Jul/2026 21:39:08] "HEAD / HTTP/1.1" 200 0
[23/Jul/2026 21:39:38] "HEAD / HTTP/1.1" 200 0
[23/Jul/2026 21:39:43] "HEAD / HTTP/1.1" 200 0
[23/Jul/2026 21:39:45] "HEAD / HTTP/1.1" 200 0
[23/Jul/2026 21:39:49] "HEAD / HTTP/1.1" 200 0
[23/Jul/2026 21:40:08] "HEAD / HTTP/1.1" 200 0
[23/Jul/2026 21:40:38] "HEAD / HTTP/1.1" 200 0
[23/Jul/2026 21:41:35] "HEAD / HTTP/1.1" 200 0
Restored 3 datasets from disk
[2026-07-23 21:42:01,918] INFO django: Restored 3 datasets from disk
[2026-07-23 21:42:02,145] INFO analysis.data_loader: [LOAD_FROM_DB_LAZY] table=_data_64 rows=9990 cols=57
[2026-07-23 21:42:02,153] INFO analysis.data_loader: [LOAD_FROM_DB_LAZY] table=_data_66 rows=2000 cols=9
[2026-07-23 21:42:02,295] INFO analysis.data_loader: [LOAD_FROM_DB_LAZY] table=_data_68 rows=9990 cols=57
esponse = wrapped_callback(request, *callback_args, **callback_kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "E:\hjq\天璇\simple_analysis\views.py", line 796, in index
File "E:\hjq\天璇\runtime\python\Lib\site-packages\django\shortcuts.py", line 24, in render
content = loader.render_to_string(template_name, context, request, using=using)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "E:\hjq\天璇\runtime\python\Lib\site-packages\django\template\loader.py", line 61, in render_to_string
template = get_template(template_name, using=using)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "E:\hjq\天璇\runtime\python\Lib\site-packages\django\template\loader.py", line 19, in get_template
raise TemplateDoesNotExist(template_name, chain=chain)
django.template.exceptions.TemplateDoesNotExist: simple_analysis/simple_analysis.html
[2026-07-23 21:41:56,536] ERROR django.request: Internal Server Error: /simple/
Traceback (most recent call last):
File "E:\hjq\天璇\runtime\python\Lib\site-packages\django\core\handlers\exception.py", line 55, in inner
response = get_response(request)
^^^^^^^^^^^^^^^^^^^^^
File "E:\hjq\天璇\runtime\python\Lib\site-packages\django\core\handlers\base.py", line 197, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "E:\hjq\天璇\simple_analysis\views.py", line 796, in index
File "E:\hjq\天璇\runtime\python\Lib\site-packages\django\shortcuts.py", line 24, in render
content = loader.render_to_string(template_name, context, request, using=using)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "E:\hjq\天璇\runtime\python\Lib\site-packages\django\template\loader.py", line 61, in render_to_string
template = get_template(template_name, using=using)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "E:\hjq\天璇\runtime\python\Lib\site-packages\django\template\loader.py", line 19, in get_template
raise TemplateDoesNotExist(template_name, chain=chain)
django.template.exceptions.TemplateDoesNotExist: simple_analysis/simple_analysis.html
[23/Jul/2026 21:41:56] "GET /simple/ HTTP/1.1" 500 80559
Not Found: /favicon.ico
[2026-07-23 21:41:56,814] WARNING django.request: Not Found: /favicon.ico
[23/Jul/2026 21:41:56] "GET /favicon.ico HTTP/1.1" 404 6872
Restored 3 datasets from disk
[2026-07-23 21:45:23,039] INFO django: Restored 3 datasets from disk
[2026-07-23 21:45:23,182] INFO analysis.data_loader: [LOAD_FROM_DB_LAZY] table=_data_64 rows=9990 cols=57
[2026-07-23 21:45:23,190] INFO analysis.data_loader: [LOAD_FROM_DB_LAZY] table=_data_66 rows=2000 cols=9
[2026-07-23 21:45:23,314] INFO analysis.data_loader: [LOAD_FROM_DB_LAZY] table=_data_68 rows=9990 cols=57
[23/Jul/2026 21:45:28] "GET / HTTP/1.1" 200 12645
[23/Jul/2026 21:45:31] "GET /upload/ HTTP/1.1" 200 19078
[23/Jul/2026 21:45:44] "POST /runs/5/delete/ HTTP/1.1" 200 17
[23/Jul/2026 21:45:44] "GET /upload/ HTTP/1.1" 200 18534
[23/Jul/2026 21:45:46] "POST /runs/4/delete/ HTTP/1.1" 200 17
[23/Jul/2026 21:45:46] "GET /upload/ HTTP/1.1" 200 17939
[23/Jul/2026 21:45:48] "POST /runs/3/delete/ HTTP/1.1" 200 17
[23/Jul/2026 21:45:48] "GET /upload/ HTTP/1.1" 200 17395
[23/Jul/2026 21:45:51] "POST /runs/2/delete/ HTTP/1.1" 200 17
[23/Jul/2026 21:45:51] "GET /upload/ HTTP/1.1" 200 16853
[23/Jul/2026 21:45:52] "POST /runs/1/delete/ HTTP/1.1" 200 17
[23/Jul/2026 21:45:52] "GET /upload/ HTTP/1.1" 200 16136
[23/Jul/2026 21:46:23] "HEAD / HTTP/1.1" 200 0
[23/Jul/2026 21:46:53] "HEAD / HTTP/1.1" 200 0
[23/Jul/2026 21:47:23] "HEAD / HTTP/1.1" 200 0
[23/Jul/2026 21:47:53] "HEAD / HTTP/1.1" 200 0
[23/Jul/2026 21:48:23] "HEAD / HTTP/1.1" 200 0
[23/Jul/2026 21:48:53] "HEAD / HTTP/1.1" 200 0
[23/Jul/2026 21:49:23] "HEAD / HTTP/1.1" 200 0
[23/Jul/2026 21:49:53] "HEAD / HTTP/1.1" 200 0
[23/Jul/2026 21:50:23] "HEAD / HTTP/1.1" 200 0
Restored 3 datasets from disk
[2026-07-23 21:50:50,464] INFO django: Restored 3 datasets from disk
[23/Jul/2026 21:50:54] "GET /analyze/manual/ HTTP/1.1" 200 65847
[23/Jul/2026 21:50:54] "GET /tools/plan/ HTTP/1.1" 200 13
[23/Jul/2026 21:50:57] "GET /analyze/auto/ HTTP/1.1" 200 39486
[23/Jul/2026 21:51:09] "GET /runs/ HTTP/1.1" 200 9674
[2026-07-23 21:51:10,324] INFO analysis.geoip: [GEOIP] loaded 0 ranges (0 skipped)
[2026-07-23 21:51:10,325] INFO analysis.geoip: [GEOIP] loaded 12 cached entries
[23/Jul/2026 21:51:10] "GET /globe/ HTTP/1.1" 200 18520
[23/Jul/2026 21:51:10] "GET /static/tianxuan/three.min.js HTTP/1.1" 304 0
[23/Jul/2026 21:51:10] "GET /static/tianxuan/world_borders.js HTTP/1.1" 304 0
[23/Jul/2026 21:51:10] "GET /static/tianxuan/three.min.js HTTP/1.1" 304 0
[23/Jul/2026 21:51:10] "GET /static/tianxuan/world_borders.js HTTP/1.1" 304 0
[23/Jul/2026 21:51:10] "GET /static/tianxuan/earth_atmos_2048.jpg HTTP/1.1" 304 0
[23/Jul/2026 21:51:27] "GET /config/ HTTP/1.1" 200 14372
Restored 1 datasets from disk
[2026-07-24 12:30:18,755] INFO analysis.data_loader: [LOAD] files=1 total_rows_est=200
[2026-07-24 12:30:18,742] INFO django: Restored 1 datasets from disk
[2026-07-24 12:30:18,811] INFO analysis.data_loader: [LOAD_SCHEMA] col=0cph dtype=String
[2026-07-24 12:30:18,811] INFO analysis.data_loader: [LOAD_SCHEMA] col=0crv dtype=String
[2026-07-24 12:30:18,811] INFO analysis.data_loader: [LOAD_SCHEMA] col=0rnd dtype=String
[2026-07-24 12:30:18,811] INFO analysis.data_loader: [LOAD_SCHEMA] col=0rnt dtype=String
[2026-07-24 12:30:18,811] INFO analysis.data_loader: [LOAD_SCHEMA] col=0ver dtype=String
[2026-07-24 12:30:18,811] INFO analysis.data_loader: [LOAD_SCHEMA] col=1ipp dtype=Int64
[2026-07-24 12:30:18,811] INFO analysis.data_loader: [LOAD_SCHEMA] col=2tmo dtype=Float64
[2026-07-24 12:30:18,811] INFO analysis.data_loader: [LOAD_SCHEMA] col=4dbn dtype=Int64
[2026-07-24 12:30:18,817] INFO analysis.data_loader: [LOAD_SCHEMA] col=4dur dtype=Float64
[2026-07-24 12:30:18,817] INFO analysis.data_loader: [LOAD_SCHEMA] col=4ksz dtype=Int64
[2026-07-24 12:30:18,817] INFO analysis.data_loader: [LOAD_SCHEMA] col=4srs dtype=Int64
[2026-07-24 12:30:18,817] INFO analysis.data_loader: [LOAD_SCHEMA] col=8ack dtype=Int64
[2026-07-24 12:30:18,817] INFO analysis.data_loader: [LOAD_SCHEMA] col=8byt dtype=Int64
[2026-07-24 12:30:18,817] INFO analysis.data_loader: [LOAD_SCHEMA] col=8dbd dtype=Int64
[2026-07-24 12:30:18,818] INFO analysis.data_loader: [LOAD_SCHEMA] col=8did dtype=Int64
[2026-07-24 12:30:18,818] INFO analysis.data_loader: [LOAD_SCHEMA] col=8pak dtype=Int64
[2026-07-24 12:30:18,818] INFO analysis.data_loader: [LOAD_SCHEMA] col=8ppk dtype=Int64
[2026-07-24 12:30:18,818] INFO analysis.data_loader: [LOAD_SCHEMA] col=8seq dtype=Float64
[2026-07-24 12:30:18,818] INFO analysis.data_loader: [LOAD_SCHEMA] col=8ses dtype=Float64
[2026-07-24 12:30:18,818] INFO analysis.data_loader: [LOAD_SCHEMA] col=:ipd dtype=String
[2026-07-24 12:30:18,818] INFO analysis.data_loader: [LOAD_SCHEMA] col=:ipd.anon dtype=String
[2026-07-24 12:30:18,822] INFO analysis.data_loader: [LOAD_SCHEMA] col=:ipd.city dtype=String
[2026-07-24 12:30:18,822] INFO analysis.data_loader: [LOAD_SCHEMA] col=:ipd.doma dtype=String
[2026-07-24 12:30:18,822] INFO analysis.data_loader: [LOAD_SCHEMA] col=:ipd.ispn dtype=String
[2026-07-24 12:30:18,822] INFO analysis.data_loader: [LOAD_SCHEMA] col=:ipd.latd dtype=String
[2026-07-24 12:30:18,822] INFO analysis.data_loader: [LOAD_SCHEMA] col=:ipd.lond dtype=String
[2026-07-24 12:30:18,822] INFO analysis.data_loader: [LOAD_SCHEMA] col=:ipd.orgn dtype=String
[2026-07-24 12:30:18,822] INFO analysis.data_loader: [LOAD_SCHEMA] col=:ips dtype=String
[2026-07-24 12:30:18,822] INFO analysis.data_loader: [LOAD_SCHEMA] col=:ips.anon dtype=String
[2026-07-24 12:30:18,822] INFO analysis.data_loader: [LOAD_SCHEMA] col=:ips.city dtype=String
[2026-07-24 12:30:18,822] INFO analysis.data_loader: [LOAD_SCHEMA] col=:ips.doma dtype=String
[2026-07-24 12:30:18,822] INFO analysis.data_loader: [LOAD_SCHEMA] col=:ips.ispn dtype=String
[2026-07-24 12:30:18,822] INFO analysis.data_loader: [LOAD_SCHEMA] col=:ips.latd dtype=String
[2026-07-24 12:30:18,822] INFO analysis.data_loader: [LOAD_SCHEMA] col=:ips.lond dtype=String
[2026-07-24 12:30:18,822] INFO analysis.data_loader: [LOAD_SCHEMA] col=:ips.orgn dtype=String
[2026-07-24 12:30:18,827] INFO analysis.data_loader: [LOAD_SCHEMA] col=:prd dtype=Int64
[2026-07-24 12:30:18,827] INFO analysis.data_loader: [LOAD_SCHEMA] col=:prs dtype=Int64
[2026-07-24 12:30:18,827] INFO analysis.data_loader: [LOAD_SCHEMA] col=cipher-suite dtype=String
[2026-07-24 12:30:18,827] INFO analysis.data_loader: [LOAD_SCHEMA] col=client-ip dtype=String
[2026-07-24 12:30:18,827] INFO analysis.data_loader: [LOAD_SCHEMA] col=cnam dtype=String
[2026-07-24 12:30:18,827] INFO analysis.data_loader: [LOAD_SCHEMA] col=crcc dtype=String
[2026-07-24 12:30:18,827] INFO analysis.data_loader: [LOAD_SCHEMA] col=dcnt dtype=String
[2026-07-24 12:30:18,827] INFO analysis.data_loader: [LOAD_SCHEMA] col=ecdhe-named-curve dtype=String
[2026-07-24 12:30:18,827] INFO analysis.data_loader: [LOAD_SCHEMA] col=name dtype=String
[2026-07-24 12:30:18,827] INFO analysis.data_loader: [LOAD_SCHEMA] col=orga dtype=String
[2026-07-24 12:30:18,827] INFO analysis.data_loader: [LOAD_SCHEMA] col=orgu dtype=Int64
[2026-07-24 12:30:18,827] INFO analysis.data_loader: [LOAD_SCHEMA] col=scnt dtype=String
[2026-07-24 12:30:18,827] INFO analysis.data_loader: [LOAD_SCHEMA] col=server-ip dtype=String
[2026-07-24 12:30:18,827] INFO analysis.data_loader: [LOAD_SCHEMA] col=snam dtype=String
[2026-07-24 12:30:18,827] INFO analysis.data_loader: [LOAD_SCHEMA] col=source-node dtype=String
[2026-07-24 12:30:18,832] INFO analysis.data_loader: [LOAD_SCHEMA] col=tabl dtype=String
[2026-07-24 12:30:18,832] INFO analysis.data_loader: [LOAD_SCHEMA] col=time dtype=String
[2026-07-24 12:30:18,832] INFO analysis.data_loader: [LOAD_SCHEMA] col=timestamp dtype=Float64
[2026-07-24 12:30:18,840] INFO analysis.data_loader: [CLEAN] Generic numeric coercion applied: [':ipd.lond', ':ipd.doma', ':ipd.ispn', 'cnam', ':ips.ispn', ':ips.doma', 'orga', 'snam', 'time', ':ips.orgn', ':ipd.orgn', 'crcc', ':ips.latd', ':ipd.latd', ':ips.city', ':ipd.city', ':ips.lond']
[2026-07-24 12:30:18,840] INFO analysis.data_loader: [CLEAN] HEX columns added hex_pairs_count: ['0cph', '0crv', '0ver', '0rnt', '0rnd']
[24/Jul/2026 12:30:37] "HEAD / HTTP/1.1" 200 0
[24/Jul/2026 12:31:07] "HEAD / HTTP/1.1" 200 0
+2 -59
View File
@@ -1,65 +1,8 @@
Performing system checks...
System check identified no issues (0 silenced).
July 23, 2026 - 13:06:51
July 24, 2026 - 12:30:18
Django version 4.2.30, using settings 'tianxuan.settings'
Starting development server at http://127.0.0.1:18766/
Quit the server with CTRL-BREAK.
Performing system checks...
System check identified no issues (0 silenced).
Performing system checks...
System check identified no issues (0 silenced).
Performing system checks...
System check identified no issues (0 silenced).
July 23, 2026 - 21:16:22
Django version 4.2.30, using settings 'tianxuan.settings'
Starting development server at http://0.0.0.0:8000/
Quit the server with CTRL-BREAK.
Performing system checks...
System check identified no issues (0 silenced).
Performing system checks...
System check identified no issues (0 silenced).
July 23, 2026 - 21:22:58
Django version 4.2.30, using settings 'tianxuan.settings'
Starting development server at http://0.0.0.0:8000/
Quit the server with CTRL-BREAK.
Performing system checks...
System check identified no issues (0 silenced).
July 23, 2026 - 21:37:07
Django version 4.2.30, using settings 'tianxuan.settings'
Starting development server at http://0.0.0.0:8000/
Quit the server with CTRL-BREAK.
Performing system checks...
System check identified no issues (0 silenced).
July 23, 2026 - 21:42:01
Django version 4.2.30, using settings 'tianxuan.settings'
Starting development server at http://0.0.0.0:8000/
Quit the server with CTRL-BREAK.
Performing system checks...
System check identified no issues (0 silenced).
July 23, 2026 - 21:45:23
Django version 4.2.30, using settings 'tianxuan.settings'
Starting development server at http://0.0.0.0:8000/
Quit the server with CTRL-BREAK.
Performing system checks...
System check identified no issues (0 silenced).
July 23, 2026 - 21:50:50
Django version 4.2.30, using settings 'tianxuan.settings'
Starting development server at http://0.0.0.0:8000/
Starting development server at http://127.0.0.1:8765/
Quit the server with CTRL-BREAK.
+23 -6
View File
@@ -1,4 +1,4 @@
{% extends 'base.html' %}
{% extends 'base.html' %}
{% load static %}
{% block title %}簇 #{{ cluster.cluster_label }} — 运行 #{{ run.display_id }}{% endblock %}
{% block content %}
@@ -53,11 +53,11 @@
<td>{{ forloop.counter }}</td>
<td><code>{{ f.feature_name }}</code></td>
<td><span class="{% if f.distinguishing_score > 0 %}feat-positive{% else %}feat-negative{% endif %}">{{ f.distinguishing_score|floatformat:3 }}</span></td>
<td>{{ f.mean|floatformat:3|default:"-" }}</td>
<td class="ts-val" data-col="{{ f.feature_name }}">{{ f.mean|floatformat:3|default:"-" }}</td>
<td>{{ f.std|floatformat:3|default:"-" }}</td>
<td>{{ f.median|floatformat:3|default:"-" }}</td>
<td>{{ f.p25|floatformat:3|default:"-" }}</td>
<td>{{ f.p75|floatformat:3|default:"-" }}</td>
<td class="ts-val" data-col="{{ f.feature_name }}">{{ f.median|floatformat:3|default:"-" }}</td>
<td class="ts-val" data-col="{{ f.feature_name }}">{{ f.p25|floatformat:3|default:"-" }}</td>
<td class="ts-val" data-col="{{ f.feature_name }}">{{ f.p75|floatformat:3|default:"-" }}</td>
</tr>
{% endfor %}
</tbody>
@@ -121,4 +121,21 @@
<div class="empty-state"><p>该簇无数据行</p></div>
{% endif %}
</div>
{% endblock %}
{% endblock %}
<script>
(function(){
var tsCols=["timestamp","time","epoch","ts","8dbd","created_at","updated_at","event_time","datetime"];
document.querySelectorAll(".ts-val").forEach(function(el){
var col=el.dataset.col,isTs=false;
for(var i=0;i<tsCols.length;i++){if(col.toLowerCase().indexOf(tsCols[i])>=0){isTs=true;break;}}
if(!isTs)return;
var raw=parseFloat(el.textContent);
if(isNaN(raw)||raw<1e8||raw>2e12)return;
var d=new Date(raw*1000);
if(d.getFullYear()<2000||d.getFullYear()>2100)return;
el.textContent=d.toISOString().replace("T"," ").substring(0,16);
el.title="epoch: "+raw;
});
})();
</script>
+3 -2
View File
@@ -9,7 +9,7 @@
.left-panel.globe-open { flex:0 0 55%; }
.right-panel { flex:0 0 0; overflow:hidden; transition:flex 0.3s; border-left:1px solid #e0e0e0; }
.right-panel.globe-open { flex:0 0 45%; }
.globe-toggle-handle { position:absolute; z-index:10; top:50%; left:calc(100% - 24px); transform:translateY(-50%); width:24px; height:48px; background:#1a1a2e; color:#fff; border:none; border-radius:0 6px 6px 0; cursor:pointer; display:flex; align-items:center; justify-content:center; font-size:14px; transition:left 0.3s, background 0.15s; }
.globe-toggle-handle { position:absolute; z-index:10; top:50%; left:calc(100% - 24px); transform:translateY(-50%); width:24px; height:48px; background:#1a1a2e; color:#fff; border:none; border-radius:6px 0 0 6px; cursor:pointer; display:flex; align-items:center; justify-content:center; font-size:14px; transition:left 0.3s, background 0.15s; }
.globe-toggle-handle:hover { background:#4361ee; }
.globe-toggle-handle.open { left:calc(55% - 24px); border-radius:6px 0 0 6px; }
/* ── Scatter ── */
@@ -61,7 +61,7 @@
<!-- ── UMAP 2D / 3D ── -->
<div class="card">
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:0.5rem;">
<h3 style="margin:0;">UMAP 嵌入 <span style="font-weight:400;font-size:0.8rem;color:#888;"><script>document.write(scatterCount)</script> 个点</span></h3>
<h3 style="margin:0;">UMAP 嵌入 <span style="font-weight:400;font-size:0.8rem;color:#888;" id="pointCount">计算中...</span></h3>
<span id="viewToggle" style="display:none;">
<button id="btn2D" class="btn btn-sm" style="font-weight:bold;padding:2px 10px;">2D</button>
<button id="btn3D" class="btn btn-sm btn-outline" style="padding:2px 10px;">3D</button>
@@ -137,6 +137,7 @@
<script>
const scatterData = {{ scatter_data_json|safe }};
const scatterCount = scatterData.length;
document.getElementById('pointCount').textContent = scatterCount + ' 个点';
const hasZ = {{ has_z|yesno:"true,false" }};
const colors = ['#4361ee','#f72585','#7209b7','#4cc9f0','#f8961e','#43aa8b','#f94144','#577590','#e9c46a','#9b5de5',
'#1f77b4','#ff7f0e','#2ca02c','#d62728','#9467bd','#8c564b','#e377c2','#7f7f7f','#bcbd22','#17becf',