v1.1.3: row-level clustering + dataset selector with status filter

- Add cluster_mode toggle (raw row-level vs entity-aggregated) in manual.html
- manual_run_analysis supports cluster_mode='raw' skipping entity aggregation
- Replace simple dataset dropdown with selector showing status badges and filter tabs
- Filter tabs: 全部/待分析/已分析/错误, default 待分析
- E2E verified: raw mode entity_count=total_flows, entity mode regression OK
This commit is contained in:
PM-pinou
2026-07-21 14:00:18 +08:00
parent 9de1d93146
commit ec83d5df85
2 changed files with 190 additions and 3 deletions
+61 -1
View File
@@ -593,6 +593,46 @@ def manual_page(request):
store = SessionStore()
datasets = store.list_datasets() if hasattr(store, 'list_datasets') else []
# Get upload AnalysisRun statuses for dataset badges
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
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'],
})
else:
datasets_with_status.append({**ds, 'run_status': 'ready', 'total_flows': None, 'display_id': None})
# Map statuses to user-facing labels for the filter tabs
# ready → 待分析, completed → 已分析, failed → 错误
# Everything else (pending/loading/profiling/aggregating/clustering/extracting) → analyzing → 分析中
for ds_info in datasets_with_status:
s = ds_info['run_status']
if s in ('completed',):
ds_info['status_label'] = '已分析'
elif s in ('failed',):
ds_info['status_label'] = '错误'
elif s in ('pending', 'loading', 'profiling', 'aggregating', 'clustering', 'extracting'):
ds_info['status_label'] = '分析中'
else:
ds_info['status_label'] = '待分析'
return render(request, 'tianxuan/manual.html', {
'core_tools': core_tools,
'diag_tools': diag_tools,
@@ -600,6 +640,7 @@ def manual_page(request):
'other_tools': other_tools,
'tools_meta': _json.dumps(meta_list, ensure_ascii=False),
'datasets': datasets,
'datasets_with_status_json': _json.dumps(datasets_with_status, ensure_ascii=False),
})
@@ -858,6 +899,7 @@ def manual_run_analysis(request):
algorithm = body.get('algorithm', 'hdbscan')
min_cluster_size = int(body.get('min_cluster_size', 5))
head = body.get('head')
cluster_mode = body.get('cluster_mode', 'entity')
def _analysis_fn(run, ctx):
from analysis.session_store import SessionStore
@@ -867,6 +909,7 @@ def manual_run_analysis(request):
min_cluster_size = ctx['min_cluster_size']
head = ctx.get('head')
pk = ctx['pk']
cluster_mode = ctx.get('cluster_mode', 'entity')
store = SessionStore()
try:
@@ -888,6 +931,23 @@ def manual_run_analysis(request):
ds_id = upload_ds_id if store.get_dataset(upload_ds_id) else f'upload_{pk}'
# ── Cluster mode: entity aggregation vs raw ──
if cluster_mode == 'entity':
# Build entity profiles first, then cluster on entity dataset
from analysis.tool_registry import _handle_build_entity_profiles
import asyncio
entity_result = asyncio.new_event_loop().run_until_complete(
_handle_build_entity_profiles(dataset_id=ds_id, auto_detect=True)
)
if 'error' in entity_result:
run.error_message = f'实体聚合失败: {entity_result["error"]}'
run.status = 'failed'
run.save(update_fields=['status', 'error_message'])
return
ds_id = entity_result['dataset_id']
run.entity_count = entity_result.get('n_entities')
run.save(update_fields=['entity_count'])
# Use the unified clustering pipeline (clustering → extraction → PCA)
from analysis.views import _run_clustering_pipeline
_run_clustering_pipeline(
@@ -910,7 +970,7 @@ def manual_run_analysis(request):
args=(pk, _analysis_fn),
kwargs=dict(feature_columns=feature_columns, algorithm=algorithm,
min_cluster_size=min_cluster_size, head=head, pk=pk,
display_id=run.display_id),
display_id=run.display_id, cluster_mode=cluster_mode),
daemon=True,
)
t.start()