feat: initialize bangwen recruitment aggregation and search engine
- Rust core engine with HTTP ingestion gateway, hybrid search, and white-box resume matching - Python edge collectors supporting declarative TOML and specialized adapters - PostgreSQL schema migrations with pgvector embedding support - Hardened security controls: strict CSP, terminal sanitization, and input boundary validation - Multi-service container orchestration with dedicated non-root execution
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
import httpx
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class AttachmentItem(BaseModel):
|
||||
name: str | None = None
|
||||
url: str
|
||||
mime: str | None = None
|
||||
|
||||
|
||||
class PositionItem(BaseModel):
|
||||
post_code: str | None = None
|
||||
dept: str | None = None
|
||||
post_name: str
|
||||
headcount: int | None = None
|
||||
education_min: str | None = None
|
||||
degree_required: bool | None = None
|
||||
majors: list[str] | None = None
|
||||
major_unlimited: bool | None = False
|
||||
age_max: int | None = None
|
||||
bianzhi: str | None = None
|
||||
salary_min: int | None = None
|
||||
salary_max: int | None = None
|
||||
work_city: str | None = None
|
||||
other_req: str | None = None
|
||||
extract_method: str = "edge_python"
|
||||
confidence: float | None = 1.0
|
||||
|
||||
|
||||
class AnnouncementItem(BaseModel):
|
||||
url: str
|
||||
url_hash: str | None = None
|
||||
title: str
|
||||
kind: str = "招聘"
|
||||
employer: str | None = None
|
||||
employer_type: str | None = None
|
||||
published_at: str | None = None
|
||||
apply_start: str | None = None
|
||||
apply_end: str | None = None
|
||||
region_hint: str | None = None
|
||||
body_text: str | None = None
|
||||
attachments: list[AttachmentItem] = Field(default_factory=list)
|
||||
positions: list[PositionItem] = Field(default_factory=list)
|
||||
reconcile_positions: bool | None = None
|
||||
|
||||
|
||||
class CollectorMeta(BaseModel):
|
||||
name: str
|
||||
runtime: str | None = "python/3.12"
|
||||
auth_status: str = "none"
|
||||
duration_ms: int | None = None
|
||||
error_message: str | None = None
|
||||
|
||||
|
||||
class IngestPayload(BaseModel):
|
||||
protocol_version: str = "bangwen/v1"
|
||||
source_id: str
|
||||
batch_id: str
|
||||
collector: CollectorMeta | None = None
|
||||
items: list[AnnouncementItem]
|
||||
|
||||
|
||||
def emit_payload(
|
||||
payload: IngestPayload,
|
||||
push_url: str | None = None,
|
||||
token: str | None = None,
|
||||
) -> None:
|
||||
data = payload.model_dump(mode="json")
|
||||
if push_url:
|
||||
token = token or os.environ.get("INGEST_TOKEN")
|
||||
headers = {}
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token.strip()}"
|
||||
max_retries = 3
|
||||
last_exc = None
|
||||
for attempt in range(1, max_retries + 1):
|
||||
try:
|
||||
resp = httpx.post(push_url, json=data, headers=headers, timeout=30.0)
|
||||
resp.raise_for_status()
|
||||
sys.stderr.write(
|
||||
f"Pushed {len(payload.items)} items to {push_url}: {resp.text}\n"
|
||||
)
|
||||
return
|
||||
except Exception as e:
|
||||
last_exc = e
|
||||
sys.stderr.write(
|
||||
f"Attempt {attempt}/{max_retries} failed to push to {push_url}: {e}\n"
|
||||
)
|
||||
if attempt < max_retries:
|
||||
time.sleep(2 ** attempt)
|
||||
if last_exc:
|
||||
raise last_exc
|
||||
else:
|
||||
json_output = json.dumps(data, ensure_ascii=False)
|
||||
sys.stdout.write(json_output + "\n")
|
||||
sys.stdout.flush()
|
||||
Reference in New Issue
Block a user