尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗An AI-curated tech news monitor. TechPulse scrapes Hacker News on a schedule with a headless browser, uses Claude to classify, score and summarise every story against your interests, stores everything in PostgreSQL, and sends a Slack alert only when something matters. A FastAPI back end serves a REST API and a live dashboard.
Built as a compact portfolio project covering browser automation, LLM integration, API design, data modelling, testing and containerised delivery.
🎬 Demo video (4:33): docs/techpulse-demo.mp4 (regenerate with python demo/record_demo.py)
flowchart LR
S[APScheduler<br/>every 30 min] --> P
A[POST /api/runs] --> P
subgraph P[Pipeline · one run at a time]
direction LR
SC[Scrape<br/>Playwright or httpx + retries] --> ST[Upsert<br/>SQLAlchemy]
ST --> EN[Enrich<br/>Claude structured outputs]
EN --> AL[Alert<br/>Slack webhook]
end
ST --- DB[(PostgreSQL)]
DB --- API[FastAPI REST API]
API --- UI[Dashboard]
| Stage | What happens | Tech |
|---|---|---|
| Schedule | Interval trigger, or on demand via API. An asyncio.Lock prevents overlapping runs. | APScheduler |
| Scrape | Headless browser loads N pages and extracts rank, title, URL, points, author and comments in one in-page JS pass. Images, fonts and CSS are blocked. Retries use exponential backoff. | Playwright, tenacity |
| Store | Idempotent upsert: new stories are inserted, and existing ones get fresh points and comments. | SQLAlchemy 2, PostgreSQL / SQLite |
| Enrich | Batches of 30 un-enriched stories are sent to Claude. The response is parsed directly into Pydantic models (category enum, relevance 1–10, summary, tags). | Anthropic SDK, structured outputs |
| Alert | Stories at or above ALERT_THRESHOLD that haven't been alerted are posted to Slack, exactly once. | httpx |
| Audit | Every run is recorded with its trigger, status, counts, enricher, duration and error. | runs table |
relevance IS NULL and are picked up on the next run.alerted flag is set when an alert is sent.ANTHROPIC_API_KEY a deterministic keyword enricher takes over. With SCRAPER_SOURCE=fixture, the real Playwright extraction runs against a saved page, which CI and demos use.cp .env.example .env # optional: add ANTHROPIC_API_KEY, SLACK_WEBHOOK_URL
docker compose up --build
Open http://localhost:8000 for the dashboard, or http://localhost:8000/docs for the API docs.
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev,browser]"
python -m playwright install chromium # or set BROWSER_CHANNEL=chrome
cp .env.example .env
techpulse run-once # one pipeline pass, prints a summary
techpulse serve # API + dashboard + scheduler on :8000
The same code runs as a Vercel Function. When VERCEL=1 is set, the app adapts automatically:
| Concern | Docker / local | Vercel |
|---|---|---|
| Scraper | Playwright (headless Chromium) | httpx + HTML parser. Same output; both are tested against the same fixture. |
| Scheduling | In-process APScheduler | Vercel Cron (daily on Hobby), plus an optional GitHub Actions workflow every 2 h, both calling GET /api/cron with Authorization: Bearer $CRON_SECRET |
| "Run now" | Background task (202) | Runs inline, with MANUAL_RUN_COOLDOWN_SECONDS to prevent abuse |
| Database | SQLite / Compose Postgres | Neon Postgres via DATABASE_URL (falls back to per-instance /tmp SQLite) |
pyproject.toml sets [tool.vercel] entrypoint).DATABASE_URL; postgres:// URLs are normalised automatically.CRON_SECRET (random, ≥16 chars), MANUAL_RUN_COOLDOWN_SECONDS=300, and optionally ANTHROPIC_API_KEY, SLACK_WEBHOOK_URL, SCRAPER_PAGES=2, SCHEDULE_LABEL="Vercel Cron · daily".TECHPULSE_URL and CRON_SECRET to enable .github/workflows/scheduled-run.yml.All settings come from environment variables or .env, and Pydantic validates them.
| Variable | Default | Purpose |
|---|---|---|
ANTHROPIC_API_KEY | – | Enables Claude enrichment. Without it, the keyword fallback is used. |
CLAUDE_MODEL | claude-opus-5-5 | Model used for enrichment |
INTERESTS | AI/LLMs, automation, Python, … | What "relevant" means to you |
ALERT_THRESHOLD | 8 | Minimum relevance (1–10) that triggers an alert |
SLACK_WEBHOOK_URL | – | Slack incoming webhook. If unset, alerts go to the log. |
SCHEDULE_ENABLED / SCHEDULE_INTERVAL_MINUTES | true / 30 | Scheduler |
SCRAPER_ENGINE | auto | browser (Playwright), http (httpx), or auto (browser if installed and not serverless) |
SCRAPER_SOURCE | live | live or fixture (offline) |
CRON_SECRET | – | Required for GET /api/cron (sent as Authorization: Bearer …) |
MANUAL_RUN_COOLDOWN_SECONDS | 0 | Minimum gap between runs triggered with the Run button or API |
SCRAPER_PAGES | 1 | HN pages per run (1–5) |
BROWSER_CHANNEL | – | Use an installed chrome/msedge instead of the bundled Chromium |
DATABASE_URL | sqlite:///./data/techpulse.db | Any SQLAlchemy URL. Compose sets PostgreSQL. |
| Method | Path | Description |
|---|---|---|
GET | /api/stories | Filter with category, min_relevance, q, sort=relevance|points|recent, limit |
GET | /api/stories/{hn_id} | A single story |
GET | /api/runs | Run history |
POST | /api/runs | Trigger a run (202; 409 if one is already running; 429 during cooldown) |
GET | /api/cron | Scheduled run for cron callers. Requires Bearer CRON_SECRET. Returns the finished run. |
GET | /api/stats | KPIs, category breakdown, last and next run, active configuration |
GET | /health | Liveness and DB check |
pytest -v # 29 tests
ruff check . && ruff format --check .
TestClient, including validation errors.tests/fixtures/hn_front_page.html, plus a check that they produce identical results./api/cron auth, inline runs, cooldown, Postgres URL normalisation, Vercel defaults.GitHub Actions (.github/workflows/ci.yml) runs lint, then tests, then a Docker build, then a smoke test against the running container in fixture mode.
techpulse/
config.py settings (pydantic-settings)
db.py engine/session wrapper
models.py Story, Run (SQLAlchemy 2 typed ORM)
scraper.py Playwright scraper + retry
enricher.py ClaudeEnricher (structured outputs) + KeywordEnricher fallback
notifier.py Slack / log alerts
pipeline.py scrape → upsert → enrich → alert, audited per run
api.py FastAPI app factory, scheduler lifespan, routes
cli.py `techpulse serve | run-once`
templates/ static/ dashboard (vanilla JS on the public API)
tests/ pytest suite + HN HTML fixture
demo/ slides + scripted, narrated demo-video recorder
demo/record_demo.py produces the walkthrough video automatically:
edge-tts)imageio-ffmpeg)pip install -e ".[video]"
python demo/record_demo.py # -> demo/out/techpulse-demo.mp4
Set ANTHROPIC_API_KEY before recording to show Claude enrichment instead of the offline fallback.