eduevidence 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +506 -0
- package/README.zh-CN.md +434 -0
- package/SKILL.md +195 -0
- package/bin/eduevidence.js +127 -0
- package/domains/education/manifest.json +183 -0
- package/domains/education/outcome_taxonomy.json +127 -0
- package/domains/manifest.json +26 -0
- package/domains/policy/frame.schema.json +234 -0
- package/domains/policy/manifest.json +10 -0
- package/domains/policy/methodology_checklist.json +109 -0
- package/domains/policy/outcome_taxonomy.json +53 -0
- package/domains/policy/references/causal-identification.md +45 -0
- package/domains/policy/references/cost-evidence.md +44 -0
- package/domains/policy/references/equity.md +42 -0
- package/domains/policy/references/evidence-hierarchy.md +41 -0
- package/domains/policy/references/implementation-evidence.md +47 -0
- package/eduevidence_cli.py +26 -0
- package/engine/__init__.py +11 -0
- package/engine/__pycache__/__init__.cpython-312.pyc +0 -0
- package/engine/__pycache__/analysis.cpython-312.pyc +0 -0
- package/engine/__pycache__/bias.cpython-312.pyc +0 -0
- package/engine/__pycache__/briefs.cpython-312.pyc +0 -0
- package/engine/__pycache__/capabilities.cpython-312.pyc +0 -0
- package/engine/__pycache__/citation_check.cpython-312.pyc +0 -0
- package/engine/__pycache__/contracts.cpython-312.pyc +0 -0
- package/engine/__pycache__/datasets.cpython-312.pyc +0 -0
- package/engine/__pycache__/events.cpython-312.pyc +0 -0
- package/engine/__pycache__/evidence_graph.cpython-312.pyc +0 -0
- package/engine/__pycache__/evidence_review.cpython-312.pyc +0 -0
- package/engine/__pycache__/evidencecore.cpython-312.pyc +0 -0
- package/engine/__pycache__/gap_lens.cpython-312.pyc +0 -0
- package/engine/__pycache__/gaps.cpython-312.pyc +0 -0
- package/engine/__pycache__/graph_store.cpython-312.pyc +0 -0
- package/engine/__pycache__/graph_validate.cpython-312.pyc +0 -0
- package/engine/__pycache__/ids.cpython-312.pyc +0 -0
- package/engine/__pycache__/library.cpython-312.pyc +0 -0
- package/engine/__pycache__/library_builtin.cpython-312.pyc +0 -0
- package/engine/__pycache__/living.cpython-312.pyc +0 -0
- package/engine/__pycache__/log.cpython-312.pyc +0 -0
- package/engine/__pycache__/meta_analysis.cpython-312.pyc +0 -0
- package/engine/__pycache__/meta_synthesis.cpython-312.pyc +0 -0
- package/engine/__pycache__/migration.cpython-312.pyc +0 -0
- package/engine/__pycache__/mode_router.cpython-312.pyc +0 -0
- package/engine/__pycache__/paths.cpython-312.pyc +0 -0
- package/engine/__pycache__/pilot.cpython-312.pyc +0 -0
- package/engine/__pycache__/planner.cpython-312.pyc +0 -0
- package/engine/__pycache__/project.cpython-312.pyc +0 -0
- package/engine/__pycache__/projections.cpython-312.pyc +0 -0
- package/engine/__pycache__/robustness.cpython-312.pyc +0 -0
- package/engine/__pycache__/run.cpython-312.pyc +0 -0
- package/engine/__pycache__/semantics.cpython-312.pyc +0 -0
- package/engine/__pycache__/study_design.cpython-312.pyc +0 -0
- package/engine/__pycache__/synthesis.cpython-312.pyc +0 -0
- package/engine/__pycache__/tribunal.cpython-312.pyc +0 -0
- package/engine/__pycache__/update.cpython-312.pyc +0 -0
- package/engine/__pycache__/versions.cpython-312.pyc +0 -0
- package/engine/analysis.py +308 -0
- package/engine/bias.py +178 -0
- package/engine/briefs.py +106 -0
- package/engine/capabilities.py +99 -0
- package/engine/citation_check.py +192 -0
- package/engine/contracts.py +117 -0
- package/engine/datasets.py +165 -0
- package/engine/events.py +67 -0
- package/engine/evidence_graph.py +571 -0
- package/engine/evidence_review.py +88 -0
- package/engine/evidencecore.py +182 -0
- package/engine/gap_lens.py +132 -0
- package/engine/gaps.py +169 -0
- package/engine/graph_store.py +335 -0
- package/engine/graph_validate.py +87 -0
- package/engine/ids.py +77 -0
- package/engine/library.py +268 -0
- package/engine/library_builtin.py +301 -0
- package/engine/living.py +671 -0
- package/engine/log.py +39 -0
- package/engine/meta_analysis.py +333 -0
- package/engine/meta_synthesis.py +111 -0
- package/engine/migration.py +397 -0
- package/engine/mode_router.py +72 -0
- package/engine/paths.py +15 -0
- package/engine/pilot.py +368 -0
- package/engine/planner.py +126 -0
- package/engine/project.py +118 -0
- package/engine/projections.py +240 -0
- package/engine/robustness.py +109 -0
- package/engine/run.py +85 -0
- package/engine/semantics.py +135 -0
- package/engine/study_design.py +87 -0
- package/engine/synthesis.py +187 -0
- package/engine/tribunal.py +408 -0
- package/engine/update.py +113 -0
- package/engine/versions.py +12 -0
- package/install.sh +510 -0
- package/integrations/__init__.py +1 -0
- package/integrations/__pycache__/__init__.cpython-312.pyc +0 -0
- package/integrations/__pycache__/agent_mcp.cpython-312.pyc +0 -0
- package/integrations/__pycache__/smart_web_fetch.cpython-312.pyc +0 -0
- package/integrations/agent_mcp.py +856 -0
- package/integrations/smart_web_fetch.py +59 -0
- package/package.json +50 -0
- package/pyproject.toml +55 -0
- package/references/applicability-policy.md +88 -0
- package/references/education-framing.md +132 -0
- package/references/effect_size_formulas.md +35 -0
- package/references/evaluation-design.md +111 -0
- package/references/evidence-quality.md +79 -0
- package/references/grade_framework.md +29 -0
- package/references/intervention-design.md +98 -0
- package/references/methodology-audit.md +103 -0
- package/references/outcome-taxonomy.md +106 -0
- package/references/retrieval-protocol.md +142 -0
- package/references/skeptic-protocol.md +93 -0
- package/references/social_science_pitfalls.md +48 -0
- package/references/source-validity.md +140 -0
- package/references/tribunal-policy.md +112 -0
- package/references/wwc_standards.md +29 -0
- package/retrieval/__init__.py +1 -0
- package/retrieval/__pycache__/__init__.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/corpus_store.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/dedupe.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/failures.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/fetch.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/search.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/source.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/validate.cpython-312.pyc +0 -0
- package/retrieval/corpus_store.py +181 -0
- package/retrieval/dedupe.py +127 -0
- package/retrieval/failures.py +90 -0
- package/retrieval/fetch.py +435 -0
- package/retrieval/search.py +493 -0
- package/retrieval/source.py +160 -0
- package/retrieval/validate.py +257 -0
- package/schemas/agent-mcp-approval.schema.json +57 -0
- package/schemas/chart-spec.schema.json +88 -0
- package/schemas/cross-model-review.schema.json +28 -0
- package/schemas/education-frame.schema.json +106 -0
- package/schemas/evaluation.schema.json +35 -0
- package/schemas/evidence.schema.json +81 -0
- package/schemas/fetch-result.schema.json +119 -0
- package/schemas/intervention.schema.json +46 -0
- package/schemas/methodology.schema.json +102 -0
- package/schemas/report-result.schema.json +381 -0
- package/schemas/report-spec.schema.json +130 -0
- package/schemas/source.schema.json +311 -0
- package/schemas/v2/analysis-plan.schema.json +28 -0
- package/schemas/v2/analysis-run.schema.json +33 -0
- package/schemas/v2/claim.schema.json +26 -0
- package/schemas/v2/dataset-asset.schema.json +40 -0
- package/schemas/v2/decision-snapshot.schema.json +53 -0
- package/schemas/v2/evidence-link.schema.json +38 -0
- package/schemas/v2/finding.schema.json +47 -0
- package/schemas/v2/graph-revision.schema.json +30 -0
- package/schemas/v2/knowledge-gap.schema.json +40 -0
- package/schemas/v2/methodology-audit.schema.json +30 -0
- package/schemas/v2/outcome.schema.json +18 -0
- package/schemas/v2/project.schema.json +31 -0
- package/schemas/v2/research-intent.schema.json +24 -0
- package/schemas/v2/run.schema.json +43 -0
- package/schemas/v2/source.schema.json +24 -0
- package/schemas/v2/study-design.schema.json +67 -0
- package/schemas/v2/study.schema.json +37 -0
- package/schemas/v3/pilot-outcome.schema.json +132 -0
- package/schemas/v3/run-manifest.schema.json +193 -0
- package/schemas/v3/synthesis.schema.json +86 -0
- package/schemas/v4/drift-report.schema.json +66 -0
- package/schemas/v4/evidence-library.schema.json +42 -0
- package/schemas/v4/living-subscription.schema.json +55 -0
- package/schemas/v4/meta-analysis.schema.json +152 -0
- package/schemas/verdict.schema.json +56 -0
- package/scripts/__init__.py +0 -0
- package/scripts/__pycache__/__init__.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_evaluator.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_judge.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_routing.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_v2.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_v3.cpython-312.pyc +0 -0
- package/scripts/__pycache__/build_result.cpython-312.pyc +0 -0
- package/scripts/__pycache__/claim_audit.cpython-312.pyc +0 -0
- package/scripts/__pycache__/complexity_gate.cpython-312.pyc +0 -0
- package/scripts/__pycache__/compute_confidence.cpython-312.pyc +0 -0
- package/scripts/__pycache__/dashboard_server.cpython-312.pyc +0 -0
- package/scripts/__pycache__/did_regression.cpython-312.pyc +0 -0
- package/scripts/__pycache__/effect_calculator.cpython-312.pyc +0 -0
- package/scripts/__pycache__/evidence_matrix.cpython-312.pyc +0 -0
- package/scripts/__pycache__/evidence_score.cpython-312.pyc +0 -0
- package/scripts/__pycache__/evidence_semantics.cpython-312.pyc +0 -0
- package/scripts/__pycache__/fetch_benchmark.cpython-312.pyc +0 -0
- package/scripts/__pycache__/lint_report_layout.cpython-312.pyc +0 -0
- package/scripts/__pycache__/orchestrator.cpython-312.pyc +0 -0
- package/scripts/__pycache__/pre_verdict_gate.cpython-312.pyc +0 -0
- package/scripts/__pycache__/recompute_demo_quality.cpython-312.pyc +0 -0
- package/scripts/__pycache__/render_report.cpython-312.pyc +0 -0
- package/scripts/__pycache__/render_report_html.cpython-312.pyc +0 -0
- package/scripts/__pycache__/run_workspace.cpython-312.pyc +0 -0
- package/scripts/__pycache__/skill_lint.cpython-312.pyc +0 -0
- package/scripts/__pycache__/startup_probe.cpython-312.pyc +0 -0
- package/scripts/__pycache__/sync_killer_demo_report.cpython-312.pyc +0 -0
- package/scripts/__pycache__/test_adversarial_empirical.cpython-312-pytest-9.0.2.pyc +0 -0
- package/scripts/__pycache__/test_adversarial_empirical.cpython-312-pytest-9.1.1.pyc +0 -0
- package/scripts/__pycache__/validate_schema.cpython-312.pyc +0 -0
- package/scripts/audit_dois.py +292 -0
- package/scripts/bake_pack.sh +37 -0
- package/scripts/benchmark.py +183 -0
- package/scripts/benchmark_evaluator.py +371 -0
- package/scripts/benchmark_judge.py +535 -0
- package/scripts/benchmark_routing.py +120 -0
- package/scripts/benchmark_v2.py +304 -0
- package/scripts/benchmark_v3.py +552 -0
- package/scripts/build_esl_artifacts.py +1921 -0
- package/scripts/build_evidence_library.py +307 -0
- package/scripts/build_killer_demo.py +295 -0
- package/scripts/build_result.py +311 -0
- package/scripts/check_version_consistency.py +96 -0
- package/scripts/citation_check.py +123 -0
- package/scripts/claim_audit.py +157 -0
- package/scripts/complexity_gate.py +180 -0
- package/scripts/compute_confidence.py +176 -0
- package/scripts/dashboard_server.py +536 -0
- package/scripts/did_regression.py +315 -0
- package/scripts/effect_calculator.py +99 -0
- package/scripts/enrich_projects_human_and_lieflat.py +315 -0
- package/scripts/evidence_matrix.py +129 -0
- package/scripts/evidence_score.py +234 -0
- package/scripts/evidence_semantics.py +87 -0
- package/scripts/fetch_benchmark.py +177 -0
- package/scripts/generate_metrics.py +99 -0
- package/scripts/generate_new_projects.py +686 -0
- package/scripts/generate_promo.py +192 -0
- package/scripts/lint_report_layout.py +182 -0
- package/scripts/orchestrator.py +1456 -0
- package/scripts/pre_verdict_gate.py +513 -0
- package/scripts/quickstart.py +121 -0
- package/scripts/rebake_all_5themes.py +88 -0
- package/scripts/recompute_demo_quality.py +205 -0
- package/scripts/render_report.py +270 -0
- package/scripts/render_report_html.py +356 -0
- package/scripts/retraction_watch.py +110 -0
- package/scripts/run_workspace.py +337 -0
- package/scripts/serve_web.py +54 -0
- package/scripts/skill_lint.py +150 -0
- package/scripts/startup_probe.py +265 -0
- package/scripts/sync_killer_demo_report.py +270 -0
- package/scripts/test_adversarial_empirical.py +541 -0
- package/scripts/validate_schema.py +256 -0
- package/skill/agents/education-planner.md +80 -0
- package/skill/agents/evaluation-designer.md +74 -0
- package/skill/agents/evidence-analyst.md +106 -0
- package/skill/agents/evidence-judge.md +111 -0
- package/skill/agents/evidence-retriever.md +80 -0
- package/skill/agents/intervention-designer.md +82 -0
- package/skill/agents/method-reviewer.md +104 -0
- package/skill/agents/skeptic.md +89 -0
- package/skill/sub-skills/aihot-trend-analysis/SKILL.md +31 -0
- package/skill/sub-skills/contradiction-analysis/SKILL.md +17 -0
- package/skill/sub-skills/data-analysis/SKILL.md +23 -0
- package/skill/sub-skills/ethics-review/SKILL.md +25 -0
- package/skill/sub-skills/evidence-extraction/SKILL.md +19 -0
- package/skill/sub-skills/evidence-review/SKILL.md +18 -0
- package/skill/sub-skills/gap-analysis/SKILL.md +25 -0
- package/skill/sub-skills/literature-review/SKILL.md +21 -0
- package/skill/sub-skills/methodology-audit/SKILL.md +20 -0
- package/skill/sub-skills/report-generation/SKILL.md +51 -0
- package/skill/sub-skills/research-planning/SKILL.md +21 -0
- package/skill/sub-skills/study-design/SKILL.md +16 -0
- package/skill/task-briefs/adjudicate.md +17 -0
- package/skill/task-briefs/audit.md +15 -0
- package/skill/task-briefs/challenge.md +15 -0
- package/skill/task-briefs/evaluate.md +13 -0
- package/skill/task-briefs/extract.md +16 -0
- package/skill/task-briefs/frame.md +17 -0
- package/skill/task-briefs/intervene.md +14 -0
- package/skill/task-briefs/present.md +16 -0
- package/skill/task-briefs/retrieve.md +15 -0
- package/visualization/eduevidence-report/assets/base.css +337 -0
- package/visualization/eduevidence-report/motion/motion.css +157 -0
- package/visualization/eduevidence-report/motion/motion.js +107 -0
- package/visualization/eduevidence-report/references/bilingual-style.md +7 -0
- package/visualization/eduevidence-report/references/component-catalog.md +145 -0
- package/visualization/eduevidence-report/references/evidence-expansion.md +65 -0
- package/visualization/eduevidence-report/references/full-report-outline.md +86 -0
- package/visualization/eduevidence-report/references/layout-constraints.md +63 -0
- package/visualization/eduevidence-report/references/lieflat-composition.md +79 -0
- package/visualization/eduevidence-report/references/motion-system.md +31 -0
- package/visualization/eduevidence-report/schemas/adapter-envelope.schema.json +22 -0
- package/visualization/eduevidence-report/schemas/visual-layout.schema.json +87 -0
- package/visualization/eduevidence-report/scripts/__pycache__/adapter_contract.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_artifact_manifest.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_charts.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_figures.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_infographics.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_report.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/charts_data.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/lieflat_engine.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/zh_labels.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/adapter_contract.py +72 -0
- package/visualization/eduevidence-report/scripts/build_artifact_manifest.py +70 -0
- package/visualization/eduevidence-report/scripts/build_charts.py +283 -0
- package/visualization/eduevidence-report/scripts/build_figures.py +515 -0
- package/visualization/eduevidence-report/scripts/build_infographics.py +268 -0
- package/visualization/eduevidence-report/scripts/build_report.py +3211 -0
- package/visualization/eduevidence-report/scripts/charts_data.py +617 -0
- package/visualization/eduevidence-report/scripts/check_mobile_layout.js +220 -0
- package/visualization/eduevidence-report/scripts/lieflat_engine.py +852 -0
- package/visualization/eduevidence-report/scripts/zh_labels.py +245 -0
- package/visualization/eduevidence-report/themes/academic.css +94 -0
- package/visualization/eduevidence-report/themes/claude.css +96 -0
- package/visualization/eduevidence-report/themes/datalab-dark.css +147 -0
- package/visualization/eduevidence-report/themes/datalab.css +151 -0
- package/visualization/eduevidence-report/themes/presentation.css +140 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
"""scripts/dashboard_server.py — EduEvidence 5.0 Local Web Studio.
|
|
2
|
+
|
|
3
|
+
Lightweight static + API server for the 3-page Web Studio (web/):
|
|
4
|
+
|
|
5
|
+
1. 仪表盘 (Dashboard) — cross-project KPIs, effect-size comparison, asset matrix
|
|
6
|
+
2. 报告浏览 (Report Browser) — browse baked report files (default + reports-5themes/* variants)
|
|
7
|
+
in an iframe; themes are fixed at generation time
|
|
8
|
+
3. 数据可视化 (Data Visualization) — per-project forest plot / effect-size distribution /
|
|
9
|
+
outcome-dimension summary / SSOT evidence graph
|
|
10
|
+
|
|
11
|
+
No agent dispatch, no subprocess streaming. Every value is read directly from the
|
|
12
|
+
project artifacts the skill pipeline emits (examples/<id>/result.json +
|
|
13
|
+
evidence_graph.json), so a newly completed project appears automatically.
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
python3 scripts/dashboard_server.py --host 127.0.0.1 --port 8765
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import http.server
|
|
22
|
+
import json
|
|
23
|
+
import sys
|
|
24
|
+
import urllib.parse
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
28
|
+
WEB_DIR = ROOT / "web"
|
|
29
|
+
EXAMPLES_DIR = ROOT / "examples"
|
|
30
|
+
|
|
31
|
+
if str(ROOT) not in sys.path:
|
|
32
|
+
sys.path.insert(0, str(ROOT))
|
|
33
|
+
|
|
34
|
+
from engine.evidence_graph import EvidenceGraph # noqa: E402
|
|
35
|
+
|
|
36
|
+
import sys as _sys
|
|
37
|
+
_VIZ_SCRIPTS = ROOT / "visualization" / "eduevidence-report" / "scripts"
|
|
38
|
+
if str(_VIZ_SCRIPTS) not in _sys.path:
|
|
39
|
+
_sys.path.insert(0, str(_VIZ_SCRIPTS))
|
|
40
|
+
from zh_labels import OUTCOME_ZH, ACTION_ZH, STUDY_ZH, AUTHORITY_ZH, CONFIDENCE_ZH # noqa: E402
|
|
41
|
+
|
|
42
|
+
# Friendly display names for known demo projects; everything else falls back to
|
|
43
|
+
# the question text inside result.json so new projects need no manual entry.
|
|
44
|
+
PROJECT_TITLES: Dict[str, str] = {
|
|
45
|
+
"ai-coding-assistant-evidence": "高校大一引入 AI 编程助手(真实文献旗舰示例)",
|
|
46
|
+
"highschool-math-ai-tutor": "高中数学引入大模型自适应 AI Tutor 评估",
|
|
47
|
+
"esl-academic-writing-ai": "大学 ESL 学术英语写作与同行评审 AI 评估",
|
|
48
|
+
"ai-tutor": "大学高数课程 AI Tutor 评估",
|
|
49
|
+
"ai-writing-assistant": "AI 写作助手评估",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def unquote_path(path: str) -> str:
|
|
54
|
+
"""Decode a URL path once and reject encoded separators / NUL bytes."""
|
|
55
|
+
decoded = urllib.parse.unquote(path)
|
|
56
|
+
if "\x00" in decoded or "\\" in decoded:
|
|
57
|
+
raise ValueError("unsafe path characters")
|
|
58
|
+
return decoded
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _read_json(path: Path) -> Optional[Any]:
|
|
62
|
+
try:
|
|
63
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
64
|
+
except Exception:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _decision_fields(decision: Any) -> tuple[Any, Any]:
|
|
69
|
+
"""Return (verdict, confidence) tolerating the three decision shapes in the wild."""
|
|
70
|
+
if not isinstance(decision, dict):
|
|
71
|
+
return None, None
|
|
72
|
+
verdict = decision.get("verdict") or decision.get("recommended_action") or decision.get("decision")
|
|
73
|
+
confidence = decision.get("confidence_score")
|
|
74
|
+
if not isinstance(confidence, (int, float)):
|
|
75
|
+
confidence = decision.get("confidence")
|
|
76
|
+
return verdict, confidence
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _extract_effect(ev: Dict[str, Any]) -> tuple[Any, Any, Any]:
|
|
80
|
+
"""Return (value, ci_lower, ci_upper) from both nested and flat effect_size shapes."""
|
|
81
|
+
es = ev.get("effect_size")
|
|
82
|
+
if isinstance(es, dict):
|
|
83
|
+
return es.get("value"), (es.get("ci_lower") or es.get("ci_lo")), (es.get("ci_upper") or es.get("ci_hi"))
|
|
84
|
+
value = es if isinstance(es, (int, float)) else None
|
|
85
|
+
if value is None:
|
|
86
|
+
for key in ("hedges_g", "g", "effect_size_value"):
|
|
87
|
+
if isinstance(ev.get(key), (int, float)):
|
|
88
|
+
value = ev[key]
|
|
89
|
+
break
|
|
90
|
+
return value, ev.get("ci_lower"), ev.get("ci_upper")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _graph_node_count(path: Path) -> int:
|
|
94
|
+
"""与 evidence_graph.export_echarts_graph() 同口径的节点数。"""
|
|
95
|
+
try:
|
|
96
|
+
graph = EvidenceGraph.from_json(path.read_text(encoding="utf-8"))
|
|
97
|
+
export = graph.export_echarts_graph()
|
|
98
|
+
nodes = export.get("nodes") or []
|
|
99
|
+
return len(nodes)
|
|
100
|
+
except Exception:
|
|
101
|
+
data = _read_json(path) or {}
|
|
102
|
+
|
|
103
|
+
def _size(key: str) -> int:
|
|
104
|
+
value = data.get(key) or {}
|
|
105
|
+
return len(value) if isinstance(value, dict) else len(value)
|
|
106
|
+
|
|
107
|
+
return _size("papers") + _size("evidence") + _size("claims")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _direction_counts(evidence: list) -> Dict[str, int]:
|
|
111
|
+
"""按 evidence 的 relation_to_claim / direction / effect_direction 聚合。"""
|
|
112
|
+
counts = {"support": 0, "contradict": 0, "neutral": 0}
|
|
113
|
+
for ev in evidence:
|
|
114
|
+
if not isinstance(ev, dict):
|
|
115
|
+
continue
|
|
116
|
+
d = (ev.get("relation_to_claim") or ev.get("direction")
|
|
117
|
+
or ev.get("effect_direction") or "").lower()
|
|
118
|
+
if d in ("support", "supports", "positive", "pos"):
|
|
119
|
+
counts["support"] += 1
|
|
120
|
+
elif d in ("contradict", "contradicts", "negative", "neg"):
|
|
121
|
+
counts["contradict"] += 1
|
|
122
|
+
else:
|
|
123
|
+
counts["neutral"] += 1
|
|
124
|
+
return counts
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _outcome_rollup(evidence: list) -> List[Dict[str, Any]]:
|
|
128
|
+
"""无 outcome_mapping 时按 outcome_type 聚合方向计数(回退链)。"""
|
|
129
|
+
roll: Dict[str, Dict[str, int]] = {}
|
|
130
|
+
for ev in evidence:
|
|
131
|
+
if not isinstance(ev, dict):
|
|
132
|
+
continue
|
|
133
|
+
ot = ev.get("outcome_type") or ev.get("outcome") or "other"
|
|
134
|
+
d = (ev.get("relation_to_claim") or ev.get("direction")
|
|
135
|
+
or ev.get("effect_direction") or "").lower()
|
|
136
|
+
bucket = roll.setdefault(ot, {"support": 0, "contradict": 0, "neutral": 0})
|
|
137
|
+
if d in ("support", "supports", "positive", "pos"):
|
|
138
|
+
bucket["support"] += 1
|
|
139
|
+
elif d in ("contradict", "contradicts", "negative", "neg"):
|
|
140
|
+
bucket["contradict"] += 1
|
|
141
|
+
else:
|
|
142
|
+
bucket["neutral"] += 1
|
|
143
|
+
return [{"outcome_type": k, **v} for k, v in roll.items()]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _known_project_ids() -> set:
|
|
147
|
+
"""Project ids = directories with a real result.json (not the deduped display list)."""
|
|
148
|
+
if not EXAMPLES_DIR.exists():
|
|
149
|
+
return set()
|
|
150
|
+
return {d.name for d in EXAMPLES_DIR.iterdir()
|
|
151
|
+
if d.is_dir() and (d / "result.json").exists()}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def scan_local_projects() -> List[Dict[str, Any]]:
|
|
155
|
+
projects: List[Dict[str, Any]] = []
|
|
156
|
+
if not EXAMPLES_DIR.exists():
|
|
157
|
+
return projects
|
|
158
|
+
|
|
159
|
+
for proj_dir in sorted(EXAMPLES_DIR.iterdir()):
|
|
160
|
+
if not proj_dir.is_dir():
|
|
161
|
+
continue
|
|
162
|
+
result_path = proj_dir / "result.json"
|
|
163
|
+
if not result_path.exists():
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
result = _read_json(result_path) or {}
|
|
167
|
+
meta = result.get("meta") or {}
|
|
168
|
+
frame = result.get("research_frame") or {}
|
|
169
|
+
question = meta.get("question") or frame.get("question") or ""
|
|
170
|
+
verdict, confidence = _decision_fields(result.get("decision"))
|
|
171
|
+
|
|
172
|
+
evidence = result.get("evidence") or []
|
|
173
|
+
forest = result.get("forest_plot_data") or []
|
|
174
|
+
effect_values: List[float] = []
|
|
175
|
+
for ev in evidence:
|
|
176
|
+
value, _, _ = _extract_effect(ev)
|
|
177
|
+
if isinstance(value, (int, float)):
|
|
178
|
+
effect_values.append(float(value))
|
|
179
|
+
for f in forest:
|
|
180
|
+
if isinstance(f.get("effect_size"), (int, float)):
|
|
181
|
+
effect_values.append(float(f["effect_size"]))
|
|
182
|
+
|
|
183
|
+
graph_path = proj_dir / "evidence_graph.json"
|
|
184
|
+
html_path = proj_dir / "EduEvidence_Report.html"
|
|
185
|
+
report_variants: List[Dict[str, str]] = []
|
|
186
|
+
themes_dir = proj_dir / "reports-5themes"
|
|
187
|
+
if themes_dir.is_dir():
|
|
188
|
+
for variant_file in sorted(list(themes_dir.glob("EduEvidence_Report_*.html")) + list(themes_dir.glob("report_*.html"))):
|
|
189
|
+
theme_name = variant_file.stem.replace("EduEvidence_Report_", "").replace("report_", "")
|
|
190
|
+
if not any(v["theme"] == theme_name for v in report_variants):
|
|
191
|
+
report_variants.append({
|
|
192
|
+
"theme": theme_name,
|
|
193
|
+
"path": str(variant_file),
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
zh_result = _read_json(proj_dir / "result.zh.json") or {}
|
|
197
|
+
zh_question = ((zh_result.get("meta") or {}).get("question")
|
|
198
|
+
or (zh_result.get("research_frame") or {}).get("question") or "")
|
|
199
|
+
|
|
200
|
+
projects.append({
|
|
201
|
+
"id": proj_dir.name,
|
|
202
|
+
"title": PROJECT_TITLES.get(proj_dir.name) or (zh_question[:72] if zh_question else (question[:72] or proj_dir.name)),
|
|
203
|
+
"title_zh": zh_question[:72] if zh_question else None,
|
|
204
|
+
"domain": meta.get("domain") or "education",
|
|
205
|
+
"question": question,
|
|
206
|
+
"verdict": verdict,
|
|
207
|
+
"confidence": confidence,
|
|
208
|
+
"evidence_count": len(evidence) if isinstance(evidence, list) else 0,
|
|
209
|
+
"forest_count": len(forest) if isinstance(forest, list) else 0,
|
|
210
|
+
"effect_count": len(effect_values),
|
|
211
|
+
"mean_effect_size": round(sum(effect_values) / len(effect_values), 3) if effect_values else None,
|
|
212
|
+
"direction_counts": _direction_counts(evidence) if isinstance(evidence, list) else
|
|
213
|
+
{"support": 0, "contradict": 0, "neutral": 0},
|
|
214
|
+
"has_graph": graph_path.exists(),
|
|
215
|
+
"node_count": _graph_node_count(graph_path) if graph_path.exists() else 0,
|
|
216
|
+
"html_report_path": str(html_path) if html_path.exists() else None,
|
|
217
|
+
"report_variants": report_variants,
|
|
218
|
+
})
|
|
219
|
+
# Canonical question deduplication: keep the project with the highest evidence count
|
|
220
|
+
deduped: Dict[str, Dict[str, Any]] = {}
|
|
221
|
+
for p in projects:
|
|
222
|
+
norm_q = (p.get("question") or p["id"]).strip().lower()
|
|
223
|
+
if norm_q not in deduped or (p["evidence_count"] > deduped[norm_q]["evidence_count"]):
|
|
224
|
+
deduped[norm_q] = p
|
|
225
|
+
return list(deduped.values())
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def build_stats(projects: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
229
|
+
return {
|
|
230
|
+
"total_projects": len(projects),
|
|
231
|
+
"total_evidence": sum(p["evidence_count"] for p in projects),
|
|
232
|
+
"total_effect_sizes": sum(p["effect_count"] for p in projects),
|
|
233
|
+
"total_nodes": sum(p["node_count"] for p in projects),
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def get_aggregate_stats(projects: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
238
|
+
"""V2 兼容封装(旧 API/测试沿用):build_stats 字段。
|
|
239
|
+
|
|
240
|
+
Token / 成本矩阵已移除(provenance 纠偏):历史版本在此返回硬编码的
|
|
241
|
+
假 token 用量与模型成本,没有任何真实运行记录支撑。运行时未采集
|
|
242
|
+
usage 前,这些指标一律如实标注 NOT_CAPTURED,不再虚构数值。
|
|
243
|
+
"""
|
|
244
|
+
stats = build_stats(projects)
|
|
245
|
+
stats["total_tokens"] = None
|
|
246
|
+
stats["usage_measurement_status"] = "NOT_CAPTURED"
|
|
247
|
+
return stats
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def build_viz_payload(proj_id: str) -> Dict[str, Any]:
|
|
251
|
+
proj_dir = EXAMPLES_DIR / proj_id
|
|
252
|
+
result = _read_json(proj_dir / "result.json") or {}
|
|
253
|
+
meta = result.get("meta") or {}
|
|
254
|
+
frame = result.get("research_frame") or {}
|
|
255
|
+
question = meta.get("question") or frame.get("question") or ""
|
|
256
|
+
verdict, confidence = _decision_fields(result.get("decision"))
|
|
257
|
+
title = PROJECT_TITLES.get(proj_id) or question[:72] or proj_id
|
|
258
|
+
|
|
259
|
+
forest_items: List[Dict[str, Any]] = []
|
|
260
|
+
for f in (result.get("forest_plot_data") or []):
|
|
261
|
+
forest_items.append({
|
|
262
|
+
"study_label": f.get("study_label") or f.get("evidence_id") or "",
|
|
263
|
+
"venue": f.get("venue") or "",
|
|
264
|
+
"outcome_dimension": f.get("outcome_dimension") or f.get("outcome_metric") or "",
|
|
265
|
+
"effect_size": f.get("effect_size"),
|
|
266
|
+
"ci_lower": f.get("ci_lower"),
|
|
267
|
+
"ci_upper": f.get("ci_upper"),
|
|
268
|
+
"sample_size": f.get("sample_size"),
|
|
269
|
+
"direction": f.get("direction"),
|
|
270
|
+
"wwc_rating": f.get("wwc_rating") or "",
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
effect_items: List[Dict[str, Any]] = []
|
|
274
|
+
for ev in (result.get("evidence") or []):
|
|
275
|
+
value, lo, hi = _extract_effect(ev)
|
|
276
|
+
if isinstance(value, (int, float)):
|
|
277
|
+
effect_items.append({
|
|
278
|
+
"study_label": ev.get("study_label") or ev.get("title") or ev.get("evidence_id") or "",
|
|
279
|
+
"value": value,
|
|
280
|
+
"ci_lower": lo,
|
|
281
|
+
"ci_upper": hi,
|
|
282
|
+
"outcome_dimension": ev.get("outcome_dimension") or ev.get("outcome_type") or "",
|
|
283
|
+
"direction": ev.get("relation_to_claim") or ev.get("direction") or ev.get("effect_direction") or "",
|
|
284
|
+
})
|
|
285
|
+
if not effect_items:
|
|
286
|
+
for f in (result.get("forest_plot_data") or []):
|
|
287
|
+
if isinstance(f.get("effect_size"), (int, float)):
|
|
288
|
+
effect_items.append({
|
|
289
|
+
"study_label": f.get("study_label") or f.get("evidence_id") or "",
|
|
290
|
+
"value": f["effect_size"],
|
|
291
|
+
"ci_lower": f.get("ci_lower"),
|
|
292
|
+
"ci_upper": f.get("ci_upper"),
|
|
293
|
+
"outcome_dimension": f.get("outcome_dimension") or f.get("outcome_metric") or "",
|
|
294
|
+
"direction": f.get("direction"),
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
evidence_list = result.get("evidence") or []
|
|
298
|
+
outcome_items: List[Dict[str, Any]] = []
|
|
299
|
+
om = result.get("outcome_mapping") or {}
|
|
300
|
+
entries = (om.get("entries") or []) if isinstance(om, dict) else (om if isinstance(om, list) else [])
|
|
301
|
+
if entries:
|
|
302
|
+
for e in entries:
|
|
303
|
+
outcome_items.append({
|
|
304
|
+
"outcome_type": e.get("outcome_type") or e.get("outcome") or "",
|
|
305
|
+
"status": e.get("status") or "",
|
|
306
|
+
"support_count": e.get("support_count") or 0,
|
|
307
|
+
"contradict_count": e.get("contradict_count") or 0,
|
|
308
|
+
"neutral_count": e.get("neutral_count") or 0,
|
|
309
|
+
})
|
|
310
|
+
else:
|
|
311
|
+
# 回退链:无 outcome_mapping 时按 evidence 方向聚合
|
|
312
|
+
outcome_items = _outcome_rollup(evidence_list)
|
|
313
|
+
|
|
314
|
+
# 方向分布(任何课题都有值)
|
|
315
|
+
direction_counts = _direction_counts(evidence_list) if isinstance(evidence_list, list) else \
|
|
316
|
+
{"support": 0, "contradict": 0, "neutral": 0}
|
|
317
|
+
|
|
318
|
+
graph = None
|
|
319
|
+
graph_path = proj_dir / "evidence_graph.json"
|
|
320
|
+
if graph_path.exists():
|
|
321
|
+
try:
|
|
322
|
+
graph = EvidenceGraph.from_json(graph_path.read_text(encoding="utf-8")).export_echarts_graph()
|
|
323
|
+
except Exception:
|
|
324
|
+
graph = None
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
"id": proj_id,
|
|
328
|
+
"title": title,
|
|
329
|
+
"question": question,
|
|
330
|
+
"verdict": verdict,
|
|
331
|
+
"confidence": confidence,
|
|
332
|
+
"forest": forest_items,
|
|
333
|
+
"effect_sizes": effect_items,
|
|
334
|
+
"outcome_mapping": outcome_items,
|
|
335
|
+
"direction_counts": direction_counts,
|
|
336
|
+
"labels": {
|
|
337
|
+
"outcomes": OUTCOME_ZH,
|
|
338
|
+
"actions": ACTION_ZH,
|
|
339
|
+
"studies": STUDY_ZH,
|
|
340
|
+
"authority": AUTHORITY_ZH,
|
|
341
|
+
"confidence": CONFIDENCE_ZH,
|
|
342
|
+
},
|
|
343
|
+
"graph": graph,
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class StudioHandler(http.server.SimpleHTTPRequestHandler):
|
|
354
|
+
"""Serves web/ statically and exposes the artifact JSON API."""
|
|
355
|
+
|
|
356
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
357
|
+
super().__init__(*args, directory=str(WEB_DIR), **kwargs)
|
|
358
|
+
|
|
359
|
+
# -- response helpers -------------------------------------------------
|
|
360
|
+
def _send_bytes(self, data: bytes, content_type: str, status: int = 200,
|
|
361
|
+
extra_headers: Optional[Dict[str, str]] = None) -> None:
|
|
362
|
+
self.send_response(status)
|
|
363
|
+
self.send_header("Content-Type", content_type)
|
|
364
|
+
self.send_header("Content-Length", str(len(data)))
|
|
365
|
+
# 允许落地页(独立静态源)跨源探测/拉取只读 API 与控制台资源
|
|
366
|
+
self.send_header("Access-Control-Allow-Origin", "*")
|
|
367
|
+
if extra_headers:
|
|
368
|
+
for name, value in extra_headers.items():
|
|
369
|
+
self.send_header(name, value)
|
|
370
|
+
self.end_headers()
|
|
371
|
+
self.wfile.write(data)
|
|
372
|
+
|
|
373
|
+
def _send_report_bytes(self, data: bytes) -> None:
|
|
374
|
+
# Reports are static, self-contained documents: no remote origins, no
|
|
375
|
+
# same-origin access; sandbox-friendly CSP + X-Content-Type-Options.
|
|
376
|
+
self._send_bytes(data, "text/html; charset=utf-8", extra_headers={
|
|
377
|
+
"Content-Security-Policy":
|
|
378
|
+
"default-src 'none'; script-src 'unsafe-inline'; "
|
|
379
|
+
"style-src 'unsafe-inline'; img-src data:; "
|
|
380
|
+
"font-src data:; connect-src 'none'; frame-ancestors 'self'",
|
|
381
|
+
"X-Content-Type-Options": "nosniff",
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
def _send_json(self, payload: Any, status: int = 200) -> None:
|
|
385
|
+
self._send_bytes(json.dumps(payload, ensure_ascii=False).encode("utf-8"),
|
|
386
|
+
"application/json; charset=utf-8", status)
|
|
387
|
+
|
|
388
|
+
def _serve_file(self, path: Path, content_type: str) -> None:
|
|
389
|
+
if path.exists():
|
|
390
|
+
self._send_bytes(path.read_bytes(), content_type)
|
|
391
|
+
else:
|
|
392
|
+
self._send_json({"error": "not found"}, 404)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
# -- POST: disabled (read-only Studio) --------------------------------
|
|
396
|
+
def do_POST(self) -> None:
|
|
397
|
+
self._send_json({"error": "method not allowed"}, 405)
|
|
398
|
+
|
|
399
|
+
# -- GET --------------------------------------------------------------
|
|
400
|
+
def do_GET(self) -> None:
|
|
401
|
+
parsed = urllib.parse.urlparse(self.path)
|
|
402
|
+
path = parsed.path
|
|
403
|
+
query = urllib.parse.parse_qs(parsed.query)
|
|
404
|
+
|
|
405
|
+
# Only the three-page Studio entry + the read-only artifact API.
|
|
406
|
+
if path in ("/", "/index.html", "/dashboard", "/studio", "/console"):
|
|
407
|
+
self._serve_file(WEB_DIR / "index.html", "text/html; charset=utf-8")
|
|
408
|
+
return
|
|
409
|
+
|
|
410
|
+
if path == "/api/projects":
|
|
411
|
+
projects = scan_local_projects()
|
|
412
|
+
self._send_json({"projects": projects, "stats": build_stats(projects)})
|
|
413
|
+
return
|
|
414
|
+
|
|
415
|
+
if path == "/api/labels":
|
|
416
|
+
self._send_json({
|
|
417
|
+
"outcomes": OUTCOME_ZH,
|
|
418
|
+
"actions": ACTION_ZH,
|
|
419
|
+
"studies": STUDY_ZH,
|
|
420
|
+
"authority": AUTHORITY_ZH,
|
|
421
|
+
"confidence": CONFIDENCE_ZH,
|
|
422
|
+
})
|
|
423
|
+
return
|
|
424
|
+
|
|
425
|
+
if path.startswith("/api/projects/"):
|
|
426
|
+
suffix = urllib.parse.unquote(path[len("/api/projects/"):]).rstrip("/")
|
|
427
|
+
if suffix.endswith("/viz"):
|
|
428
|
+
suffix = suffix[:-len("/viz")].rstrip("/")
|
|
429
|
+
if suffix not in _known_project_ids():
|
|
430
|
+
self._send_json({"error": "unknown project"}, 404)
|
|
431
|
+
return
|
|
432
|
+
self._send_json(build_viz_payload(suffix))
|
|
433
|
+
return
|
|
434
|
+
self._send_json({"error": "not found"}, 404)
|
|
435
|
+
return
|
|
436
|
+
|
|
437
|
+
if path == "/report":
|
|
438
|
+
self._serve_report(query)
|
|
439
|
+
return
|
|
440
|
+
|
|
441
|
+
# Static assets only from web/ (allowlisted), with URL-decode once and
|
|
442
|
+
# containment; everything else is a JSON 404 (never super().do_GET()).
|
|
443
|
+
if path.startswith("/js/") or path.startswith("/css/") or path == "/styles.css":
|
|
444
|
+
self._serve_web_asset(path)
|
|
445
|
+
return
|
|
446
|
+
|
|
447
|
+
self._send_json({"error": "not found"}, 404)
|
|
448
|
+
|
|
449
|
+
def _serve_web_asset(self, path: str) -> None:
|
|
450
|
+
"""Serve a static asset strictly inside WEB_DIR (decode once, no escapes)."""
|
|
451
|
+
try:
|
|
452
|
+
candidate = (WEB_DIR / unquote_path(path).lstrip("/")).resolve()
|
|
453
|
+
except (ValueError, OSError):
|
|
454
|
+
self._send_json({"error": "not found"}, 404)
|
|
455
|
+
return
|
|
456
|
+
base = WEB_DIR.resolve()
|
|
457
|
+
if base != candidate and base not in candidate.parents:
|
|
458
|
+
self._send_json({"error": "not found"}, 404)
|
|
459
|
+
return
|
|
460
|
+
if not candidate.is_file() or candidate.suffix not in (".js", ".css"):
|
|
461
|
+
self._send_json({"error": "not found"}, 404)
|
|
462
|
+
return
|
|
463
|
+
ctype = "application/javascript; charset=utf-8" if candidate.suffix == ".js" \
|
|
464
|
+
else "text/css; charset=utf-8"
|
|
465
|
+
self._send_bytes(candidate.read_bytes(), ctype)
|
|
466
|
+
|
|
467
|
+
def _serve_report(self, query: Dict[str, List[str]]) -> None:
|
|
468
|
+
"""Serve a baked report for a known project and explicit theme."""
|
|
469
|
+
proj_id = query.get("id", [""])[0]
|
|
470
|
+
theme = query.get("theme", ["default"])[0]
|
|
471
|
+
|
|
472
|
+
if proj_id not in _known_project_ids():
|
|
473
|
+
self._send_json({"error": "unknown project"}, 404)
|
|
474
|
+
return
|
|
475
|
+
|
|
476
|
+
proj_dir = EXAMPLES_DIR / proj_id
|
|
477
|
+
if theme == "default":
|
|
478
|
+
html_path = proj_dir / "EduEvidence_Report.html"
|
|
479
|
+
if not html_path.is_file():
|
|
480
|
+
self._send_json({"error": "report not found"}, 404)
|
|
481
|
+
return
|
|
482
|
+
else:
|
|
483
|
+
if not theme or not all(c.isalnum() or c in "-_" for c in theme):
|
|
484
|
+
self._send_json({"error": "unknown report theme"}, 404)
|
|
485
|
+
return
|
|
486
|
+
variants_dir = proj_dir / "reports-5themes"
|
|
487
|
+
html_path = variants_dir / f"EduEvidence_Report_{theme}.html"
|
|
488
|
+
if (not html_path.is_file() or
|
|
489
|
+
html_path.resolve().parent != variants_dir.resolve()):
|
|
490
|
+
self._send_json({"error": "unknown report theme"}, 404)
|
|
491
|
+
return
|
|
492
|
+
|
|
493
|
+
self._send_report_bytes(html_path.read_bytes())
|
|
494
|
+
|
|
495
|
+
def run_dashboard_server(host: str = "127.0.0.1", port: int = 8765) -> None:
|
|
496
|
+
server = None
|
|
497
|
+
actual_port = port
|
|
498
|
+
for offset in range(10):
|
|
499
|
+
try:
|
|
500
|
+
candidate = port + offset
|
|
501
|
+
server = http.server.ThreadingHTTPServer((host, candidate), StudioHandler)
|
|
502
|
+
server.allow_reuse_address = True
|
|
503
|
+
actual_port = candidate
|
|
504
|
+
break
|
|
505
|
+
except OSError as e:
|
|
506
|
+
if e.errno == 48:
|
|
507
|
+
continue
|
|
508
|
+
raise
|
|
509
|
+
|
|
510
|
+
if server is None:
|
|
511
|
+
print(f"❌ 端口 {port}-{port + 9} 均被占用。")
|
|
512
|
+
return
|
|
513
|
+
|
|
514
|
+
print("============================================================")
|
|
515
|
+
print(f"🚀 EduEvidence Web Studio running at http://{host}:{actual_port}/")
|
|
516
|
+
print(" 📊 仪表盘 /dashboard")
|
|
517
|
+
print(" 📄 报告浏览 /report")
|
|
518
|
+
print(" 📈 数据可视化 /#viz (Web UI)")
|
|
519
|
+
print(" 📦 数据契约 /api/projects · /api/projects/<id>/viz")
|
|
520
|
+
print("============================================================")
|
|
521
|
+
try:
|
|
522
|
+
server.serve_forever()
|
|
523
|
+
except KeyboardInterrupt:
|
|
524
|
+
server.server_close()
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def main() -> None:
|
|
528
|
+
parser = argparse.ArgumentParser()
|
|
529
|
+
parser.add_argument("--host", default="127.0.0.1")
|
|
530
|
+
parser.add_argument("--port", type=int, default=8765)
|
|
531
|
+
args = parser.parse_args()
|
|
532
|
+
run_dashboard_server(args.host, args.port)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
if __name__ == "__main__":
|
|
536
|
+
main()
|