chore: consolidate all session fixes — migrations squash, views/tools refactor, cluster UI, geoip upgrade, simple_analysis removal, non-blocking startup, chinese UI, API key removal
This commit is contained in:
@@ -0,0 +1 @@
|
||||
11572
|
||||
@@ -771,3 +771,8 @@ def load_csv_directory(
|
||||
total_row_count = min(total_row_count, head)
|
||||
|
||||
return merged_lf, merged_schema, total_row_count, len(paths), memory_mb
|
||||
|
||||
|
||||
# Auto-profile all public functions in this module
|
||||
from analysis.profile_util import auto_profile_module # noqa: E402
|
||||
auto_profile_module(__name__)
|
||||
|
||||
@@ -331,3 +331,8 @@ def profile_dataset(
|
||||
}
|
||||
|
||||
return truncate_profile(result, max_kb=max_kb)
|
||||
|
||||
|
||||
# Auto-profile all public functions in this module
|
||||
from analysis.profile_util import auto_profile_module # noqa: E402
|
||||
auto_profile_module(__name__)
|
||||
|
||||
+149
-100
@@ -1,14 +1,18 @@
|
||||
"""
|
||||
离线 GeoIP 查询。内嵌 IPv4 区间 → 城市/经纬度映射。
|
||||
离线 GeoIP 查询。纯本地运行,无网络请求。
|
||||
|
||||
从 ``data/geoip_data.txt`` 加载 IP 段数据,在模块首次导入时构建
|
||||
排序索引,提供 O(log N) 的 IP 查询能力。
|
||||
三级存储:
|
||||
1. 快速缓存 (data/geoip_cache.json) — 已查过的 IP,磁盘持久化
|
||||
2. 离线数据库 (data/geoip_data.txt) — 手动整理的优先 IP 段
|
||||
3. MMDB 数据库 — 若 python_geoip_geolite2 包已安装,加载其
|
||||
内置的 GeoLite2-City.mmdb 作为最全面回退。
|
||||
|
||||
数据约 200KB,覆盖 23 个主要城市 IP 段。
|
||||
可通过编辑 ``data/geoip_data.txt`` 扩展。
|
||||
查询链:缓存 > geoip_data.txt > MMDB > 写入缓存。
|
||||
"""
|
||||
import bisect
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
@@ -18,32 +22,22 @@ logger = logging.getLogger(__name__)
|
||||
# Data structures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# [(start_int, end_int, lat, lon, city, country), ...]
|
||||
# Built at module load time from data/geoip_data.txt
|
||||
_GEO_DATA: list[tuple[int, int, float, float, str, str]] = []
|
||||
|
||||
# Sorted list of start integers (for bisect lookup) + parallel data list
|
||||
_STARTS: list[int] = []
|
||||
_RANGES: list[tuple[int, int, float, float, str, str]] = []
|
||||
|
||||
_MMDB_READER = None
|
||||
|
||||
_CACHE: dict[str, Optional[dict]] = {}
|
||||
_CACHE_DIRTY = False
|
||||
_CACHE_PATH = Path(__file__).resolve().parent.parent / 'data' / 'geoip_cache.json'
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# IP conversion
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def ip_to_int(ip_str: str) -> Optional[int]:
|
||||
"""Convert an IPv4 dotted-string to its 32-bit integer representation.
|
||||
|
||||
Returns ``None`` if *ip_str* is not a valid IPv4 address (including
|
||||
when any octet is outside 0-255).
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> ip_to_int('8.8.8.8')
|
||||
134744072
|
||||
>>> ip_to_int('1.0.0.0')
|
||||
16777216
|
||||
"""
|
||||
parts = ip_str.strip().split('.')
|
||||
if len(parts) != 4:
|
||||
return None
|
||||
@@ -57,78 +51,144 @@ def ip_to_int(ip_str: str) -> Optional[int]:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Index building
|
||||
# geoip_data.txt loader
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _build_index() -> tuple[list[int], list[tuple[int, int, float, float, str, str]]]:
|
||||
"""Build sorted parallel lists for bisect-based IP lookup.
|
||||
|
||||
Processes ``_GEO_DATA``, sorts by start integer, and returns
|
||||
``(starts, ranges)`` where *starts* is a sorted list of start integers
|
||||
and *ranges* is the corresponding data list at the same indices.
|
||||
"""
|
||||
def _build_index():
|
||||
if not _GEO_DATA:
|
||||
return [], []
|
||||
|
||||
sorted_data = sorted(_GEO_DATA, key=lambda x: x[0])
|
||||
starts = [entry[0] for entry in sorted_data]
|
||||
return starts, sorted_data
|
||||
|
||||
|
||||
def _load_data_file() -> None:
|
||||
"""Load IP range data from ``data/geoip_data.txt`` into ``_GEO_DATA``.
|
||||
|
||||
Called once at module import time. Each non-blank, non-comment line
|
||||
in the file should contain six comma-separated fields::
|
||||
|
||||
start_ip,end_ip,lat,lon,city,country
|
||||
"""
|
||||
data_path = Path(__file__).parent.parent / 'data' / 'geoip_data.txt'
|
||||
|
||||
data_path = Path(__file__).resolve().parent.parent / 'data' / 'geoip_data.txt'
|
||||
if not data_path.exists():
|
||||
logger.warning('[GEOIP] data file not found: %s', data_path)
|
||||
return
|
||||
|
||||
loaded: list[tuple[int, int, float, float, str, str]] = []
|
||||
loaded = []
|
||||
skipped = 0
|
||||
|
||||
with open(data_path, 'r', encoding='utf-8') as f:
|
||||
for line_no, raw in enumerate(f, start=1):
|
||||
for raw in f:
|
||||
stripped = raw.strip()
|
||||
if not stripped or stripped.startswith('#'):
|
||||
continue
|
||||
|
||||
parts = [p.strip() for p in stripped.split(',')]
|
||||
if len(parts) != 6:
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
start_ip_str, end_ip_str, lat_str, lon_str, city, country = parts
|
||||
|
||||
start_int = ip_to_int(start_ip_str)
|
||||
end_int = ip_to_int(end_ip_str)
|
||||
|
||||
if start_int is None or end_int is None:
|
||||
sip, eip, lat_s, lon_s, city, country = parts
|
||||
si = ip_to_int(sip)
|
||||
ei = ip_to_int(eip)
|
||||
if si is None or ei is None or si > ei:
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
try:
|
||||
lat = float(lat_str)
|
||||
lon = float(lon_str)
|
||||
lat = float(lat_s)
|
||||
lon = float(lon_s)
|
||||
except ValueError:
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
loaded.append((start_int, end_int, lat, lon, city, country))
|
||||
|
||||
loaded.append((si, ei, lat, lon, city, country))
|
||||
_GEO_DATA.clear()
|
||||
_GEO_DATA.extend(loaded)
|
||||
|
||||
global _STARTS, _RANGES
|
||||
_STARTS, _RANGES = _build_index()
|
||||
logger.info('[GEOIP] loaded %d ranges (%d skipped)', len(loaded), skipped)
|
||||
|
||||
logger.info('[GEOIP] loaded %d ranges from %s (%d skipped)',
|
||||
len(loaded), data_path, skipped)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MMDB reader (GeoLite2-City.mmdb bundled with python_geoip_geolite2)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _get_mmdb_reader():
|
||||
global _MMDB_READER
|
||||
if _MMDB_READER is not None:
|
||||
return _MMDB_READER
|
||||
# Try multiple locations for the MMDB file
|
||||
candidates = []
|
||||
try:
|
||||
import _geoip_geolite2
|
||||
candidates.append(Path(_geoip_geolite2.__file__).parent / 'GeoLite2-City.mmdb')
|
||||
except Exception:
|
||||
pass
|
||||
# Fallback: temp directory without Chinese chars in path
|
||||
candidates.append(Path(os.environ.get('TEMP', 'C:/temp')) / 'opencode' / 'GeoLite2-City.mmdb')
|
||||
candidates.append(Path.home() / 'AppData' / 'Local' / 'Temp' / 'opencode' / 'GeoLite2-City.mmdb')
|
||||
|
||||
for mmdb_path in candidates:
|
||||
if mmdb_path.exists():
|
||||
try:
|
||||
import maxminddb
|
||||
_MMDB_READER = maxminddb.open_database(mmdb_path)
|
||||
logger.info('[GEOIP] loaded MMDB: %s (%.1f MB)',
|
||||
mmdb_path.name, mmdb_path.stat().st_size / 1e6)
|
||||
break
|
||||
except Exception as exc:
|
||||
logger.debug('[GEOIP] failed to open %s: %s', mmdb_path, exc)
|
||||
continue
|
||||
|
||||
if _MMDB_READER is None:
|
||||
logger.debug('[GEOIP] MMDB not available — falling back to txt database only')
|
||||
return _MMDB_READER
|
||||
|
||||
|
||||
def _mmdb_lookup(ip_str: str) -> Optional[dict]:
|
||||
reader = _get_mmdb_reader()
|
||||
if reader is None:
|
||||
return None
|
||||
try:
|
||||
result = reader.get(ip_str)
|
||||
if result is None:
|
||||
return None
|
||||
location = result.get('location', {})
|
||||
city_data = result.get('city', {})
|
||||
country_data = result.get('country', {})
|
||||
lat = location.get('latitude')
|
||||
lon = location.get('longitude')
|
||||
if lat is None or lon is None:
|
||||
return None
|
||||
city = ''
|
||||
if city_data and 'names' in city_data:
|
||||
city = city_data['names'].get('zh-CN') or city_data['names'].get('en', '')
|
||||
country = ''
|
||||
if country_data and 'names' in country_data:
|
||||
country = country_data['names'].get('zh-CN') or country_data['names'].get('en', '')
|
||||
return {'lat': float(lat), 'lon': float(lon), 'city': city, 'country': country}
|
||||
except Exception as exc:
|
||||
logger.debug('[GEOIP] MMDB lookup failed for %s: %s', ip_str, exc)
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Disk cache
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _load_cache() -> None:
|
||||
if not _CACHE_PATH.exists():
|
||||
return
|
||||
try:
|
||||
data = json.loads(_CACHE_PATH.read_text(encoding='utf-8'))
|
||||
_CACHE.update(data)
|
||||
logger.info('[GEOIP] loaded %d cached entries', len(data))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _save_cache() -> None:
|
||||
global _CACHE_DIRTY
|
||||
if not _CACHE_DIRTY:
|
||||
return
|
||||
try:
|
||||
_CACHE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
_CACHE_PATH.write_text(
|
||||
json.dumps(_CACHE, ensure_ascii=False, indent=2),
|
||||
encoding='utf-8',
|
||||
)
|
||||
_CACHE_DIRTY = False
|
||||
except Exception as exc:
|
||||
logger.warning('[GEOIP] cache write failed: %s', exc)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -136,55 +196,44 @@ def _load_data_file() -> None:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def lookup(ip_str: str) -> Optional[dict]:
|
||||
"""查询 IP 对应的地理位置。
|
||||
"""查询 IP 地理位置。纯本地,无网络。
|
||||
|
||||
Uses binary search (``bisect_right``) on the sorted start-IP list to
|
||||
locate the range *ip_str* falls within, then checks the end bound.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ip_str:
|
||||
IPv4 address as a dotted string (e.g. ``'8.8.8.8'``).
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict or None
|
||||
A dictionary with keys ``lat``, ``lon``, ``city``, ``country``
|
||||
if the IP is found in the database; ``None`` otherwise.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> result = lookup('8.8.8.8')
|
||||
>>> result['city']
|
||||
'Mountain View'
|
||||
>>> lookup('10.0.0.1') is None
|
||||
True
|
||||
查询链:磁盘缓存 > geoip_data.txt (O(log N)) > MMDB (GeoLite2) > 写入缓存。
|
||||
"""
|
||||
# 1. Cache hit
|
||||
cached = _CACHE.get(ip_str)
|
||||
if cached is not None:
|
||||
return cached
|
||||
if ip_str in _CACHE:
|
||||
return None
|
||||
|
||||
result = None
|
||||
|
||||
# 2. geoip_data.txt
|
||||
target = ip_to_int(ip_str)
|
||||
if target is None:
|
||||
return None
|
||||
if target is not None and _STARTS:
|
||||
idx = bisect.bisect_right(_STARTS, target) - 1
|
||||
if idx >= 0:
|
||||
start, end, lat, lon, city, country = _RANGES[idx]
|
||||
if start <= target <= end:
|
||||
result = {'lat': lat, 'lon': lon, 'city': city, 'country': country}
|
||||
|
||||
if not _STARTS:
|
||||
return None
|
||||
# 3. MMDB fallback (GeoLite2, ~3M entries world-wide)
|
||||
if result is None:
|
||||
result = _mmdb_lookup(ip_str)
|
||||
|
||||
idx = bisect.bisect_right(_STARTS, target) - 1
|
||||
if idx < 0:
|
||||
return None
|
||||
# 4. Cache for next time
|
||||
global _CACHE_DIRTY
|
||||
_CACHE[ip_str] = result
|
||||
_CACHE_DIRTY = True
|
||||
_save_cache()
|
||||
|
||||
start, end, lat, lon, city, country = _RANGES[idx]
|
||||
if start <= target <= end:
|
||||
return {
|
||||
'lat': lat,
|
||||
'lon': lon,
|
||||
'city': city,
|
||||
'country': country,
|
||||
}
|
||||
|
||||
return None
|
||||
return result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Module-level initialisation
|
||||
# Module-level init
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_load_data_file()
|
||||
_load_cache()
|
||||
|
||||
+60
-1
@@ -1,11 +1,19 @@
|
||||
# Generated by Django 4.2.30 on 2026-07-13 14:36
|
||||
# Generated by Django 4.2.30 on 2026-07-23 11:15
|
||||
|
||||
from django.db import migrations, models
|
||||
from django.db.models import F
|
||||
import django.db.migrations.operations.special
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
def populate_display_id(apps, schema_editor):
|
||||
AnalysisRun = apps.get_model('tianxuan_analysis', 'AnalysisRun')
|
||||
AnalysisRun.objects.filter(display_id__isnull=True).update(display_id=F('id'))
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('tianxuan_analysis', '0001_initial'), ('tianxuan_analysis', '0002_analysisrun_progress_msg_analysisrun_progress_pct_and_more'), ('tianxuan_analysis', '0003_analysisrun_sqlite_table'), ('tianxuan_analysis', '0004_tls_ref_data'), ('tianxuan_analysis', '0005_remove_entity_column'), ('tianxuan_analysis', '0006_analysisrun_display_id_analysisrun_llm_thinking_and_more'), ('tianxuan_analysis', '0007_populate_display_id'), ('tianxuan_analysis', '0008_entityprofile_embedding_z_and_more')]
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
@@ -26,6 +34,10 @@ class Migration(migrations.Migration):
|
||||
('entity_count', models.IntegerField(blank=True, null=True)),
|
||||
('cluster_count', models.IntegerField(blank=True, null=True)),
|
||||
('entity_column', models.CharField(blank=True, default='', help_text='Auto-detected entity column name', max_length=256)),
|
||||
('progress_msg', models.CharField(blank=True, default='', max_length=256)),
|
||||
('progress_pct', models.IntegerField(default=0)),
|
||||
('run_log', models.TextField(blank=True, default='')),
|
||||
('sqlite_table', models.CharField(blank=True, default='', help_text='SQLite table name for persisted raw data', max_length=256)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-created_at'],
|
||||
@@ -81,4 +93,51 @@ class Migration(migrations.Migration):
|
||||
'unique_together': {('cluster', 'feature_name')},
|
||||
},
|
||||
),
|
||||
migrations.RunSQL(
|
||||
sql="\nCREATE TABLE IF NOT EXISTS tls_ref_data (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n field_code TEXT NOT NULL UNIQUE,\n meaning TEXT NOT NULL DEFAULT '',\n data_type TEXT NOT NULL DEFAULT ''\n);\n",
|
||||
reverse_sql='\nDROP TABLE IF EXISTS tls_ref_data;\n',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='analysisrun',
|
||||
name='entity_column',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='analysisrun',
|
||||
name='display_id',
|
||||
field=models.IntegerField(blank=True, help_text='User-facing numeric ID (recycles gaps)', null=True, unique=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='analysisrun',
|
||||
name='llm_thinking',
|
||||
field=models.TextField(blank=True, default='', help_text='Accumulated LLM reasoning / thinking text'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='analysisrun',
|
||||
name='run_type',
|
||||
field=models.CharField(choices=[('upload', '手动上传'), ('manual', '手动分析'), ('auto', 'LLM自动')], default='upload', max_length=16),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='analysisrun',
|
||||
name='tool_calls_json',
|
||||
field=models.JSONField(blank=True, default=list, help_text='Structured tool call list: [{step, name, input, output}, ...]'),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=populate_display_id,
|
||||
reverse_code=django.db.migrations.operations.special.RunPython.noop,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='entityprofile',
|
||||
name='embedding_z',
|
||||
field=models.FloatField(blank=True, help_text='UMAP-3D Z coordinate', null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='entityprofile',
|
||||
name='embedding_x',
|
||||
field=models.FloatField(blank=True, help_text='UMAP-2D X coordinate', null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='entityprofile',
|
||||
name='embedding_y',
|
||||
field=models.FloatField(blank=True, help_text='UMAP-2D Y coordinate', null=True),
|
||||
),
|
||||
]
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
# Generated by Django 4.2.30 on 2026-07-16 05:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tianxuan_analysis', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='analysisrun',
|
||||
name='progress_msg',
|
||||
field=models.CharField(blank=True, default='', max_length=256),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='analysisrun',
|
||||
name='progress_pct',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='analysisrun',
|
||||
name='run_log',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 4.2.30 on 2026-07-20 05:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tianxuan_analysis', '0002_analysisrun_progress_msg_analysisrun_progress_pct_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='analysisrun',
|
||||
name='sqlite_table',
|
||||
field=models.CharField(blank=True, default='', help_text='SQLite table name for persisted raw data', max_length=256),
|
||||
),
|
||||
]
|
||||
@@ -1,38 +0,0 @@
|
||||
"""Migration 0004 — create raw SQLite table ``tls_ref_data``.
|
||||
|
||||
Replaces the ``TlsDB.csv`` reference file with a proper SQLite table.
|
||||
The table stores TLS field-code metadata: human-readable meaning and
|
||||
data type for each field code used in TLS flow data.
|
||||
"""
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
# Raw SQL for creating the tls_ref_data table
|
||||
CREATE_TLS_REF_DATA = """
|
||||
CREATE TABLE IF NOT EXISTS tls_ref_data (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
field_code TEXT NOT NULL UNIQUE,
|
||||
meaning TEXT NOT NULL DEFAULT '',
|
||||
data_type TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
"""
|
||||
|
||||
DROP_TLS_REF_DATA = """
|
||||
DROP TABLE IF EXISTS tls_ref_data;
|
||||
"""
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
"""Create ``tls_ref_data`` table via raw SQL — no Django model needed."""
|
||||
|
||||
dependencies = [
|
||||
('tianxuan_analysis', '0003_analysisrun_sqlite_table'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunSQL(
|
||||
sql=CREATE_TLS_REF_DATA,
|
||||
reverse_sql=DROP_TLS_REF_DATA,
|
||||
state_operations=None,
|
||||
),
|
||||
]
|
||||
@@ -1,17 +0,0 @@
|
||||
# Generated by Django 4.2.30 on 2026-07-20 05:20
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tianxuan_analysis', '0004_tls_ref_data'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='analysisrun',
|
||||
name='entity_column',
|
||||
),
|
||||
]
|
||||
@@ -1,33 +0,0 @@
|
||||
# Generated by Django 4.2.30 on 2026-07-20 05:25
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tianxuan_analysis', '0005_remove_entity_column'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='analysisrun',
|
||||
name='display_id',
|
||||
field=models.IntegerField(blank=True, help_text='User-facing numeric ID (recycles gaps)', null=True, unique=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='analysisrun',
|
||||
name='llm_thinking',
|
||||
field=models.TextField(blank=True, default='', help_text='Accumulated LLM reasoning / thinking text'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='analysisrun',
|
||||
name='run_type',
|
||||
field=models.CharField(choices=[('upload', '手动上传'), ('manual', '手动分析'), ('auto', 'LLM自动')], default='upload', max_length=16),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='analysisrun',
|
||||
name='tool_calls_json',
|
||||
field=models.JSONField(blank=True, default=list, help_text='Structured tool call list: [{step, name, input, output}, ...]'),
|
||||
),
|
||||
]
|
||||
@@ -1,20 +0,0 @@
|
||||
# Generated by Django 4.2.30 on 2026-07-20 05:26
|
||||
|
||||
from django.db import migrations
|
||||
from django.db.models import F
|
||||
|
||||
|
||||
def populate_display_id(apps, schema_editor):
|
||||
AnalysisRun = apps.get_model('tianxuan_analysis', 'AnalysisRun')
|
||||
AnalysisRun.objects.filter(display_id__isnull=True).update(display_id=F('id'))
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tianxuan_analysis', '0006_analysisrun_display_id_analysisrun_llm_thinking_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(populate_display_id, migrations.RunPython.noop),
|
||||
]
|
||||
@@ -1,28 +0,0 @@
|
||||
# Generated by Django 4.2.30 on 2026-07-23 04:21
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tianxuan_analysis', '0007_populate_display_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='entityprofile',
|
||||
name='embedding_z',
|
||||
field=models.FloatField(blank=True, help_text='UMAP-3D Z coordinate', null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='entityprofile',
|
||||
name='embedding_x',
|
||||
field=models.FloatField(blank=True, help_text='UMAP-2D X coordinate', null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='entityprofile',
|
||||
name='embedding_y',
|
||||
field=models.FloatField(blank=True, help_text='UMAP-2D Y coordinate', null=True),
|
||||
),
|
||||
]
|
||||
@@ -400,3 +400,8 @@ def describe_cluster(
|
||||
def _signed(value: float) -> str:
|
||||
"""Return a signed string representation, e.g. ``"+2.1"``."""
|
||||
return f"{value:+.1f}"
|
||||
|
||||
|
||||
# Auto-profile all public functions in this module
|
||||
from analysis.profile_util import auto_profile_module # noqa: E402
|
||||
auto_profile_module(__name__)
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
"""Line profiling utility for the TianXuan backend.
|
||||
|
||||
Usage in any backend module::
|
||||
|
||||
from analysis.profile_util import profile
|
||||
|
||||
@profile
|
||||
def my_function(...):
|
||||
...
|
||||
|
||||
When ``TIANXUAN_PROFILE=1`` env var is set AND line_profiler is installed,
|
||||
the decorator activates real line-level profiling and prints stats at exit.
|
||||
Otherwise the decorator is a no-op.
|
||||
|
||||
To auto-profile ALL public functions in a module automatically, add at the
|
||||
bottom of the file::
|
||||
|
||||
from analysis.profile_util import auto_profile_module
|
||||
auto_profile_module(__name__)
|
||||
|
||||
Auto-profiling is also gated by ``TIANXUAN_PROFILE=1``.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
import atexit
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_PROFILER_ACTIVE = os.environ.get('TIANXUAN_PROFILE', '') == '1'
|
||||
|
||||
if _PROFILER_ACTIVE:
|
||||
try:
|
||||
from line_profiler import profile as _line_profile
|
||||
except ImportError:
|
||||
logger.warning('TIANXUAN_PROFILE=1 but line_profiler not installed')
|
||||
_PROFILER_ACTIVE = False
|
||||
|
||||
|
||||
def profile(func):
|
||||
"""Decorate a function for line-level profiling.
|
||||
|
||||
Active only when ``TIANXUAN_PROFILE=1`` env var is set and
|
||||
line_profiler is installed.
|
||||
"""
|
||||
if _PROFILER_ACTIVE:
|
||||
return _line_profile(func)
|
||||
return func
|
||||
|
||||
|
||||
def auto_profile_module(module_name: str) -> int:
|
||||
"""Apply @profile to every public (non-underscore) function in *module_name*.
|
||||
|
||||
Must be called at module level, typically at the bottom of the file::
|
||||
|
||||
from analysis.profile_util import auto_profile_module
|
||||
auto_profile_module(__name__)
|
||||
|
||||
Active only when ``TIANXUAN_PROFILE=1`` env var is set and
|
||||
line_profiler is installed. Returns number of functions wrapped, or 0
|
||||
when inactive.
|
||||
"""
|
||||
if not _PROFILER_ACTIVE:
|
||||
return 0
|
||||
|
||||
try:
|
||||
from line_profiler import LineProfiler
|
||||
except ImportError:
|
||||
return 0
|
||||
|
||||
module = sys.modules.get(module_name)
|
||||
if module is None:
|
||||
logger.warning('auto_profile_module: module %r not found', module_name)
|
||||
return 0
|
||||
|
||||
lp = LineProfiler()
|
||||
count = 0
|
||||
for name in dir(module):
|
||||
if name.startswith('_'):
|
||||
continue
|
||||
obj = getattr(module, name)
|
||||
if callable(obj):
|
||||
try:
|
||||
lp.add_function(obj)
|
||||
count += 1
|
||||
except (TypeError, ValueError):
|
||||
pass # skip non-Python or special objects
|
||||
|
||||
atexit.register(lambda: lp.print_stats(output_unit=1e-6))
|
||||
logger.info('auto_profile: wrapped %d functions in %s', count, module_name)
|
||||
return count
|
||||
@@ -323,12 +323,20 @@ class SessionStore:
|
||||
def drop_dataset(self, dataset_id: str) -> bool:
|
||||
"""Remove a dataset from the store and force garbage collection.
|
||||
|
||||
Cascades to child datasets (e.g. filtered results) via parent_id tracking.
|
||||
Returns True if the dataset existed.
|
||||
"""
|
||||
with self._datalock:
|
||||
existed = dataset_id in self._stores and self._stores[dataset_id].get('type') == 'dataset'
|
||||
if existed:
|
||||
del self._stores[dataset_id]
|
||||
# Cascade: drop all child datasets that reference the dropped one as parent
|
||||
child_ids = [
|
||||
k for k, v in self._stores.items()
|
||||
if v.get('type') == 'dataset' and v.get('parent_id') == dataset_id
|
||||
]
|
||||
for cid in child_ids:
|
||||
del self._stores[cid]
|
||||
if existed:
|
||||
gc.collect()
|
||||
self._save_to_disk()
|
||||
@@ -473,3 +481,8 @@ class SessionStore:
|
||||
types[t] = types.get(t, 0) + 1
|
||||
parts = [f"{k}={v}" for k, v in sorted(types.items())]
|
||||
return f"<SessionStore {'; '.join(parts)}>"
|
||||
|
||||
|
||||
# Auto-profile all public functions in this module
|
||||
from analysis.profile_util import auto_profile_module # noqa: E402
|
||||
auto_profile_module(__name__)
|
||||
|
||||
+53
-24
@@ -24,12 +24,7 @@ from mcp.types import Tool
|
||||
from .data_loader import load_csv_directory
|
||||
from .data_profiler import profile_dataset as _profile_dataset
|
||||
from .session_store import SessionStore, _generate_id
|
||||
|
||||
# Conditional line_profiler support (no-op when not installed)
|
||||
try:
|
||||
profile # type: ignore[name-defined]
|
||||
except NameError:
|
||||
profile = lambda f: f
|
||||
from .profile_util import profile
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
@@ -249,14 +244,15 @@ def get_tools_meta() -> list[Tool]:
|
||||
},
|
||||
"algorithm": {
|
||||
"type": "string",
|
||||
"enum": ["hdbscan", "kmeans"],
|
||||
"description": "Clustering algorithm",
|
||||
"default": "hdbscan",
|
||||
"enum": ["agglomerative", "hdbscan", "kmeans"],
|
||||
"description": "Clustering algorithm (default: agglomerative/Ward)",
|
||||
"default": "agglomerative",
|
||||
},
|
||||
"params": {
|
||||
"type": "object",
|
||||
"description": (
|
||||
"Algorithm parameters. HDBSCAN: min_cluster_size (5), "
|
||||
"Algorithm parameters. Agglomerative: n_clusters (5), metric ('euclidean'), "
|
||||
"linkage ('ward'). HDBSCAN: min_cluster_size (5), "
|
||||
"min_samples (None), metric ('euclidean'). "
|
||||
"KMeans: n_clusters (3), random_state (42)."
|
||||
),
|
||||
@@ -452,14 +448,15 @@ def get_tools_meta() -> list[Tool]:
|
||||
},
|
||||
"algorithm": {
|
||||
"type": "string",
|
||||
"enum": ["hdbscan", "kmeans"],
|
||||
"description": "Clustering algorithm",
|
||||
"default": "hdbscan",
|
||||
"enum": ["agglomerative", "hdbscan", "kmeans"],
|
||||
"description": "Clustering algorithm (default: agglomerative/Ward)",
|
||||
"default": "agglomerative",
|
||||
},
|
||||
"params": {
|
||||
"type": "object",
|
||||
"description": (
|
||||
"Algorithm parameters. HDBSCAN: min_cluster_size (5), "
|
||||
"Algorithm parameters. Agglomerative: n_clusters (5), metric ('euclidean'), "
|
||||
"linkage ('ward'). HDBSCAN: min_cluster_size (5), "
|
||||
"min_samples (None), metric ('euclidean'). "
|
||||
"KMeans: n_clusters (3), random_state (42)."
|
||||
),
|
||||
@@ -1130,7 +1127,7 @@ async def _handle_preprocess_data(
|
||||
async def _handle_run_clustering(
|
||||
dataset_id: str,
|
||||
cluster_columns: list[str],
|
||||
algorithm: str = 'hdbscan',
|
||||
algorithm: str = 'agglomerative',
|
||||
params: Optional[dict] = None,
|
||||
random_state: int = 42,
|
||||
) -> dict:
|
||||
@@ -1311,13 +1308,21 @@ async def _handle_run_clustering(
|
||||
)
|
||||
|
||||
# Clustering
|
||||
from sklearn.cluster import HDBSCAN, MiniBatchKMeans
|
||||
from sklearn.cluster import HDBSCAN, MiniBatchKMeans, AgglomerativeClustering
|
||||
if algorithm == 'kmeans':
|
||||
n_clusters = p.get('n_clusters', 3)
|
||||
model = MiniBatchKMeans(
|
||||
n_clusters=n_clusters, random_state=random_state,
|
||||
batch_size=1024, n_init='auto',
|
||||
)
|
||||
elif algorithm in ('agglomerative', 'ward'):
|
||||
n_clusters = p.get('n_clusters', 5)
|
||||
model = AgglomerativeClustering(
|
||||
n_clusters=n_clusters,
|
||||
metric=p.get('metric', 'euclidean'),
|
||||
linkage=p.get('linkage', 'ward'),
|
||||
distance_threshold=None,
|
||||
)
|
||||
else:
|
||||
model = HDBSCAN(
|
||||
min_cluster_size=p['min_cluster_size'],
|
||||
@@ -1731,11 +1736,15 @@ def _save_features_to_db(
|
||||
|
||||
from analysis.models import ClusterResult as ClusterResultModel
|
||||
|
||||
# Compute actual cluster sizes from features (not from cluster_entry — avoids scope issues)
|
||||
# Compute actual cluster sizes from full label list (not from features count!)
|
||||
# Use the cluster_entry which has the complete labels array
|
||||
store = SessionStore()
|
||||
cluster_entry = store.get_cluster_result(cluster_result_id)
|
||||
label_counts = {}
|
||||
for feat in features:
|
||||
lbl = feat['cluster_label']
|
||||
label_counts[lbl] = label_counts.get(lbl, 0) + 1
|
||||
if cluster_entry:
|
||||
full_labels = cluster_entry.get('labels', [])
|
||||
for lbl in full_labels:
|
||||
label_counts[lbl] = label_counts.get(lbl, 0) + 1
|
||||
|
||||
unique_labels = sorted(set(
|
||||
f['cluster_label'] for f in features
|
||||
@@ -2171,7 +2180,14 @@ async def _handle_compute_scores(dataset_id: str) -> dict:
|
||||
|
||||
lf = entry['lazyframe']
|
||||
lf = _add_adaptive_scores(lf, [])
|
||||
store.store_dataset(dataset_id, lf, schema=entry.get('schema', {}),
|
||||
# Update schema to include new score columns (proxy_score, risk_level, threat_score)
|
||||
try:
|
||||
updated_schema = lf.collect_schema()
|
||||
new_schema = {name: str(dtype) for name, dtype in
|
||||
zip(updated_schema.names(), updated_schema.dtypes())}
|
||||
except Exception:
|
||||
new_schema = entry.get('schema', {})
|
||||
store.store_dataset(dataset_id, lf, schema=new_schema,
|
||||
metadata=entry.get('metadata', {}))
|
||||
return {'status': 'scored', 'dataset_id': dataset_id}
|
||||
|
||||
@@ -2596,12 +2612,20 @@ async def _handle_explore_distributions(dataset_id: str, columns: list[str] = No
|
||||
|
||||
if not columns:
|
||||
columns = [c for c in schema if schema[c].split('(')[0].strip() in numeric_types][:10]
|
||||
# Limit columns to prevent excessive computation
|
||||
if len(columns) > 20:
|
||||
columns = columns[:20]
|
||||
|
||||
try:
|
||||
n = lf.select(pl.len()).collect(streaming=True).item()
|
||||
sample_lf = lf if n <= max_sample else lf.collect(streaming=True).sample(n=max_sample, seed=42).lazy()
|
||||
df = sample_lf.select(columns).collect(streaming=True)
|
||||
if n > max_sample:
|
||||
# Use LazyFrame.sample() on the limited columns to avoid full-materialize
|
||||
sample_lf = lf.select(columns).sample(n=max_sample, seed=42)
|
||||
else:
|
||||
sample_lf = lf.select(columns)
|
||||
df = sample_lf.collect(streaming=True)
|
||||
except Exception as exc:
|
||||
raise
|
||||
return {'error': f'explore_distributions failed: {exc}', 'truncated': False}
|
||||
|
||||
stats = {}
|
||||
for col in columns:
|
||||
@@ -3337,3 +3361,8 @@ async def _handle_compute_distance_matrix(
|
||||
'total_rows_in_dataset': n_total,
|
||||
'rows_evaluated': n_evaluated,
|
||||
}
|
||||
|
||||
|
||||
# Auto-profile all public functions in this module
|
||||
from analysis.profile_util import auto_profile_module # noqa: E402
|
||||
auto_profile_module(__name__)
|
||||
|
||||
+2
-1
@@ -15,7 +15,7 @@ urlpatterns = [
|
||||
path('runs/<int:display_id>/', views.run_detail, name='run_detail'),
|
||||
path('runs/<int:display_id>/status/', views.run_status_api, name='run_status'),
|
||||
path('clusters/<int:display_id>/', views.cluster_overview, name='cluster_overview'),
|
||||
path('clusters/<int:display_id>/<int:cluster_label>/', views.cluster_detail, name='cluster_detail'),
|
||||
path('clusters/<int:display_id>/<path:cluster_label>/', views.cluster_detail, name='cluster_detail'),
|
||||
path('entities/<int:entity_id>/', views.entity_profile, name='entity_profile'),
|
||||
path('config/', views.config_view, name='config'),
|
||||
path('api/llm/test/', views.llm_test, name='llm_test'),
|
||||
@@ -25,6 +25,7 @@ urlpatterns = [
|
||||
path('logs/', views.log_viewer, name='log_viewer'),
|
||||
path('globe/', views.globe_view, name='globe'),
|
||||
path('tools/run/', views.tool_lab_run, name='tool_lab_run'),
|
||||
path('tools/reload-run/', views.reload_run_data, name='reload_run_data'),
|
||||
path('tools/plan/', views.tool_plan, name='tool_plan'),
|
||||
path('tools/filter/', views.apply_filter, name='apply_filter'),
|
||||
]
|
||||
|
||||
+348
-112
@@ -15,12 +15,7 @@ from django.views.decorators.csrf import csrf_exempt
|
||||
from config import get_config, save_config, Config
|
||||
from .models import AnalysisRun, ClusterResult, EntityProfile
|
||||
from . import nl_describe
|
||||
|
||||
# Conditional line_profiler support (no-op when not installed)
|
||||
try:
|
||||
profile # type: ignore[name-defined]
|
||||
except NameError:
|
||||
profile = lambda f: f
|
||||
from .profile_util import profile
|
||||
|
||||
|
||||
def dashboard(request):
|
||||
@@ -91,23 +86,38 @@ def cluster_overview(request, display_id):
|
||||
entities = run.entities.exclude(embedding_x=None).exclude(embedding_y=None).only(
|
||||
'embedding_x', 'embedding_y', 'embedding_z', 'cluster_label', 'entity_value'
|
||||
)
|
||||
has_z = any(e.embedding_z is not None for e in entities)
|
||||
scatter_data = [
|
||||
{'x': e.embedding_x, 'y': e.embedding_y, 'z': e.embedding_z,
|
||||
'label': e.cluster_label, 'entity': e.entity_value}
|
||||
{'x': e.embedding_x, 'y': e.embedding_y, 'z': e.embedding_z if has_z else 0,
|
||||
'label': e.cluster_label if e.cluster_label is not None else -1, 'entity': e.entity_value}
|
||||
for e in entities
|
||||
]
|
||||
|
||||
# Noise cluster features for enhanced noise card
|
||||
# Noise cluster: features + entity detail
|
||||
noise_features = []
|
||||
noise_summary = ''
|
||||
noise_entity_count = 0
|
||||
if noise:
|
||||
noise_features_qs = noise.features.all().order_by('-distinguishing_score')[:10]
|
||||
noise_features_qs = noise.features.all().order_by('-distinguishing_score')[:15]
|
||||
noise_features = [
|
||||
{'feature_name': f.feature_name, 'score': f.distinguishing_score, 'mean': f.mean}
|
||||
{'feature_name': f.feature_name, 'score': f.distinguishing_score, 'mean': f.mean,
|
||||
'std': f.std, 'p25': f.p25, 'p75': f.p75}
|
||||
for f in noise_features_qs
|
||||
]
|
||||
if noise_features:
|
||||
noise_summary = nl_describe.describe_cluster(noise_features)
|
||||
noise_entity_count = noise.size
|
||||
|
||||
# LLM workflow timeline for auto runs
|
||||
llm_timeline_json = ''
|
||||
if run.run_type == 'auto':
|
||||
llm_thinking = run.llm_thinking or ''
|
||||
tool_calls = run.tool_calls_json or []
|
||||
if tool_calls or llm_thinking:
|
||||
llm_timeline_json = json.dumps({
|
||||
'thinking': llm_thinking,
|
||||
'tool_calls': tool_calls,
|
||||
})
|
||||
|
||||
# Build geo scatter data from feature_json (lat/lon)
|
||||
geo_data = []
|
||||
@@ -136,12 +146,14 @@ def cluster_overview(request, display_id):
|
||||
top_features = {}
|
||||
for c in clusters:
|
||||
features_qs = c.features.all().order_by('-distinguishing_score')[:10]
|
||||
top_features[str(c.cluster_label)] = [
|
||||
{'feature_name': f.feature_name, 'score': f.distinguishing_score, 'mean': f.mean}
|
||||
feats = [
|
||||
{'feature_name': f.feature_name, 'score': f.distinguishing_score, 'mean': f.mean,
|
||||
'std': f.std}
|
||||
for f in features_qs
|
||||
]
|
||||
# Generate NL summary — attach to cluster object for template access
|
||||
feats = top_features[str(c.cluster_label)]
|
||||
top_features[str(c.cluster_label)] = feats
|
||||
# Attach features + NL summary directly to cluster object for template
|
||||
c._top_features = feats
|
||||
c.nl_summary = nl_describe.describe_cluster(feats) if feats else ''
|
||||
|
||||
return render(request, 'analysis/cluster_overview.html', {
|
||||
@@ -149,6 +161,7 @@ def cluster_overview(request, display_id):
|
||||
'clusters': clusters,
|
||||
'noise': noise,
|
||||
'scatter_data_json': json.dumps(scatter_data),
|
||||
'has_z': has_z,
|
||||
'geo_data_json': json.dumps(geo_data),
|
||||
'geo_skipped': geo_skipped,
|
||||
'geo_count': len(geo_data),
|
||||
@@ -156,21 +169,88 @@ def cluster_overview(request, display_id):
|
||||
'top_features_json': json.dumps(top_features),
|
||||
'noise_features': noise_features,
|
||||
'noise_summary': noise_summary,
|
||||
'noise_entity_count': noise_entity_count,
|
||||
'llm_timeline_json': llm_timeline_json,
|
||||
# Globe flow data for the sidebar
|
||||
'globe_flows_json': json.dumps(_get_globe_flows(run)),
|
||||
})
|
||||
|
||||
|
||||
def _get_globe_flows(run, max_rows=500):
|
||||
"""Extract flow data for 3D globe visualization from a run."""
|
||||
import json
|
||||
from analysis.data_loader import load_csv_directory, load_from_db
|
||||
try:
|
||||
lf = None
|
||||
if run.sqlite_table:
|
||||
lf = load_from_db(run.sqlite_table)
|
||||
if lf is None and run.csv_glob:
|
||||
lf, _, _, _, _ = load_csv_directory(run.csv_glob)
|
||||
if lf is None:
|
||||
return []
|
||||
df = lf.head(max_rows).collect()
|
||||
from analysis.geoip import lookup as geo_lookup
|
||||
flows = []
|
||||
TLS_HEX_MAP = {'0303': 'TLSv1.2', '0304': 'TLSv1.3', '0302': 'TLSv1.1', '0301': 'TLSv1.0'}
|
||||
src_ip_col = next((c for c in df.columns if 'src_ip' in c.lower() or ':ips' in c.lower()), None)
|
||||
dst_ip_col = next((c for c in df.columns if 'dst_ip' in c.lower() or ':ipd' in c.lower()), None)
|
||||
tls_col = next((c for c in df.columns if c.lower() in ('0ver', 'tls_version', '_tlsver', 'version')), None)
|
||||
bytes_col = next((c for c in df.columns if c.lower() in ('8ack', '8pak', '8byt', 'bytes_sent', 'bytes')), None)
|
||||
for row in df.iter_rows(named=True):
|
||||
sip = str(row.get(src_ip_col, '')) if src_ip_col else ''
|
||||
dip = str(row.get(dst_ip_col, '')) if dst_ip_col else ''
|
||||
if not sip or not dip:
|
||||
continue
|
||||
sg = geo_lookup(sip)
|
||||
dg = geo_lookup(dip)
|
||||
if not sg or not dg:
|
||||
continue
|
||||
tls_val = str(row.get(tls_col, '') or '') if tls_col else ''
|
||||
tls_ver = TLS_HEX_MAP.get(tls_val.replace(' ', ''), tls_val)
|
||||
flows.append({
|
||||
'slat': sg['lat'], 'slon': sg['lon'],
|
||||
'dlat': dg['lat'], 'dlon': dg['lon'],
|
||||
'tls': tls_ver,
|
||||
'bytes': float(row.get(bytes_col, 0) or 0) if bytes_col else 0,
|
||||
})
|
||||
return flows
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def cluster_detail(request, display_id, cluster_label):
|
||||
"""Detailed view of a single cluster with entity list."""
|
||||
"""Detailed view of a single cluster with entity list, SVD features, and NL summary."""
|
||||
run = get_object_or_404(AnalysisRun, display_id=display_id)
|
||||
try:
|
||||
cluster_label = int(cluster_label)
|
||||
except (ValueError, TypeError):
|
||||
raise Http404("Invalid cluster label")
|
||||
cluster = get_object_or_404(ClusterResult, run=run, cluster_label=cluster_label)
|
||||
features = cluster.features.all().order_by('-distinguishing_score')[:50]
|
||||
entities = cluster.entities.all()[:50]
|
||||
entities = cluster.entities.all()[:100]
|
||||
nl_summary = ''
|
||||
feats_list = [{'feature_name': f.feature_name, 'score': f.distinguishing_score,
|
||||
'mean': f.mean, 'std': f.std} for f in features[:10]]
|
||||
if feats_list:
|
||||
nl_summary = nl_describe.describe_cluster(feats_list)
|
||||
|
||||
# Build entity data with feature_json values
|
||||
entity_data = []
|
||||
for e in entities:
|
||||
entry = {'id': e.id, 'value': e.entity_value,
|
||||
'embedding_x': e.embedding_x, 'embedding_y': e.embedding_y}
|
||||
if e.feature_json:
|
||||
entry['features'] = {k: round(float(v), 4) if isinstance(v, (int, float)) else str(v)
|
||||
for k, v in e.feature_json.items()}
|
||||
entity_data.append(entry)
|
||||
|
||||
return render(request, 'analysis/cluster_detail.html', {
|
||||
'run': run,
|
||||
'cluster': cluster,
|
||||
'features': features,
|
||||
'entities': entities,
|
||||
'entity_data_json': json.dumps(entity_data),
|
||||
'nl_summary': nl_summary,
|
||||
})
|
||||
|
||||
|
||||
@@ -737,33 +817,49 @@ def manual_page(request):
|
||||
|
||||
# Get datasets from SessionStore
|
||||
store = SessionStore()
|
||||
datasets = store.list_datasets() if hasattr(store, 'list_datasets') else []
|
||||
session_datasets = store.list_datasets() if hasattr(store, 'list_datasets') else []
|
||||
session_ds_map = {ds['dataset_id']: ds for ds in session_datasets}
|
||||
|
||||
# Get upload AnalysisRun statuses for dataset badges
|
||||
# Build dataset entries from ALL AnalysisRun records (upload/manual/auto, any status)
|
||||
from analysis.models import AnalysisRun
|
||||
upload_runs = AnalysisRun.objects.filter(run_type='upload').order_by('-created_at')[:50]
|
||||
run_status_map = {
|
||||
f'upload_{r.display_id}': {
|
||||
'status': r.status,
|
||||
'total_flows': r.total_flows,
|
||||
'display_id': r.display_id,
|
||||
}
|
||||
for r in upload_runs
|
||||
}
|
||||
|
||||
# Build datasets_with_status: each dataset annotated with run status
|
||||
all_runs = AnalysisRun.objects.all().order_by('-created_at')[:100]
|
||||
datasets_with_status = []
|
||||
for ds in datasets:
|
||||
run_info = run_status_map.get(ds['dataset_id'])
|
||||
if run_info:
|
||||
datasets_with_status.append({
|
||||
**ds,
|
||||
'run_status': run_info['status'],
|
||||
'total_flows': run_info['total_flows'],
|
||||
'display_id': run_info['display_id'],
|
||||
})
|
||||
|
||||
for run in all_runs:
|
||||
# Determine how the dataset was keyed in SessionStore
|
||||
if run.run_type == 'upload':
|
||||
ds_id = f'upload_{run.display_id}'
|
||||
else:
|
||||
datasets_with_status.append({**ds, 'run_status': 'ready', 'total_flows': None, 'display_id': None})
|
||||
ds_id = f'run_{run.display_id}'
|
||||
|
||||
session_entry = session_ds_map.get(ds_id)
|
||||
|
||||
if session_entry:
|
||||
# Full data available in SessionStore — merge with run status
|
||||
datasets_with_status.append({
|
||||
**session_entry,
|
||||
'dataset_id': ds_id,
|
||||
'run_status': run.status,
|
||||
'total_flows': run.total_flows,
|
||||
'display_id': run.display_id,
|
||||
'needs_reload': False,
|
||||
})
|
||||
elif run.csv_glob or run.sqlite_table:
|
||||
# Data not in SessionStore but recoverable from disk/DB
|
||||
datasets_with_status.append({
|
||||
'dataset_id': ds_id,
|
||||
'run_status': run.status,
|
||||
'display_id': run.display_id,
|
||||
'total_flows': run.total_flows,
|
||||
'row_count': run.total_flows,
|
||||
'file_count': None,
|
||||
'column_count': 0,
|
||||
'columns': [],
|
||||
'svd_components': 0,
|
||||
'needs_reload': True,
|
||||
'csv_glob': run.csv_glob,
|
||||
'sqlite_table': run.sqlite_table or '',
|
||||
})
|
||||
|
||||
# Map statuses to user-facing labels for the filter tabs
|
||||
# ready → 待分析, completed → 已分析, failed → 错误
|
||||
@@ -772,12 +868,16 @@ def manual_page(request):
|
||||
s = ds_info['run_status']
|
||||
if s in ('completed',):
|
||||
ds_info['status_label'] = '已分析'
|
||||
ds_info['badge_class'] = 'completed'
|
||||
elif s in ('failed',):
|
||||
ds_info['status_label'] = '错误'
|
||||
ds_info['badge_class'] = 'failed'
|
||||
elif s in ('pending', 'loading', 'profiling', 'aggregating', 'clustering', 'extracting'):
|
||||
ds_info['status_label'] = '分析中'
|
||||
ds_info['badge_class'] = 'analyzing'
|
||||
else:
|
||||
ds_info['status_label'] = '待分析'
|
||||
ds_info['badge_class'] = 'ready'
|
||||
|
||||
return render(request, 'tianxuan/manual.html', {
|
||||
'core_tools': core_tools,
|
||||
@@ -785,7 +885,7 @@ def manual_page(request):
|
||||
'analysis_tools': analysis_tools,
|
||||
'other_tools': other_tools,
|
||||
'tools_meta': _json.dumps(meta_list, ensure_ascii=False),
|
||||
'datasets': datasets,
|
||||
'datasets': datasets_with_status,
|
||||
'datasets_with_status_json': _json.dumps(datasets_with_status, ensure_ascii=False),
|
||||
})
|
||||
|
||||
@@ -1135,7 +1235,7 @@ def manual_run_analysis(request):
|
||||
run.run_type = 'manual'
|
||||
run.save(update_fields=['run_type'])
|
||||
feature_columns = body.get('feature_columns')
|
||||
algorithm = body.get('algorithm', 'hdbscan')
|
||||
algorithm = body.get('algorithm', 'agglomerative')
|
||||
min_cluster_size = int(body.get('min_cluster_size', 5))
|
||||
head = body.get('head')
|
||||
cluster_mode = body.get('cluster_mode', 'raw')
|
||||
@@ -1236,30 +1336,10 @@ def apply_filter(request):
|
||||
filtered_id = result.get('filtered_dataset_id', '')
|
||||
row_count = result.get('row_count', 0)
|
||||
|
||||
# Re-store with filtered_X ID pattern
|
||||
if dataset_id.startswith('upload_'):
|
||||
display_suffix = dataset_id.split('_', 1)[1]
|
||||
new_id = f'filtered_{display_suffix}'
|
||||
else:
|
||||
new_id = filtered_id
|
||||
|
||||
from analysis.session_store import SessionStore
|
||||
store = SessionStore()
|
||||
entry = store.get_dataset(filtered_id)
|
||||
if entry and new_id != filtered_id:
|
||||
store.store_dataset(
|
||||
dataset_id=new_id,
|
||||
lazyframe=entry['lazyframe'],
|
||||
schema=entry.get('schema', {}),
|
||||
metadata=dict(entry.get('metadata', {}), parent_dataset_id=dataset_id),
|
||||
parent_id=dataset_id,
|
||||
)
|
||||
new_id_to_use = new_id
|
||||
else:
|
||||
new_id_to_use = filtered_id
|
||||
|
||||
# Use the unique flt_xxx ID from _handle_filter_data — never rename
|
||||
# to filtered_{num} which would collide on repeated filters of the same source.
|
||||
return JsonResponse({
|
||||
'dataset_id': new_id_to_use,
|
||||
'dataset_id': filtered_id,
|
||||
'row_count': row_count,
|
||||
})
|
||||
except Exception as e:
|
||||
@@ -1287,22 +1367,74 @@ def run_llm_analysis_view(request):
|
||||
|
||||
from analysis.session_store import SessionStore
|
||||
store = SessionStore()
|
||||
# Use display_id for dataset key (consistent with _background_process)
|
||||
|
||||
# Determine dataset key format (consistent with manual_page)
|
||||
upload_ds_id = f'upload_{display_id}'
|
||||
run_ds_id = f'run_{display_id}'
|
||||
entry = store.get_dataset(upload_ds_id)
|
||||
if entry is None:
|
||||
# Fallback: try PK-based key (old pipeline)
|
||||
upload_ds_id = f'upload_{pk}'
|
||||
entry = store.get_dataset(upload_ds_id)
|
||||
entry = store.get_dataset(run_ds_id)
|
||||
|
||||
if entry is None:
|
||||
# Try entity dataset as fallback
|
||||
entity_ds_id = f'entity_{pk}'
|
||||
entry = store.get_dataset(entity_ds_id)
|
||||
# Try primary key fallback
|
||||
upload_pk_id = f'upload_{pk}'
|
||||
entity_pk_id = f'entity_{pk}'
|
||||
entry = store.get_dataset(upload_pk_id)
|
||||
if entry is None:
|
||||
return JsonResponse({'error': '数据集不存在,请先上传并等待预处理完成'})
|
||||
entry = store.get_dataset(entity_pk_id)
|
||||
|
||||
dataset_id = upload_ds_id if store.get_dataset(upload_ds_id) else f'entity_{pk}'
|
||||
if entry is None:
|
||||
# Auto-reload from sqlite_table or csv_glob
|
||||
from analysis.data_loader import load_csv_directory, load_from_db
|
||||
lf = None
|
||||
schema = {}
|
||||
if run.sqlite_table:
|
||||
try:
|
||||
lf = load_from_db(run.sqlite_table)
|
||||
except Exception:
|
||||
pass
|
||||
if lf is None and run.csv_glob:
|
||||
try:
|
||||
lf, schema, _, _, _ = load_csv_directory(run.csv_glob)
|
||||
except Exception:
|
||||
pass
|
||||
if lf is None:
|
||||
return JsonResponse({'error': '数据集不存在,请先上传并等待预处理完成'})
|
||||
entry_key = upload_ds_id if run.run_type == 'upload' else run_ds_id
|
||||
store.store_dataset(entry_key, lf, schema=schema, metadata={
|
||||
'row_count': run.total_flows,
|
||||
'csv_glob': run.csv_glob or '',
|
||||
'sqlite_table': run.sqlite_table or '',
|
||||
})
|
||||
# Re-fetch entry
|
||||
entry = store.get_dataset(entry_key)
|
||||
|
||||
# Determine the active dataset_id
|
||||
for candidate in [upload_ds_id, run_ds_id, f'upload_{pk}', f'entity_{pk}']:
|
||||
if store.get_dataset(candidate):
|
||||
dataset_id = candidate
|
||||
break
|
||||
else:
|
||||
return JsonResponse({'error': '数据集不存在,请先上传并等待预处理完成'})
|
||||
|
||||
# ── Optional: apply filter before LLM pipeline ──
|
||||
filters = body.get('filters', [])
|
||||
if filters and len(filters) > 0:
|
||||
logic = body.get('logic', 'and')
|
||||
try:
|
||||
from analysis.tool_registry import _handle_filter_data
|
||||
filter_result = asyncio.run(_handle_filter_data(
|
||||
dataset_id=dataset_id,
|
||||
filters=filters,
|
||||
logic=logic,
|
||||
))
|
||||
if 'error' not in filter_result:
|
||||
filtered_id = filter_result.get('filtered_dataset_id', '')
|
||||
if filtered_id and store.get_dataset(filtered_id):
|
||||
dataset_id = filtered_id
|
||||
except Exception:
|
||||
import traceback
|
||||
logger.warning(f'[LLM auto] Filter failed (non-fatal): {traceback.format_exc()}')
|
||||
|
||||
def _analysis_fn(run, ctx):
|
||||
from tianxuan.llm_orchestrator import run_llm_pipeline, LLMConfig
|
||||
@@ -1319,26 +1451,22 @@ def run_llm_analysis_view(request):
|
||||
|
||||
save_fields = ['progress_pct', 'progress_msg']
|
||||
if tool_name == 'thinking':
|
||||
# Accumulate LLM thinking text
|
||||
# Accumulate LLM thinking text (appended to llm_thinking)
|
||||
if result:
|
||||
run.llm_thinking = (run.llm_thinking or '') + f'\n[{step}] {result}'
|
||||
save_fields = ['progress_pct', 'progress_msg', 'llm_thinking']
|
||||
elif tool_name == 'complete':
|
||||
pass # No extra tracking needed
|
||||
else:
|
||||
# Real tool call — log it
|
||||
if result:
|
||||
if isinstance(result, dict):
|
||||
# Format dict as JSON, truncate large values
|
||||
import json as _json
|
||||
summary = _json.dumps(result, default=str, ensure_ascii=False)[:500]
|
||||
else:
|
||||
summary = str(result)[:500]
|
||||
run.run_log += f'[{step}] {tool_name}: {summary}...\n'
|
||||
# Single tool call — always has result
|
||||
if isinstance(result, dict):
|
||||
import json as _json
|
||||
summary = _json.dumps(result, default=str, ensure_ascii=False)[:500]
|
||||
else:
|
||||
run.run_log += f'[{step}] {tool_name}\n'
|
||||
summary = str(result)[:500]
|
||||
run.run_log += f'[{step}] {tool_name}: {summary}...\n'
|
||||
|
||||
# Capture structured tool call data
|
||||
# One entry per tool call
|
||||
if meta and isinstance(meta, dict) and 'args' in meta:
|
||||
tool_calls = list(run.tool_calls_json or [])
|
||||
tool_calls.append({
|
||||
@@ -1408,7 +1536,7 @@ def run_llm_analysis_view(request):
|
||||
store=store,
|
||||
entity_ds_id=entity_ds_id,
|
||||
feature_columns=None,
|
||||
algorithm='hdbscan',
|
||||
algorithm='agglomerative',
|
||||
min_cluster_size=5,
|
||||
run_umap=True,
|
||||
head=ctx.get('head'),
|
||||
@@ -1488,37 +1616,67 @@ def auto_page(request):
|
||||
|
||||
cfg = get_config()
|
||||
|
||||
# Get upload runs for dataset selector (with status badges)
|
||||
runs = AnalysisRun.objects.filter(run_type='upload').order_by('-created_at')[:50]
|
||||
# Get datasets from SessionStore + ALL runs from DB
|
||||
store = SessionStore()
|
||||
session_datasets = store.list_datasets() if hasattr(store, 'list_datasets') else []
|
||||
session_ds_map = {ds['dataset_id']: ds for ds in session_datasets}
|
||||
|
||||
# Build datasets_with_status for filter tabs
|
||||
from analysis.models import AnalysisRun as _AR
|
||||
upload_runs = _AR.objects.filter(run_type='upload').order_by('-created_at')[:50]
|
||||
# Build dataset entries from ALL AnalysisRun records
|
||||
all_runs = AnalysisRun.objects.all().order_by('-created_at')[:100]
|
||||
datasets_with_status = []
|
||||
for r in upload_runs:
|
||||
s = r.status
|
||||
if s in ('completed',):
|
||||
label = '已分析'
|
||||
elif s in ('failed',):
|
||||
label = '错误'
|
||||
elif s in ('pending', 'loading', 'profiling', 'aggregating', 'clustering', 'extracting'):
|
||||
label = '分析中'
|
||||
|
||||
for run in all_runs:
|
||||
if run.run_type == 'upload':
|
||||
ds_id = f'upload_{run.display_id}'
|
||||
else:
|
||||
label = '待分析'
|
||||
datasets_with_status.append({
|
||||
'dataset_id': r.display_id,
|
||||
'display_id': r.display_id,
|
||||
'run_status': s,
|
||||
'status_label': label,
|
||||
'row_count': r.total_flows,
|
||||
'file_count': getattr(r, 'file_count', None),
|
||||
'csv_glob': r.csv_glob or '',
|
||||
'sqlite_table': r.sqlite_table or '',
|
||||
})
|
||||
ds_id = f'run_{run.display_id}'
|
||||
|
||||
session_entry = session_ds_map.get(ds_id)
|
||||
|
||||
if session_entry:
|
||||
datasets_with_status.append({
|
||||
**session_entry,
|
||||
'dataset_id': ds_id,
|
||||
'run_status': run.status,
|
||||
'total_flows': run.total_flows,
|
||||
'display_id': run.display_id,
|
||||
'needs_reload': False,
|
||||
'csv_glob': run.csv_glob or '',
|
||||
'sqlite_table': run.sqlite_table or '',
|
||||
})
|
||||
elif run.csv_glob or run.sqlite_table:
|
||||
datasets_with_status.append({
|
||||
'dataset_id': ds_id,
|
||||
'run_status': run.status,
|
||||
'display_id': run.display_id,
|
||||
'total_flows': run.total_flows,
|
||||
'row_count': run.total_flows,
|
||||
'file_count': None,
|
||||
'columns': [],
|
||||
'column_count': 0,
|
||||
'svd_components': 0,
|
||||
'needs_reload': True,
|
||||
'csv_glob': run.csv_glob or '',
|
||||
'sqlite_table': run.sqlite_table or '',
|
||||
})
|
||||
|
||||
for ds_info in datasets_with_status:
|
||||
s = ds_info['run_status']
|
||||
if s in ('completed',):
|
||||
ds_info['status_label'] = '已分析'
|
||||
ds_info['badge_class'] = 'completed'
|
||||
elif s in ('failed',):
|
||||
ds_info['status_label'] = '错误'
|
||||
ds_info['badge_class'] = 'failed'
|
||||
elif s in ('pending', 'loading', 'profiling', 'aggregating', 'clustering', 'extracting'):
|
||||
ds_info['status_label'] = '分析中'
|
||||
ds_info['badge_class'] = 'analyzing'
|
||||
else:
|
||||
ds_info['status_label'] = '待分析'
|
||||
ds_info['badge_class'] = 'ready'
|
||||
|
||||
return render(request, 'tianxuan/auto.html', {
|
||||
'llm_configured': bool(cfg.llm.base_url and cfg.llm.api_key),
|
||||
'runs': runs,
|
||||
'datasets_with_status_json': _json.dumps(datasets_with_status, ensure_ascii=False),
|
||||
})
|
||||
|
||||
@@ -1872,6 +2030,79 @@ def tool_lab_run(request):
|
||||
return JsonResponse({'status': 'ok', 'result': result})
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def reload_run_data(request):
|
||||
"""Reload a run's data from csv_glob or sqlite_table into SessionStore."""
|
||||
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)
|
||||
|
||||
display_id = body.get('display_id')
|
||||
if not display_id:
|
||||
return JsonResponse({'error': 'Missing display_id'}, status=400)
|
||||
|
||||
from analysis.models import AnalysisRun
|
||||
from analysis.data_loader import load_csv_directory, load_from_db
|
||||
|
||||
run = get_object_or_404(AnalysisRun, display_id=display_id)
|
||||
|
||||
# Determine dataset_id (same keying as manual_page)
|
||||
ds_id = f'upload_{display_id}' if run.run_type == 'upload' else f'run_{display_id}'
|
||||
|
||||
store = SessionStore()
|
||||
# Check if already loaded
|
||||
existing = store.get_dataset(ds_id)
|
||||
if existing is not None:
|
||||
return JsonResponse({'status': 'ok', 'dataset_id': ds_id, 'reloaded': False})
|
||||
|
||||
# Try loading from sqlite_table first, then csv_glob
|
||||
lf = None
|
||||
schema = {}
|
||||
row_count = 0
|
||||
file_count = 0
|
||||
|
||||
if run.sqlite_table:
|
||||
try:
|
||||
lf = load_from_db(run.sqlite_table)
|
||||
if lf is not None:
|
||||
row_count = lf.select(pl.len()).collect(streaming=True)[0, 0]
|
||||
except Exception as exc:
|
||||
import logging
|
||||
logging.getLogger(__name__).warning(f'Failed to load from sqlite_table: {exc}')
|
||||
|
||||
if lf is None and run.csv_glob:
|
||||
try:
|
||||
from pathlib import Path
|
||||
csv_path = Path(run.csv_glob)
|
||||
if csv_path.is_file() or csv_path.is_dir() or csv_path.parent.is_dir():
|
||||
lf, schema, row_count, file_count, _ = load_csv_directory(run.csv_glob)
|
||||
except Exception as exc:
|
||||
import logging
|
||||
logging.getLogger(__name__).warning(f'Failed to load from csv_glob: {exc}')
|
||||
|
||||
if lf is None:
|
||||
return JsonResponse({'error': 'No data source available (upload dir or sqlite table gone)'}, status=404)
|
||||
|
||||
store.store_dataset(ds_id, lf, schema=schema, metadata={
|
||||
'row_count': row_count,
|
||||
'file_count': file_count,
|
||||
'csv_glob': run.csv_glob or '',
|
||||
'sqlite_table': run.sqlite_table or '',
|
||||
})
|
||||
|
||||
return JsonResponse({
|
||||
'status': 'ok',
|
||||
'dataset_id': ds_id,
|
||||
'reloaded': True,
|
||||
'row_count': row_count,
|
||||
'columns': list(schema.keys()),
|
||||
})
|
||||
|
||||
|
||||
# ── Save / Load / Delete analysis plans ─────────────────────────────
|
||||
|
||||
from pathlib import Path
|
||||
@@ -2070,3 +2301,8 @@ def retry_run(request, display_id):
|
||||
|
||||
return redirect('analysis:run_detail', display_id=run.display_id)
|
||||
|
||||
|
||||
# Auto-profile all public functions in this module
|
||||
from analysis.profile_util import auto_profile_module # noqa: E402
|
||||
auto_profile_module(__name__)
|
||||
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ class Config(BaseModel):
|
||||
recursive: bool = False
|
||||
|
||||
class Clustering(BaseModel):
|
||||
algorithm: str = 'hdbscan'
|
||||
algorithm: str = 'agglomerative'
|
||||
min_cluster_size: int = 5
|
||||
random_state: int = 42
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"8.8.8.8": {
|
||||
"lat": 37.422,
|
||||
"lon": -122.084,
|
||||
"city": "Sunnyvale",
|
||||
"country": "US"
|
||||
},
|
||||
"1.1.1.1": {
|
||||
"lat": 37.7749,
|
||||
"lon": -122.4194,
|
||||
"city": "San Francisco",
|
||||
"country": "US"
|
||||
},
|
||||
"52.0.0.1": {
|
||||
"lat": 39.0438,
|
||||
"lon": -77.4874,
|
||||
"city": "Ashburn",
|
||||
"country": "US"
|
||||
},
|
||||
"104.16.1.1": {
|
||||
"lat": 37.7749,
|
||||
"lon": -122.4194,
|
||||
"city": "San Francisco",
|
||||
"country": "US"
|
||||
},
|
||||
"47.96.1.1": {
|
||||
"lat": 31.2304,
|
||||
"lon": 121.4737,
|
||||
"city": "Shanghai",
|
||||
"country": "CN"
|
||||
},
|
||||
"218.1.1.1": null,
|
||||
"157.1.1.1": {
|
||||
"lat": 34.6937,
|
||||
"lon": 135.5023,
|
||||
"city": "Japan",
|
||||
"country": "JP"
|
||||
},
|
||||
"10.0.0.1": null,
|
||||
"192.168.1.1": null,
|
||||
"13.250.0.1": null,
|
||||
"185.234.0.1": null,
|
||||
"103.235.0.1": {
|
||||
"lat": 20.0,
|
||||
"lon": 77.0,
|
||||
"city": "",
|
||||
"country": "印度"
|
||||
}
|
||||
}
|
||||
@@ -1,808 +0,0 @@
|
||||
# ===== Offline GeoIP Database — 天璇 (TianXuan) =====
|
||||
# Format: start_ip,end_ip,lat,lon,city,country
|
||||
# Layers: Private, DNS, AWS, Azure, GCP, Alibaba, Cloudflare, Akamai, Fastly, CN_ISP, US_ISP, EU_ISP, JP_ISP
|
||||
# Strategy: priority-ordered, overlap-resolved (first wins), gaps filled with city blocks
|
||||
|
||||
1.0.0.0,1.0.0.0,31.2304,121.4737,Shanghai,CN
|
||||
1.0.0.1,1.0.0.1,37.7749,-122.4194,San Francisco,US
|
||||
1.0.0.2,1.1.1.0,31.2304,121.4737,Shanghai,CN
|
||||
1.1.1.1,1.1.1.1,37.7749,-122.4194,San Francisco,US
|
||||
1.1.1.2,1.255.255.255,31.2304,121.4737,Shanghai,CN
|
||||
2.0.0.0,2.15.255.255,51.5074,-0.1278,London,GB
|
||||
2.16.0.0,2.23.255.255,42.3736,-71.1097,Cambridge,US
|
||||
2.24.0.0,2.255.255.255,51.5074,-0.1278,London,GB
|
||||
3.0.0.0,3.1.255.255,1.3521,103.8198,Singapore,SG
|
||||
3.2.0.0,3.5.255.255,31.2304,121.4737,Shanghai,CN
|
||||
3.6.0.0,3.7.255.255,19.076,72.8777,Mumbai,IN
|
||||
3.8.0.0,3.15.255.255,51.5074,-0.1278,London,GB
|
||||
3.16.0.0,3.23.255.255,39.9042,116.4074,Beijing,CN
|
||||
3.24.0.0,3.27.255.255,-33.8688,151.2093,Sydney,AU
|
||||
3.28.0.0,3.33.255.255,35.6762,139.6503,Tokyo,JP
|
||||
3.34.0.0,3.35.255.255,37.5665,126.978,Seoul,KR
|
||||
3.36.0.0,3.51.255.255,37.5665,126.978,Seoul,KR
|
||||
3.52.0.0,3.63.255.255,1.3521,103.8198,Singapore,SG
|
||||
3.64.0.0,3.79.255.255,50.1109,8.6821,Frankfurt,DE
|
||||
3.80.0.0,3.95.255.255,51.5074,-0.1278,London,GB
|
||||
3.96.0.0,3.99.255.255,45.5017,-73.5673,Montreal,CA
|
||||
3.100.0.0,3.111.255.255,50.1109,8.6821,Frankfurt,DE
|
||||
3.112.0.0,3.115.255.255,35.6762,139.6503,Tokyo,JP
|
||||
3.116.0.0,3.131.255.255,40.7128,-74.006,New York,US
|
||||
3.132.0.0,3.147.255.255,37.7749,-122.4194,San Francisco,US
|
||||
3.148.0.0,3.163.255.255,-33.8688,151.2093,Sydney,AU
|
||||
3.164.0.0,3.179.255.255,19.076,72.8777,Mumbai,IN
|
||||
3.180.0.0,3.195.255.255,48.8566,2.3522,Paris,FR
|
||||
3.196.0.0,3.207.255.255,52.3676,4.9041,Amsterdam,NL
|
||||
3.208.0.0,3.215.255.255,39.0438,-77.4874,Ashburn,US
|
||||
3.216.0.0,3.223.255.255,-23.5505,-46.6333,Sao Paulo,BR
|
||||
3.224.0.0,3.239.255.255,39.0438,-77.4874,Ashburn,US
|
||||
3.240.0.0,4.111.255.255,55.7558,37.6173,Moscow,RU
|
||||
4.112.0.0,4.239.255.255,41.8781,-87.6298,Chicago,US
|
||||
4.240.0.0,4.255.255.255,32.7767,-96.797,Dallas,US
|
||||
5.0.0.0,5.255.255.255,51.5074,-0.1278,London,GB
|
||||
6.0.0.0,6.127.255.255,33.749,-84.388,Atlanta,US
|
||||
6.128.0.0,6.255.255.255,43.6532,-79.3832,Toronto,CA
|
||||
7.0.0.0,7.127.255.255,22.3193,114.1694,Hong Kong,HK
|
||||
7.128.0.0,7.255.255.255,25.033,121.5654,Taipei,TW
|
||||
8.0.0.0,8.8.4.3,40.4168,-3.7038,Madrid,ES
|
||||
8.8.4.4,8.8.4.4,37.3861,-122.0839,Mountain View,US
|
||||
8.8.4.5,8.8.8.7,45.4642,9.19,Milan,IT
|
||||
8.8.8.8,8.8.8.8,37.3861,-122.0839,Mountain View,US
|
||||
8.8.8.9,8.136.8.8,59.3293,18.0686,Stockholm,SE
|
||||
8.136.8.9,9.8.8.8,34.6937,135.5022,Osaka,JP
|
||||
9.8.8.9,9.136.8.8,23.1291,113.2644,Guangzhou,CN
|
||||
9.136.8.9,9.255.255.255,22.5431,114.0579,Shenzhen,CN
|
||||
10.0.0.0,10.255.255.255,39.9042,116.4074,Beijing,CN
|
||||
11.0.0.0,11.127.255.255,52.52,13.405,Berlin,DE
|
||||
11.128.0.0,11.255.255.255,25.2048,55.2708,Dubai,AE
|
||||
12.0.0.0,12.255.255.255,32.7767,-96.797,Dallas,US
|
||||
13.0.0.0,13.15.255.255,13.7563,100.5018,Bangkok,TH
|
||||
13.16.0.0,13.31.255.255,31.2304,121.4737,Shanghai,CN
|
||||
13.32.0.0,13.47.255.255,39.9042,116.4074,Beijing,CN
|
||||
13.48.0.0,13.51.255.255,59.3293,18.0686,Stockholm,SE
|
||||
13.52.0.0,13.55.255.255,37.7749,-122.4194,San Francisco,US
|
||||
13.56.0.0,13.59.255.255,37.7749,-122.4194,San Francisco,US
|
||||
13.60.0.0,13.63.255.255,35.6762,139.6503,Tokyo,JP
|
||||
13.64.0.0,13.95.255.255,37.7749,-122.4194,San Francisco,US
|
||||
13.96.0.0,13.111.255.255,37.5665,126.978,Seoul,KR
|
||||
13.112.0.0,13.115.255.255,35.6762,139.6503,Tokyo,JP
|
||||
13.116.0.0,13.123.255.255,1.3521,103.8198,Singapore,SG
|
||||
13.124.0.0,13.124.255.255,37.5665,126.978,Seoul,KR
|
||||
13.125.0.0,13.125.255.255,51.5074,-0.1278,London,GB
|
||||
13.126.0.0,13.127.255.255,19.076,72.8777,Mumbai,IN
|
||||
13.128.0.0,13.143.255.255,50.1109,8.6821,Frankfurt,DE
|
||||
13.144.0.0,13.159.255.255,40.7128,-74.006,New York,US
|
||||
13.160.0.0,13.175.255.255,37.7749,-122.4194,San Francisco,US
|
||||
13.176.0.0,13.191.255.255,-33.8688,151.2093,Sydney,AU
|
||||
13.192.0.0,13.207.255.255,19.076,72.8777,Mumbai,IN
|
||||
13.208.0.0,13.208.255.255,34.6937,135.5022,Osaka,JP
|
||||
13.209.0.0,13.209.255.255,37.5665,126.978,Seoul,KR
|
||||
13.210.0.0,13.211.255.255,-33.8688,151.2093,Sydney,AU
|
||||
13.212.0.0,13.215.255.255,1.3521,103.8198,Singapore,SG
|
||||
13.216.0.0,13.227.255.255,48.8566,2.3522,Paris,FR
|
||||
13.228.0.0,13.229.255.255,1.3521,103.8198,Singapore,SG
|
||||
13.230.0.0,13.231.255.255,35.6762,139.6503,Tokyo,JP
|
||||
13.232.0.0,13.243.255.255,52.3676,4.9041,Amsterdam,NL
|
||||
13.244.0.0,13.245.255.255,-33.9249,18.4241,Cape Town,ZA
|
||||
13.246.0.0,13.255.255.255,-23.5505,-46.6333,Sao Paulo,BR
|
||||
14.0.0.0,14.255.255.255,39.9042,116.4074,Beijing,CN
|
||||
15.0.0.0,15.127.255.255,55.7558,37.6173,Moscow,RU
|
||||
15.128.0.0,15.151.255.255,41.8781,-87.6298,Chicago,US
|
||||
15.152.0.0,15.152.255.255,34.6937,135.5022,Osaka,JP
|
||||
15.153.0.0,15.159.255.255,32.7767,-96.797,Dallas,US
|
||||
15.160.0.0,15.160.255.255,45.4642,9.19,Milan,IT
|
||||
15.161.0.0,15.163.255.255,33.749,-84.388,Atlanta,US
|
||||
15.164.0.0,15.164.255.255,37.5665,126.978,Seoul,KR
|
||||
15.165.0.0,15.180.255.255,43.6532,-79.3832,Toronto,CA
|
||||
15.181.0.0,15.183.255.255,22.3193,114.1694,Hong Kong,HK
|
||||
15.184.0.0,15.185.255.255,26.0667,50.5577,Bahrain,BH
|
||||
15.186.0.0,15.187.255.255,25.033,121.5654,Taipei,TW
|
||||
15.188.0.0,15.188.255.255,48.8566,2.3522,Paris,FR
|
||||
15.189.0.0,15.204.255.255,40.4168,-3.7038,Madrid,ES
|
||||
15.205.0.0,15.205.255.255,45.4642,9.19,Milan,IT
|
||||
15.206.0.0,15.206.255.255,19.076,72.8777,Mumbai,IN
|
||||
15.207.0.0,15.221.255.255,59.3293,18.0686,Stockholm,SE
|
||||
15.222.0.0,15.223.255.255,45.5017,-73.5673,Montreal,CA
|
||||
15.224.0.0,15.227.255.255,34.6937,135.5022,Osaka,JP
|
||||
15.228.0.0,15.229.255.255,-23.5505,-46.6333,Sao Paulo,BR
|
||||
15.230.0.0,15.245.255.255,23.1291,113.2644,Guangzhou,CN
|
||||
15.246.0.0,16.5.255.255,22.5431,114.0579,Shenzhen,CN
|
||||
16.6.0.0,16.15.255.255,52.52,13.405,Berlin,DE
|
||||
16.16.0.0,16.16.255.255,59.3293,18.0686,Stockholm,SE
|
||||
16.17.0.0,16.144.255.255,25.2048,55.2708,Dubai,AE
|
||||
16.145.0.0,16.169.255.255,13.7563,100.5018,Bangkok,TH
|
||||
16.170.0.0,16.171.255.255,59.3293,18.0686,Stockholm,SE
|
||||
16.172.0.0,17.43.255.255,31.2304,121.4737,Shanghai,CN
|
||||
17.44.0.0,17.171.255.255,39.9042,116.4074,Beijing,CN
|
||||
17.172.0.0,18.43.255.255,35.6762,139.6503,Tokyo,JP
|
||||
18.44.0.0,18.135.255.255,37.5665,126.978,Seoul,KR
|
||||
18.136.0.0,18.139.255.255,1.3521,103.8198,Singapore,SG
|
||||
18.140.0.0,18.155.255.255,1.3521,103.8198,Singapore,SG
|
||||
18.156.0.0,18.161.255.255,51.5074,-0.1278,London,GB
|
||||
18.162.0.0,18.163.255.255,22.3193,114.1694,Hong Kong,HK
|
||||
18.164.0.0,18.167.255.255,50.1109,8.6821,Frankfurt,DE
|
||||
18.168.0.0,18.171.255.255,51.5074,-0.1278,London,GB
|
||||
18.172.0.0,18.175.255.255,40.7128,-74.006,New York,US
|
||||
18.176.0.0,18.177.255.255,35.6762,139.6503,Tokyo,JP
|
||||
18.178.0.0,18.183.255.255,37.7749,-122.4194,San Francisco,US
|
||||
18.184.0.0,18.187.255.255,50.1109,8.6821,Frankfurt,DE
|
||||
18.188.0.0,18.193.255.255,-33.8688,151.2093,Sydney,AU
|
||||
18.194.0.0,18.195.255.255,50.1109,8.6821,Frankfurt,DE
|
||||
18.196.0.0,18.211.255.255,19.076,72.8777,Mumbai,IN
|
||||
18.212.0.0,18.227.255.255,48.8566,2.3522,Paris,FR
|
||||
18.228.0.0,18.229.255.255,-23.5505,-46.6333,Sao Paulo,BR
|
||||
18.230.0.0,18.231.255.255,52.3676,4.9041,Amsterdam,NL
|
||||
18.232.0.0,18.235.255.255,39.0438,-77.4874,Ashburn,US
|
||||
18.236.0.0,19.107.255.255,-23.5505,-46.6333,Sao Paulo,BR
|
||||
19.108.0.0,19.235.255.255,55.7558,37.6173,Moscow,RU
|
||||
19.236.0.0,19.255.255.255,41.8781,-87.6298,Chicago,US
|
||||
20.0.0.0,20.31.255.255,39.0438,-77.4874,Ashburn,US
|
||||
20.32.0.0,20.35.255.255,32.7767,-96.797,Dallas,US
|
||||
20.36.0.0,20.39.255.255,-33.8688,151.2093,Sydney,AU
|
||||
20.40.0.0,20.40.255.255,33.749,-84.388,Atlanta,US
|
||||
20.41.0.0,20.41.255.255,37.5665,126.978,Seoul,KR
|
||||
20.42.0.0,20.42.255.255,43.6532,-79.3832,Toronto,CA
|
||||
20.43.0.0,20.43.255.255,48.8566,2.3522,Paris,FR
|
||||
20.44.0.0,20.45.255.255,35.6762,139.6503,Tokyo,JP
|
||||
20.46.0.0,20.48.255.255,22.3193,114.1694,Hong Kong,HK
|
||||
20.49.0.0,20.49.255.255,51.5074,-0.1278,London,GB
|
||||
20.50.0.0,20.51.255.255,25.033,121.5654,Taipei,TW
|
||||
20.52.0.0,20.52.255.255,50.1109,8.6821,Frankfurt,DE
|
||||
20.53.0.0,20.55.255.255,40.4168,-3.7038,Madrid,ES
|
||||
20.56.0.0,20.56.255.255,52.3676,4.9041,Amsterdam,NL
|
||||
20.57.0.0,20.72.255.255,45.4642,9.19,Milan,IT
|
||||
20.73.0.0,20.88.255.255,59.3293,18.0686,Stockholm,SE
|
||||
20.89.0.0,20.104.255.255,34.6937,135.5022,Osaka,JP
|
||||
20.105.0.0,20.120.255.255,23.1291,113.2644,Guangzhou,CN
|
||||
20.121.0.0,20.136.255.255,22.5431,114.0579,Shenzhen,CN
|
||||
20.137.0.0,20.152.255.255,52.52,13.405,Berlin,DE
|
||||
20.153.0.0,20.168.255.255,25.2048,55.2708,Dubai,AE
|
||||
20.169.0.0,20.183.255.255,13.7563,100.5018,Bangkok,TH
|
||||
20.184.0.0,20.185.255.255,1.3521,103.8198,Singapore,SG
|
||||
20.186.0.0,20.191.255.255,31.2304,121.4737,Shanghai,CN
|
||||
20.192.0.0,20.192.255.255,19.076,72.8777,Mumbai,IN
|
||||
20.193.0.0,20.195.255.255,39.9042,116.4074,Beijing,CN
|
||||
20.196.0.0,20.196.255.255,22.3193,114.1694,Hong Kong,HK
|
||||
20.197.0.0,20.197.255.255,-23.5505,-46.6333,Sao Paulo,BR
|
||||
20.198.0.0,21.69.255.255,35.6762,139.6503,Tokyo,JP
|
||||
21.70.0.0,21.197.255.255,37.5665,126.978,Seoul,KR
|
||||
21.198.0.0,22.69.255.255,1.3521,103.8198,Singapore,SG
|
||||
22.70.0.0,22.197.255.255,51.5074,-0.1278,London,GB
|
||||
22.198.0.0,22.255.255.255,50.1109,8.6821,Frankfurt,DE
|
||||
23.0.0.0,23.15.255.255,42.3736,-71.1097,Cambridge,US
|
||||
23.16.0.0,23.31.255.255,40.7128,-74.006,New York,US
|
||||
23.32.0.0,23.63.255.255,42.3736,-71.1097,Cambridge,US
|
||||
23.64.0.0,23.67.255.255,42.3736,-71.1097,Cambridge,US
|
||||
23.68.0.0,23.71.255.255,37.7749,-122.4194,San Francisco,US
|
||||
23.72.0.0,23.79.255.255,42.3736,-71.1097,Cambridge,US
|
||||
23.80.0.0,23.95.255.255,-33.8688,151.2093,Sydney,AU
|
||||
23.96.0.0,23.103.255.255,41.8781,-87.6298,Chicago,US
|
||||
23.104.0.0,23.119.255.255,19.076,72.8777,Mumbai,IN
|
||||
23.120.0.0,23.135.255.255,48.8566,2.3522,Paris,FR
|
||||
23.136.0.0,23.151.255.255,52.3676,4.9041,Amsterdam,NL
|
||||
23.152.0.0,23.167.255.255,-23.5505,-46.6333,Sao Paulo,BR
|
||||
23.168.0.0,23.183.255.255,55.7558,37.6173,Moscow,RU
|
||||
23.184.0.0,23.191.255.255,41.8781,-87.6298,Chicago,US
|
||||
23.192.0.0,23.223.255.255,42.3736,-71.1097,Cambridge,US
|
||||
23.224.0.0,23.239.255.255,32.7767,-96.797,Dallas,US
|
||||
23.240.0.0,23.255.255.255,33.749,-84.388,Atlanta,US
|
||||
24.0.0.0,24.255.255.255,40.7128,-74.006,New York,US
|
||||
25.0.0.0,25.255.255.255,51.5074,-0.1278,London,GB
|
||||
26.0.0.0,26.127.255.255,43.6532,-79.3832,Toronto,CA
|
||||
26.128.0.0,26.255.255.255,22.3193,114.1694,Hong Kong,HK
|
||||
27.0.0.0,27.255.255.255,31.2304,121.4737,Shanghai,CN
|
||||
28.0.0.0,28.127.255.255,25.033,121.5654,Taipei,TW
|
||||
28.128.0.0,28.255.255.255,40.4168,-3.7038,Madrid,ES
|
||||
29.0.0.0,29.127.255.255,45.4642,9.19,Milan,IT
|
||||
29.128.0.0,29.255.255.255,59.3293,18.0686,Stockholm,SE
|
||||
30.0.0.0,30.127.255.255,34.6937,135.5022,Osaka,JP
|
||||
30.128.0.0,30.255.255.255,23.1291,113.2644,Guangzhou,CN
|
||||
31.0.0.0,31.255.255.255,51.5074,-0.1278,London,GB
|
||||
32.0.0.0,32.255.255.255,33.749,-84.388,Atlanta,US
|
||||
33.0.0.0,33.127.255.255,22.5431,114.0579,Shenzhen,CN
|
||||
33.128.0.0,33.255.255.255,52.52,13.405,Berlin,DE
|
||||
34.0.0.0,34.207.255.255,39.0438,-77.4874,Ashburn,US
|
||||
34.208.0.0,34.223.255.255,45.5152,-122.6784,Portland,US
|
||||
34.224.0.0,34.239.255.255,39.0438,-77.4874,Ashburn,US
|
||||
34.240.0.0,34.247.255.255,53.3498,-6.2603,Dublin,IE
|
||||
34.248.0.0,34.255.255.255,39.0438,-77.4874,Ashburn,US
|
||||
35.0.0.0,35.127.255.255,25.2048,55.2708,Dubai,AE
|
||||
35.128.0.0,35.151.255.255,13.7563,100.5018,Bangkok,TH
|
||||
35.152.0.0,35.152.255.255,45.4642,9.19,Milan,IT
|
||||
35.153.0.0,35.153.255.255,31.2304,121.4737,Shanghai,CN
|
||||
35.154.0.0,35.155.255.255,19.076,72.8777,Mumbai,IN
|
||||
35.156.0.0,35.159.255.255,50.1109,8.6821,Frankfurt,DE
|
||||
35.160.0.0,35.175.255.255,39.9042,116.4074,Beijing,CN
|
||||
35.176.0.0,35.177.255.255,51.5074,-0.1278,London,GB
|
||||
35.178.0.0,35.179.255.255,51.5074,-0.1278,London,GB
|
||||
35.180.0.0,35.183.255.255,48.8566,2.3522,Paris,FR
|
||||
35.184.0.0,35.191.255.255,35.6762,139.6503,Tokyo,JP
|
||||
35.192.0.0,35.195.255.255,25.033,121.5654,Taipei,TW
|
||||
35.196.0.0,35.198.255.255,37.5665,126.978,Seoul,KR
|
||||
35.199.0.0,35.199.255.255,-23.5505,-46.6333,Sao Paulo,BR
|
||||
35.200.0.0,35.203.255.255,35.6762,139.6503,Tokyo,JP
|
||||
35.204.0.0,35.207.255.255,52.3676,4.9041,Amsterdam,NL
|
||||
35.208.0.0,35.215.255.255,1.3521,103.8198,Singapore,SG
|
||||
35.216.0.0,35.219.255.255,37.5665,126.978,Seoul,KR
|
||||
35.220.0.0,35.221.255.255,32.7767,-96.797,Dallas,US
|
||||
35.222.0.0,35.223.255.255,41.8781,-87.6298,Chicago,US
|
||||
35.224.0.0,35.239.255.255,39.0438,-77.4874,Ashburn,US
|
||||
35.240.0.0,35.247.255.255,51.5074,-0.1278,London,GB
|
||||
35.248.0.0,35.255.255.255,51.5074,-0.1278,London,GB
|
||||
36.0.0.0,36.255.255.255,23.1291,113.2644,Guangzhou,CN
|
||||
37.0.0.0,37.255.255.255,48.8566,2.3522,Paris,FR
|
||||
38.0.0.0,38.127.255.255,50.1109,8.6821,Frankfurt,DE
|
||||
38.128.0.0,38.255.255.255,40.7128,-74.006,New York,US
|
||||
39.0.0.0,39.95.255.255,37.7749,-122.4194,San Francisco,US
|
||||
39.96.0.0,39.103.255.255,39.9042,116.4074,Beijing,CN
|
||||
39.104.0.0,39.231.255.255,-33.8688,151.2093,Sydney,AU
|
||||
39.232.0.0,40.63.255.255,19.076,72.8777,Mumbai,IN
|
||||
40.64.0.0,40.127.255.255,39.0438,-77.4874,Ashburn,US
|
||||
40.128.0.0,40.255.255.255,48.8566,2.3522,Paris,FR
|
||||
41.0.0.0,41.127.255.255,52.3676,4.9041,Amsterdam,NL
|
||||
41.128.0.0,41.255.255.255,-23.5505,-46.6333,Sao Paulo,BR
|
||||
42.0.0.0,42.255.255.255,39.9042,116.4074,Beijing,CN
|
||||
43.0.0.0,43.127.255.255,55.7558,37.6173,Moscow,RU
|
||||
43.128.0.0,43.255.255.255,41.8781,-87.6298,Chicago,US
|
||||
44.0.0.0,44.127.255.255,32.7767,-96.797,Dallas,US
|
||||
44.128.0.0,44.191.255.255,33.749,-84.388,Atlanta,US
|
||||
44.192.0.0,44.223.255.255,39.0438,-77.4874,Ashburn,US
|
||||
44.224.0.0,44.255.255.255,45.5152,-122.6784,Portland,US
|
||||
45.0.0.0,45.255.255.255,40.7128,-74.006,New York,US
|
||||
46.0.0.0,46.136.255.255,52.3676,4.9041,Amsterdam,NL
|
||||
46.137.0.0,46.137.127.255,1.3521,103.8198,Singapore,SG
|
||||
46.137.128.0,46.255.255.255,52.3676,4.9041,Amsterdam,NL
|
||||
47.0.0.0,47.15.255.255,43.6532,-79.3832,Toronto,CA
|
||||
47.16.0.0,47.31.255.255,22.3193,114.1694,Hong Kong,HK
|
||||
47.32.0.0,47.47.255.255,25.033,121.5654,Taipei,TW
|
||||
47.48.0.0,47.51.255.255,40.4168,-3.7038,Madrid,ES
|
||||
47.52.0.0,47.55.255.255,22.3193,114.1694,Hong Kong,HK
|
||||
47.56.0.0,47.57.255.255,22.3193,114.1694,Hong Kong,HK
|
||||
47.58.0.0,47.73.255.255,45.4642,9.19,Milan,IT
|
||||
47.74.0.0,47.75.255.255,1.3521,103.8198,Singapore,SG
|
||||
47.76.0.0,47.77.255.255,37.7749,-122.4194,San Francisco,US
|
||||
47.78.0.0,47.87.255.255,59.3293,18.0686,Stockholm,SE
|
||||
47.88.0.0,47.88.255.255,1.3521,103.8198,Singapore,SG
|
||||
47.89.0.0,47.89.255.255,22.3193,114.1694,Hong Kong,HK
|
||||
47.90.0.0,47.90.255.255,34.6937,135.5022,Osaka,JP
|
||||
47.91.0.0,47.91.255.255,35.6762,139.6503,Tokyo,JP
|
||||
47.92.0.0,47.95.255.255,39.9042,116.4074,Beijing,CN
|
||||
47.96.0.0,47.97.255.255,30.2741,120.1551,Hangzhou,CN
|
||||
47.98.0.0,47.99.255.255,30.2741,120.1551,Hangzhou,CN
|
||||
47.100.0.0,47.103.255.255,31.2304,121.4737,Shanghai,CN
|
||||
47.104.0.0,47.105.255.255,23.1291,113.2644,Guangzhou,CN
|
||||
47.106.0.0,47.106.255.255,22.5431,114.0579,Shenzhen,CN
|
||||
47.107.0.0,47.107.255.255,22.5431,114.0579,Shenzhen,CN
|
||||
47.108.0.0,47.109.255.255,30.5728,104.0668,Chengdu,CN
|
||||
47.110.0.0,47.111.255.255,52.52,13.405,Berlin,DE
|
||||
47.112.0.0,47.112.255.255,19.076,72.8777,Mumbai,IN
|
||||
47.113.0.0,47.113.255.255,25.2048,55.2708,Dubai,AE
|
||||
47.114.0.0,47.114.255.255,-33.8688,151.2093,Sydney,AU
|
||||
47.115.0.0,47.115.255.255,22.5431,114.0579,Shenzhen,CN
|
||||
47.116.0.0,47.116.255.255,50.1109,8.6821,Frankfurt,DE
|
||||
47.117.0.0,47.117.255.255,35.6762,139.6503,Tokyo,JP
|
||||
47.118.0.0,47.118.255.255,51.5074,-0.1278,London,GB
|
||||
47.119.0.0,47.119.255.255,25.2048,55.2708,Dubai,AE
|
||||
47.120.0.0,47.120.255.255,3.139,101.6869,Kuala Lumpur,MY
|
||||
47.121.0.0,47.121.255.255,13.7563,100.5018,Bangkok,TH
|
||||
47.122.0.0,47.122.255.255,-6.2088,106.8456,Jakarta,ID
|
||||
47.123.0.0,47.250.255.255,31.2304,121.4737,Shanghai,CN
|
||||
47.251.0.0,47.252.255.255,39.9042,116.4074,Beijing,CN
|
||||
47.253.0.0,47.253.255.255,37.7749,-122.4194,San Francisco,US
|
||||
47.254.0.0,47.254.255.255,50.1109,8.6821,Frankfurt,DE
|
||||
47.255.0.0,48.126.255.255,35.6762,139.6503,Tokyo,JP
|
||||
48.127.0.0,48.254.255.255,37.5665,126.978,Seoul,KR
|
||||
48.255.0.0,49.126.255.255,1.3521,103.8198,Singapore,SG
|
||||
49.127.0.0,49.254.255.255,51.5074,-0.1278,London,GB
|
||||
49.255.0.0,50.17.255.255,50.1109,8.6821,Frankfurt,DE
|
||||
50.18.0.0,50.19.255.255,37.7749,-122.4194,San Francisco,US
|
||||
50.20.0.0,50.35.255.255,40.7128,-74.006,New York,US
|
||||
50.36.0.0,50.51.255.255,37.7749,-122.4194,San Francisco,US
|
||||
50.52.0.0,50.67.255.255,-33.8688,151.2093,Sydney,AU
|
||||
50.68.0.0,50.83.255.255,19.076,72.8777,Mumbai,IN
|
||||
50.84.0.0,50.99.255.255,48.8566,2.3522,Paris,FR
|
||||
50.100.0.0,50.111.255.255,52.3676,4.9041,Amsterdam,NL
|
||||
50.112.0.0,50.112.255.255,45.5152,-122.6784,Portland,US
|
||||
50.113.0.0,50.127.255.255,-23.5505,-46.6333,Sao Paulo,BR
|
||||
50.128.0.0,50.255.255.255,41.8781,-87.6298,Chicago,US
|
||||
51.0.0.0,51.102.255.255,51.5074,-0.1278,London,GB
|
||||
51.103.0.0,51.103.255.255,50.1109,8.6821,Frankfurt,DE
|
||||
51.104.0.0,51.104.255.255,51.5074,-0.1278,London,GB
|
||||
51.105.0.0,51.105.255.255,51.5074,-0.1278,London,GB
|
||||
51.106.0.0,51.139.255.255,51.5074,-0.1278,London,GB
|
||||
51.140.0.0,51.143.255.255,51.5074,-0.1278,London,GB
|
||||
51.144.0.0,51.255.255.255,51.5074,-0.1278,London,GB
|
||||
52.0.0.0,52.1.255.255,39.0438,-77.4874,Ashburn,US
|
||||
52.2.0.0,52.3.255.255,39.0438,-77.4874,Ashburn,US
|
||||
52.4.0.0,52.7.255.255,39.0438,-77.4874,Ashburn,US
|
||||
52.8.0.0,52.15.255.255,55.7558,37.6173,Moscow,RU
|
||||
52.16.0.0,52.19.255.255,53.3498,-6.2603,Dublin,IE
|
||||
52.20.0.0,52.23.255.255,39.0438,-77.4874,Ashburn,US
|
||||
52.24.0.0,52.27.255.255,41.8781,-87.6298,Chicago,US
|
||||
52.28.0.0,52.31.255.255,50.1109,8.6821,Frankfurt,DE
|
||||
52.32.0.0,52.46.255.255,32.7767,-96.797,Dallas,US
|
||||
52.47.0.0,52.47.255.255,48.8566,2.3522,Paris,FR
|
||||
52.48.0.0,52.51.255.255,53.3498,-6.2603,Dublin,IE
|
||||
52.52.0.0,52.56.255.255,33.749,-84.388,Atlanta,US
|
||||
52.57.0.0,52.57.255.255,50.1109,8.6821,Frankfurt,DE
|
||||
52.58.0.0,52.59.255.255,43.6532,-79.3832,Toronto,CA
|
||||
52.60.0.0,52.60.255.255,45.5017,-73.5673,Montreal,CA
|
||||
52.61.0.0,52.61.255.255,22.3193,114.1694,Hong Kong,HK
|
||||
52.62.0.0,52.62.255.255,-33.8688,151.2093,Sydney,AU
|
||||
52.63.0.0,52.65.255.255,25.033,121.5654,Taipei,TW
|
||||
52.66.0.0,52.66.255.255,19.076,72.8777,Mumbai,IN
|
||||
52.67.0.0,52.73.255.255,40.4168,-3.7038,Madrid,ES
|
||||
52.74.0.0,52.74.255.255,1.3521,103.8198,Singapore,SG
|
||||
52.75.0.0,52.77.255.255,45.4642,9.19,Milan,IT
|
||||
52.78.0.0,52.78.255.255,37.5665,126.978,Seoul,KR
|
||||
52.79.0.0,52.94.255.255,59.3293,18.0686,Stockholm,SE
|
||||
52.95.0.0,52.110.255.255,34.6937,135.5022,Osaka,JP
|
||||
52.111.0.0,52.126.255.255,23.1291,113.2644,Guangzhou,CN
|
||||
52.127.0.0,52.138.255.255,22.5431,114.0579,Shenzhen,CN
|
||||
52.139.0.0,52.139.255.255,1.3521,103.8198,Singapore,SG
|
||||
52.140.0.0,52.140.255.255,52.52,13.405,Berlin,DE
|
||||
52.141.0.0,52.141.255.255,37.5665,126.978,Seoul,KR
|
||||
52.142.0.0,52.144.255.255,25.2048,55.2708,Dubai,AE
|
||||
52.145.0.0,52.145.255.255,39.0438,-77.4874,Ashburn,US
|
||||
52.146.0.0,52.146.255.255,13.7563,100.5018,Bangkok,TH
|
||||
52.147.0.0,52.147.255.255,-33.8688,151.2093,Sydney,AU
|
||||
52.148.0.0,52.163.255.255,31.2304,121.4737,Shanghai,CN
|
||||
52.164.0.0,52.179.255.255,39.9042,116.4074,Beijing,CN
|
||||
52.180.0.0,52.184.255.255,35.6762,139.6503,Tokyo,JP
|
||||
52.185.0.0,52.185.255.255,35.6762,139.6503,Tokyo,JP
|
||||
52.186.0.0,52.201.255.255,37.5665,126.978,Seoul,KR
|
||||
52.202.0.0,52.217.255.255,1.3521,103.8198,Singapore,SG
|
||||
52.218.0.0,52.223.255.255,51.5074,-0.1278,London,GB
|
||||
52.224.0.0,52.255.255.255,39.0438,-77.4874,Ashburn,US
|
||||
53.0.0.0,53.127.255.255,50.1109,8.6821,Frankfurt,DE
|
||||
53.128.0.0,53.255.255.255,40.7128,-74.006,New York,US
|
||||
54.0.0.0,54.63.255.255,37.7749,-122.4194,San Francisco,US
|
||||
54.64.0.0,54.65.255.255,35.6762,139.6503,Tokyo,JP
|
||||
54.66.0.0,54.66.255.255,-33.8688,151.2093,Sydney,AU
|
||||
54.67.0.0,54.67.255.255,-33.8688,151.2093,Sydney,AU
|
||||
54.68.0.0,54.71.255.255,45.5152,-122.6784,Portland,US
|
||||
54.72.0.0,54.79.255.255,53.3498,-6.2603,Dublin,IE
|
||||
54.80.0.0,54.95.255.255,39.0438,-77.4874,Ashburn,US
|
||||
54.96.0.0,54.111.255.255,19.076,72.8777,Mumbai,IN
|
||||
54.112.0.0,54.127.255.255,48.8566,2.3522,Paris,FR
|
||||
54.128.0.0,54.143.255.255,52.3676,4.9041,Amsterdam,NL
|
||||
54.144.0.0,54.151.255.255,-23.5505,-46.6333,Sao Paulo,BR
|
||||
54.152.0.0,54.159.255.255,39.0438,-77.4874,Ashburn,US
|
||||
54.160.0.0,54.191.255.255,39.0438,-77.4874,Ashburn,US
|
||||
54.192.0.0,54.205.255.255,55.7558,37.6173,Moscow,RU
|
||||
54.206.0.0,54.206.255.255,-33.8688,151.2093,Sydney,AU
|
||||
54.207.0.0,54.207.255.255,41.8781,-87.6298,Chicago,US
|
||||
54.208.0.0,54.209.255.255,39.0438,-77.4874,Ashburn,US
|
||||
54.210.0.0,54.223.255.255,32.7767,-96.797,Dallas,US
|
||||
54.224.0.0,54.239.255.255,39.0438,-77.4874,Ashburn,US
|
||||
54.240.0.0,54.240.255.255,33.749,-84.388,Atlanta,US
|
||||
54.241.0.0,54.241.255.255,37.7749,-122.4194,San Francisco,US
|
||||
54.242.0.0,54.243.255.255,43.6532,-79.3832,Toronto,CA
|
||||
54.244.0.0,54.245.255.255,45.5152,-122.6784,Portland,US
|
||||
54.246.0.0,55.117.255.255,22.3193,114.1694,Hong Kong,HK
|
||||
55.118.0.0,55.245.255.255,25.033,121.5654,Taipei,TW
|
||||
55.246.0.0,56.117.255.255,40.4168,-3.7038,Madrid,ES
|
||||
56.118.0.0,56.245.255.255,45.4642,9.19,Milan,IT
|
||||
56.246.0.0,57.117.255.255,59.3293,18.0686,Stockholm,SE
|
||||
57.118.0.0,57.245.255.255,34.6937,135.5022,Osaka,JP
|
||||
57.246.0.0,57.255.255.255,23.1291,113.2644,Guangzhou,CN
|
||||
58.0.0.0,58.255.255.255,22.5431,114.0579,Shenzhen,CN
|
||||
59.0.0.0,59.109.255.255,31.2304,121.4737,Shanghai,CN
|
||||
59.110.0.0,59.110.255.255,39.9042,116.4074,Beijing,CN
|
||||
59.111.0.0,59.255.255.255,31.2304,121.4737,Shanghai,CN
|
||||
60.0.0.0,60.255.255.255,39.9042,116.4074,Beijing,CN
|
||||
61.0.0.0,61.255.255.255,31.2304,121.4737,Shanghai,CN
|
||||
62.0.0.0,62.255.255.255,52.3676,4.9041,Amsterdam,NL
|
||||
63.0.0.0,63.15.255.255,22.5431,114.0579,Shenzhen,CN
|
||||
63.16.0.0,63.31.255.255,52.52,13.405,Berlin,DE
|
||||
63.32.0.0,63.35.255.255,53.3498,-6.2603,Dublin,IE
|
||||
63.36.0.0,63.163.255.255,25.2048,55.2708,Dubai,AE
|
||||
63.164.0.0,63.255.255.255,13.7563,100.5018,Bangkok,TH
|
||||
64.0.0.0,64.255.255.255,41.8781,-87.6298,Chicago,US
|
||||
65.0.0.0,65.255.255.255,37.7749,-122.4194,San Francisco,US
|
||||
66.0.0.0,66.255.255.255,32.7767,-96.797,Dallas,US
|
||||
67.0.0.0,67.159.255.255,25.7617,-80.1918,Miami,US
|
||||
67.160.0.0,67.191.255.255,39.9526,-75.1652,Philadelphia,US
|
||||
67.192.0.0,67.255.255.255,25.7617,-80.1918,Miami,US
|
||||
68.0.0.0,68.127.255.255,31.2304,121.4737,Shanghai,CN
|
||||
68.128.0.0,68.255.255.255,39.9042,116.4074,Beijing,CN
|
||||
69.0.0.0,69.135.255.255,40.7128,-74.006,New York,US
|
||||
69.136.0.0,69.143.255.255,42.3601,-71.0589,Boston,US
|
||||
69.144.0.0,69.239.255.255,40.7128,-74.006,New York,US
|
||||
69.240.0.0,69.255.255.255,47.6062,-122.3321,Seattle,US
|
||||
70.0.0.0,70.255.255.255,41.8781,-87.6298,Chicago,US
|
||||
71.0.0.0,71.127.255.255,35.6762,139.6503,Tokyo,JP
|
||||
71.128.0.0,71.159.255.255,37.5665,126.978,Seoul,KR
|
||||
71.160.0.0,71.175.255.255,40.7128,-74.006,New York,US
|
||||
71.176.0.0,71.191.255.255,1.3521,103.8198,Singapore,SG
|
||||
71.192.0.0,71.255.255.255,37.7749,-122.4194,San Francisco,US
|
||||
72.0.0.0,72.15.255.255,51.5074,-0.1278,London,GB
|
||||
72.16.0.0,72.31.255.255,50.1109,8.6821,Frankfurt,DE
|
||||
72.32.0.0,72.47.255.255,40.7128,-74.006,New York,US
|
||||
72.48.0.0,72.63.255.255,37.7749,-122.4194,San Francisco,US
|
||||
72.64.0.0,72.127.255.255,32.7767,-96.797,Dallas,US
|
||||
72.128.0.0,72.143.255.255,-33.8688,151.2093,Sydney,AU
|
||||
72.144.0.0,72.159.255.255,19.076,72.8777,Mumbai,IN
|
||||
72.160.0.0,72.175.255.255,48.8566,2.3522,Paris,FR
|
||||
72.176.0.0,72.191.255.255,52.3676,4.9041,Amsterdam,NL
|
||||
72.192.0.0,72.207.255.255,-23.5505,-46.6333,Sao Paulo,BR
|
||||
72.208.0.0,72.223.255.255,55.7558,37.6173,Moscow,RU
|
||||
72.224.0.0,72.239.255.255,41.8781,-87.6298,Chicago,US
|
||||
72.240.0.0,72.245.255.255,32.7767,-96.797,Dallas,US
|
||||
72.246.0.0,72.247.255.255,42.3736,-71.1097,Cambridge,US
|
||||
72.248.0.0,72.255.255.255,33.749,-84.388,Atlanta,US
|
||||
73.0.0.0,73.255.255.255,33.749,-84.388,Atlanta,US
|
||||
74.0.0.0,74.15.255.255,43.6532,-79.3832,Toronto,CA
|
||||
74.16.0.0,74.31.255.255,22.3193,114.1694,Hong Kong,HK
|
||||
74.32.0.0,74.47.255.255,25.033,121.5654,Taipei,TW
|
||||
74.48.0.0,74.63.255.255,40.4168,-3.7038,Madrid,ES
|
||||
74.64.0.0,74.79.255.255,45.4642,9.19,Milan,IT
|
||||
74.80.0.0,74.95.255.255,59.3293,18.0686,Stockholm,SE
|
||||
74.96.0.0,74.127.255.255,33.749,-84.388,Atlanta,US
|
||||
74.128.0.0,74.143.255.255,34.6937,135.5022,Osaka,JP
|
||||
74.144.0.0,74.159.255.255,23.1291,113.2644,Guangzhou,CN
|
||||
74.160.0.0,74.175.255.255,22.5431,114.0579,Shenzhen,CN
|
||||
74.176.0.0,74.191.255.255,52.52,13.405,Berlin,DE
|
||||
74.192.0.0,74.207.255.255,25.2048,55.2708,Dubai,AE
|
||||
74.208.0.0,74.223.255.255,13.7563,100.5018,Bangkok,TH
|
||||
74.224.0.0,74.239.255.255,31.2304,121.4737,Shanghai,CN
|
||||
74.240.0.0,74.255.255.255,39.9042,116.4074,Beijing,CN
|
||||
75.0.0.0,75.255.255.255,37.7749,-122.4194,San Francisco,US
|
||||
76.0.0.0,76.255.255.255,29.7604,-95.3698,Houston,US
|
||||
77.0.0.0,77.255.255.255,52.3676,4.9041,Amsterdam,NL
|
||||
78.0.0.0,78.255.255.255,52.52,13.405,Berlin,DE
|
||||
79.0.0.0,79.191.255.255,52.52,13.405,Berlin,DE
|
||||
79.192.0.0,79.255.255.255,50.1109,8.6821,Frankfurt,DE
|
||||
80.0.0.0,80.127.255.255,52.3676,4.9041,Amsterdam,NL
|
||||
80.128.0.0,80.159.255.255,52.52,13.405,Berlin,DE
|
||||
80.160.0.0,80.255.255.255,52.3676,4.9041,Amsterdam,NL
|
||||
81.0.0.0,81.255.255.255,51.5074,-0.1278,London,GB
|
||||
82.0.0.0,82.255.255.255,52.3676,4.9041,Amsterdam,NL
|
||||
83.0.0.0,83.255.255.255,52.3676,4.9041,Amsterdam,NL
|
||||
84.0.0.0,84.255.255.255,52.3676,4.9041,Amsterdam,NL
|
||||
85.0.0.0,85.255.255.255,52.3676,4.9041,Amsterdam,NL
|
||||
86.0.0.0,86.255.255.255,52.3676,4.9041,Amsterdam,NL
|
||||
87.0.0.0,87.127.255.255,52.52,13.405,Berlin,DE
|
||||
87.128.0.0,87.191.255.255,48.1351,11.582,Munich,DE
|
||||
87.192.0.0,87.255.255.255,52.52,13.405,Berlin,DE
|
||||
88.0.0.0,88.255.255.255,52.3676,4.9041,Amsterdam,NL
|
||||
89.0.0.0,89.255.255.255,52.3676,4.9041,Amsterdam,NL
|
||||
90.0.0.0,90.255.255.255,52.3676,4.9041,Amsterdam,NL
|
||||
91.0.0.0,91.63.255.255,50.1109,8.6821,Frankfurt,DE
|
||||
91.64.0.0,91.191.255.255,35.6762,139.6503,Tokyo,JP
|
||||
91.192.0.0,91.255.255.255,37.5665,126.978,Seoul,KR
|
||||
92.0.0.0,92.121.255.255,55.7558,37.6173,Moscow,RU
|
||||
92.122.0.0,92.123.255.255,42.3736,-71.1097,Cambridge,US
|
||||
92.124.0.0,92.255.255.255,55.7558,37.6173,Moscow,RU
|
||||
93.0.0.0,93.127.255.255,1.3521,103.8198,Singapore,SG
|
||||
93.128.0.0,93.191.255.255,51.5074,-0.1278,London,GB
|
||||
93.192.0.0,93.255.255.255,52.52,13.405,Berlin,DE
|
||||
94.0.0.0,94.255.255.255,55.7558,37.6173,Moscow,RU
|
||||
95.0.0.0,95.99.255.255,52.52,13.405,Berlin,DE
|
||||
95.100.0.0,95.101.255.255,42.3736,-71.1097,Cambridge,US
|
||||
95.102.0.0,95.255.255.255,52.52,13.405,Berlin,DE
|
||||
96.0.0.0,96.5.255.255,50.1109,8.6821,Frankfurt,DE
|
||||
96.6.0.0,96.7.255.255,42.3736,-71.1097,Cambridge,US
|
||||
96.8.0.0,96.15.255.255,40.7128,-74.006,New York,US
|
||||
96.16.0.0,96.17.255.255,42.3736,-71.1097,Cambridge,US
|
||||
96.18.0.0,96.145.255.255,37.7749,-122.4194,San Francisco,US
|
||||
96.146.0.0,96.223.255.255,-33.8688,151.2093,Sydney,AU
|
||||
96.224.0.0,96.255.255.255,40.7128,-74.006,New York,US
|
||||
97.0.0.0,97.127.255.255,19.076,72.8777,Mumbai,IN
|
||||
97.128.0.0,97.255.255.255,48.8566,2.3522,Paris,FR
|
||||
98.0.0.0,98.127.255.255,52.3676,4.9041,Amsterdam,NL
|
||||
98.128.0.0,98.191.255.255,-23.5505,-46.6333,Sao Paulo,BR
|
||||
98.192.0.0,98.255.255.255,41.8781,-87.6298,Chicago,US
|
||||
99.0.0.0,99.78.255.255,40.7128,-74.006,New York,US
|
||||
99.79.0.0,99.79.255.255,45.5017,-73.5673,Montreal,CA
|
||||
99.80.0.0,99.255.255.255,40.7128,-74.006,New York,US
|
||||
100.0.0.0,100.63.255.255,41.8781,-87.6298,Chicago,US
|
||||
100.64.0.0,100.191.255.255,55.7558,37.6173,Moscow,RU
|
||||
100.192.0.0,100.255.255.255,41.8781,-87.6298,Chicago,US
|
||||
101.0.0.0,101.131.255.255,39.9042,116.4074,Beijing,CN
|
||||
101.132.0.0,101.132.255.255,31.2304,121.4737,Shanghai,CN
|
||||
101.133.0.0,101.199.255.255,39.9042,116.4074,Beijing,CN
|
||||
101.200.0.0,101.201.255.255,39.9042,116.4074,Beijing,CN
|
||||
101.202.0.0,101.255.255.255,39.9042,116.4074,Beijing,CN
|
||||
102.0.0.0,102.127.255.255,32.7767,-96.797,Dallas,US
|
||||
102.128.0.0,102.255.255.255,33.749,-84.388,Atlanta,US
|
||||
103.0.0.0,103.21.243.255,43.6532,-79.3832,Toronto,CA
|
||||
103.21.244.0,103.21.247.255,37.7749,-122.4194,San Francisco,US
|
||||
103.21.248.0,103.22.199.255,22.3193,114.1694,Hong Kong,HK
|
||||
103.22.200.0,103.22.203.255,37.7749,-122.4194,San Francisco,US
|
||||
103.22.204.0,103.31.3.255,25.033,121.5654,Taipei,TW
|
||||
103.31.4.0,103.31.7.255,37.7749,-122.4194,San Francisco,US
|
||||
103.31.8.0,103.159.7.255,40.4168,-3.7038,Madrid,ES
|
||||
103.159.8.0,103.255.255.255,45.4642,9.19,Milan,IT
|
||||
104.0.0.0,104.15.255.255,32.7767,-96.797,Dallas,US
|
||||
104.16.0.0,104.23.255.255,37.7749,-122.4194,San Francisco,US
|
||||
104.24.0.0,104.27.255.255,37.7749,-122.4194,San Francisco,US
|
||||
104.28.0.0,104.39.255.255,32.7767,-96.797,Dallas,US
|
||||
104.40.0.0,104.47.255.255,39.0438,-77.4874,Ashburn,US
|
||||
104.48.0.0,104.63.255.255,32.7767,-96.797,Dallas,US
|
||||
104.64.0.0,104.127.255.255,42.3736,-71.1097,Cambridge,US
|
||||
104.128.0.0,104.151.255.255,32.7767,-96.797,Dallas,US
|
||||
104.152.0.0,104.153.255.255,50.1109,8.6821,Frankfurt,DE
|
||||
104.154.0.0,104.155.255.255,41.8781,-87.6298,Chicago,US
|
||||
104.156.0.0,104.195.255.255,32.7767,-96.797,Dallas,US
|
||||
104.196.0.0,104.199.255.255,39.0438,-77.4874,Ashburn,US
|
||||
104.200.0.0,104.210.255.255,32.7767,-96.797,Dallas,US
|
||||
104.211.0.0,104.211.255.255,19.076,72.8777,Mumbai,IN
|
||||
104.212.0.0,104.255.255.255,32.7767,-96.797,Dallas,US
|
||||
105.0.0.0,105.127.255.255,59.3293,18.0686,Stockholm,SE
|
||||
105.128.0.0,105.255.255.255,34.6937,135.5022,Osaka,JP
|
||||
106.0.0.0,106.13.255.255,23.1291,113.2644,Guangzhou,CN
|
||||
106.14.0.0,106.15.255.255,31.2304,121.4737,Shanghai,CN
|
||||
106.16.0.0,106.143.255.255,22.5431,114.0579,Shenzhen,CN
|
||||
106.144.0.0,106.255.255.255,52.52,13.405,Berlin,DE
|
||||
107.0.0.0,107.255.255.255,33.749,-84.388,Atlanta,US
|
||||
108.0.0.0,108.162.191.255,41.8781,-87.6298,Chicago,US
|
||||
108.162.192.0,108.162.255.255,37.7749,-122.4194,San Francisco,US
|
||||
108.163.0.0,108.255.255.255,41.8781,-87.6298,Chicago,US
|
||||
109.0.0.0,109.255.255.255,51.5074,-0.1278,London,GB
|
||||
110.0.0.0,110.255.255.255,23.1291,113.2644,Guangzhou,CN
|
||||
111.0.0.0,111.255.255.255,31.2304,121.4737,Shanghai,CN
|
||||
112.0.0.0,112.255.255.255,39.9042,116.4074,Beijing,CN
|
||||
113.0.0.0,113.255.255.255,31.2304,121.4737,Shanghai,CN
|
||||
114.0.0.0,114.255.255.255,39.9042,116.4074,Beijing,CN
|
||||
115.0.0.0,115.255.255.255,31.2304,121.4737,Shanghai,CN
|
||||
116.0.0.0,116.255.255.255,39.9042,116.4074,Beijing,CN
|
||||
117.0.0.0,117.255.255.255,31.2304,121.4737,Shanghai,CN
|
||||
118.0.0.0,118.255.255.255,39.9042,116.4074,Beijing,CN
|
||||
119.0.0.0,119.255.255.255,31.2304,121.4737,Shanghai,CN
|
||||
120.0.0.0,120.23.255.255,39.9042,116.4074,Beijing,CN
|
||||
120.24.0.0,120.27.255.255,39.9042,116.4074,Beijing,CN
|
||||
120.28.0.0,120.47.255.255,39.9042,116.4074,Beijing,CN
|
||||
120.48.0.0,120.49.255.255,39.9042,116.4074,Beijing,CN
|
||||
120.50.0.0,120.54.255.255,39.9042,116.4074,Beijing,CN
|
||||
120.55.0.0,120.55.255.255,31.2304,121.4737,Shanghai,CN
|
||||
120.56.0.0,120.75.255.255,39.9042,116.4074,Beijing,CN
|
||||
120.76.0.0,120.77.255.255,30.2741,120.1551,Hangzhou,CN
|
||||
120.78.0.0,120.79.255.255,22.5431,114.0579,Shenzhen,CN
|
||||
120.80.0.0,120.81.255.255,23.1291,113.2644,Guangzhou,CN
|
||||
120.82.0.0,120.255.255.255,39.9042,116.4074,Beijing,CN
|
||||
121.0.0.0,121.39.255.255,31.2304,121.4737,Shanghai,CN
|
||||
121.40.0.0,121.43.255.255,30.2741,120.1551,Hangzhou,CN
|
||||
121.44.0.0,121.195.255.255,31.2304,121.4737,Shanghai,CN
|
||||
121.196.0.0,121.199.255.255,30.2741,120.1551,Hangzhou,CN
|
||||
121.200.0.0,121.255.255.255,31.2304,121.4737,Shanghai,CN
|
||||
122.0.0.0,122.255.255.255,39.9042,116.4074,Beijing,CN
|
||||
123.0.0.0,123.255.255.255,31.2304,121.4737,Shanghai,CN
|
||||
124.0.0.0,124.255.255.255,39.9042,116.4074,Beijing,CN
|
||||
125.0.0.0,125.255.255.255,31.2304,121.4737,Shanghai,CN
|
||||
126.0.0.0,126.255.255.255,35.6762,139.6503,Tokyo,JP
|
||||
127.0.0.0,127.127.255.255,25.2048,55.2708,Dubai,AE
|
||||
127.128.0.0,127.255.255.255,13.7563,100.5018,Bangkok,TH
|
||||
128.0.0.0,128.255.255.255,48.8566,2.3522,Paris,FR
|
||||
129.0.0.0,129.255.255.255,48.8566,2.3522,Paris,FR
|
||||
130.0.0.0,130.255.255.255,52.52,13.405,Berlin,DE
|
||||
131.0.0.0,131.0.71.255,59.3293,18.0686,Stockholm,SE
|
||||
131.0.72.0,131.0.75.255,37.7749,-122.4194,San Francisco,US
|
||||
131.0.76.0,131.255.255.255,59.3293,18.0686,Stockholm,SE
|
||||
132.0.0.0,132.255.255.255,45.4642,9.19,Milan,IT
|
||||
133.0.0.0,133.255.255.255,35.6762,139.6503,Tokyo,JP
|
||||
134.0.0.0,134.255.255.255,52.52,13.405,Berlin,DE
|
||||
135.0.0.0,135.127.255.255,31.2304,121.4737,Shanghai,CN
|
||||
135.128.0.0,135.255.255.255,39.9042,116.4074,Beijing,CN
|
||||
136.0.0.0,136.127.255.255,35.6762,139.6503,Tokyo,JP
|
||||
136.128.0.0,136.255.255.255,37.5665,126.978,Seoul,KR
|
||||
137.0.0.0,137.255.255.255,48.8566,2.3522,Paris,FR
|
||||
138.0.0.0,138.90.255.255,45.4642,9.19,Milan,IT
|
||||
138.91.0.0,138.91.255.255,37.7749,-122.4194,San Francisco,US
|
||||
138.92.0.0,138.255.255.255,45.4642,9.19,Milan,IT
|
||||
139.0.0.0,139.195.255.255,23.1291,113.2644,Guangzhou,CN
|
||||
139.196.0.0,139.199.255.255,31.2304,121.4737,Shanghai,CN
|
||||
139.200.0.0,139.255.255.255,23.1291,113.2644,Guangzhou,CN
|
||||
140.0.0.0,140.127.255.255,1.3521,103.8198,Singapore,SG
|
||||
140.128.0.0,140.255.255.255,51.5074,-0.1278,London,GB
|
||||
141.0.0.0,141.101.63.255,52.52,13.405,Berlin,DE
|
||||
141.101.64.0,141.101.127.255,37.7749,-122.4194,San Francisco,US
|
||||
141.101.128.0,141.143.255.255,52.52,13.405,Berlin,DE
|
||||
141.144.0.0,141.159.255.255,32.7767,-96.797,Dallas,US
|
||||
141.160.0.0,141.255.255.255,52.52,13.405,Berlin,DE
|
||||
142.0.0.0,142.127.255.255,50.1109,8.6821,Frankfurt,DE
|
||||
142.128.0.0,142.255.255.255,40.7128,-74.006,New York,US
|
||||
143.0.0.0,143.255.255.255,45.4642,9.19,Milan,IT
|
||||
144.0.0.0,144.127.255.255,37.7749,-122.4194,San Francisco,US
|
||||
144.128.0.0,144.255.255.255,-33.8688,151.2093,Sydney,AU
|
||||
145.0.0.0,145.255.255.255,48.8566,2.3522,Paris,FR
|
||||
146.0.0.0,146.255.255.255,51.5074,-0.1278,London,GB
|
||||
147.0.0.0,147.255.255.255,48.8566,2.3522,Paris,FR
|
||||
148.0.0.0,148.127.255.255,19.076,72.8777,Mumbai,IN
|
||||
148.128.0.0,148.255.255.255,48.8566,2.3522,Paris,FR
|
||||
149.0.0.0,149.128.255.255,52.52,13.405,Berlin,DE
|
||||
149.129.0.0,149.129.255.255,1.3521,103.8198,Singapore,SG
|
||||
149.130.0.0,149.255.255.255,52.52,13.405,Berlin,DE
|
||||
150.0.0.0,150.191.255.255,35.6762,139.6503,Tokyo,JP
|
||||
150.192.0.0,150.255.255.255,40.4168,-3.7038,Madrid,ES
|
||||
151.0.0.0,151.100.255.255,45.4642,9.19,Milan,IT
|
||||
151.101.0.0,151.101.255.255,37.7749,-122.4194,San Francisco,US
|
||||
151.102.0.0,151.255.255.255,45.4642,9.19,Milan,IT
|
||||
152.0.0.0,152.127.255.255,52.3676,4.9041,Amsterdam,NL
|
||||
152.128.0.0,152.255.255.255,-23.5505,-46.6333,Sao Paulo,BR
|
||||
153.0.0.0,153.127.255.255,55.7558,37.6173,Moscow,RU
|
||||
153.128.0.0,153.191.255.255,34.6937,135.5022,Osaka,JP
|
||||
153.192.0.0,154.63.255.255,41.8781,-87.6298,Chicago,US
|
||||
154.64.0.0,154.191.255.255,32.7767,-96.797,Dallas,US
|
||||
154.192.0.0,154.255.255.255,33.749,-84.388,Atlanta,US
|
||||
155.0.0.0,155.255.255.255,59.3293,18.0686,Stockholm,SE
|
||||
156.0.0.0,156.255.255.255,48.8566,2.3522,Paris,FR
|
||||
157.0.0.0,157.52.63.255,35.6762,139.6503,Tokyo,JP
|
||||
157.52.64.0,157.52.127.255,37.7749,-122.4194,San Francisco,US
|
||||
157.52.128.0,157.52.255.255,37.7749,-122.4194,San Francisco,US
|
||||
157.53.0.0,157.174.255.255,35.6762,139.6503,Tokyo,JP
|
||||
157.175.0.0,157.175.255.255,26.0667,50.5577,Bahrain,BH
|
||||
157.176.0.0,157.255.255.255,35.6762,139.6503,Tokyo,JP
|
||||
158.0.0.0,158.255.255.255,59.3293,18.0686,Stockholm,SE
|
||||
159.0.0.0,159.255.255.255,48.8566,2.3522,Paris,FR
|
||||
160.0.0.0,160.255.255.255,34.6937,135.5022,Osaka,JP
|
||||
161.0.0.0,161.255.255.255,48.8566,2.3522,Paris,FR
|
||||
162.0.0.0,162.127.255.255,43.6532,-79.3832,Toronto,CA
|
||||
162.128.0.0,162.157.255.255,22.3193,114.1694,Hong Kong,HK
|
||||
162.158.0.0,162.159.255.255,37.7749,-122.4194,San Francisco,US
|
||||
162.160.0.0,162.175.255.255,25.033,121.5654,Taipei,TW
|
||||
162.176.0.0,162.191.255.255,40.4168,-3.7038,Madrid,ES
|
||||
162.192.0.0,162.207.255.255,45.4642,9.19,Milan,IT
|
||||
162.208.0.0,162.223.255.255,59.3293,18.0686,Stockholm,SE
|
||||
162.224.0.0,162.239.255.255,34.6937,135.5022,Osaka,JP
|
||||
162.240.0.0,162.255.255.255,23.1291,113.2644,Guangzhou,CN
|
||||
163.0.0.0,163.255.255.255,35.6762,139.6503,Tokyo,JP
|
||||
164.0.0.0,164.255.255.255,34.6937,135.5022,Osaka,JP
|
||||
165.0.0.0,165.127.255.255,22.5431,114.0579,Shenzhen,CN
|
||||
165.128.0.0,165.255.255.255,52.52,13.405,Berlin,DE
|
||||
166.0.0.0,166.127.255.255,25.2048,55.2708,Dubai,AE
|
||||
166.128.0.0,166.255.255.255,13.7563,100.5018,Bangkok,TH
|
||||
167.0.0.0,167.81.255.255,31.2304,121.4737,Shanghai,CN
|
||||
167.82.0.0,167.82.127.255,37.7749,-122.4194,San Francisco,US
|
||||
167.82.128.0,167.82.191.255,37.7749,-122.4194,San Francisco,US
|
||||
167.82.192.0,167.210.191.255,39.9042,116.4074,Beijing,CN
|
||||
167.210.192.0,168.82.191.255,35.6762,139.6503,Tokyo,JP
|
||||
168.82.192.0,168.210.191.255,37.5665,126.978,Seoul,KR
|
||||
168.210.192.0,169.82.191.255,1.3521,103.8198,Singapore,SG
|
||||
169.82.192.0,169.210.191.255,51.5074,-0.1278,London,GB
|
||||
169.210.192.0,170.82.191.255,50.1109,8.6821,Frankfurt,DE
|
||||
170.82.192.0,170.210.191.255,40.7128,-74.006,New York,US
|
||||
170.210.192.0,170.255.255.255,37.7749,-122.4194,San Francisco,US
|
||||
171.0.0.0,171.255.255.255,59.3293,18.0686,Stockholm,SE
|
||||
172.0.0.0,172.15.255.255,-33.8688,151.2093,Sydney,AU
|
||||
172.16.0.0,172.31.255.255,31.2304,121.4737,Shanghai,CN
|
||||
172.32.0.0,172.47.255.255,19.076,72.8777,Mumbai,IN
|
||||
172.48.0.0,172.63.255.255,48.8566,2.3522,Paris,FR
|
||||
172.64.0.0,172.71.255.255,37.7749,-122.4194,San Francisco,US
|
||||
172.72.0.0,172.87.255.255,52.3676,4.9041,Amsterdam,NL
|
||||
172.88.0.0,172.103.255.255,-23.5505,-46.6333,Sao Paulo,BR
|
||||
172.104.0.0,172.111.63.255,55.7558,37.6173,Moscow,RU
|
||||
172.111.64.0,172.111.127.255,37.7749,-122.4194,San Francisco,US
|
||||
172.111.128.0,172.239.127.255,41.8781,-87.6298,Chicago,US
|
||||
172.239.128.0,173.47.255.255,32.7767,-96.797,Dallas,US
|
||||
173.48.0.0,173.63.255.255,25.7617,-80.1918,Miami,US
|
||||
173.64.0.0,173.191.255.255,33.749,-84.388,Atlanta,US
|
||||
173.192.0.0,173.245.47.255,43.6532,-79.3832,Toronto,CA
|
||||
173.245.48.0,173.245.63.255,37.7749,-122.4194,San Francisco,US
|
||||
173.245.64.0,174.117.63.255,22.3193,114.1694,Hong Kong,HK
|
||||
174.117.64.0,174.245.63.255,25.033,121.5654,Taipei,TW
|
||||
174.245.64.0,175.117.63.255,40.4168,-3.7038,Madrid,ES
|
||||
175.117.64.0,175.245.63.255,45.4642,9.19,Milan,IT
|
||||
175.245.64.0,175.255.255.255,59.3293,18.0686,Stockholm,SE
|
||||
176.0.0.0,176.255.255.255,51.5074,-0.1278,London,GB
|
||||
177.0.0.0,177.127.255.255,34.6937,135.5022,Osaka,JP
|
||||
177.128.0.0,177.255.255.255,23.1291,113.2644,Guangzhou,CN
|
||||
178.0.0.0,178.127.255.255,22.5431,114.0579,Shenzhen,CN
|
||||
178.128.0.0,178.255.255.255,52.52,13.405,Berlin,DE
|
||||
179.0.0.0,179.127.255.255,25.2048,55.2708,Dubai,AE
|
||||
179.128.0.0,179.255.255.255,13.7563,100.5018,Bangkok,TH
|
||||
180.0.0.0,180.255.255.255,31.2304,121.4737,Shanghai,CN
|
||||
181.0.0.0,181.127.255.255,31.2304,121.4737,Shanghai,CN
|
||||
181.128.0.0,181.255.255.255,39.9042,116.4074,Beijing,CN
|
||||
182.0.0.0,182.91.255.255,39.9042,116.4074,Beijing,CN
|
||||
182.92.0.0,182.92.255.255,39.9042,116.4074,Beijing,CN
|
||||
182.93.0.0,182.255.255.255,39.9042,116.4074,Beijing,CN
|
||||
183.0.0.0,183.255.255.255,31.2304,121.4737,Shanghai,CN
|
||||
184.0.0.0,184.15.255.255,35.6762,139.6503,Tokyo,JP
|
||||
184.16.0.0,184.23.255.255,37.5665,126.978,Seoul,KR
|
||||
184.24.0.0,184.31.255.255,42.3736,-71.1097,Cambridge,US
|
||||
184.32.0.0,184.47.255.255,1.3521,103.8198,Singapore,SG
|
||||
184.48.0.0,184.63.255.255,51.5074,-0.1278,London,GB
|
||||
184.64.0.0,184.71.255.255,50.1109,8.6821,Frankfurt,DE
|
||||
184.72.0.0,184.73.255.255,37.7749,-122.4194,San Francisco,US
|
||||
184.74.0.0,184.83.255.255,40.7128,-74.006,New York,US
|
||||
184.84.0.0,184.87.255.255,42.3736,-71.1097,Cambridge,US
|
||||
184.88.0.0,184.215.255.255,37.7749,-122.4194,San Francisco,US
|
||||
184.216.0.0,184.255.255.255,-33.8688,151.2093,Sydney,AU
|
||||
185.0.0.0,185.31.15.255,51.5074,-0.1278,London,GB
|
||||
185.31.16.0,185.31.19.255,37.7749,-122.4194,San Francisco,US
|
||||
185.31.20.0,185.255.255.255,51.5074,-0.1278,London,GB
|
||||
186.0.0.0,186.127.255.255,19.076,72.8777,Mumbai,IN
|
||||
186.128.0.0,186.255.255.255,48.8566,2.3522,Paris,FR
|
||||
187.0.0.0,187.127.255.255,52.3676,4.9041,Amsterdam,NL
|
||||
187.128.0.0,187.255.255.255,-23.5505,-46.6333,Sao Paulo,BR
|
||||
188.0.0.0,188.114.95.255,51.5074,-0.1278,London,GB
|
||||
188.114.96.0,188.114.111.255,37.7749,-122.4194,San Francisco,US
|
||||
188.114.112.0,188.255.255.255,51.5074,-0.1278,London,GB
|
||||
189.0.0.0,189.127.255.255,55.7558,37.6173,Moscow,RU
|
||||
189.128.0.0,189.255.255.255,41.8781,-87.6298,Chicago,US
|
||||
190.0.0.0,190.93.239.255,32.7767,-96.797,Dallas,US
|
||||
190.93.240.0,190.93.255.255,37.7749,-122.4194,San Francisco,US
|
||||
190.94.0.0,190.221.255.255,33.749,-84.388,Atlanta,US
|
||||
190.222.0.0,191.93.255.255,43.6532,-79.3832,Toronto,CA
|
||||
191.94.0.0,191.221.255.255,22.3193,114.1694,Hong Kong,HK
|
||||
191.222.0.0,191.231.255.255,25.033,121.5654,Taipei,TW
|
||||
191.232.0.0,191.233.255.255,-23.5505,-46.6333,Sao Paulo,BR
|
||||
191.234.0.0,191.249.255.255,40.4168,-3.7038,Madrid,ES
|
||||
191.250.0.0,192.9.255.255,45.4642,9.19,Milan,IT
|
||||
192.10.0.0,192.25.255.255,59.3293,18.0686,Stockholm,SE
|
||||
192.26.0.0,192.41.255.255,34.6937,135.5022,Osaka,JP
|
||||
192.42.0.0,192.49.255.255,23.1291,113.2644,Guangzhou,CN
|
||||
192.50.0.0,192.50.255.255,35.1815,136.9066,Nagoya,JP
|
||||
192.51.0.0,192.66.255.255,22.5431,114.0579,Shenzhen,CN
|
||||
192.67.0.0,192.82.255.255,52.52,13.405,Berlin,DE
|
||||
192.83.0.0,192.98.255.255,25.2048,55.2708,Dubai,AE
|
||||
192.99.0.0,192.114.255.255,13.7563,100.5018,Bangkok,TH
|
||||
192.115.0.0,192.130.255.255,31.2304,121.4737,Shanghai,CN
|
||||
192.131.0.0,192.146.255.255,39.9042,116.4074,Beijing,CN
|
||||
192.147.0.0,192.162.255.255,35.6762,139.6503,Tokyo,JP
|
||||
192.163.0.0,192.167.255.255,37.5665,126.978,Seoul,KR
|
||||
192.168.0.0,192.168.255.255,23.1291,113.2644,Guangzhou,CN
|
||||
192.169.0.0,192.184.255.255,1.3521,103.8198,Singapore,SG
|
||||
192.185.0.0,192.200.255.255,51.5074,-0.1278,London,GB
|
||||
192.201.0.0,192.216.255.255,50.1109,8.6821,Frankfurt,DE
|
||||
192.217.0.0,192.232.255.255,40.7128,-74.006,New York,US
|
||||
192.233.0.0,192.248.255.255,37.7749,-122.4194,San Francisco,US
|
||||
192.249.0.0,192.255.255.255,-33.8688,151.2093,Sydney,AU
|
||||
193.0.0.0,193.255.255.255,52.52,13.405,Berlin,DE
|
||||
194.0.0.0,194.255.255.255,48.8566,2.3522,Paris,FR
|
||||
195.0.0.0,195.255.255.255,52.3676,4.9041,Amsterdam,NL
|
||||
196.0.0.0,196.127.255.255,19.076,72.8777,Mumbai,IN
|
||||
196.128.0.0,196.255.255.255,48.8566,2.3522,Paris,FR
|
||||
197.0.0.0,197.127.255.255,52.3676,4.9041,Amsterdam,NL
|
||||
197.128.0.0,197.234.239.255,-23.5505,-46.6333,Sao Paulo,BR
|
||||
197.234.240.0,197.234.243.255,37.7749,-122.4194,San Francisco,US
|
||||
197.234.244.0,197.250.243.255,55.7558,37.6173,Moscow,RU
|
||||
197.250.244.0,198.10.243.255,41.8781,-87.6298,Chicago,US
|
||||
198.10.244.0,198.26.243.255,32.7767,-96.797,Dallas,US
|
||||
198.26.244.0,198.41.127.255,33.749,-84.388,Atlanta,US
|
||||
198.41.128.0,198.41.255.255,37.7749,-122.4194,San Francisco,US
|
||||
198.42.0.0,198.169.255.255,43.6532,-79.3832,Toronto,CA
|
||||
198.170.0.0,199.27.71.255,22.3193,114.1694,Hong Kong,HK
|
||||
199.27.72.0,199.27.79.255,37.7749,-122.4194,San Francisco,US
|
||||
199.27.80.0,199.155.79.255,25.033,121.5654,Taipei,TW
|
||||
199.155.80.0,199.231.255.255,40.4168,-3.7038,Madrid,ES
|
||||
199.232.0.0,199.232.255.255,37.7749,-122.4194,San Francisco,US
|
||||
199.233.0.0,200.104.255.255,45.4642,9.19,Milan,IT
|
||||
200.105.0.0,200.232.255.255,59.3293,18.0686,Stockholm,SE
|
||||
200.233.0.0,201.104.255.255,34.6937,135.5022,Osaka,JP
|
||||
201.105.0.0,201.232.255.255,23.1291,113.2644,Guangzhou,CN
|
||||
201.233.0.0,201.255.255.255,22.5431,114.0579,Shenzhen,CN
|
||||
202.0.0.0,202.255.255.255,39.9042,116.4074,Beijing,CN
|
||||
203.0.0.0,203.255.255.255,39.9042,116.4074,Beijing,CN
|
||||
204.0.0.0,204.127.255.255,52.52,13.405,Berlin,DE
|
||||
204.128.0.0,204.255.255.255,25.2048,55.2708,Dubai,AE
|
||||
205.0.0.0,205.127.255.255,13.7563,100.5018,Bangkok,TH
|
||||
205.128.0.0,205.255.255.255,31.2304,121.4737,Shanghai,CN
|
||||
206.0.0.0,206.127.255.255,39.9042,116.4074,Beijing,CN
|
||||
206.128.0.0,206.255.255.255,35.6762,139.6503,Tokyo,JP
|
||||
207.0.0.0,207.127.255.255,37.5665,126.978,Seoul,KR
|
||||
207.128.0.0,207.255.255.255,1.3521,103.8198,Singapore,SG
|
||||
208.0.0.0,208.127.255.255,51.5074,-0.1278,London,GB
|
||||
208.128.0.0,208.255.255.255,50.1109,8.6821,Frankfurt,DE
|
||||
209.0.0.0,209.127.255.255,40.7128,-74.006,New York,US
|
||||
209.128.0.0,209.255.255.255,37.7749,-122.4194,San Francisco,US
|
||||
210.0.0.0,210.255.255.255,31.2304,121.4737,Shanghai,CN
|
||||
211.0.0.0,211.255.255.255,39.9042,116.4074,Beijing,CN
|
||||
212.0.0.0,212.255.255.255,51.5074,-0.1278,London,GB
|
||||
213.0.0.0,213.255.255.255,52.3676,4.9041,Amsterdam,NL
|
||||
214.0.0.0,214.127.255.255,-33.8688,151.2093,Sydney,AU
|
||||
214.128.0.0,214.255.255.255,19.076,72.8777,Mumbai,IN
|
||||
215.0.0.0,215.127.255.255,48.8566,2.3522,Paris,FR
|
||||
215.128.0.0,215.255.255.255,52.3676,4.9041,Amsterdam,NL
|
||||
216.0.0.0,216.127.255.255,-23.5505,-46.6333,Sao Paulo,BR
|
||||
216.128.0.0,216.255.255.255,55.7558,37.6173,Moscow,RU
|
||||
217.0.0.0,217.79.255.255,52.52,13.405,Berlin,DE
|
||||
217.80.0.0,217.95.255.255,48.1351,11.582,Munich,DE
|
||||
217.96.0.0,217.223.255.255,52.52,13.405,Berlin,DE
|
||||
217.224.0.0,217.255.255.255,50.1109,8.6821,Frankfurt,DE
|
||||
218.0.0.0,218.255.255.255,31.2304,121.4737,Shanghai,CN
|
||||
219.0.0.0,219.255.255.255,39.9042,116.4074,Beijing,CN
|
||||
220.0.0.0,220.255.255.255,23.1291,113.2644,Guangzhou,CN
|
||||
221.0.0.0,221.255.255.255,31.2304,121.4737,Shanghai,CN
|
||||
222.0.0.0,222.255.255.255,39.9042,116.4074,Beijing,CN
|
||||
223.0.0.0,223.255.255.255,39.9042,116.4074,Beijing,CN
|
||||
|
||||
Binary file not shown.
@@ -1,7 +1,30 @@
|
||||
@echo off
|
||||
set PYTHONUTF8=1
|
||||
cd /d "%~dp0"
|
||||
echo Starting TianXuan...
|
||||
start /B runtime\python\python.exe scripts/start_server.py
|
||||
timeout /t 3 >nul
|
||||
timeout /t 5 >nul
|
||||
|
||||
:: Start server in a separate window (detached, survives parent exit)
|
||||
:: /MIN runs minimized so it doesn't pop up a console window
|
||||
start "" /MIN runtime\python\python.exe scripts\start_server.py
|
||||
|
||||
:: Wait for PID file (up to 10 seconds)
|
||||
set WAIT=0
|
||||
:waitloop
|
||||
if exist .server_pid goto gotpid
|
||||
ping -n 2 127.0.0.1 >nul
|
||||
set /a WAIT+=1
|
||||
if %WAIT% lss 5 goto waitloop
|
||||
|
||||
:gotpid
|
||||
if exist .server_pid (
|
||||
set /p PID=<.server_pid
|
||||
)
|
||||
if defined PID (
|
||||
echo TianXuan started. PID: %PID%
|
||||
echo Visit: http://127.0.0.1:8000/
|
||||
echo.
|
||||
echo To stop: taskkill /F /PID %PID%
|
||||
) else (
|
||||
echo TianXuan started.
|
||||
echo Visit: http://127.0.0.1:8000/
|
||||
echo To stop: taskkill /F /IM python.exe
|
||||
)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Download latest GeoLite2-City.mmdb for offline GeoIP resolution.
|
||||
|
||||
Usage: runtime\python\python.exe scripts\download_geoip.py
|
||||
|
||||
Downloads the latest GeoLite2-City.mmdb from MaxMind's mirror (via
|
||||
python-geoip-geolite2) and copies it to a non-Chinese path so the
|
||||
maxminddb C extension can read it on Windows.
|
||||
|
||||
Requirements: python-geoip-geolite2, maxminddb
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
print('Installing/updating python-geoip-geolite2...')
|
||||
subprocess.check_call([
|
||||
sys.executable, '-m', 'pip', 'install', '--upgrade',
|
||||
'python-geoip-geolite2', 'maxminddb',
|
||||
])
|
||||
|
||||
import _geoip_geolite2
|
||||
src = Path(_geoip_geolite2.__file__).parent / 'GeoLite2-City.mmdb'
|
||||
dst = Path(os.environ.get('TEMP', 'C:/temp')) / 'opencode' / 'GeoLite2-City.mmdb'
|
||||
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(str(src), str(dst))
|
||||
print(f'Copied {src.name} ({src.stat().st_size / 1e6:.1f} MB)')
|
||||
print(f' to {dst}')
|
||||
print('GeoIP database ready.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
+30
-16
@@ -1,13 +1,10 @@
|
||||
"""Start the development server with host and port from config.yaml.
|
||||
|
||||
Reads server.host and server.port from config/config.yaml via the project's
|
||||
config loader, runs ``manage.py runserver <host>:<port>``, and opens a browser
|
||||
to http://127.0.0.1:<port> .
|
||||
|
||||
Usage (by run.bat):
|
||||
start /B runtime\\python\\python.exe scripts\\start_server.py
|
||||
All output (stdout + stderr) is redirected to log files so the process
|
||||
can run in the background without blocking any terminal.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import webbrowser
|
||||
@@ -22,21 +19,38 @@ from config import get_config
|
||||
|
||||
def main() -> None:
|
||||
cfg = get_config()
|
||||
|
||||
host = cfg.server.host or '0.0.0.0'
|
||||
port = cfg.server.port or 80
|
||||
|
||||
port = cfg.server.port or 8000
|
||||
addr = f'{host}:{port}'
|
||||
manage_py = Path(__file__).resolve().parent.parent / 'manage.py'
|
||||
manage_py = _project_root / 'manage.py'
|
||||
|
||||
# Open browser — use 127.0.0.1 regardless of bind address so the user's
|
||||
# browser always reaches the local machine.
|
||||
# Log files — capture ALL output so nothing goes to console
|
||||
stdout_log = _project_root / 'server_stdout.txt'
|
||||
stderr_log = _project_root / 'server_stderr.txt'
|
||||
pid_file = _project_root / '.server_pid'
|
||||
|
||||
# Write PID so run.bat can print it
|
||||
pid_file.write_text(str(os.getpid()), encoding='utf-8')
|
||||
|
||||
# Open browser
|
||||
webbrowser.open(f'http://127.0.0.1:{port}/')
|
||||
|
||||
# Run the server. subprocess.call is blocking, so this script stays alive
|
||||
# as long as the dev server does — the same effect as calling manage.py
|
||||
# directly under start /B.
|
||||
sys.exit(subprocess.call([sys.executable, str(manage_py), 'runserver', addr, '--noreload']))
|
||||
# Run server with ALL output redirected to files
|
||||
with open(stdout_log, 'a', encoding='utf-8') as out_fh, \
|
||||
open(stderr_log, 'a', encoding='utf-8') as err_fh:
|
||||
rc = subprocess.call(
|
||||
[sys.executable, str(manage_py), 'runserver', addr, '--noreload'],
|
||||
stdout=out_fh,
|
||||
stderr=err_fh,
|
||||
)
|
||||
|
||||
# Clean up PID file on exit
|
||||
try:
|
||||
pid_file.unlink(missing_ok=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
sys.exit(rc)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
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
|
||||
[23/Jul/2026 21:51:31] "GET /analyze/auto/ HTTP/1.1" 200 39486
|
||||
@@ -0,0 +1,65 @@
|
||||
Performing system checks...
|
||||
|
||||
System check identified no issues (0 silenced).
|
||||
July 23, 2026 - 13:06:51
|
||||
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/
|
||||
Quit the server with CTRL-BREAK.
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SimpleAnalysisConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'simple_analysis'
|
||||
label = 'simple_analysis'
|
||||
@@ -1,8 +0,0 @@
|
||||
/* 天璇 简单分析模块 — 前端辅助逻辑
|
||||
* 主要逻辑内嵌在 simple_analysis.html 中,
|
||||
* 此文件供未来扩展或拆分使用。
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
console.log('简单分析模块已加载');
|
||||
})();
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 696 B |
Binary file not shown.
|
Before Width: | Height: | Size: 2.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 618 B |
@@ -1,661 +0,0 @@
|
||||
/* required styles */
|
||||
|
||||
.leaflet-pane,
|
||||
.leaflet-tile,
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow,
|
||||
.leaflet-tile-container,
|
||||
.leaflet-pane > svg,
|
||||
.leaflet-pane > canvas,
|
||||
.leaflet-zoom-box,
|
||||
.leaflet-image-layer,
|
||||
.leaflet-layer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
.leaflet-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
.leaflet-tile,
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
/* Prevents IE11 from highlighting tiles in blue */
|
||||
.leaflet-tile::selection {
|
||||
background: transparent;
|
||||
}
|
||||
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
|
||||
.leaflet-safari .leaflet-tile {
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
}
|
||||
/* hack that prevents hw layers "stretching" when loading new tiles */
|
||||
.leaflet-safari .leaflet-tile-container {
|
||||
width: 1600px;
|
||||
height: 1600px;
|
||||
-webkit-transform-origin: 0 0;
|
||||
}
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow {
|
||||
display: block;
|
||||
}
|
||||
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
|
||||
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
|
||||
.leaflet-container .leaflet-overlay-pane svg {
|
||||
max-width: none !important;
|
||||
max-height: none !important;
|
||||
}
|
||||
.leaflet-container .leaflet-marker-pane img,
|
||||
.leaflet-container .leaflet-shadow-pane img,
|
||||
.leaflet-container .leaflet-tile-pane img,
|
||||
.leaflet-container img.leaflet-image-layer,
|
||||
.leaflet-container .leaflet-tile {
|
||||
max-width: none !important;
|
||||
max-height: none !important;
|
||||
width: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.leaflet-container img.leaflet-tile {
|
||||
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
|
||||
mix-blend-mode: plus-lighter;
|
||||
}
|
||||
|
||||
.leaflet-container.leaflet-touch-zoom {
|
||||
-ms-touch-action: pan-x pan-y;
|
||||
touch-action: pan-x pan-y;
|
||||
}
|
||||
.leaflet-container.leaflet-touch-drag {
|
||||
-ms-touch-action: pinch-zoom;
|
||||
/* Fallback for FF which doesn't support pinch-zoom */
|
||||
touch-action: none;
|
||||
touch-action: pinch-zoom;
|
||||
}
|
||||
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
|
||||
-ms-touch-action: none;
|
||||
touch-action: none;
|
||||
}
|
||||
.leaflet-container {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.leaflet-container a {
|
||||
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
|
||||
}
|
||||
.leaflet-tile {
|
||||
filter: inherit;
|
||||
visibility: hidden;
|
||||
}
|
||||
.leaflet-tile-loaded {
|
||||
visibility: inherit;
|
||||
}
|
||||
.leaflet-zoom-box {
|
||||
width: 0;
|
||||
height: 0;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
z-index: 800;
|
||||
}
|
||||
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
|
||||
.leaflet-overlay-pane svg {
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
.leaflet-pane { z-index: 400; }
|
||||
|
||||
.leaflet-tile-pane { z-index: 200; }
|
||||
.leaflet-overlay-pane { z-index: 400; }
|
||||
.leaflet-shadow-pane { z-index: 500; }
|
||||
.leaflet-marker-pane { z-index: 600; }
|
||||
.leaflet-tooltip-pane { z-index: 650; }
|
||||
.leaflet-popup-pane { z-index: 700; }
|
||||
|
||||
.leaflet-map-pane canvas { z-index: 100; }
|
||||
.leaflet-map-pane svg { z-index: 200; }
|
||||
|
||||
.leaflet-vml-shape {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
}
|
||||
.lvml {
|
||||
behavior: url(#default#VML);
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
|
||||
/* control positioning */
|
||||
|
||||
.leaflet-control {
|
||||
position: relative;
|
||||
z-index: 800;
|
||||
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||
pointer-events: auto;
|
||||
}
|
||||
.leaflet-top,
|
||||
.leaflet-bottom {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
.leaflet-top {
|
||||
top: 0;
|
||||
}
|
||||
.leaflet-right {
|
||||
right: 0;
|
||||
}
|
||||
.leaflet-bottom {
|
||||
bottom: 0;
|
||||
}
|
||||
.leaflet-left {
|
||||
left: 0;
|
||||
}
|
||||
.leaflet-control {
|
||||
float: left;
|
||||
clear: both;
|
||||
}
|
||||
.leaflet-right .leaflet-control {
|
||||
float: right;
|
||||
}
|
||||
.leaflet-top .leaflet-control {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.leaflet-bottom .leaflet-control {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.leaflet-left .leaflet-control {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.leaflet-right .leaflet-control {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
|
||||
/* zoom and fade animations */
|
||||
|
||||
.leaflet-fade-anim .leaflet-popup {
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 0.2s linear;
|
||||
-moz-transition: opacity 0.2s linear;
|
||||
transition: opacity 0.2s linear;
|
||||
}
|
||||
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
|
||||
opacity: 1;
|
||||
}
|
||||
.leaflet-zoom-animated {
|
||||
-webkit-transform-origin: 0 0;
|
||||
-ms-transform-origin: 0 0;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
svg.leaflet-zoom-animated {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.leaflet-zoom-anim .leaflet-zoom-animated {
|
||||
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
}
|
||||
.leaflet-zoom-anim .leaflet-tile,
|
||||
.leaflet-pan-anim .leaflet-tile {
|
||||
-webkit-transition: none;
|
||||
-moz-transition: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.leaflet-zoom-anim .leaflet-zoom-hide {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
|
||||
/* cursors */
|
||||
|
||||
.leaflet-interactive {
|
||||
cursor: pointer;
|
||||
}
|
||||
.leaflet-grab {
|
||||
cursor: -webkit-grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: grab;
|
||||
}
|
||||
.leaflet-crosshair,
|
||||
.leaflet-crosshair .leaflet-interactive {
|
||||
cursor: crosshair;
|
||||
}
|
||||
.leaflet-popup-pane,
|
||||
.leaflet-control {
|
||||
cursor: auto;
|
||||
}
|
||||
.leaflet-dragging .leaflet-grab,
|
||||
.leaflet-dragging .leaflet-grab .leaflet-interactive,
|
||||
.leaflet-dragging .leaflet-marker-draggable {
|
||||
cursor: move;
|
||||
cursor: -webkit-grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* marker & overlays interactivity */
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow,
|
||||
.leaflet-image-layer,
|
||||
.leaflet-pane > svg path,
|
||||
.leaflet-tile-container {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.leaflet-marker-icon.leaflet-interactive,
|
||||
.leaflet-image-layer.leaflet-interactive,
|
||||
.leaflet-pane > svg path.leaflet-interactive,
|
||||
svg.leaflet-image-layer.leaflet-interactive path {
|
||||
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* visual tweaks */
|
||||
|
||||
.leaflet-container {
|
||||
background: #ddd;
|
||||
outline-offset: 1px;
|
||||
}
|
||||
.leaflet-container a {
|
||||
color: #0078A8;
|
||||
}
|
||||
.leaflet-zoom-box {
|
||||
border: 2px dotted #38f;
|
||||
background: rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
|
||||
/* general typography */
|
||||
.leaflet-container {
|
||||
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||
font-size: 12px;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
|
||||
/* general toolbar styles */
|
||||
|
||||
.leaflet-bar {
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.leaflet-bar a {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #ccc;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
}
|
||||
.leaflet-bar a,
|
||||
.leaflet-control-layers-toggle {
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
display: block;
|
||||
}
|
||||
.leaflet-bar a:hover,
|
||||
.leaflet-bar a:focus {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
.leaflet-bar a:first-child {
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
.leaflet-bar a:last-child {
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom: none;
|
||||
}
|
||||
.leaflet-bar a.leaflet-disabled {
|
||||
cursor: default;
|
||||
background-color: #f4f4f4;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-bar a {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
.leaflet-touch .leaflet-bar a:first-child {
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
}
|
||||
.leaflet-touch .leaflet-bar a:last-child {
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
}
|
||||
|
||||
/* zoom control */
|
||||
|
||||
.leaflet-control-zoom-in,
|
||||
.leaflet-control-zoom-out {
|
||||
font: bold 18px 'Lucida Console', Monaco, monospace;
|
||||
text-indent: 1px;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
|
||||
/* layers control */
|
||||
|
||||
.leaflet-control-layers {
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
|
||||
background: #fff;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.leaflet-control-layers-toggle {
|
||||
background-image: url(images/layers.png);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
.leaflet-retina .leaflet-control-layers-toggle {
|
||||
background-image: url(images/layers-2x.png);
|
||||
background-size: 26px 26px;
|
||||
}
|
||||
.leaflet-touch .leaflet-control-layers-toggle {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
.leaflet-control-layers .leaflet-control-layers-list,
|
||||
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
|
||||
display: none;
|
||||
}
|
||||
.leaflet-control-layers-expanded .leaflet-control-layers-list {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
.leaflet-control-layers-expanded {
|
||||
padding: 6px 10px 6px 6px;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
}
|
||||
.leaflet-control-layers-scrollbar {
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
padding-right: 5px;
|
||||
}
|
||||
.leaflet-control-layers-selector {
|
||||
margin-top: 2px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
.leaflet-control-layers label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-size: 1.08333em;
|
||||
}
|
||||
.leaflet-control-layers-separator {
|
||||
height: 0;
|
||||
border-top: 1px solid #ddd;
|
||||
margin: 5px -10px 5px -6px;
|
||||
}
|
||||
|
||||
/* Default icon URLs */
|
||||
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
|
||||
background-image: url(images/marker-icon.png);
|
||||
}
|
||||
|
||||
|
||||
/* attribution and scale controls */
|
||||
|
||||
.leaflet-container .leaflet-control-attribution {
|
||||
background: #fff;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
margin: 0;
|
||||
}
|
||||
.leaflet-control-attribution,
|
||||
.leaflet-control-scale-line {
|
||||
padding: 0 5px;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.leaflet-control-attribution a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.leaflet-control-attribution a:hover,
|
||||
.leaflet-control-attribution a:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.leaflet-attribution-flag {
|
||||
display: inline !important;
|
||||
vertical-align: baseline !important;
|
||||
width: 1em;
|
||||
height: 0.6669em;
|
||||
}
|
||||
.leaflet-left .leaflet-control-scale {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.leaflet-bottom .leaflet-control-scale {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.leaflet-control-scale-line {
|
||||
border: 2px solid #777;
|
||||
border-top: none;
|
||||
line-height: 1.1;
|
||||
padding: 2px 5px 1px;
|
||||
white-space: nowrap;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
text-shadow: 1px 1px #fff;
|
||||
}
|
||||
.leaflet-control-scale-line:not(:first-child) {
|
||||
border-top: 2px solid #777;
|
||||
border-bottom: none;
|
||||
margin-top: -2px;
|
||||
}
|
||||
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
|
||||
border-bottom: 2px solid #777;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-control-attribution,
|
||||
.leaflet-touch .leaflet-control-layers,
|
||||
.leaflet-touch .leaflet-bar {
|
||||
box-shadow: none;
|
||||
}
|
||||
.leaflet-touch .leaflet-control-layers,
|
||||
.leaflet-touch .leaflet-bar {
|
||||
border: 2px solid rgba(0,0,0,0.2);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
|
||||
/* popup */
|
||||
|
||||
.leaflet-popup {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.leaflet-popup-content-wrapper {
|
||||
padding: 1px;
|
||||
text-align: left;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.leaflet-popup-content {
|
||||
margin: 13px 24px 13px 20px;
|
||||
line-height: 1.3;
|
||||
font-size: 13px;
|
||||
font-size: 1.08333em;
|
||||
min-height: 1px;
|
||||
}
|
||||
.leaflet-popup-content p {
|
||||
margin: 17px 0;
|
||||
margin: 1.3em 0;
|
||||
}
|
||||
.leaflet-popup-tip-container {
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
margin-top: -1px;
|
||||
margin-left: -20px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
.leaflet-popup-tip {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
padding: 1px;
|
||||
|
||||
margin: -10px auto 0;
|
||||
pointer-events: auto;
|
||||
|
||||
-webkit-transform: rotate(45deg);
|
||||
-moz-transform: rotate(45deg);
|
||||
-ms-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.leaflet-popup-content-wrapper,
|
||||
.leaflet-popup-tip {
|
||||
background: white;
|
||||
color: #333;
|
||||
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
|
||||
}
|
||||
.leaflet-container a.leaflet-popup-close-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
border: none;
|
||||
text-align: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font: 16px/24px Tahoma, Verdana, sans-serif;
|
||||
color: #757575;
|
||||
text-decoration: none;
|
||||
background: transparent;
|
||||
}
|
||||
.leaflet-container a.leaflet-popup-close-button:hover,
|
||||
.leaflet-container a.leaflet-popup-close-button:focus {
|
||||
color: #585858;
|
||||
}
|
||||
.leaflet-popup-scrolled {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.leaflet-oldie .leaflet-popup-content-wrapper {
|
||||
-ms-zoom: 1;
|
||||
}
|
||||
.leaflet-oldie .leaflet-popup-tip {
|
||||
width: 24px;
|
||||
margin: 0 auto;
|
||||
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
|
||||
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
|
||||
}
|
||||
|
||||
.leaflet-oldie .leaflet-control-zoom,
|
||||
.leaflet-oldie .leaflet-control-layers,
|
||||
.leaflet-oldie .leaflet-popup-content-wrapper,
|
||||
.leaflet-oldie .leaflet-popup-tip {
|
||||
border: 1px solid #999;
|
||||
}
|
||||
|
||||
|
||||
/* div icon */
|
||||
|
||||
.leaflet-div-icon {
|
||||
background: #fff;
|
||||
border: 1px solid #666;
|
||||
}
|
||||
|
||||
|
||||
/* Tooltip */
|
||||
/* Base styles for the element that has a tooltip */
|
||||
.leaflet-tooltip {
|
||||
position: absolute;
|
||||
padding: 6px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 3px;
|
||||
color: #222;
|
||||
white-space: nowrap;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
|
||||
}
|
||||
.leaflet-tooltip.leaflet-interactive {
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.leaflet-tooltip-top:before,
|
||||
.leaflet-tooltip-bottom:before,
|
||||
.leaflet-tooltip-left:before,
|
||||
.leaflet-tooltip-right:before {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
border: 6px solid transparent;
|
||||
background: transparent;
|
||||
content: "";
|
||||
}
|
||||
|
||||
/* Directions */
|
||||
|
||||
.leaflet-tooltip-bottom {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.leaflet-tooltip-top {
|
||||
margin-top: -6px;
|
||||
}
|
||||
.leaflet-tooltip-bottom:before,
|
||||
.leaflet-tooltip-top:before {
|
||||
left: 50%;
|
||||
margin-left: -6px;
|
||||
}
|
||||
.leaflet-tooltip-top:before {
|
||||
bottom: 0;
|
||||
margin-bottom: -12px;
|
||||
border-top-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-bottom:before {
|
||||
top: 0;
|
||||
margin-top: -12px;
|
||||
margin-left: -6px;
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-left {
|
||||
margin-left: -6px;
|
||||
}
|
||||
.leaflet-tooltip-right {
|
||||
margin-left: 6px;
|
||||
}
|
||||
.leaflet-tooltip-left:before,
|
||||
.leaflet-tooltip-right:before {
|
||||
top: 50%;
|
||||
margin-top: -6px;
|
||||
}
|
||||
.leaflet-tooltip-left:before {
|
||||
right: 0;
|
||||
margin-right: -12px;
|
||||
border-left-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-right:before {
|
||||
left: 0;
|
||||
margin-left: -12px;
|
||||
border-right-color: #fff;
|
||||
}
|
||||
|
||||
/* Printing */
|
||||
|
||||
@media print {
|
||||
/* Prevent printers from removing background-images of controls. */
|
||||
.leaflet-control {
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -1,11 +0,0 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'simple_analysis'
|
||||
urlpatterns = [
|
||||
path('', views.index, name='index'),
|
||||
path('upload/', views.upload_csv, name='upload'),
|
||||
path('filter/', views.filter_data, name='filter'),
|
||||
path('cluster/', views.run_clustering, name='cluster'),
|
||||
path('cluster/<int:label>/', views.cluster_detail, name='cluster_detail'),
|
||||
]
|
||||
@@ -1,796 +0,0 @@
|
||||
"""简单分析模块 — 独立的上传→筛选→聚类→地图可视化工作流。
|
||||
|
||||
不与天璇原有分析模块共享数据库或 session store,所有中间数据
|
||||
存储在模块级内存缓存(SimpleAnalysisCache)或临时目录中。
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import tempfile
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from collections import Counter
|
||||
|
||||
import numpy as np
|
||||
import polars as pl
|
||||
from django.conf import settings
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import render
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_POST, require_GET
|
||||
from sklearn.cluster import HDBSCAN
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
from sklearn.cluster import DBSCAN
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ── In-memory cache ──────────────────────────────────────────────────────────
|
||||
# Thread-safe for single-user desktop usage; each user session gets a unique key.
|
||||
_cache: dict[str, dict] = {}
|
||||
_TEMP_DIR = Path(settings.FILE_UPLOAD_TEMP_DIR) / 'simple_analysis'
|
||||
_TEMP_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def _new_id() -> str:
|
||||
return uuid.uuid4().hex[:12]
|
||||
|
||||
|
||||
def _cached(key: str) -> dict | None:
|
||||
return _cache.get(key)
|
||||
|
||||
|
||||
def _store(key: str, data: dict) -> None:
|
||||
_cache[key] = data
|
||||
|
||||
|
||||
# ── Column-name helpers ──────────────────────────────────────────────────────
|
||||
|
||||
def _norm(name: str) -> str:
|
||||
"""Normalise column name: lower-case, strip whitespace, replace hyphens."""
|
||||
return name.strip().lower().replace('-', '_').replace(' ', '_')
|
||||
|
||||
|
||||
def _find_col(col_names: list[str], *patterns: str) -> str | None:
|
||||
"""Find first column matching any pattern (exact or suffix after stripping)."""
|
||||
normed = {c: _norm(c) for c in col_names}
|
||||
for p in patterns:
|
||||
np_norm = _norm(p)
|
||||
for orig, n in normed.items():
|
||||
if n == np_norm or n.endswith('_' + np_norm) or n.startswith(np_norm + '_'):
|
||||
return orig
|
||||
return None
|
||||
|
||||
|
||||
# ── Step 1: Upload ───────────────────────────────────────────────────────────
|
||||
|
||||
@csrf_exempt
|
||||
@require_POST
|
||||
def upload_csv(request):
|
||||
"""Upload one or more CSV files, merge, return column info + cnam frequency."""
|
||||
files = request.FILES.getlist('files')
|
||||
if not files:
|
||||
return JsonResponse({'error': '未上传任何文件'}, status=400)
|
||||
|
||||
session_id = _new_id()
|
||||
upload_dir = _TEMP_DIR / session_id
|
||||
upload_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
saved_paths = []
|
||||
for f in files:
|
||||
dest = upload_dir / f.name
|
||||
with open(dest, 'wb') as out:
|
||||
for chunk in f.chunks():
|
||||
out.write(chunk)
|
||||
saved_paths.append(str(dest))
|
||||
|
||||
# Load & merge CSV files using Polars
|
||||
try:
|
||||
lf = pl.scan_csv(
|
||||
saved_paths[0] if len(saved_paths) == 1 else str(upload_dir / '*.csv'),
|
||||
infer_schema_length=10000,
|
||||
try_parse_dates=True,
|
||||
)
|
||||
if len(saved_paths) > 1:
|
||||
frames = [pl.scan_csv(p, infer_schema_length=10000) for p in saved_paths]
|
||||
lf = pl.concat(frames, how='diagonal_relaxed')
|
||||
|
||||
df = lf.collect(streaming=True)
|
||||
except Exception as e:
|
||||
shutil.rmtree(upload_dir, ignore_errors=True)
|
||||
return JsonResponse({'error': f'CSV 解析失败: {e}'}, status=400)
|
||||
|
||||
col_names = df.columns
|
||||
total_rows = len(df)
|
||||
|
||||
# Detect key columns — expanded patterns to match actual CSV column names
|
||||
cnam_col = _find_col(col_names, 'cnam', 'snam', 'class_name', 'category', 'target', 'name')
|
||||
cnrs_col = _find_col(col_names, 'cnrs', 'conn_success', 'result', '4srs')
|
||||
isrs_col = _find_col(col_names, 'isrs', 'is_resolved', 'resolved')
|
||||
src_lat_col = _find_col(col_names, ':ips.latd', 'src_latitude', 'src_lat', 'source_lat')
|
||||
src_lon_col = _find_col(col_names, ':ips.lond', 'src_longitude', 'src_lon', 'source_lon')
|
||||
dst_lat_col = _find_col(col_names, ':ipd.latd', 'dst_latitude', 'dst_lat', 'dest_lat')
|
||||
dst_lon_col = _find_col(col_names, ':ipd.lond', 'dst_longitude', 'dst_lon', 'dest_lon')
|
||||
ips_col = _find_col(col_names, ':ips', 'src_ip', 'source_ip', 'ip_src')
|
||||
ipd_col = _find_col(col_names, ':ipd', 'dst_ip', 'dest_ip', 'destination_ip', 'ip_dst')
|
||||
|
||||
# CNAM frequency
|
||||
cnam_freq = []
|
||||
top_cnam = None
|
||||
if cnam_col:
|
||||
try:
|
||||
cnam_series = df[cnam_col].cast(pl.Utf8).fill_null('')
|
||||
counts = Counter(cnam_series.to_list())
|
||||
total_cnam = sum(counts.values())
|
||||
for val, cnt in counts.most_common():
|
||||
cnam_freq.append({
|
||||
'value': val if val else '(空)',
|
||||
'count': cnt,
|
||||
'pct': round(cnt / total_cnam * 100, 1),
|
||||
})
|
||||
if cnam_freq:
|
||||
top_cnam = cnam_freq[0]
|
||||
except Exception as e:
|
||||
logger.warning('cnam frequency failed: %s', e)
|
||||
|
||||
# Store metadata in cache
|
||||
meta = {
|
||||
'session_id': session_id,
|
||||
'upload_dir': str(upload_dir),
|
||||
'total_rows': total_rows,
|
||||
'col_names': col_names,
|
||||
'cnam_col': cnam_col,
|
||||
'cnrs_col': cnrs_col,
|
||||
'isrs_col': isrs_col,
|
||||
'src_lat_col': src_lat_col,
|
||||
'src_lon_col': src_lon_col,
|
||||
'dst_lat_col': dst_lat_col,
|
||||
'dst_lon_col': dst_lon_col,
|
||||
'ips_col': ips_col,
|
||||
'ipd_col': ipd_col,
|
||||
}
|
||||
_store(session_id, {'meta': meta, 'df': df})
|
||||
|
||||
return JsonResponse({
|
||||
'session_id': session_id,
|
||||
'total_rows': total_rows,
|
||||
'columns': col_names[:50],
|
||||
'detected_columns': {
|
||||
'cnam': cnam_col,
|
||||
'cnrs': cnrs_col,
|
||||
'isrs': isrs_col,
|
||||
'src_lat': src_lat_col,
|
||||
'src_lon': src_lon_col,
|
||||
'dst_lat': dst_lat_col,
|
||||
'dst_lon': dst_lon_col,
|
||||
'ips': ips_col,
|
||||
'ipd': ipd_col,
|
||||
},
|
||||
'cnam_frequency': cnam_freq[:30],
|
||||
'top_cnam': top_cnam,
|
||||
})
|
||||
|
||||
|
||||
# ── Step 2: Filter ───────────────────────────────────────────────────────────
|
||||
|
||||
@csrf_exempt
|
||||
@require_POST
|
||||
def filter_data(request):
|
||||
"""Apply cnam + cnrs + isrs filters; return preview rows."""
|
||||
body = json.loads(request.body)
|
||||
session_id = body.get('session_id')
|
||||
selected_cnam = body.get('selected_cnam', '')
|
||||
|
||||
entry = _cached(session_id)
|
||||
if entry is None:
|
||||
return JsonResponse({'error': '会话已过期,请重新上传'}, status=400)
|
||||
|
||||
df: pl.DataFrame = entry['df']
|
||||
meta = entry['meta']
|
||||
cnam_col = meta['cnam_col']
|
||||
cnrs_col = meta['cnrs_col']
|
||||
isrs_col = meta['isrs_col']
|
||||
|
||||
# Validate required columns
|
||||
missing = []
|
||||
if not cnam_col:
|
||||
missing.append('cnam/snam (目标类别)')
|
||||
|
||||
if missing:
|
||||
return JsonResponse({
|
||||
'error': f'缺少关键列: {", ".join(missing)}。请检查CSV文件格式。',
|
||||
'missing_columns': missing,
|
||||
}, status=400)
|
||||
|
||||
# Apply filter — cnrs and isrs are optional; only apply if column exists
|
||||
try:
|
||||
cond = (pl.col(cnam_col).cast(pl.Utf8) == selected_cnam)
|
||||
if cnrs_col:
|
||||
cond = cond & (pl.col(cnrs_col).cast(pl.Utf8).str.strip_chars() == '+')
|
||||
if isrs_col:
|
||||
cond = cond & (pl.col(isrs_col).cast(pl.Utf8).str.strip_chars() == '+')
|
||||
filtered = df.filter(cond)
|
||||
except Exception as e:
|
||||
return JsonResponse({'error': f'筛选失败: {e}'}, status=400)
|
||||
|
||||
filtered_rows = len(filtered)
|
||||
meta['filtered_cnam'] = selected_cnam
|
||||
meta['filtered_rows'] = filtered_rows
|
||||
entry['filtered_df'] = filtered
|
||||
|
||||
# Preview data (first 20 rows)
|
||||
preview_cols = [c for c in filtered.columns if not c.startswith('_')][:30]
|
||||
preview_rows = []
|
||||
for row in filtered.head(20).select(preview_cols).iter_rows(named=True):
|
||||
clean = {}
|
||||
for k, v in row.items():
|
||||
if v is None or (isinstance(v, float) and np.isnan(v)):
|
||||
clean[k] = None
|
||||
else:
|
||||
clean[k] = str(v)[:120]
|
||||
preview_rows.append(clean)
|
||||
|
||||
return JsonResponse({
|
||||
'filtered_rows': filtered_rows,
|
||||
'preview_columns': preview_cols,
|
||||
'preview_rows': preview_rows,
|
||||
'total_before_filter': len(df),
|
||||
})
|
||||
|
||||
|
||||
# ── Step 3: Cluster ──────────────────────────────────────────────────────────
|
||||
|
||||
@csrf_exempt
|
||||
@require_POST
|
||||
def run_clustering(request):
|
||||
"""Run HDBSCAN geo-clustering or IP-subnet soft-clustering."""
|
||||
body = json.loads(request.body)
|
||||
session_id = body.get('session_id')
|
||||
mode = body.get('mode', 'geo') # 'geo' or 'subnet'
|
||||
min_cluster_size = int(body.get('min_cluster_size', 5))
|
||||
cluster_selection_epsilon = float(body.get('cluster_selection_epsilon', 0.005))
|
||||
min_records_per_subnet = int(body.get('min_records_per_subnet', 3))
|
||||
max_cluster_size = int(body.get('max_cluster_size', 100))
|
||||
|
||||
entry = _cached(session_id)
|
||||
if entry is None:
|
||||
return JsonResponse({'error': '会话已过期,请重新上传'}, status=400)
|
||||
|
||||
filtered = entry.get('filtered_df')
|
||||
if filtered is None:
|
||||
return JsonResponse({'error': '请先执行筛选(步骤二)'}, status=400)
|
||||
|
||||
meta = entry['meta']
|
||||
src_lat_col = meta['src_lat_col']
|
||||
src_lon_col = meta['src_lon_col']
|
||||
ips_col = meta['ips_col']
|
||||
|
||||
if mode == 'geo':
|
||||
result = _cluster_geo(entry, filtered, meta, min_cluster_size, cluster_selection_epsilon)
|
||||
else:
|
||||
result = _cluster_subnet(entry, filtered, meta, min_records_per_subnet, max_cluster_size)
|
||||
# Update cache with any modifications made by cluster functions
|
||||
_store(session_id, entry)
|
||||
return result
|
||||
|
||||
|
||||
def _cluster_geo(entry: dict, df: pl.DataFrame, meta: dict,
|
||||
min_cluster_size: int, cluster_selection_epsilon: float = 0.005):
|
||||
"""HDBSCAN clustering on source IP latitude/longitude using Haversine distance."""
|
||||
src_lat_col = meta['src_lat_col']
|
||||
src_lon_col = meta['src_lon_col']
|
||||
ips_col = meta['ips_col']
|
||||
|
||||
if not src_lat_col or not src_lon_col:
|
||||
return JsonResponse({'error': '缺少源经纬度列(:ips.latd / :ips.lond),无法执行地理聚类'}, status=400)
|
||||
|
||||
# Drop rows with missing lat/lon
|
||||
clean = df.filter(
|
||||
pl.col(src_lat_col).is_not_null()
|
||||
& pl.col(src_lon_col).is_not_null()
|
||||
)
|
||||
|
||||
if len(clean) == 0:
|
||||
return JsonResponse({'error': '所有记录均缺失经纬度,无法聚类'}, status=400)
|
||||
|
||||
# Clean lat/lon: cast to string, filter out '+'/'-' artifacts
|
||||
try:
|
||||
lat_raw = clean[src_lat_col].cast(pl.Utf8)
|
||||
lon_raw = clean[src_lon_col].cast(pl.Utf8)
|
||||
# Build validity mask: exclude '+', '-', empty, and None
|
||||
valid_mask = (
|
||||
lat_raw.is_not_null() & lon_raw.is_not_null()
|
||||
& (lat_raw != '+') & (lat_raw != '-')
|
||||
& (lon_raw != '+') & (lon_raw != '-')
|
||||
& (lat_raw != '') & (lon_raw != '')
|
||||
)
|
||||
clean = clean.filter(valid_mask)
|
||||
if len(clean) == 0:
|
||||
return JsonResponse({'error': '无有效经纬度记录(排除符号值后)'}, status=400)
|
||||
lat_vals = clean[src_lat_col].cast(pl.Float64, strict=False).to_numpy()
|
||||
lon_vals = clean[src_lon_col].cast(pl.Float64, strict=False).to_numpy()
|
||||
# Final NaN check
|
||||
lat_vals = np.where(np.isnan(lat_vals), 0.0, lat_vals)
|
||||
lon_vals = np.where(np.isnan(lon_vals), 0.0, lon_vals)
|
||||
except Exception as e:
|
||||
return JsonResponse({'error': f'经纬度列转换失败: {e}'}, status=400)
|
||||
|
||||
# Filter to valid lat/lon ranges
|
||||
valid_range_mask = (
|
||||
(lat_vals >= -90) & (lat_vals <= 90)
|
||||
& (lon_vals >= -180) & (lon_vals <= 180)
|
||||
& ~np.isnan(lat_vals) & ~np.isnan(lon_vals)
|
||||
)
|
||||
lat_vals = lat_vals[valid_range_mask]
|
||||
lon_vals = lon_vals[valid_range_mask]
|
||||
clean = clean.filter(
|
||||
pl.Series(valid_range_mask)
|
||||
)
|
||||
|
||||
if len(clean) < min_cluster_size:
|
||||
return JsonResponse({
|
||||
'error': f'有效经纬度记录数({len(clean)})少于最小聚类规模({min_cluster_size})',
|
||||
}, status=400)
|
||||
|
||||
# ── HDBSCAN with Haversine distance ────────────────────────────────────
|
||||
# Convert degrees to radians for haversine metric
|
||||
lat_rad = np.radians(lat_vals)
|
||||
lon_rad = np.radians(lon_vals)
|
||||
coords = np.column_stack([lat_rad, lon_rad])
|
||||
|
||||
clusterer = HDBSCAN(
|
||||
min_cluster_size=min_cluster_size,
|
||||
min_samples=min(3, min_cluster_size),
|
||||
metric='haversine',
|
||||
cluster_selection_epsilon=cluster_selection_epsilon,
|
||||
)
|
||||
labels = clusterer.fit_predict(coords)
|
||||
|
||||
n_clusters = int(len(set(labels)) - (1 if -1 in labels else 0))
|
||||
n_noise = int((labels == -1).sum())
|
||||
noise_ratio = n_noise / len(labels) if len(labels) > 0 else 0
|
||||
|
||||
# Check for degenerate clustering (fallback suggestion)
|
||||
fallback_suggested = False
|
||||
if noise_ratio > 0.8 or n_clusters <= 1:
|
||||
fallback_suggested = True
|
||||
|
||||
# Build cluster overview
|
||||
cluster_map = {}
|
||||
unique_labels = sorted(set(labels))
|
||||
data_points_by_cluster = {}
|
||||
for lbl in unique_labels:
|
||||
mask = labels == lbl
|
||||
idxs = np.where(mask)[0]
|
||||
cluster_lats = lat_vals[idxs]
|
||||
cluster_lons = lon_vals[idxs]
|
||||
center_lat = float(np.median(cluster_lats))
|
||||
center_lon = float(np.median(cluster_lons))
|
||||
|
||||
# Unique IPs
|
||||
if ips_col:
|
||||
ips_in_cluster = clean.filter(pl.Series(mask))[ips_col].cast(pl.Utf8).unique().to_list()
|
||||
n_unique_ips = len(ips_in_cluster)
|
||||
else:
|
||||
ips_in_cluster = []
|
||||
n_unique_ips = 0
|
||||
|
||||
cluster_map[int(lbl)] = {
|
||||
'label': int(lbl),
|
||||
'size': int(mask.sum()),
|
||||
'center_lat': round(float(center_lat), 4),
|
||||
'center_lon': round(float(center_lon), 4),
|
||||
'n_unique_ips': int(n_unique_ips),
|
||||
'is_noise': bool(lbl == -1),
|
||||
}
|
||||
|
||||
# Collect individual data points for map rendering (lat, lon, index)
|
||||
pts = []
|
||||
for pt_idx, i in enumerate(idxs):
|
||||
pts.append({
|
||||
'lat': round(float(cluster_lats[pt_idx]), 4),
|
||||
'lon': round(float(cluster_lons[pt_idx]), 4),
|
||||
'idx': int(i),
|
||||
})
|
||||
data_points_by_cluster[int(lbl)] = pts
|
||||
|
||||
# Annotate original filtered DataFrame with cluster labels
|
||||
label_series = np.full(len(df), -1, dtype=int)
|
||||
# Map back: only the valid rows got labels
|
||||
valid_indices = np.where(valid_range_mask)[0]
|
||||
for i, idx in enumerate(valid_indices):
|
||||
label_series[idx] = int(labels[i])
|
||||
|
||||
entry['cluster_labels'] = label_series.tolist()
|
||||
entry['cluster_map'] = cluster_map
|
||||
entry['cluster_mode'] = 'geo'
|
||||
entry['n_clusters'] = n_clusters
|
||||
entry['n_noise'] = n_noise
|
||||
|
||||
# Update filtered_df with cluster label column
|
||||
df_with_labels = df.with_columns(pl.Series('_cluster_label', label_series))
|
||||
entry['filtered_df'] = df_with_labels
|
||||
|
||||
return JsonResponse({
|
||||
'n_clusters': n_clusters,
|
||||
'n_noise': n_noise,
|
||||
'n_valid_points': len(clean),
|
||||
'clusters': cluster_map,
|
||||
'labels': label_series.tolist(),
|
||||
'data_points': data_points_by_cluster,
|
||||
'fallback_suggested': fallback_suggested,
|
||||
})
|
||||
|
||||
|
||||
def _cluster_subnet(entry: dict, df: pl.DataFrame, meta: dict,
|
||||
min_records_per_subnet: int = 3, max_cluster_size: int = 100):
|
||||
"""Soft clustering by /24 subnet prefix.
|
||||
|
||||
Subnets with fewer than *min_records_per_subnet* records are marked noise (-1).
|
||||
Subnets with more than *max_cluster_size* records are auto-split using geo
|
||||
sub-clustering (HDBSCAN + haversine) on the available lat/lon within the subnet.
|
||||
"""
|
||||
ips_col = meta['ips_col']
|
||||
src_lat_col = meta['src_lat_col']
|
||||
src_lon_col = meta['src_lon_col']
|
||||
if not ips_col:
|
||||
return JsonResponse({'error': '缺少源IP列(:ips),无法执行子网聚类'}, status=400)
|
||||
|
||||
try:
|
||||
ip_series = df[ips_col].cast(pl.Utf8)
|
||||
except Exception as e:
|
||||
return JsonResponse({'error': f'IP列转换失败: {e}'}, status=400)
|
||||
|
||||
# Extract /24 subnet
|
||||
def _subnet24(ip: str) -> str | None:
|
||||
if not ip or ip == 'None' or ip == '':
|
||||
return None
|
||||
parts = ip.split('.')
|
||||
if len(parts) < 4:
|
||||
return None
|
||||
return '.'.join(parts[:3]) + '.0/24'
|
||||
|
||||
subnets = [None if v is None else _subnet24(str(v)) for v in ip_series.to_list()]
|
||||
subnet_count = Counter(s for s in subnets if s is not None)
|
||||
|
||||
# Assign cluster labels based on subnet frequency
|
||||
labels = []
|
||||
cluster_map: dict[int, dict] = {}
|
||||
next_label = 0
|
||||
subnet_to_label: dict[str, int] = {}
|
||||
# Track which records belong to large subnets for geo splitting
|
||||
large_subnet_indices: dict[str, list[int]] = {}
|
||||
|
||||
for row_idx, s in enumerate(subnets):
|
||||
if s is None:
|
||||
labels.append(-1)
|
||||
else:
|
||||
cnt = subnet_count.get(s, 0)
|
||||
if cnt < min_records_per_subnet:
|
||||
labels.append(-1) # noise (too few records)
|
||||
elif cnt > max_cluster_size:
|
||||
# Large subnet → will apply geo sub-clustering later
|
||||
if s not in subnet_to_label:
|
||||
subnet_to_label[s] = next_label
|
||||
cluster_map[next_label] = {
|
||||
'label': next_label,
|
||||
'size': 0,
|
||||
'subnet': s,
|
||||
'is_noise': False,
|
||||
'_is_large': True,
|
||||
'_large_subnet_name': s,
|
||||
}
|
||||
next_label += 1
|
||||
large_subnet_indices[s] = []
|
||||
lbl = subnet_to_label[s]
|
||||
labels.append(lbl)
|
||||
cluster_map[lbl]['size'] = cluster_map[lbl].get('size', 0) + 1
|
||||
large_subnet_indices[s].append(row_idx)
|
||||
else:
|
||||
if s not in subnet_to_label:
|
||||
subnet_to_label[s] = next_label
|
||||
cluster_map[next_label] = {
|
||||
'label': next_label,
|
||||
'size': 0,
|
||||
'subnet': s,
|
||||
'is_noise': False,
|
||||
}
|
||||
next_label += 1
|
||||
lbl = subnet_to_label[s]
|
||||
labels.append(lbl)
|
||||
cluster_map[lbl]['size'] = cluster_map[lbl].get('size', 0) + 1
|
||||
|
||||
# ── Geo sub-clustering for large subnets ────────────────────────────────
|
||||
if large_subnet_indices and src_lat_col and src_lon_col:
|
||||
for subnet_name, indices in large_subnet_indices.items():
|
||||
old_label = subnet_to_label[subnet_name]
|
||||
# Remove the temporary large-subnet cluster entry
|
||||
cluster_map.pop(old_label, None)
|
||||
|
||||
# Get lat/lon for records in this subnet
|
||||
subnet_df = df[pl.Series(indices)]
|
||||
try:
|
||||
sub_lats = subnet_df[src_lat_col].cast(pl.Float64).to_numpy()
|
||||
sub_lons = subnet_df[src_lon_col].cast(pl.Float64).to_numpy()
|
||||
valid = (
|
||||
~np.isnan(sub_lats) & ~np.isnan(sub_lons)
|
||||
& (sub_lats >= -90) & (sub_lats <= 90)
|
||||
& (sub_lons >= -180) & (sub_lons <= 180)
|
||||
)
|
||||
sub_lats = sub_lats[valid]
|
||||
sub_lons = sub_lons[valid]
|
||||
if len(sub_lats) >= 3:
|
||||
# Run HDBSCAN with haversine on this subnet's points
|
||||
coords = np.column_stack([np.radians(sub_lats), np.radians(sub_lons)])
|
||||
sub_cluster = HDBSCAN(
|
||||
min_cluster_size=min(3, len(sub_lats) // 5),
|
||||
min_samples=2,
|
||||
metric='haversine',
|
||||
cluster_selection_epsilon=0.005,
|
||||
)
|
||||
sub_labels = sub_cluster.fit_predict(coords)
|
||||
|
||||
# Create sub-clusters
|
||||
sub_label_map: dict[int, int] = {}
|
||||
valid_idx_iter = 0
|
||||
for orig_rank, orig_idx in enumerate(indices):
|
||||
if not valid[orig_rank - valid_idx_iter]:
|
||||
# Was invalid, check if we skipped
|
||||
pass
|
||||
|
||||
# Re-map: skip invalid points (mark noise)
|
||||
valid_counter = 0
|
||||
for orig_rank, orig_idx in enumerate(indices):
|
||||
if not valid[orig_rank]:
|
||||
labels[orig_idx] = -1 # no lat/lon → noise
|
||||
else:
|
||||
sl = int(sub_labels[valid_counter])
|
||||
valid_counter += 1
|
||||
if sl == -1:
|
||||
labels[orig_idx] = -1
|
||||
else:
|
||||
sub_cluster_name = f'{subnet_name}/sub{sl}'
|
||||
if sub_cluster_name not in subnet_to_label:
|
||||
subnet_to_label[sub_cluster_name] = next_label
|
||||
cluster_map[next_label] = {
|
||||
'label': next_label,
|
||||
'size': 0,
|
||||
'subnet': sub_cluster_name,
|
||||
'is_noise': False,
|
||||
}
|
||||
next_label += 1
|
||||
new_lbl = subnet_to_label[sub_cluster_name]
|
||||
labels[orig_idx] = new_lbl
|
||||
cluster_map[new_lbl]['size'] = cluster_map[new_lbl].get('size', 0) + 1
|
||||
else:
|
||||
# Not enough geo data → keep as single cluster
|
||||
cluster_map[old_label] = {
|
||||
'label': old_label,
|
||||
'size': len(indices),
|
||||
'subnet': subnet_name,
|
||||
'is_noise': False,
|
||||
}
|
||||
subnet_to_label[subnet_name] = old_label
|
||||
except Exception as e:
|
||||
logger.warning('Subnet geo-splitting failed for %s: %s', subnet_name, e)
|
||||
# Keep the original cluster
|
||||
cluster_map[old_label] = {
|
||||
'label': old_label,
|
||||
'size': len(indices),
|
||||
'subnet': subnet_name,
|
||||
'is_noise': False,
|
||||
}
|
||||
subnet_to_label[subnet_name] = old_label
|
||||
|
||||
# Update cluster maps with centers and unique IP counts
|
||||
for lbl in list(cluster_map.keys()):
|
||||
mask = np.array(labels) == lbl
|
||||
ips_in_cluster = df.filter(pl.Series(mask))[ips_col].cast(pl.Utf8).unique().to_list()
|
||||
cluster_map[lbl]['n_unique_ips'] = len(ips_in_cluster)
|
||||
# Try to get geo center if lat/lon available
|
||||
if src_lat_col and src_lon_col:
|
||||
try:
|
||||
clat = df.filter(pl.Series(mask))[src_lat_col].cast(pl.Float64).drop_nulls()
|
||||
clon = df.filter(pl.Series(mask))[src_lon_col].cast(pl.Float64).drop_nulls()
|
||||
if len(clat) > 0 and len(clon) > 0:
|
||||
cluster_map[lbl]['center_lat'] = round(float(clat.median()), 4)
|
||||
cluster_map[lbl]['center_lon'] = round(float(clon.median()), 4)
|
||||
except Exception:
|
||||
cluster_map[lbl]['center_lat'] = None
|
||||
cluster_map[lbl]['center_lon'] = None
|
||||
else:
|
||||
cluster_map[lbl]['center_lat'] = None
|
||||
cluster_map[lbl]['center_lon'] = None
|
||||
|
||||
n_noise = labels.count(-1)
|
||||
n_clusters = len(cluster_map)
|
||||
|
||||
# Build data_points for map
|
||||
data_points_by_cluster: dict[int, list] = {}
|
||||
for lbl in list(cluster_map.keys()):
|
||||
mask = np.array(labels) == lbl
|
||||
pts = []
|
||||
if src_lat_col and src_lon_col:
|
||||
try:
|
||||
sub = df.filter(pl.Series(mask))
|
||||
lats = sub[src_lat_col].cast(pl.Float64).to_numpy()
|
||||
lons = sub[src_lon_col].cast(pl.Float64).to_numpy()
|
||||
for i in range(len(sub)):
|
||||
if not np.isnan(lats[i]) and not np.isnan(lons[i]):
|
||||
pts.append({'lat': round(float(lats[i]), 4),
|
||||
'lon': round(float(lons[i]), 4),
|
||||
'idx': int(i)})
|
||||
except Exception:
|
||||
pass
|
||||
data_points_by_cluster[lbl] = pts
|
||||
|
||||
# Also collect noisy points' coordinates
|
||||
noise_mask = np.array(labels) == -1
|
||||
noise_points = []
|
||||
if src_lat_col and src_lon_col:
|
||||
try:
|
||||
noise_sub = df.filter(pl.Series(noise_mask))
|
||||
nlats = noise_sub[src_lat_col].cast(pl.Float64).to_numpy()
|
||||
nlons = noise_sub[src_lon_col].cast(pl.Float64).to_numpy()
|
||||
for i in range(len(noise_sub)):
|
||||
if not np.isnan(nlats[i]) and not np.isnan(nlons[i]):
|
||||
noise_points.append({'lat': round(float(nlats[i]), 4),
|
||||
'lon': round(float(nlons[i]), 4),
|
||||
'idx': int(i)})
|
||||
except Exception:
|
||||
pass
|
||||
if noise_points:
|
||||
data_points_by_cluster[-1] = noise_points
|
||||
|
||||
# Annotate DataFrame
|
||||
df_with_labels = df.with_columns(pl.Series('_cluster_label', labels))
|
||||
entry['cluster_labels'] = labels
|
||||
entry['cluster_map'] = cluster_map
|
||||
entry['cluster_mode'] = 'subnet'
|
||||
entry['filtered_df'] = df_with_labels
|
||||
entry['n_clusters'] = n_clusters
|
||||
entry['n_noise'] = n_noise
|
||||
|
||||
return JsonResponse({
|
||||
'n_clusters': n_clusters,
|
||||
'n_noise': n_noise,
|
||||
'n_valid_points': len(df),
|
||||
'clusters': cluster_map,
|
||||
'labels': labels,
|
||||
'data_points': data_points_by_cluster,
|
||||
})
|
||||
|
||||
|
||||
# ── Step 4: Cluster detail ───────────────────────────────────────────────────
|
||||
|
||||
@require_GET
|
||||
def cluster_detail(request, label: int):
|
||||
"""Return detailed profile for a specific cluster."""
|
||||
session_id = request.GET.get('session_id')
|
||||
if not session_id:
|
||||
return JsonResponse({'error': '缺少 session_id'}, status=400)
|
||||
|
||||
entry = _cached(session_id)
|
||||
if entry is None:
|
||||
return JsonResponse({'error': '会话已过期'}, status=400)
|
||||
|
||||
df = entry.get('filtered_df')
|
||||
meta = entry.get('meta', {})
|
||||
cluster_map = entry.get('cluster_map', {})
|
||||
|
||||
if df is None:
|
||||
return JsonResponse({'error': '数据未就绪'}, status=400)
|
||||
|
||||
if '_cluster_label' not in df.columns:
|
||||
return JsonResponse({'error': '未执行聚类'}, status=400)
|
||||
|
||||
cluster_df = df.filter(pl.col('_cluster_label') == label)
|
||||
rows = len(cluster_df)
|
||||
if rows == 0:
|
||||
return JsonResponse({'error': f'簇 {label} 无数据'}, status=404)
|
||||
|
||||
cluster_info = cluster_map.get(label, {})
|
||||
|
||||
# Helper: get unique values for a column
|
||||
def _top_values(col_name: str, top_n: int = 5) -> list:
|
||||
if col_name not in df.columns:
|
||||
return []
|
||||
try:
|
||||
vals = (
|
||||
cluster_df[col_name]
|
||||
.cast(pl.Utf8)
|
||||
.fill_null('(空)')
|
||||
.value_counts()
|
||||
.sort('count', descending=True)
|
||||
.head(top_n)
|
||||
)
|
||||
total = vals['count'].sum()
|
||||
return [
|
||||
{'value': v['counts'] if isinstance(v, dict) else str(row[0]),
|
||||
'count': int(row[1]),
|
||||
'pct': round(int(row[1]) / total * 100, 1)}
|
||||
for row in vals.iter_rows()
|
||||
]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
# Source IPs
|
||||
ips_col = meta.get('ips_col')
|
||||
unique_ips = []
|
||||
if ips_col and ips_col in cluster_df.columns:
|
||||
try:
|
||||
ip_vals = cluster_df[ips_col].cast(pl.Utf8).unique().to_list()
|
||||
unique_ips = sorted(str(v) for v in ip_vals if v not in (None, '', 'None'))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Detect useful columns for profiling
|
||||
col_names = df.columns
|
||||
dst_ip_col = _find_col(col_names, ':ipd', 'dst_ip', 'dest_ip', 'destination_ip')
|
||||
dst_port_col = _find_col(col_names, 'dst_port', 'dest_port', 'port', 'dport')
|
||||
tls_ver_col = _find_col(col_names, '0ver', 'tls_version', 'version', 'tlsver')
|
||||
cipher_col = _find_col(col_names, '0cph', 'cipher_suite', 'cipher', 'tls_cipher')
|
||||
sni_col = _find_col(col_names, 'sni', 'server_name', 'tls_sni')
|
||||
bytes_col = _find_col(col_names, '8byt', 'bytes_sent', 'bytes', 'src_bytes')
|
||||
dur_col = _find_col(col_names, '4dur', 'duration', 'dur', 'flow_duration')
|
||||
proto_col = _find_col(col_names, 'proto', 'protocol', 'l4_proto')
|
||||
|
||||
profile = {
|
||||
'label': label,
|
||||
'size': rows,
|
||||
'cluster_info': cluster_info,
|
||||
'unique_ips': unique_ips[:100],
|
||||
'unique_ips_count': len(unique_ips),
|
||||
'top_dst_ips': _top_values(dst_ip_col) if dst_ip_col else [],
|
||||
'top_dst_ports': _top_values(dst_port_col) if dst_port_col else [],
|
||||
'top_tls_versions': _top_values(tls_ver_col) if tls_ver_col else [],
|
||||
'top_ciphers': _top_values(cipher_col) if cipher_col else [],
|
||||
'top_sni': _top_values(sni_col) if sni_col else [],
|
||||
'top_protocols': _top_values(proto_col) if proto_col else [],
|
||||
# Traffic stats
|
||||
'traffic': {},
|
||||
}
|
||||
|
||||
# Traffic stats
|
||||
if bytes_col and bytes_col in cluster_df.columns:
|
||||
try:
|
||||
total_bytes = cluster_df[bytes_col].cast(pl.Float64).sum()
|
||||
profile['traffic']['total_bytes'] = round(total_bytes, 2) if total_bytes else 0
|
||||
except Exception:
|
||||
pass
|
||||
if dur_col and dur_col in cluster_df.columns:
|
||||
try:
|
||||
total_dur = cluster_df[dur_col].cast(pl.Float64).sum()
|
||||
avg_dur = cluster_df[dur_col].cast(pl.Float64).mean()
|
||||
profile['traffic']['total_duration'] = round(total_dur, 2) if total_dur else 0
|
||||
profile['traffic']['avg_duration'] = round(avg_dur, 2) if avg_dur else 0
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Geo bounding box (if lat/lon available)
|
||||
src_lat_col = meta.get('src_lat_col')
|
||||
src_lon_col = meta.get('src_lon_col')
|
||||
if src_lat_col and src_lon_col and src_lat_col in cluster_df.columns:
|
||||
try:
|
||||
lats = cluster_df[src_lat_col].cast(pl.Float64).drop_nulls()
|
||||
lons = cluster_df[src_lon_col].cast(pl.Float64).drop_nulls()
|
||||
if len(lats) > 0 and len(lons) > 0:
|
||||
profile['geo_bounds'] = {
|
||||
'lat_min': round(float(lats.min()), 4),
|
||||
'lat_max': round(float(lats.max()), 4),
|
||||
'lon_min': round(float(lons.min()), 4),
|
||||
'lon_max': round(float(lons.max()), 4),
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return JsonResponse(profile)
|
||||
|
||||
|
||||
# ── Main page ────────────────────────────────────────────────────────────────
|
||||
|
||||
def index(request):
|
||||
"""Render the simple analysis workbench page."""
|
||||
return render(request, 'simple_analysis/simple_analysis.html')
|
||||
@@ -1,62 +1,120 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Cluster #{{ cluster.cluster_label }} - Run #{{ run.display_id }}{% endblock %}
|
||||
{% load static %}
|
||||
{% block title %}Cluster #{{ cluster.cluster_label }} — Run #{{ run.display_id }}{% endblock %}
|
||||
{% block content %}
|
||||
<style>
|
||||
.entity-card { border:1px solid #e0e0e0; border-radius:6px; padding:0.6rem; margin-bottom:0.4rem; background:#fafafa; }
|
||||
.entity-card .val { font-weight:600; font-size:0.85rem; }
|
||||
.entity-card .feat { font-size:0.75rem; color:#666; display:inline-block; margin-right:0.5rem; }
|
||||
.feat-positive { color:#2e7d32; }
|
||||
.feat-negative { color:#c62828; }
|
||||
.feat-table { width:100%; border-collapse:collapse; font-size:0.8rem; }
|
||||
.feat-table th { background:#f5f5f5; padding:0.3rem 0.5rem; text-align:left; font-weight:600; border-bottom:2px solid #ddd; }
|
||||
.feat-table td { padding:0.25rem 0.5rem; border-bottom:1px solid #eee; }
|
||||
</style>
|
||||
|
||||
<div class="card">
|
||||
<a href="{% url 'analysis:cluster_overview' run.display_id %}" style="color:#4361ee;text-decoration:none;font-size:0.9rem;">← Back to overview</a>
|
||||
<h2 style="margin-top:0.5rem;">Cluster #{{ cluster.cluster_label }}</h2>
|
||||
<p>{{ cluster.size }} entities ({{ cluster.proportion|floatformat:2 }}% of total)</p>
|
||||
<p>Silhouette Score: {{ cluster.silhouette_score|floatformat:4|default:"-" }}</p>
|
||||
<a href="{% url 'analysis:cluster_overview' run.display_id %}" style="color:#4361ee;text-decoration:none;font-size:0.9rem;">← Cluster Overview</a>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:0.5rem;margin-top:0.5rem;">
|
||||
<div>
|
||||
<h2 style="margin:0;">Cluster #{{ cluster.cluster_label }}
|
||||
{% if cluster.cluster_label == -1 %}<span class="badge badge-warning">Noise</span>{% endif %}
|
||||
</h2>
|
||||
<p style="margin:0.25rem 0 0;color:#666;font-size:0.9rem;">
|
||||
{{ cluster.size }} entities
|
||||
{% if cluster.proportion %} — {{ cluster.proportion|floatformat:1 }}% of total{% endif %}
|
||||
{% if cluster.silhouette_score is not None %} — Silhouette: {{ cluster.silhouette_score|floatformat:4 }}{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% if nl_summary %}
|
||||
<div style="background:#f8f9fa;border-left:3px solid #4361ee;padding:0.6rem;margin:0.75rem 0;border-radius:0 4px 4px 0;font-size:0.85rem;line-height:1.6;color:#333;">{{ nl_summary }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="grid-2">
|
||||
<div class="card">
|
||||
<h2>Feature Summary (top 50)</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Feature</th>
|
||||
<th>Mean</th>
|
||||
<th>Std</th>
|
||||
<th>Median</th>
|
||||
<th>Dist. Score</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for f in features %}
|
||||
<tr>
|
||||
<td><code>{{ f.feature_name }}</code></td>
|
||||
<td>{{ f.mean|floatformat:3|default:"-" }}</td>
|
||||
<td>{{ f.std|floatformat:3|default:"-" }}</td>
|
||||
<td>{{ f.median|floatformat:3|default:"-" }}</td>
|
||||
<td>{{ f.distinguishing_score|floatformat:3|default:"-" }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="5">No features</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- ── Feature Analysis ── -->
|
||||
<div class="card">
|
||||
<h3>📊 Feature Analysis <span style="font-weight:400;font-size:0.8rem;color:#888;">{{ features|length }} features</span></h3>
|
||||
{% if features %}
|
||||
<table class="feat-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Feature</th>
|
||||
<th>Dist. Score</th>
|
||||
<th>Mean</th>
|
||||
<th>Std</th>
|
||||
<th>Median</th>
|
||||
<th>P25</th>
|
||||
<th>P75</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for f in features %}
|
||||
<tr>
|
||||
<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>{{ 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>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="empty-state"><p>No feature data for this cluster.</p></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Entities (top 50)</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Entity</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for e in entities %}
|
||||
<tr>
|
||||
<td><code>{{ e.entity_value }}</code></td>
|
||||
<td><a href="{% url 'analysis:entity_profile' e.id %}" class="btn btn-primary">Profile</a></td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="2">No entities</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- ── SVD Singular Values ── -->
|
||||
{% if cluster.cluster_label != -1 %}
|
||||
{# SVD-extracted features (from _run_clustering_pipeline cluster_svd_extract) have higher distinguishing scores #}
|
||||
{% with svd_features=features|slice:":5" %}
|
||||
{% if svd_features %}
|
||||
<div class="card">
|
||||
<h3>🔬 SVD Key Features <span style="font-weight:400;font-size:0.8rem;color:#888;">Top 5 by distinguishing score</span></h3>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.5rem;">
|
||||
{% for f in svd_features %}
|
||||
<div style="background:#f8f9fa;border-radius:6px;padding:0.5rem;border:1px solid #e0e0e0;">
|
||||
<div style="font-weight:600;font-size:0.8rem;">{{ f.feature_name }}</div>
|
||||
<div style="font-size:0.75rem;color:#666;margin-top:0.2rem;">
|
||||
Score: <span class="{% if f.distinguishing_score > 0 %}feat-positive{% else %}feat-negative{% endif %}">{{ f.distinguishing_score|floatformat:2 }}</span>
|
||||
| Mean: {{ f.mean|floatformat:2 }}
|
||||
| Std: {{ f.std|floatformat:2 }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
<!-- ── Entity List ── -->
|
||||
<div class="card">
|
||||
<h3>👤 Entities <span style="font-weight:400;font-size:0.8rem;color:#888;">{{ entities|length }} shown</span></h3>
|
||||
<div id="entityList">
|
||||
{% for e in entities %}
|
||||
<div class="entity-card">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<span class="val">{{ e.entity_value }}</span>
|
||||
<a href="{% url 'analysis:entity_profile' e.id %}" class="btn btn-sm" style="font-size:0.75rem;padding:0.2rem 0.6rem;background:#4361ee;color:#fff;text-decoration:none;border-radius:4px;">Profile</a>
|
||||
</div>
|
||||
{% if e.feature_json %}
|
||||
<div style="margin-top:0.3rem;">
|
||||
{% for k, v in e.feature_json.items|slice:":8" %}
|
||||
<span class="feat"><strong>{{ k }}</strong>: {% if v is None %}-{% else %}{{ v|floatformat:2 }}{% endif %}</span>
|
||||
{% endfor %}
|
||||
{% if e.feature_json.items|length > 8 %}<span class="feat" style="color:#999;">+{{ e.feature_json.items|length|add:"-8" }} more</span>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="empty-state"><p>No entities in this cluster.</p></div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
File diff suppressed because it is too large
Load Diff
+30
-1
@@ -81,7 +81,6 @@
|
||||
<a href="{% url 'analysis:run_list' %}">运行记录</a>
|
||||
<a href="{% url 'analysis:globe' %}">态势</a>
|
||||
<a href="{% url 'analysis:config' %}">配置</a>
|
||||
<a href="{% url 'simple_analysis:index' %}" style="color:#f4a261;font-weight:600;">简单分析</a>
|
||||
</nav>
|
||||
<div class="container">
|
||||
{% block content %}{% endblock %}
|
||||
@@ -123,6 +122,36 @@
|
||||
const msg = event.reason && event.reason.message ? event.reason.message : String(event.reason || 'Unknown error');
|
||||
showError('未处理的错误: ' + msg);
|
||||
});
|
||||
|
||||
// ── Server heartbeat: detect when Django crashes ──
|
||||
(function() {
|
||||
var heartbeatBanner = null;
|
||||
|
||||
function checkServer() {
|
||||
fetch('/', { method: 'HEAD', cache: 'no-store' })
|
||||
.then(function() {
|
||||
if (heartbeatBanner) {
|
||||
heartbeatBanner.remove();
|
||||
heartbeatBanner = null;
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
if (!heartbeatBanner) {
|
||||
heartbeatBanner = document.createElement('div');
|
||||
heartbeatBanner.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:10000;background:#dc3545;color:#fff;text-align:center;padding:0.5rem;font-size:0.85rem;font-weight:600;';
|
||||
heartbeatBanner.textContent = '⚠️ 服务器连接断开 — Django 进程可能已崩溃。请重启应用。';
|
||||
document.body.prepend(heartbeatBanner);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check every 30 seconds
|
||||
setInterval(checkServer, 30000);
|
||||
// Also check when the page becomes visible again
|
||||
document.addEventListener('visibilitychange', function() {
|
||||
if (!document.hidden) checkServer();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+351
-40
@@ -148,6 +148,39 @@
|
||||
<button class="btn btn-primary" style="margin-top:1rem;" id="startAutoBtn" onclick="startAuto()" {% if not llm_configured %}disabled{% endif %}>开始自动分析</button>
|
||||
</div>
|
||||
|
||||
<!-- Filter builder card -->
|
||||
<div class="card" id="filterCard" style="background:#f0f4ff;border-color:#c8d6e5;">
|
||||
<h3>🔍 数据筛选(可选)</h3>
|
||||
<div id="filterNoDataset" style="text-align:center;color:#999;padding:1rem;font-size:0.9rem;">选择数据集后可用</div>
|
||||
<div id="filterRows" style="display:none;">
|
||||
<div class="filter-row" data-idx="0" style="display:flex;gap:0.5rem;align-items:center;margin-bottom:0.4rem;">
|
||||
<select class="filter-col" style="flex:1;padding:0.35rem;border:1px solid #ccc;border-radius:4px;"><option value="">-- 列 --</option></select>
|
||||
<select class="filter-op" style="width:130px;padding:0.35rem;border:1px solid #ccc;border-radius:4px;">
|
||||
<option value="eq">==</option><option value="neq">!=</option>
|
||||
<option value="gt">></option><option value="lt"><</option>
|
||||
<option value="gte">>=</option><option value="lte"><=</option>
|
||||
<option value="contains">contains</option>
|
||||
<option value="is_null">is_null</option>
|
||||
<option value="not_null">not_null</option>
|
||||
<option value="in">in</option>
|
||||
</select>
|
||||
<input class="filter-val" placeholder="值" style="flex:1;padding:0.35rem;border:1px solid #ccc;border-radius:4px;">
|
||||
<button class="btn" onclick="removeFilterRow(this)" style="background:#dc3545;color:#fff;font-size:0.8rem;padding:0.2rem 0.5rem;display:none;">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="filterControls" style="display:none;margin:0.5rem 0;gap:0.5rem;align-items:center;">
|
||||
<label style="font-size:0.85rem;white-space:nowrap;">逻辑:</label>
|
||||
<select id="filterLogic" style="padding:0.35rem;border:1px solid #ccc;border-radius:4px;">
|
||||
<option value="and">AND</option><option value="or">OR</option>
|
||||
</select>
|
||||
<button class="btn" onclick="addFilterRow()" style="font-size:0.8rem;background:#e8f0fe;color:#4361ee;">+ 添加条件</button>
|
||||
<button class="btn" onclick="resetFilter()" style="font-size:0.8rem;background:#fce4ec;color:#c62828;">重置</button>
|
||||
<button class="btn btn-primary" onclick="applyFilter()" style="margin-left:auto;">应用筛选</button>
|
||||
</div>
|
||||
<div id="filterResult" style="display:none;margin-top:0.5rem;padding:0.5rem;background:#e8f5e9;border-radius:4px;font-size:0.85rem;"></div>
|
||||
<div id="activeFilterInfo" style="display:none;margin-top:0.5rem;padding:0.35rem 0.5rem;background:#fff3cd;border-radius:4px;font-size:0.8rem;color:#856404;"></div>
|
||||
</div>
|
||||
|
||||
<div id="autoProgress" style="display:none;margin-top:1rem;">
|
||||
<p>LLM 编排中... <span id="autoStatus"></span></p>
|
||||
<div style="background:#eee;height:8px;border-radius:4px;overflow:hidden;">
|
||||
@@ -171,16 +204,27 @@
|
||||
const DATASETS_WITH_STATUS = {{ datasets_with_status_json|safe }};
|
||||
let activeFilter = 'all';
|
||||
let selectedRunId = null;
|
||||
let activeFilteredDatasetId = null;
|
||||
let currentFilters = [];
|
||||
let currentFilterLogic = 'and';
|
||||
|
||||
// ── Build column map from session datasets ──
|
||||
const DATASET_COLUMNS = {};
|
||||
DATASETS_WITH_STATUS.forEach(ds => {
|
||||
if (ds.columns && ds.columns.length > 0) {
|
||||
DATASET_COLUMNS[ds.dataset_id] = ds.columns;
|
||||
}
|
||||
});
|
||||
|
||||
// ── Dataset table rendering ──
|
||||
function buildDatasetTable(filter) {
|
||||
const container = document.getElementById('datasetTable');
|
||||
let rows = DATASETS_WITH_STATUS.filter(ds => {
|
||||
if (filter === 'all') return true;
|
||||
if (filter === 'ready') return ds.run_status === 'ready' || (!ds.run_status || ds.run_status === 'ready');
|
||||
if (filter === 'analyzing') return ['pending', 'loading', 'profiling', 'aggregating', 'clustering', 'extracting'].includes(ds.run_status);
|
||||
if (filter === 'completed') return ds.run_status === 'completed';
|
||||
if (filter === 'failed') return ds.run_status === 'failed';
|
||||
if (filter === 'ready') return ds.badge_class === 'ready';
|
||||
if (filter === 'analyzing') return ds.badge_class === 'analyzing';
|
||||
if (filter === 'completed') return ds.badge_class === 'completed';
|
||||
if (filter === 'failed') return ds.badge_class === 'failed';
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -192,9 +236,219 @@ function buildDatasetTable(filter) {
|
||||
let html = '<table class="ds-table"><thead><tr><th></th><th>ID</th><th>状态</th><th>行数</th><th>文件数</th><th>来源</th></tr></thead><tbody>';
|
||||
rows.forEach(ds => {
|
||||
const label = ds.status_label || '待分析';
|
||||
const badgeClass = (ds.run_status === 'completed') ? 'completed' :
|
||||
(ds.run_status === 'failed') ? 'failed' :
|
||||
(ds.run_status && ds.run_status !== 'ready') ? 'analyzing' : 'ready';
|
||||
const badgeClass = ds.badge_class || 'ready';
|
||||
const dispId = ds.display_id || ds.dataset_id;
|
||||
const rowCount = ds.row_count != null ? ds.row_count.toLocaleString() : '?';
|
||||
const fileCount = ds.file_count != null ? ds.file_count : '?';
|
||||
const source = ds.sqlite_table ? ('SQLite: ' + ds.sqlite_table) : (ds.csv_glob || '?');
|
||||
const checked = (selectedRunId === String(ds.dataset_id)) ? 'checked' : '';
|
||||
const reloadIndicator = ds.needs_reload ? ' <span style="color:#e65100;font-size:0.7rem;">(需加载)</span>' : '';
|
||||
html += '<tr data-ds-id="' + ds.dataset_id + '" onclick="selectRow(\'' + ds.dataset_id + '\')" class="' + (selectedRunId === String(ds.dataset_id) ? 'selected' : '') + '">' +
|
||||
'<td><input type="radio" name="auto_run_id" value="' + ds.dataset_id + '" ' + checked + ' onclick="event.stopPropagation();selectRow(\'' + ds.dataset_id + '\')"></td>' +
|
||||
'<td style="font-weight:600;">#' + dispId + '</td>' +
|
||||
'<td><span class="status-badge ' + badgeClass + '">' + label + reloadIndicator + '</span></td>' +
|
||||
'<td>' + rowCount + '</td>' +
|
||||
'<td>' + fileCount + '</td>' +
|
||||
'<td style="max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' + escapeHtml(source) + '</td>' +
|
||||
'</tr>';
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function selectRow(dsId) {
|
||||
selectedRunId = String(dsId);
|
||||
// Update radio buttons and row highlight
|
||||
document.querySelectorAll('#datasetTable input[name="auto_run_id"]').forEach(r => {
|
||||
r.checked = (r.value === String(dsId));
|
||||
});
|
||||
document.querySelectorAll('#datasetTable tr').forEach(tr => tr.classList.remove('selected'));
|
||||
const row = document.querySelector('#datasetTable tr[data-ds-id="' + dsId + '"]');
|
||||
if (row) row.classList.add('selected');
|
||||
|
||||
// Check if data needs reload
|
||||
const dsInfo = DATASETS_WITH_STATUS.find(ds => ds.dataset_id === dsId);
|
||||
if (dsInfo && dsInfo.needs_reload) {
|
||||
// Auto-reload data
|
||||
if (row) row.style.opacity = '0.6';
|
||||
fetch('/tools/reload-run/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type':'application/json', 'X-CSRFToken':'{{ csrf_token }}' },
|
||||
body: JSON.stringify({ display_id: dsInfo.display_id || dsInfo.dataset_id }),
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
showError('加载数据集失败: ' + data.error);
|
||||
if (row) row.style.opacity = '1';
|
||||
return;
|
||||
}
|
||||
dsInfo.needs_reload = false;
|
||||
if (data.columns) {
|
||||
dsInfo.columns = data.columns;
|
||||
DATASET_COLUMNS[dsId] = data.columns;
|
||||
}
|
||||
if (row) row.style.opacity = '1';
|
||||
// Rebuild table to remove reload indicator
|
||||
buildDatasetTable(activeFilter);
|
||||
selectRow(dsId);
|
||||
showSuccess('数据集已加载: ' + dsId);
|
||||
})
|
||||
.catch(err => {
|
||||
showError('加载数据集失败: ' + err.message);
|
||||
if (row) row.style.opacity = '1';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Populate filter column dropdowns
|
||||
onSelectDataset(dsId);
|
||||
}
|
||||
|
||||
function onSelectDataset(dsId) {
|
||||
const noDatasetMsg = document.getElementById('filterNoDataset');
|
||||
const filterRows = document.getElementById('filterRows');
|
||||
const filterControls = document.getElementById('filterControls');
|
||||
const filterResult = document.getElementById('filterResult');
|
||||
|
||||
if (!dsId || !DATASET_COLUMNS[dsId] || DATASET_COLUMNS[dsId].length === 0) {
|
||||
noDatasetMsg.style.display = 'block';
|
||||
filterRows.style.display = 'none';
|
||||
filterControls.style.display = 'none';
|
||||
filterResult.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
noDatasetMsg.style.display = 'none';
|
||||
filterRows.style.display = 'block';
|
||||
filterControls.style.display = 'flex';
|
||||
|
||||
const cols = DATASET_COLUMNS[dsId] || [];
|
||||
document.querySelectorAll('.filter-col').forEach(sel => {
|
||||
const cur = sel.value;
|
||||
sel.innerHTML = '<option value="">-- 列 --</option>';
|
||||
cols.forEach(c => { sel.innerHTML += '<option value="' + c + '" ' + (c===cur?'selected':'') + '>' + c + '</option>'; });
|
||||
});
|
||||
}
|
||||
|
||||
function setDatasetFilter(filter) {
|
||||
activeFilter = filter;
|
||||
document.querySelectorAll('#statusFilter .filter-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.filter === filter);
|
||||
});
|
||||
buildDatasetTable(filter);
|
||||
}
|
||||
|
||||
// Init filter tabs
|
||||
document.querySelectorAll('#statusFilter .filter-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
setDatasetFilter(this.dataset.filter);
|
||||
});
|
||||
});
|
||||
buildDatasetTable('all');
|
||||
|
||||
// ── Filter builder functions ──
|
||||
function addFilterRow() {
|
||||
const rows = document.querySelectorAll('#filterRows .filter-row');
|
||||
if (rows.length >= 5) return;
|
||||
const template = rows[0].cloneNode(true);
|
||||
template.querySelector('.filter-val').value = '';
|
||||
template.querySelector('.filter-col').selectedIndex = 0;
|
||||
template.querySelector('.filter-op').selectedIndex = 0;
|
||||
template.querySelector('button').style.display = 'inline-block';
|
||||
document.getElementById('filterRows').appendChild(template);
|
||||
if (selectedRunId) onSelectDataset(selectedRunId);
|
||||
}
|
||||
|
||||
function removeFilterRow(btn) {
|
||||
const rows = document.querySelectorAll('#filterRows .filter-row');
|
||||
if (rows.length <= 1) return;
|
||||
btn.closest('.filter-row').remove();
|
||||
}
|
||||
|
||||
function resetFilter() {
|
||||
const rows = document.querySelectorAll('#filterRows .filter-row');
|
||||
for (let i = rows.length - 1; i >= 1; i--) rows[i].remove();
|
||||
const firstRow = rows[0];
|
||||
firstRow.querySelector('.filter-val').value = '';
|
||||
firstRow.querySelector('.filter-col').selectedIndex = 0;
|
||||
firstRow.querySelector('.filter-op').selectedIndex = 0;
|
||||
firstRow.querySelector('button').style.display = 'none';
|
||||
document.getElementById('filterResult').style.display = 'none';
|
||||
document.getElementById('activeFilterInfo').style.display = 'none';
|
||||
currentFilters = [];
|
||||
activeFilteredDatasetId = null;
|
||||
}
|
||||
|
||||
function collectFilters() {
|
||||
const filters = [];
|
||||
document.querySelectorAll('#filterRows .filter-row').forEach(row => {
|
||||
const col = row.querySelector('.filter-col').value;
|
||||
const op = row.querySelector('.filter-op').value;
|
||||
let val = row.querySelector('.filter-val').value;
|
||||
if (!col || !op) return;
|
||||
if ((op === 'in') && val) {
|
||||
val = val.split(',').map(s => s.trim()).filter(s => s);
|
||||
}
|
||||
filters.push({ column: col, op: op, value: val });
|
||||
});
|
||||
return filters;
|
||||
}
|
||||
|
||||
async function applyFilter() {
|
||||
if (!selectedRunId) { showError('请先选择数据集'); return; }
|
||||
const filters = collectFilters();
|
||||
if (filters.length === 0) { showError('请添加至少一个筛选条件'); return; }
|
||||
const logic = document.getElementById('filterLogic').value;
|
||||
|
||||
try {
|
||||
const resp = await fetch('/tools/filter/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type':'application/json', 'X-CSRFToken':'{{ csrf_token }}' },
|
||||
body: JSON.stringify({ dataset_id: selectedRunId, filters, logic }),
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.error) { showError('筛选失败: ' + data.error); return; }
|
||||
|
||||
const resultDiv = document.getElementById('filterResult');
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.innerHTML = '<strong>✅ 筛选完成</strong>: 数据集 <code>' + data.dataset_id + '</code>,共 <strong>' + data.row_count + '</strong> 行';
|
||||
showSuccess('筛选完成: ' + data.row_count + ' 行');
|
||||
|
||||
currentFilters = filters;
|
||||
currentFilterLogic = logic;
|
||||
activeFilteredDatasetId = data.dataset_id;
|
||||
|
||||
const infoDiv = document.getElementById('activeFilterInfo');
|
||||
infoDiv.style.display = 'block';
|
||||
infoDiv.textContent = '已应用筛选 (' + filters.length + ' 条件) 到 ' + data.dataset_id;
|
||||
} catch(e) {
|
||||
showError('筛选请求失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ── Dataset table rendering ──
|
||||
function buildDatasetTable(filter) {
|
||||
const container = document.getElementById('datasetTable');
|
||||
let rows = DATASETS_WITH_STATUS.filter(ds => {
|
||||
if (filter === 'all') return true;
|
||||
if (filter === 'ready') return ds.badge_class === 'ready';
|
||||
if (filter === 'analyzing') return ds.badge_class === 'analyzing';
|
||||
if (filter === 'completed') return ds.badge_class === 'completed';
|
||||
if (filter === 'failed') return ds.badge_class === 'failed';
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!rows.length) {
|
||||
container.innerHTML = '<p style="text-align:center;color:#999;padding:1rem;">无匹配的数据集</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<table class="ds-table"><thead><tr><th></th><th>ID</th><th>状态</th><th>行数</th><th>文件数</th><th>来源</th></tr></thead><tbody>';
|
||||
rows.forEach(ds => {
|
||||
const label = ds.status_label || '待分析';
|
||||
const badgeClass = ds.badge_class || 'ready';
|
||||
const dispId = ds.display_id || ds.dataset_id;
|
||||
const rowCount = ds.row_count != null ? ds.row_count.toLocaleString() : '?';
|
||||
const fileCount = ds.file_count != null ? ds.file_count : '?';
|
||||
@@ -260,33 +514,64 @@ function toggleStep(el) {
|
||||
}
|
||||
|
||||
function buildTimeline(llmThinking, toolCalls) {
|
||||
// Build tool entries from tool_calls_json (no separate thought entries)
|
||||
const entries = (toolCalls || []).map(tc => ({
|
||||
step: tc.step,
|
||||
type: 'tool',
|
||||
name: tc.name,
|
||||
input: tc.input,
|
||||
output: tc.output,
|
||||
}));
|
||||
|
||||
// Match llm_thinking text to tool entries by step number
|
||||
// Parse llm_thinking into per-step segments
|
||||
const thinkingByStep = {};
|
||||
const thinkOrder = []; // track step order for interleaving
|
||||
if (llmThinking) {
|
||||
const thinkingByStep = {};
|
||||
const parts = llmThinking.split(/\n(?=\[\d+\])/);
|
||||
for (const part of parts) {
|
||||
const m = part.match(/^\[(\d+)\]\s*(.*)/s);
|
||||
if (m) {
|
||||
thinkingByStep[parseInt(m[1])] = m[2].trim();
|
||||
}
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (thinkingByStep[entry.step] !== undefined) {
|
||||
entry.thinking = thinkingByStep[entry.step];
|
||||
const step = parseInt(m[1]);
|
||||
if (!(step in thinkingByStep)) {
|
||||
thinkOrder.push(step);
|
||||
}
|
||||
thinkingByStep[step] = (thinkingByStep[step] || '') + (thinkingByStep[step] ? '\n' : '') + m[2].trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build entries: interleave thinking + tool per step
|
||||
const toolMap = {}; // step -> tool entries
|
||||
(toolCalls || []).forEach(tc => {
|
||||
const s = tc.step;
|
||||
if (!toolMap[s]) toolMap[s] = [];
|
||||
toolMap[s].push({
|
||||
type: 'tool',
|
||||
name: tc.name,
|
||||
input: tc.input,
|
||||
output: tc.output,
|
||||
});
|
||||
});
|
||||
|
||||
const entries = [];
|
||||
// Collect all step numbers that appear in either thinking or tool_calls
|
||||
const allSteps = new Set([...thinkOrder, ...Object.keys(toolMap).map(Number)]);
|
||||
const sortedSteps = Array.from(allSteps).sort((a, b) => a - b);
|
||||
|
||||
for (const step of sortedSteps) {
|
||||
// Thinking first (独立条目)
|
||||
if (thinkingByStep[step] !== undefined) {
|
||||
// Last step with only thinking and no tool call = 回答
|
||||
const isLastStep = step === sortedSteps[sortedSteps.length - 1];
|
||||
const hasTool = toolMap[step] && toolMap[step].length > 0;
|
||||
if (isLastStep && !hasTool) {
|
||||
entries.push({ step, type: 'answer', text: thinkingByStep[step] });
|
||||
} else {
|
||||
entries.push({ step, type: 'thinking', text: thinkingByStep[step] });
|
||||
}
|
||||
}
|
||||
|
||||
// Then tool entries for this step
|
||||
if (toolMap[step]) {
|
||||
for (const te of toolMap[step]) {
|
||||
// Attach thinking to first tool entry only if not already shown separately
|
||||
// (No, we already show thinking separately above)
|
||||
entries.push({ step, ...te, thinking: '' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entries.sort((a, b) => a.step - b.step);
|
||||
return entries;
|
||||
}
|
||||
|
||||
@@ -308,25 +593,33 @@ function renderTimeline(entries) {
|
||||
lastStep = entry.step;
|
||||
}
|
||||
|
||||
if (entry.type === 'tool') {
|
||||
if (entry.type === 'thinking') {
|
||||
// 思考条目 — 浅蓝背景文本块
|
||||
const div = document.createElement('div');
|
||||
div.style.cssText = 'background:#eef2ff;padding:0.6rem;border-radius:6px;font-size:0.78rem;line-height:1.6;color:#333;max-height:200px;overflow:auto;margin-bottom:0.3rem;white-space:pre-wrap;border:1px solid #d0d8f0;';
|
||||
div.innerHTML =
|
||||
'<span style="font-weight:600;color:#4361ee;font-size:0.75rem;display:block;margin-bottom:0.3rem;">💭 思考</span>' +
|
||||
escapeHtml(entry.text);
|
||||
container.appendChild(div);
|
||||
} else if (entry.type === 'answer') {
|
||||
// 回答条目 — 绿色背景
|
||||
const div = document.createElement('div');
|
||||
div.style.cssText = 'background:#e8f5e9;padding:0.6rem;border-radius:6px;font-size:0.78rem;line-height:1.6;color:#333;max-height:300px;overflow:auto;margin-bottom:0.3rem;white-space:pre-wrap;border:1px solid #c8e6c9;';
|
||||
div.innerHTML =
|
||||
'<span style="font-weight:600;color:#2e7d32;font-size:0.75rem;display:block;margin-bottom:0.3rem;">✅ 回答</span>' +
|
||||
escapeHtml(entry.text);
|
||||
container.appendChild(div);
|
||||
} else if (entry.type === 'tool') {
|
||||
const card = document.createElement('div');
|
||||
card.style.cssText = 'border:1px solid #e0e0e0;border-radius:6px;margin-bottom:0.3rem;overflow:hidden;background:#fff;';
|
||||
const inputStr = prettyJson(entry.input);
|
||||
const hasThinking = entry.thinking && (entry.output == null || entry.output === '' || entry.output === 'null' || entry.output === 'None');
|
||||
const outputStr = prettyJson(entry.output);
|
||||
let bodyInner = '';
|
||||
bodyInner +=
|
||||
'<div style="font-weight:600;font-size:0.72rem;color:#555;margin-bottom:0.2rem;">输入 (INPUT):</div>' +
|
||||
'<pre style="background:#1a1a2e;color:#e0e0e0;padding:0.5rem;border-radius:4px;font-size:0.7rem;max-height:200px;overflow:auto;margin:0 0 0.3rem 0;"><code>' + escapeHtml(inputStr) + '</code></pre>';
|
||||
if (hasThinking) {
|
||||
bodyInner +=
|
||||
'<div style="font-weight:600;font-size:0.72rem;color:#555;margin-bottom:0.2rem;">思考 (THINKING):</div>' +
|
||||
'<div style="background:#eef2ff;padding:0.6rem;border-radius:4px;font-size:0.78rem;line-height:1.6;color:#333;max-height:200px;overflow:auto;margin:0;white-space:pre-wrap;">' + escapeHtml(entry.thinking) + '</div>';
|
||||
} else {
|
||||
bodyInner +=
|
||||
'<div style="font-weight:600;font-size:0.72rem;color:#555;margin-bottom:0.2rem;">输出 (OUTPUT):</div>' +
|
||||
'<pre style="background:#1a1a2e;color:#e0e0e0;padding:0.5rem;border-radius:4px;font-size:0.7rem;max-height:200px;overflow:auto;margin:0;"><code>' + escapeHtml(outputStr) + '</code></pre>';
|
||||
}
|
||||
'<pre style="background:#1a1a2e;color:#e0e0e0;padding:0.5rem;border-radius:4px;font-size:0.7rem;max-height:200px;overflow:auto;margin:0 0 0.3rem 0;"><code>' + escapeHtml(inputStr) + '</code></pre>' +
|
||||
'<div style="font-weight:600;font-size:0.72rem;color:#555;margin-bottom:0.2rem;">输出 (OUTPUT):</div>' +
|
||||
'<pre style="background:#1a1a2e;color:#e0e0e0;padding:0.5rem;border-radius:4px;font-size:0.7rem;max-height:200px;overflow:auto;margin:0;"><code>' + escapeHtml(outputStr) + '</code></pre>';
|
||||
card.innerHTML =
|
||||
'<div class="tool-call-header" onclick="toggleStep(this)" style="padding:0.4rem 0.8rem;">' +
|
||||
'<span><span style="background:#43aa8b;color:#fff;border-radius:3px;padding:0 5px;font-size:0.7rem;font-weight:600;margin-right:6px;">🔧</span>' +
|
||||
@@ -358,6 +651,16 @@ async function startAuto() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract numeric display_id from dataset key (upload_XX or run_XX)
|
||||
let displayId;
|
||||
const uploadMatch = runId.match(/^(?:upload_|run_)(\d+)$/);
|
||||
if (uploadMatch) {
|
||||
displayId = parseInt(uploadMatch[1]);
|
||||
} else {
|
||||
displayId = parseInt(runId);
|
||||
}
|
||||
if (isNaN(displayId)) { showError('无效的数据集ID: ' + runId); return; }
|
||||
|
||||
const timelineContainer = document.getElementById('timeline');
|
||||
const timelinePanel = document.getElementById('timelinePanel');
|
||||
|
||||
@@ -369,12 +672,20 @@ async function startAuto() {
|
||||
timelinePanel.style.display = 'none';
|
||||
timelineContainer.innerHTML = '';
|
||||
|
||||
// Build request body
|
||||
const body = { run_id: displayId };
|
||||
if (activeFilteredDatasetId) {
|
||||
// Use filtered dataset: re-apply filter on the backend
|
||||
body.filters = currentFilters;
|
||||
body.logic = currentFilterLogic;
|
||||
}
|
||||
|
||||
let resp;
|
||||
try {
|
||||
resp = await fetch('/analyze/llm/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': '{{ csrf_token }}' },
|
||||
body: JSON.stringify({ run_id: parseInt(runId) }),
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
} catch (e) { showError('启动 LLM 分析失败: ' + e.message); return; }
|
||||
const data = await resp.json();
|
||||
@@ -383,7 +694,7 @@ async function startAuto() {
|
||||
|
||||
statusPoll = setInterval(async () => {
|
||||
try {
|
||||
const r = await fetch('/runs/' + runId + '/status/');
|
||||
const r = await fetch('/runs/' + displayId + '/status/');
|
||||
const s = await r.json();
|
||||
const pct = s.progress_pct || 0;
|
||||
const msg = s.progress_msg || s.status;
|
||||
@@ -403,7 +714,7 @@ async function startAuto() {
|
||||
document.getElementById('autoBar').style.width = '100%';
|
||||
document.getElementById('cancelBtn').style.display = 'none';
|
||||
document.getElementById('autoStatus').textContent = '分析完成!正在跳转...';
|
||||
setTimeout(() => { window.location.href = '/clusters/' + runId + '/'; }, 1500);
|
||||
setTimeout(() => { window.location.href = '/clusters/' + displayId + '/'; }, 1500);
|
||||
}
|
||||
if (s.status === 'failed') {
|
||||
clearInterval(statusPoll);
|
||||
|
||||
@@ -183,9 +183,9 @@ function buildDatasetTable(filter) {
|
||||
const container = document.getElementById('datasetTable');
|
||||
let rows = DATASETS_WITH_STATUS.filter(ds => {
|
||||
if (filter === 'all') return true;
|
||||
if (filter === 'ready') return ds.run_status === 'ready' || (!ds.run_status || ds.run_status === 'ready');
|
||||
if (filter === 'completed') return ds.run_status === 'completed';
|
||||
if (filter === 'failed') return ds.run_status === 'failed';
|
||||
if (filter === 'ready') return ds.badge_class === 'ready';
|
||||
if (filter === 'completed') return ds.badge_class === 'completed';
|
||||
if (filter === 'failed') return ds.badge_class === 'failed';
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -197,9 +197,7 @@ function buildDatasetTable(filter) {
|
||||
let html = '<table class="ds-table"><thead><tr><th>ID</th><th>状态</th><th>行数</th><th>文件数</th><th>SVD</th><th></th></tr></thead><tbody>';
|
||||
rows.forEach(ds => {
|
||||
const label = ds.status_label || '待分析';
|
||||
const badgeClass = (ds.run_status === 'completed') ? 'completed' :
|
||||
(ds.run_status === 'failed') ? 'failed' :
|
||||
(ds.run_status && ds.run_status !== 'ready') ? 'analyzing' : 'ready';
|
||||
const badgeClass = ds.badge_class || 'ready';
|
||||
const dispId = ds.display_id || ds.dataset_id;
|
||||
const rowCount = ds.row_count != null ? ds.row_count.toLocaleString() : '?';
|
||||
const fileCount = ds.file_count != null ? ds.file_count : '?';
|
||||
@@ -218,6 +216,58 @@ function buildDatasetTable(filter) {
|
||||
}
|
||||
|
||||
function selectDataset(dsId) {
|
||||
// Check if this dataset needs data reload
|
||||
const dsInfo = DATASETS_WITH_STATUS.find(ds => ds.dataset_id === dsId);
|
||||
if (dsInfo && dsInfo.needs_reload) {
|
||||
// Trigger background reload
|
||||
const row = document.querySelector(`#datasetTable tr[data-ds-id="${dsId}"]`);
|
||||
if (row) {
|
||||
row.style.opacity = '0.6';
|
||||
const statusCell = row.querySelector('.status-badge');
|
||||
if (statusCell) statusCell.textContent = '加载中...';
|
||||
}
|
||||
fetch('/tools/reload-run/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type':'application/json', 'X-CSRFToken':'{{ csrf_token }}' },
|
||||
body: JSON.stringify({ display_id: dsInfo.display_id }),
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
showError('加载数据集失败: ' + data.error);
|
||||
if (row) row.style.opacity = '1';
|
||||
return;
|
||||
}
|
||||
// Mark as no longer needing reload
|
||||
dsInfo.needs_reload = false;
|
||||
// Add to dropdown if not present
|
||||
const sel = document.getElementById('datasetSelect');
|
||||
if (!Array.from(sel.options).some(o => o.value === dsId)) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = dsId;
|
||||
opt.text = dsId + ' (' + data.row_count + ' rows)';
|
||||
sel.add(opt);
|
||||
}
|
||||
sel.value = dsId;
|
||||
onDatasetChange();
|
||||
// Update status badge
|
||||
if (row) {
|
||||
row.style.opacity = '1';
|
||||
const badge = row.querySelector('.status-badge');
|
||||
if (badge) {
|
||||
badge.textContent = '待分析';
|
||||
badge.className = 'status-badge ready';
|
||||
}
|
||||
}
|
||||
showSuccess('数据集已加载: ' + dsId);
|
||||
})
|
||||
.catch(err => {
|
||||
showError('加载数据集失败: ' + err.message);
|
||||
if (row) row.style.opacity = '1';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const sel = document.getElementById('datasetSelect');
|
||||
let opt = Array.from(sel.options).find(o => o.value === dsId);
|
||||
if (!opt) {
|
||||
@@ -394,9 +444,13 @@ async function executeStep(idx) {
|
||||
// Raw row-level clustering: skip entity aggregation entirely
|
||||
if (step.tool.name === 'build_entity_profiles') {
|
||||
step.status = 'done';
|
||||
resultDiv.textContent = '[已跳过] 逐行直接聚类模式,无需实体聚合';
|
||||
resultDiv.style.display = 'block';
|
||||
step._resultText = '[已跳过] 逐行直接聚类模式,无需实体聚合';
|
||||
renderSteps();
|
||||
const skipResultDiv = document.getElementById(`result-${idx}`);
|
||||
if (skipResultDiv) {
|
||||
skipResultDiv.textContent = step._resultText;
|
||||
skipResultDiv.style.display = 'block';
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Always cluster raw rows directly
|
||||
@@ -411,8 +465,7 @@ async function executeStep(idx) {
|
||||
body: JSON.stringify({ tool: step.tool.name, params }),
|
||||
});
|
||||
const data = await resp.json();
|
||||
resultDiv.textContent = JSON.stringify(data, null, 2);
|
||||
resultDiv.style.display = 'block';
|
||||
step._resultText = JSON.stringify(data, null, 2);
|
||||
step.status = data.error ? 'failed' : 'done';
|
||||
// update dataset select from result
|
||||
if (data && data.result && data.result.dataset_id) {
|
||||
@@ -425,12 +478,17 @@ async function executeStep(idx) {
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
resultDiv.textContent = 'Error: ' + err.message;
|
||||
resultDiv.style.display = 'block';
|
||||
step._resultText = 'Error: ' + err.message;
|
||||
step.status = 'failed';
|
||||
showError('步骤 ' + step.tool.name + ' 执行失败: ' + err.message);
|
||||
}
|
||||
renderSteps();
|
||||
// Re-populate result content after renderSteps() recreates the DOM
|
||||
const newResultDiv = document.getElementById(`result-${idx}`);
|
||||
if (newResultDiv && step._resultText) {
|
||||
newResultDiv.textContent = step._resultText;
|
||||
newResultDiv.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Run All (sequential) ──
|
||||
|
||||
@@ -22,8 +22,8 @@ TOOLS = [
|
||||
"description": "使用自适应权重学习在实体数据上计算自适应代理/异常/威胁评分。在build_entity_profiles之后调用。添加proxy_score/risk_level/threat_score列。",
|
||||
"parameters": {"type": "object", "properties": {"dataset_id": {"type": "string"}}, "required": ["dataset_id"]}}},
|
||||
{"type": "function", "function": {"name": "run_clustering",
|
||||
"description": "将实体数据聚类为行为组。需要实体数据集ID。探索性分析选择hdbscan(自动检测聚类数);固定簇数选择kmeans。",
|
||||
"parameters": {"type": "object", "properties": {"dataset_id": {"type": "string"}, "cluster_columns": {"type": "array", "items": {"type": "string"}}, "algorithm": {"type": "string", "enum": ["hdbscan", "kmeans"]}}, "required": ["dataset_id", "cluster_columns"]}}},
|
||||
"description": "将实体数据聚类为行为组。需要实体数据集ID。agglomerative(默认/凝聚层次聚类)适合预知簇数;hdbscan自动检测聚类数;kmeans固定簇数。",
|
||||
"parameters": {"type": "object", "properties": {"dataset_id": {"type": "string"}, "cluster_columns": {"type": "array", "items": {"type": "string"}}, "algorithm": {"type": "string", "enum": ["agglomerative", "hdbscan", "kmeans"]}}, "required": ["dataset_id", "cluster_columns"]}}},
|
||||
{"type": "function", "function": {"name": "extract_features",
|
||||
"description": "识别每个聚类的区分特征(z-score或anova)。在run_clustering之后调用。",
|
||||
"parameters": {"type": "object", "properties": {"dataset_id": {"type": "string"}, "cluster_result_id": {"type": "string"}, "method": {"type": "string", "enum": ["zscore", "anova"]}}, "required": ["dataset_id", "cluster_result_id"]}}},
|
||||
@@ -363,13 +363,20 @@ def run_llm_pipeline(dataset_id: str, entity_col: str = "",
|
||||
called_tools.add(tool_key)
|
||||
|
||||
logger.info(f'[TOOL] step={step} name={name} args={json.dumps(args)[:200]}')
|
||||
if callback:
|
||||
callback(step, name, None, meta={"args": args})
|
||||
|
||||
try:
|
||||
result = asyncio.run(handle_call(name, args))
|
||||
async def _call_with_timeout():
|
||||
return await asyncio.wait_for(
|
||||
handle_call(name, args),
|
||||
timeout=120,
|
||||
)
|
||||
result = asyncio.run(_call_with_timeout())
|
||||
except asyncio.TimeoutError:
|
||||
result = {'error': f'Tool {name} timed out after 120s'}
|
||||
logger.warning(f'[TOOL] {name} timed out')
|
||||
except Exception as e:
|
||||
raise SystemExit(f"MCP tool error: {e}") from e
|
||||
result = {'error': f'MCP tool error: {e}'}
|
||||
logger.error(f'[TOOL] {name} error: {e}')
|
||||
|
||||
if callback:
|
||||
callback(step, name, result, meta={"args": args})
|
||||
|
||||
@@ -32,7 +32,6 @@ INSTALLED_APPS = [
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"analysis.apps.AnalysisConfig",
|
||||
"simple_analysis.apps.SimpleAnalysisConfig",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
||||
+1
-1
@@ -4,5 +4,5 @@ from django.urls import path, include
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('', include('analysis.urls')),
|
||||
path('simple/', include('simple_analysis.urls')),
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user