feat: LLM-анализ — индикатор прогресса, кнопка рескана, статистика на дашборде

- Добавлен статус {"status": "analyzing"} в finding.report на время LLM-анализа
- Кнопка рескана (Retry) под LLM-отчётом в ручном режиме
- LLM-статистика на дашборде: analysed / pending
- Защита от двойного анализа через per-finding asyncio.Lock
- _llm_spinner.html — фрагмент спиннера для состояния analysing
- Удалён мёртвый код: constants, i18n, CSS, queries
- Фиксы: _env_int, индексы БД, UnicodeDecodeError, time.mktime и др.
- Шаблоны: shared includes (_status_badge, _pagination)
- AGENTS.md: workflow (lint, test, commit, rebuild)
This commit is contained in:
Marker689
2026-05-10 09:54:04 +03:00
parent c99a7bf67c
commit 6984844161
26 changed files with 261 additions and 266 deletions

View File

@@ -63,16 +63,20 @@ async def harvest(
return None
async with lock:
# Re-check DB in case another task already created and finished a scan
active = await session.scalar(
select(Scan.id).where(
Scan.nexus_asset_url == download_url,
Scan.status.in_([ScanStatus.PENDING.value, ScanStatus.SCANNING.value]),
try:
# Re-check DB in case another task already created and finished a scan
active = await session.scalar(
select(Scan.id).where(
Scan.nexus_asset_url == download_url,
Scan.status.in_([ScanStatus.PENDING.value, ScanStatus.SCANNING.value]),
)
)
)
if active:
log.info("Already scanning this URL, skipping")
return None
if active:
log.info("Already scanning this URL, skipping")
return None
finally:
async with _url_lock:
_url_locks.pop(download_url, None)
scan = Scan(
package_name=package_name,
@@ -88,10 +92,9 @@ async def harvest(
await session.commit()
await session.refresh(scan)
os.makedirs(config.temp_dir, exist_ok=True)
tmpdir = tempfile.mkdtemp(dir=config.temp_dir)
try:
os.makedirs(config.temp_dir, exist_ok=True)
tmpdir = tempfile.mkdtemp(dir=config.temp_dir)
scan.status = ScanStatus.SCANNING.value
await session.commit()
@@ -103,7 +106,7 @@ async def harvest(
await session.commit()
return scan
scan.sha256 = compute_sha256(downloaded)
scan.sha256 = await compute_sha256(downloaded)
await session.commit()
existing = await session.scalar(
@@ -148,8 +151,12 @@ async def harvest(
# Auto-trigger LLM analysis for flagged packages
llm_reports = []
if scan.flagged and config.llm_enabled:
llm_reports = await _run_llm_analysis(created_findings, session)
if scan.flagged and config.llm_enabled and config.llm_auto_analyze:
try:
llm_reports = await _run_llm_analysis(created_findings, session)
except Exception as e:
log.error("LLM analysis failed for %s==%s: %s", package_name, package_version, e)
llm_reports = []
if scan.flagged:
extra = {
@@ -199,11 +206,18 @@ async def _run_llm_analysis(findings: list[Finding], session: AsyncSession) -> l
"""Run LLM analysis on findings and persist reports to the database."""
from .llm import analyze_finding
# Mark all as analyzing so the UI shows a spinner
for finding in findings:
finding.report = {"status": "analyzing"}
await session.commit()
reports = []
for finding in findings:
report = await analyze_finding(finding.data)
if report:
finding.report = report
reports.append(report)
else:
finding.report = None
await session.commit()
return reports