fix(pegas-memory): web fetch SOCKS5h proxy fallback + stale_guard supported double-count fix

- ingest.parse_web: direct fetch; on block/timeout retry via socks5h://127.0.0.1:1080 (SOCKS_FALLBACK_PROXY); raises ValueError if both fail.
- config: add SOCKS_FALLBACK_PROXY default (socks5h://127.0.0.1:1080).
- self_improve.stale_guard: supported LLM verdict now sets status_change='supported' (authoritative), no fallthrough to age fallback -> no double-checked/skipped_no_source.
- README: document auto-fallback.
This commit is contained in:
Pegas committed 2026-10-15 22:25:22 +05:00
1 parent eca8a11e23
commit 13e0b87446
4 files changed
+44 -15

No files matched your search

+4 -1
View File
@@ -76,7 +76,10 @@ Cloudflare anti-bot (neo4j.com — 403). Поведение research-цикла:
```bash ```bash
export PROXY_URL="socks5://127.0.0.1:1080" # hysteria2 export PROXY_URL="socks5://127.0.0.1:1080" # hysteria2
``` ```
Когда прокси задан, `parse_web` ходит через него; иначе — напрямую.
**Авто-повор:** `parse_web` сначала грузит URL напрямую; при таймауте/блокировке повторяет запрос через `socks5h://127.0.0.1:1080` (дефолт,
`SOCKS_FALLBACK_PROXY`). Если и проки недоступен — падает с
`ValueError: no main content ... (direct + ... fallback failed)`.
Поддержка SOCKS5 уже установлена (`httpx[socks]`). Поддержка SOCKS5 уже установлена (`httpx[socks]`).
## Управление пробелами (gaps) ## Управление пробелами (gaps)
+2 -1
View File
@@ -19,5 +19,6 @@ ATOM_STATUSES = ("candidate", "verified", "stale", "contested")
DEDUP_THRESHOLD = float(os.getenv("DEDUP_THRESHOLD", "0.90")) # порог семантической дедупликации DEDUP_THRESHOLD = float(os.getenv("DEDUP_THRESHOLD", "0.90")) # порог семантической дедупликации
PROXY_URL = os.getenv("PROXY_URL", "") # опционально: socks5://127.0.0.1:1080 или http://... PROXY_URL = os.getenv("PROXY_URL", "") # явный прокси: используется всегда (socks5://… или http://…)
SOCKS_FALLBACK_PROXY = os.getenv("SOCKS_FALLBACK_PROXY", "socks5h://127.0.0.1:1080") # авто-повор через прокси при ошибке прямого запроса
USER_AGENT = os.getenv("USER_AGENT", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36") USER_AGENT = os.getenv("USER_AGENT", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36")
+36 -13
View File
@@ -59,26 +59,49 @@ def parse_epub(path: str) -> ParsedDoc:
doc.kind = "epub" doc.kind = "epub"
return doc return doc
def _http_client(): def _http_client(proxy=None, timeout: int = 30):
"""httpx.Client с опциональным прокси и единым User-Agent.""" """httpx.Client с единым User-Agent.
proxy: явный прокси (None = прямой запрос). Если проси не передан, но
config.PROXY_URL задан — используется он.
"""
import httpx import httpx
from . import config from . import config
kwargs = dict(timeout=30, follow_redirects=True, kwargs = dict(timeout=timeout, follow_redirects=True,
headers={"User-Agent": config.USER_AGENT}) headers={"User-Agent": config.USER_AGENT})
if config.PROXY_URL: use_proxy = proxy or (config.PROXY_URL or None)
kwargs["proxy"] = config.PROXY_URL if use_proxy:
kwargs["proxy"] = use_proxy
return httpx.Client(**kwargs) return httpx.Client(**kwargs)
def parse_web(url: str) -> ParsedDoc: def _fetch_text(url: str, proxy: str | None = None) -> str | None:
"""Fetch + extract main content. Tries direct; on error/empty retries `proxy`.
trafilatura.extract has no timeout -> fetch via httpx with timeout.
"""
import trafilatura import trafilatura
# fetch через httpx с таймаутом + прокси (trafilatura.fetch_url не имеет таймаута и виснет) for _lbl, _prx in (("direct", None), ("proxy", proxy)):
with _http_client() as client: try:
r = client.get(url) with _http_client(proxy=_prx, timeout=30) as client:
r.raise_for_status() r = client.get(url)
text = trafilatura.extract(r.text, include_comments=False, include_tables=True, r.raise_for_status()
url=url) text = trafilatura.extract(r.text, include_comments=False,
include_tables=True, url=url)
if text:
return text
except Exception:
continue
return None
def parse_web(url: str) -> ParsedDoc:
from . import config
# direct fetch + socks5h fallback on block/timeout
text = _fetch_text(url, proxy=config.SOCKS_FALLBACK_PROXY)
if not text: if not text:
raise ValueError(f"trafilatura: no main content for {url}") raise ValueError(
f"no main content for {url} "
f"(direct + {config.SOCKS_FALLBACK_PROXY} fallback failed)"
)
doc = parse_text(text, uri=url, title=url) doc = parse_text(text, uri=url, title=url)
doc.kind = "web" doc.kind = "web"
return doc return doc
+2
View File
@@ -198,6 +198,8 @@ def stale_guard(conn, *, atom_id=None, all: bool = False, limit: int = 20,
status_change = "stale" status_change = "stale"
reason = f"source_{v['verdict']}: {v.get('reason', '')}" reason = f"source_{v['verdict']}: {v.get('reason', '')}"
else: else:
# content-staleness re-fetch succeeded -> verdict is authoritative
status_change = "supported"
reason = "supported" reason = "supported"
if status_change is None: if status_change is None:
# proxy-free fallback: возраст контента # proxy-free fallback: возраст контента