更新: 11 个文件 - 2026-03-17 21:30:02
这个提交包含在:
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"generated_at": "2026-03-18T04:06:37+00:00",
|
"generated_at": "2026-03-18T04:21:45+00:00",
|
||||||
"title": "\u5f53\u524d\u67b6\u6784\u5e93",
|
"title": "\u5f53\u524d\u67b6\u6784\u5e93",
|
||||||
"summary": "\u5de5\u4f5c\u53f0\u3001\u63a7\u5236\u9762\u3001\u6570\u636e\u5c42\u3001\u6388\u6743\u8fb9\u754c\u4e0e\u7cfb\u7edf\u8986\u76d6\u7684\u5f53\u524d\u771f\u503c\u89c6\u56fe\u3002",
|
"summary": "\u5de5\u4f5c\u53f0\u3001\u63a7\u5236\u9762\u3001\u6570\u636e\u5c42\u3001\u6388\u6743\u8fb9\u754c\u4e0e\u7cfb\u7edf\u8986\u76d6\u7684\u5f53\u524d\u771f\u503c\u89c6\u56fe\u3002",
|
||||||
"sections": [
|
"sections": [
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "\u751f\u6210\u65f6\u95f4",
|
"label": "\u751f\u6210\u65f6\u95f4",
|
||||||
"value": "2026-03-18T04:06:37+00:00"
|
"value": "2026-03-18T04:21:45+00:00"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links": [
|
"links": [
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
{
|
||||||
|
"generated_at": "2026-03-18T04:21:45+00:00",
|
||||||
|
"advisory_total": 89,
|
||||||
|
"latest_statuses": {
|
||||||
|
"verified-real": 89
|
||||||
|
},
|
||||||
|
"historical_statuses": {
|
||||||
|
"verified-real": 136,
|
||||||
|
"blocked-artifact": 3,
|
||||||
|
"triage-manual": 1
|
||||||
|
},
|
||||||
|
"verified_real": 89,
|
||||||
|
"verified_synthetic": 0,
|
||||||
|
"blocked": 0,
|
||||||
|
"manual": 0,
|
||||||
|
"verified_ratio": 100.0,
|
||||||
|
"complete": true,
|
||||||
|
"systems": [
|
||||||
|
{
|
||||||
|
"system_id": "gitea",
|
||||||
|
"display_name": "Gitea",
|
||||||
|
"total": 37,
|
||||||
|
"verified_real": 37,
|
||||||
|
"verified_synthetic": 0,
|
||||||
|
"blocked": 0,
|
||||||
|
"manual": 0,
|
||||||
|
"families": [
|
||||||
|
{
|
||||||
|
"family": "authz-bypass",
|
||||||
|
"total": 3,
|
||||||
|
"verified_real": 3,
|
||||||
|
"verified_synthetic": 0,
|
||||||
|
"blocked": 0,
|
||||||
|
"manual": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "file-upload",
|
||||||
|
"total": 2,
|
||||||
|
"verified_real": 2,
|
||||||
|
"verified_synthetic": 0,
|
||||||
|
"blocked": 0,
|
||||||
|
"manual": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "proxy-boundary",
|
||||||
|
"total": 26,
|
||||||
|
"verified_real": 26,
|
||||||
|
"verified_synthetic": 0,
|
||||||
|
"blocked": 0,
|
||||||
|
"manual": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "ssrf",
|
||||||
|
"total": 1,
|
||||||
|
"verified_real": 1,
|
||||||
|
"verified_synthetic": 0,
|
||||||
|
"blocked": 0,
|
||||||
|
"manual": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "xss",
|
||||||
|
"total": 5,
|
||||||
|
"verified_real": 5,
|
||||||
|
"verified_synthetic": 0,
|
||||||
|
"blocked": 0,
|
||||||
|
"manual": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system_id": "nextjs",
|
||||||
|
"display_name": "Next.js",
|
||||||
|
"total": 26,
|
||||||
|
"verified_real": 26,
|
||||||
|
"verified_synthetic": 0,
|
||||||
|
"blocked": 0,
|
||||||
|
"manual": 0,
|
||||||
|
"families": [
|
||||||
|
{
|
||||||
|
"family": "authz-bypass",
|
||||||
|
"total": 2,
|
||||||
|
"verified_real": 2,
|
||||||
|
"verified_synthetic": 0,
|
||||||
|
"blocked": 0,
|
||||||
|
"manual": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "deserialization",
|
||||||
|
"total": 1,
|
||||||
|
"verified_real": 1,
|
||||||
|
"verified_synthetic": 0,
|
||||||
|
"blocked": 0,
|
||||||
|
"manual": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "proxy-boundary",
|
||||||
|
"total": 19,
|
||||||
|
"verified_real": 19,
|
||||||
|
"verified_synthetic": 0,
|
||||||
|
"blocked": 0,
|
||||||
|
"manual": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "ssrf",
|
||||||
|
"total": 2,
|
||||||
|
"verified_real": 2,
|
||||||
|
"verified_synthetic": 0,
|
||||||
|
"blocked": 0,
|
||||||
|
"manual": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "xss",
|
||||||
|
"total": 2,
|
||||||
|
"verified_real": 2,
|
||||||
|
"verified_synthetic": 0,
|
||||||
|
"blocked": 0,
|
||||||
|
"manual": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system_id": "undici",
|
||||||
|
"display_name": "Undici",
|
||||||
|
"total": 14,
|
||||||
|
"verified_real": 14,
|
||||||
|
"verified_synthetic": 0,
|
||||||
|
"blocked": 0,
|
||||||
|
"manual": 0,
|
||||||
|
"families": [
|
||||||
|
{
|
||||||
|
"family": "ssrf",
|
||||||
|
"total": 14,
|
||||||
|
"verified_real": 14,
|
||||||
|
"verified_synthetic": 0,
|
||||||
|
"blocked": 0,
|
||||||
|
"manual": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system_id": "vite",
|
||||||
|
"display_name": "Vite",
|
||||||
|
"total": 12,
|
||||||
|
"verified_real": 12,
|
||||||
|
"verified_synthetic": 0,
|
||||||
|
"blocked": 0,
|
||||||
|
"manual": 0,
|
||||||
|
"families": [
|
||||||
|
{
|
||||||
|
"family": "proxy-boundary",
|
||||||
|
"total": 11,
|
||||||
|
"verified_real": 11,
|
||||||
|
"verified_synthetic": 0,
|
||||||
|
"blocked": 0,
|
||||||
|
"manual": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "xss",
|
||||||
|
"total": 1,
|
||||||
|
"verified_real": 1,
|
||||||
|
"verified_synthetic": 0,
|
||||||
|
"blocked": 0,
|
||||||
|
"manual": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ingest_health": {
|
||||||
|
"failure_count": 0,
|
||||||
|
"failures": []
|
||||||
|
},
|
||||||
|
"historical_blockers": [
|
||||||
|
"Docker daemon unavailable caused provision-compose-environment blocked-artifact.",
|
||||||
|
"Family profiles previously used note-only attack runners and dry-run placeholders.",
|
||||||
|
"Baseline and browser steps were skipped when environment readiness was not enforced.",
|
||||||
|
"Latest completeness now uses one advisory -> latest run semantics instead of historical run piles."
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
<h1>当前架构库镜像</h1>
|
<h1>当前架构库镜像</h1>
|
||||||
<div class="meta">工作台内置镜像页:当前架构库结构化数据镜像。</div>
|
<div class="meta">工作台内置镜像页:当前架构库结构化数据镜像。</div>
|
||||||
<pre>{
|
<pre>{
|
||||||
"generated_at": "2026-03-18T04:06:37+00:00",
|
"generated_at": "2026-03-18T04:21:45+00:00",
|
||||||
"title": "当前架构库",
|
"title": "当前架构库",
|
||||||
"summary": "工作台、控制面、数据层、授权边界与系统覆盖的当前真值视图。",
|
"summary": "工作台、控制面、数据层、授权边界与系统覆盖的当前真值视图。",
|
||||||
"sections": [
|
"sections": [
|
||||||
@@ -137,7 +137,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "生成时间",
|
"label": "生成时间",
|
||||||
"value": "2026-03-18T04:06:37+00:00"
|
"value": "2026-03-18T04:21:45+00:00"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links": [
|
"links": [
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
<div class="meta">工作台内置镜像页:89 条 advisory 最新完整度、family 矩阵与 ingest 健康度。</div>
|
<div class="meta">工作台内置镜像页:89 条 advisory 最新完整度、family 矩阵与 ingest 健康度。</div>
|
||||||
<pre># 全库 Advisory 完整度报告
|
<pre># 全库 Advisory 完整度报告
|
||||||
|
|
||||||
- 生成时间: `2026-03-18T04:06:37+00:00`
|
- 生成时间: `2026-03-18T04:21:45+00:00`
|
||||||
- 最新 advisory 完整度: `89/89` `verified-real`
|
- 最新 advisory 完整度: `89/89` `verified-real`
|
||||||
- 合成验证数量: `0`
|
- 合成验证数量: `0`
|
||||||
- 阻塞数量: `0`
|
- 阻塞数量: `0`
|
||||||
@@ -113,14 +113,7 @@
|
|||||||
|
|
||||||
## Ingest / Source 健康度
|
## Ingest / Source 健康度
|
||||||
|
|
||||||
- source failures: `7`
|
- source failures: `0`
|
||||||
- wordpress::NVD WordPress::SSLError
|
|
||||||
- wordpress::WPScan Vulnerability Database::SSLError
|
|
||||||
- wordpress::PortSwigger Research::SSLError
|
|
||||||
- magento-open-source::Magento GitHub Advisories::SSLError
|
|
||||||
- nodejs::Node.js Security Releases::SSLError
|
|
||||||
- nginx::NGINX Security Advisories::SSLError
|
|
||||||
- gitea::GitHub Gitea Advisories::SSLError
|
|
||||||
|
|
||||||
## 剩余风险说明
|
## 剩余风险说明
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"generated_at": "2026-03-18T04:06:37+00:00",
|
"generated_at": "2026-03-18T04:21:45+00:00",
|
||||||
"advisory_count": 89,
|
"advisory_count": 89,
|
||||||
"run_count": 140,
|
"run_count": 140,
|
||||||
"statuses": {
|
"statuses": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 最新同步摘要
|
# 最新同步摘要
|
||||||
|
|
||||||
- 渲染时间: `2026-03-18T04:06:29+00:00`
|
- 渲染时间: `2026-03-18T04:19:52+00:00`
|
||||||
- 系统数量: `62`
|
- 系统数量: `62`
|
||||||
- Advisory 数量: `89`
|
- Advisory 数量: `89`
|
||||||
- 重点 Markdown 数量: `89`
|
- 重点 Markdown 数量: `89`
|
||||||
@@ -8,14 +8,4 @@
|
|||||||
- 新增记录: `0`
|
- 新增记录: `0`
|
||||||
- 更新记录: `0`
|
- 更新记录: `0`
|
||||||
- Triage 数量: `0`
|
- Triage 数量: `0`
|
||||||
- 失败的 source adapter: `7`
|
- 失败的 source adapter: `0`
|
||||||
|
|
||||||
## 失败列表
|
|
||||||
|
|
||||||
- wordpress::NVD WordPress::SSLError
|
|
||||||
- wordpress::WPScan Vulnerability Database::SSLError
|
|
||||||
- wordpress::PortSwigger Research::SSLError
|
|
||||||
- magento-open-source::Magento GitHub Advisories::SSLError
|
|
||||||
- nodejs::Node.js Security Releases::SSLError
|
|
||||||
- nginx::NGINX Security Advisories::SSLError
|
|
||||||
- gitea::GitHub Gitea Advisories::SSLError
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"generated_at": "2026-03-18T04:06:29+00:00",
|
"generated_at": "2026-03-18T04:19:52+00:00",
|
||||||
"system_count": 62,
|
"system_count": 62,
|
||||||
"advisory_count": 89,
|
"advisory_count": 89,
|
||||||
"markdown_count": 89,
|
"markdown_count": 89,
|
||||||
@@ -8,13 +8,5 @@
|
|||||||
"systems_touched": [],
|
"systems_touched": [],
|
||||||
"triage_count": 0,
|
"triage_count": 0,
|
||||||
"run_bundle_count": 89,
|
"run_bundle_count": 89,
|
||||||
"failures": [
|
"failures": []
|
||||||
"wordpress::NVD WordPress::SSLError",
|
|
||||||
"wordpress::WPScan Vulnerability Database::SSLError",
|
|
||||||
"wordpress::PortSwigger Research::SSLError",
|
|
||||||
"magento-open-source::Magento GitHub Advisories::SSLError",
|
|
||||||
"nodejs::Node.js Security Releases::SSLError",
|
|
||||||
"nginx::NGINX Security Advisories::SSLError",
|
|
||||||
"gitea::GitHub Gitea Advisories::SSLError"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 全库 Advisory 完整度报告
|
# 全库 Advisory 完整度报告
|
||||||
|
|
||||||
- 生成时间: `2026-03-18T04:06:37+00:00`
|
- 生成时间: `2026-03-18T04:21:45+00:00`
|
||||||
- 最新 advisory 完整度: `89/89` `verified-real`
|
- 最新 advisory 完整度: `89/89` `verified-real`
|
||||||
- 合成验证数量: `0`
|
- 合成验证数量: `0`
|
||||||
- 阻塞数量: `0`
|
- 阻塞数量: `0`
|
||||||
@@ -25,14 +25,7 @@
|
|||||||
|
|
||||||
## Ingest / Source 健康度
|
## Ingest / Source 健康度
|
||||||
|
|
||||||
- source failures: `7`
|
- source failures: `0`
|
||||||
- wordpress::NVD WordPress::SSLError
|
|
||||||
- wordpress::WPScan Vulnerability Database::SSLError
|
|
||||||
- wordpress::PortSwigger Research::SSLError
|
|
||||||
- magento-open-source::Magento GitHub Advisories::SSLError
|
|
||||||
- nodejs::Node.js Security Releases::SSLError
|
|
||||||
- nginx::NGINX Security Advisories::SSLError
|
|
||||||
- gitea::GitHub Gitea Advisories::SSLError
|
|
||||||
|
|
||||||
## 剩余风险说明
|
## 剩余风险说明
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from intel.normalize import normalize_candidates # noqa: E402
|
|||||||
from intel.pr import open_pr # noqa: E402
|
from intel.pr import open_pr # noqa: E402
|
||||||
from intel.render import render_case_pages, render_generated, render_registry, render_secure_code, render_system_scaffolding # noqa: E402
|
from intel.render import render_case_pages, render_generated, render_registry, render_secure_code, render_system_scaffolding # noqa: E402
|
||||||
from intel.route import route_advisories # noqa: E402
|
from intel.route import route_advisories # noqa: E402
|
||||||
from intel.sources.runner import collect_candidates # noqa: E402
|
from intel.sources.runner import collect_candidates, probe_sources # noqa: E402
|
||||||
from intel.utils import isoformat, load_all_json, now_utc, parse_since, read_json, write_json # noqa: E402
|
from intel.utils import isoformat, load_all_json, now_utc, parse_since, read_json, write_json # noqa: E402
|
||||||
from intel.validators import validate # noqa: E402
|
from intel.validators import validate # noqa: E402
|
||||||
|
|
||||||
@@ -167,19 +167,11 @@ def cmd_render(args) -> int:
|
|||||||
def cmd_source_health(args) -> int:
|
def cmd_source_health(args) -> int:
|
||||||
full_source_map = load_source_map()
|
full_source_map = load_source_map()
|
||||||
source_map = _filter_source_map(full_source_map, args.system)
|
source_map = _filter_source_map(full_source_map, args.system)
|
||||||
since_dt = parse_since(args.since, default_days=30)
|
probes, failures = probe_sources(source_map, tier=args.tier)
|
||||||
candidates, failures = collect_candidates(
|
|
||||||
source_map,
|
|
||||||
since_dt=since_dt,
|
|
||||||
tier=args.tier,
|
|
||||||
include_undated=args.include_undated,
|
|
||||||
)
|
|
||||||
render_map, advisories, triage = _load_existing_selection(full_source_map, source_map)
|
render_map, advisories, triage = _load_existing_selection(full_source_map, source_map)
|
||||||
existing_summary = read_json(GENERATED_DIR / "run-summary.json", default={}) or {}
|
existing_summary = read_json(GENERATED_DIR / "run-summary.json", default={}) or {}
|
||||||
render_generated(render_map, advisories, triage, failures, existing_summary)
|
render_generated(render_map, advisories, triage, failures, existing_summary)
|
||||||
print(
|
print(f"Source health checked {len(probes)} sources across {len(source_map['systems'])} systems; failures {len(failures)}")
|
||||||
f"Source health checked {len(candidates)} candidates across {len(source_map['systems'])} systems; failures {len(failures)}"
|
|
||||||
)
|
|
||||||
for failure in failures:
|
for failure in failures:
|
||||||
print(f"- {failure}")
|
print(f"- {failure}")
|
||||||
return 0 if not failures else 1
|
return 0 if not failures else 1
|
||||||
@@ -303,9 +295,7 @@ def main() -> int:
|
|||||||
render.set_defaults(func=cmd_render)
|
render.set_defaults(func=cmd_render)
|
||||||
|
|
||||||
source_health = subparsers.add_parser("source-health", help="Check source adapter health without mutating registry advisories")
|
source_health = subparsers.add_parser("source-health", help="Check source adapter health without mutating registry advisories")
|
||||||
source_health.add_argument("--since", default="30d")
|
|
||||||
source_health.add_argument("--tier", choices=["history-full", "rolling-24m"])
|
source_health.add_argument("--tier", choices=["history-full", "rolling-24m"])
|
||||||
source_health.add_argument("--include-undated", action="store_true")
|
|
||||||
source_health.add_argument("--system", action="append")
|
source_health.add_argument("--system", action="append")
|
||||||
source_health.set_defaults(func=cmd_source_health)
|
source_health.set_defaults(func=cmd_source_health)
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from intel.http_client import request
|
||||||
from intel.models import Candidate
|
from intel.models import Candidate
|
||||||
from intel.utils import parse_dt
|
from intel.utils import parse_dt
|
||||||
|
|
||||||
@@ -19,6 +25,76 @@ HANDLERS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _probe_source(system: Dict[str, Any], source: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
kind = source["kind"]
|
||||||
|
if kind == "ghsa-global":
|
||||||
|
headers = {"Accept": "application/vnd.github+json", "User-Agent": "websafe-intel"}
|
||||||
|
token = os.environ.get("GITHUB_TOKEN")
|
||||||
|
if token:
|
||||||
|
headers["Authorization"] = f"Bearer {token}"
|
||||||
|
response = request(
|
||||||
|
"GET",
|
||||||
|
github_global.API_URL,
|
||||||
|
headers=headers,
|
||||||
|
params={"per_page": 1, "page": 1, "ecosystem": source.get("ecosystem")},
|
||||||
|
)
|
||||||
|
if response.status_code == 403 and "rate limit" in response.text.lower():
|
||||||
|
raise requests.HTTPError("GitHub advisory rate limit exceeded; set GITHUB_TOKEN for higher quota", response=response)
|
||||||
|
response.raise_for_status()
|
||||||
|
payload = response.json()
|
||||||
|
if not isinstance(payload, list):
|
||||||
|
raise ValueError("GitHub advisory probe returned non-list payload")
|
||||||
|
return {"kind": kind, "items_seen": len(payload)}
|
||||||
|
if kind == "osv-batch":
|
||||||
|
packages = system.get("package_names", [])
|
||||||
|
if not packages:
|
||||||
|
return {"kind": kind, "items_seen": 0}
|
||||||
|
response = request(
|
||||||
|
"POST",
|
||||||
|
osv_api.QUERY_BATCH_URL,
|
||||||
|
json={"queries": [{"package": {"name": packages[0]["name"], "ecosystem": packages[0]["ecosystem"]}}]},
|
||||||
|
headers={"User-Agent": "websafe-intel"},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
payload = response.json()
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
raise ValueError("OSV probe returned non-object payload")
|
||||||
|
return {"kind": kind, "items_seen": len(payload.get("results", []))}
|
||||||
|
if kind == "kev-json":
|
||||||
|
response = request("GET", source["url"])
|
||||||
|
response.raise_for_status()
|
||||||
|
payload = response.json()
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
raise ValueError("KEV probe returned non-object payload")
|
||||||
|
return {"kind": kind, "items_seen": len(payload.get("vulnerabilities", []))}
|
||||||
|
if kind == "nvd-search":
|
||||||
|
params = {
|
||||||
|
"keywordSearch": source.get("keyword") or system["display_name"],
|
||||||
|
"resultsPerPage": 1,
|
||||||
|
}
|
||||||
|
headers = {"User-Agent": "websafe-intel"}
|
||||||
|
api_key = os.environ.get("NVD_API_KEY")
|
||||||
|
if api_key:
|
||||||
|
headers["apiKey"] = api_key
|
||||||
|
response = request("GET", nvd_api.API_URL, headers=headers, params=params)
|
||||||
|
response.raise_for_status()
|
||||||
|
payload = response.json()
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
raise ValueError("NVD probe returned non-object payload")
|
||||||
|
return {"kind": kind, "items_seen": len(payload.get("vulnerabilities", []))}
|
||||||
|
if kind == "rss-feed":
|
||||||
|
response = request("GET", source["url"])
|
||||||
|
response.raise_for_status()
|
||||||
|
root = ET.fromstring(response.content)
|
||||||
|
return {"kind": kind, "items_seen": len(root.findall(".//item"))}
|
||||||
|
if kind == "html-links":
|
||||||
|
response = request("GET", source["url"])
|
||||||
|
response.raise_for_status()
|
||||||
|
html = response.text
|
||||||
|
return {"kind": kind, "items_seen": len(html_links.ANCHOR_RE.findall(html))}
|
||||||
|
raise ValueError(f"Unsupported source kind {kind}")
|
||||||
|
|
||||||
|
|
||||||
def _passes_since(candidate: Candidate, since_dt: Optional[datetime], include_undated: bool) -> bool:
|
def _passes_since(candidate: Candidate, since_dt: Optional[datetime], include_undated: bool) -> bool:
|
||||||
if since_dt is None:
|
if since_dt is None:
|
||||||
return True
|
return True
|
||||||
@@ -55,3 +131,38 @@ def collect_candidates(
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
failures.append(f"{system['system_id']}::{source['name']}::{exc.__class__.__name__}")
|
failures.append(f"{system['system_id']}::{source['name']}::{exc.__class__.__name__}")
|
||||||
return all_candidates, failures
|
return all_candidates, failures
|
||||||
|
|
||||||
|
|
||||||
|
def probe_sources(
|
||||||
|
source_map: Dict[str, Any],
|
||||||
|
tier: Optional[str] = None,
|
||||||
|
) -> Tuple[List[Dict[str, Any]], List[str]]:
|
||||||
|
jobs: List[Tuple[Dict[str, Any], Dict[str, Any]]] = []
|
||||||
|
probes: List[Dict[str, Any]] = []
|
||||||
|
failures: List[str] = []
|
||||||
|
|
||||||
|
for system in source_map["systems"]:
|
||||||
|
if tier and system.get("tier") != tier:
|
||||||
|
continue
|
||||||
|
for bucket_name in ("official_sources", "ecosystem_sources", "research_sources"):
|
||||||
|
for source in system.get(bucket_name, []):
|
||||||
|
jobs.append((system, source))
|
||||||
|
|
||||||
|
max_workers = min(16, max(4, len(jobs) or 1))
|
||||||
|
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||||
|
future_map = {executor.submit(_probe_source, system, source): (system, source) for system, source in jobs}
|
||||||
|
for future in as_completed(future_map):
|
||||||
|
system, source = future_map[future]
|
||||||
|
try:
|
||||||
|
result = future.result()
|
||||||
|
probes.append(
|
||||||
|
{
|
||||||
|
"system_id": system["system_id"],
|
||||||
|
"source_name": source["name"],
|
||||||
|
"source_kind": source["kind"],
|
||||||
|
**result,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
failures.append(f"{system['system_id']}::{source['name']}::{exc.__class__.__name__}")
|
||||||
|
return probes, failures
|
||||||
|
|||||||
@@ -941,8 +941,8 @@ def _render_section_dashboard_shells() -> None:
|
|||||||
source_index = LOVART_TEMPLATE_DIR / "index.html"
|
source_index = LOVART_TEMPLATE_DIR / "index.html"
|
||||||
for section in SECTION_ROUTE_DIRS:
|
for section in SECTION_ROUTE_DIRS:
|
||||||
section_dir = DASHBOARD_DIR / section
|
section_dir = DASHBOARD_DIR / section
|
||||||
if section == "runs":
|
if section in {"runs", "data"}:
|
||||||
# Preserve existing /runs/<run-id>/ bundles; only refresh the section shell.
|
# Preserve existing section artifacts; only refresh the shell entrypoint.
|
||||||
ensure_dir(section_dir)
|
ensure_dir(section_dir)
|
||||||
index_path = section_dir / "index.html"
|
index_path = section_dir / "index.html"
|
||||||
if index_path.exists():
|
if index_path.exists():
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户