Daily brief
AI 每日新闻简报 · GitHub 热门 + X 热门文章 + 行情技术分析 · 23 个数据源聚合 + LLM 中文摘要 · 本地或 GitHub Actions 部署
npx -y skills add leiting-eric/DailyBrief --skill daily-briefAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
What its author says it does
Copied from the file, not written here
Operational knowledge for the daily-brief digest pipeline (this project). RSS/API fetchers, pluggable LLM enrichment (default claude CLI on Max; also anthropic/openai/deepseek/minimax API), trading section, HTML rendering, cross-platform scheduler integration (Windows Task Scheduler / macOS launchd / Linux cron). Load when the user asks about running daily / regenerating sections / debugging a failed run / adding or disabling sources / LLM quota / scheduler / why a tab shows wrong data / why a source failed / switching LLM backend. Always prefer the documented npm commands over re-implementing logic. Diagnose by reading logs/daily-*.log first, then logs/llm-calls.jsonl for LLM-side issues.
SKILL.md
12.1 KB, as published. Nobody here has run it
daily-brief — Operational Skill
This project generates a single-page HTML daily digest covering tech / finance / politics / market data / community discussion. The pipeline runs locally via the OS scheduler (Windows Task Scheduler / macOS launchd / Linux cron, default 08:00 local time) and emits daily_reports/<YYYY-MM-DD>/<YYYY-MM-DD>.html + sidecar files (each date gets its own subdir). The date label uses the system local timezone by default — set REPORT_TZ (e.g. Asia/Shanghai, UTC) in .env.local to override.
Detailed architecture lives in code; this skill is a cheat sheet for operating and diagnosing, not a re-explanation of the system.
Project root assumption
All paths in this skill are relative to the project root (the directory that contains package.json, lib/, scripts/).
Before any command, ensure the working directory is the project root. Two cases:
-
Claude Code session opened inside the project — already there, no action needed
-
Session opened elsewhere — read the config file and
cd:# Cross-platform Node one-liner (prints the project root path): node -e "const fs=require('fs'),os=require('os'),path=require('path');const cfg=path.join(os.homedir(),'.daily-brief-config');if(fs.existsSync(cfg))console.log(fs.readFileSync(cfg,'utf8').trim());else process.exit(1)"Use the printed path:
cd "$(...)"on bash /Set-Location (...)in PowerShell.
The config file is written by node scripts/install.mjs --global. If it's missing the user hasn't done a global install — tell them to run it.
Quick command reference
| Need | Command | Cost |
|---|---|---|
| Full pipeline | npm run daily | ~5-8 min, ~6 Sonnet calls |
| Fetch sanity only (no LLM) | npm run dry-run | ~30s |
| Re-render existing sidecar | npm run render [date] | <1s |
| Re-run trading section | npm run regen-trading [date] | ~2 min, 1 LLM call |
| Top-up missing summary | npm run regen-enrich <cat:sub> [date] | ~20-40s, 1 LLM call |
| Open today's report in Chrome | npm run open | instant |
| Sonnet quota + call history | npm run quota-report | instant |
[date] defaults to today's date in the report timezone (system local, or REPORT_TZ if set). The pipeline and the OS scheduler both run in local time, so the report's date label = the date when the trigger fired in the report timezone. A user with REPORT_TZ=Asia/Shanghai whose machine fires the trigger at 23:00 UTC-8 will get a "next-day Shanghai" file, e.g. daily_reports/2026-05-17/2026-05-17.html.
<cat:sub> accepted by regen-enrich: finance:news, politics:world, tech:ai-news. Single-source X 推文 (tech:x-viral) is enriched as part of daily only — no top-up path.
File map — where to change what
| Task | File |
|---|---|
| Add / disable / re-categorize a source | sources.config.json (project root — single source of truth; lib/sources/registry.ts is just a loader) |
| Rename L1 tab labels | lib/output/render.ts CATEGORY_LABELS |
| Reorder / rename L2 subcategories | SUBCATEGORY_ORDER + SUBCATEGORY_LABELS in same file |
| Change per-source item cap | SOURCE_DISPLAY_LIMITS |
| Change merged-timeline cap | MERGED_SUBGROUP_LIMITS |
| Add a Sonnet enrichment prompt | lib/ai/enrich.ts — copy XVIRAL_SYSTEM_PROMPT pattern |
| Wire an enrichment into pipeline | scripts/daily.ts — await enrichXxx(articles) in main() |
| Add a new fetcher type | New file in lib/sources/ + branch in lib/sources/dispatch.ts |
| Adjust HTML styling | inline <style> block in renderHtml() in lib/output/render.ts |
| Change scheduler trigger time | node scripts/install.mjs --at HH:MM (re-registers) |
| Wrapper script the scheduler invokes | scripts/run-daily.mjs |
How LLM enrichment works (mental model)
- Each merged L2 subcategory gets a Sonnet pass: GH-trending (per-source), finance:news, politics:world, tech:ai-news, tech:x-viral.
- Each pass = one batched Sonnet call for all items in that subgroup. Don't iterate per-item.
- Sources with
lang: "zh"in registry skip enrichment (already Chinese). - Failures are non-fatal: skipped articles just render without
summary.
Diagnostic flow
Order matters — top-to-bottom:
"今天日报没出来" / "Chrome 没弹"
- Check scheduled task state — platform-specific:
- Windows:
Get-ScheduledTaskInfo -TaskName DailyBrief→LastRunTime+LastTaskResult(0=success,267009=running, else failed) - macOS:
launchctl list | grep com.daily-brief(PID column + last exit code) - Linux: cron doesn't track per-job state; look at
logs/cron.log
- Windows:
- Tail today's log (date = local, not UTC):
node -e "const fs=require('fs'),d=new Date(),pad=n=>String(n).padStart(2,'0');console.log(fs.readFileSync('logs/daily-'+d.getFullYear()+'-'+pad(d.getMonth()+1)+'-'+pad(d.getDate())+'.log','utf8').split('\n').slice(-40).join('\n'))" - Check report files exist:
ls daily_reports/<date>/(any platform) orGet-ChildItem daily_reports\<date>\(Windows)
"某个源数据不对 / 0 条"
- Look at fetch lines near top of log —
<id> <count>or<id> FAILED — <reason> - If specific source failed: read its fetcher in
lib/sources/<source>.ts - If Cloudflare-related: see "LinuxDo lesson" below
- Single-source failure must never kill the run (try/catch per source in
daily.ts)
"LLM 调用炸 / 中文摘要缺失"
npm run quota-report— per-backend summary; forclaude-clishows 5h window, for API backends shows 24h spending- If quota hot on
claude-cli: wait or temporarily switch via.env.local(LLM_BACKEND=openaietc.) - If specific phase missing summaries:
npm run regen-enrich <cat:sub> - Each call logged to
logs/llm-calls.jsonl(legacyclaude-calls.jsonlstill read for backwards-compat) — grep"success":false, seeerrorCategory(quota/timeout/auth/other) - Which backend is active =
LLM_BACKENDenv in.env.local; not set →claude-cli
"UI 出错 / 某个 tab 显示异常"
npm run render(1 second) — often fixes display-only bugs- If still wrong: read rendered HTML for the affected panel
renderRawCategoryPanel/renderSubContentchain inrender.tsis where panel structure lives
Recurring failure patterns (institutional knowledge)
LinuxDo / Cloudflare WAF
- LinuxDo is behind Cloudflare and frequently flags datacenter-IP exits with "Just a moment..." challenges
- Do NOT add aggressive retry to its fetcher — burst requests escalate the WAF flag, causing persistent blocks
- May ship with
enabled: falsedepending on current IP rep - Browser works because of cookies + JS challenge; curl can't do either
- If re-enabling: keep single attempt, accept intermittent failures
Run-daily.mjs wrapper notes
- Tees
npm run dailystdout+stderr tologs/daily-<local-date>.logvia stream pipes (real-time, not buffered) - Exit code from
npm run dailyis propagated to the OS scheduler - On exit 0: spawns
npm run opendetached so Chrome opens without blocking - Cross-platform: same
.mjsfile works on Windows / macOS / Linux
"X 推文 出现非英文"
- API's
lang=enparam is best-effort; some slip through - Fix in
lib/sources/attentionvc.tsisEnglish()— checkslangsDetected(most reliable) +lang === "zxx"(image/code-only, kept)
"社区讨论 tab 偶发空白"
- Was a JS scope bug: sub-tab/source-tab handlers used
data-cat="tech"selector. Tech main panel AND community panel both had sub-content withdata-cat="tech", so clicking AI 媒体 in tech panel deactivated cn-community in community panel - Fixed: handlers use
btn.closest('.panel')+btn.closest('.sub-content'). If regression, look at inline<script>block at end ofrenderHtml()
Trading commentary "watchlist empty"
- Sonnet occasionally returns valid JSON with empty watchlist. 1-shot retry built into
lib/ai/trading-commentary.tswith stronger prompt - If retry also fails, render falls back to empty trading panel — run isn't aborted
Source registry conventions
Sources live in sources.config.json at the project root. lib/sources/registry.ts only loads + validates that JSON at module-init; never hardcode sources in TS.
- Every source has:
id,name,type(rss/api/scrape),url,category,subcategory?,enabled?,useCurl?,lang?,locales? useCurl: truefor sources behind Cloudflare-style TLS-fingerprint blockslang: "zh"(or "en") for sources already in a specific language — enrich skips them when REPORT_LOCALE matcheslocales: ["zh"|"en"]filters which REPORT_LOCALE keeps the source. Omit → both- Disabled sources stay in the JSON with
enabled: false+ anotesfield explaining why — don't delete - Run
npm run sourcesto see the table by category with current enable/filter status
Render layout (current, may evolve)
L1 tabs in order: tech / trading / politics / finance / community
技术动态 (tech)
L2: GitHub Trending (per-source, cap 20)
L2: X 推文 (single source attentionvc-ai, cap 20, preserve fetch order)
L2: AI 媒体 (merged 7 RSS sources, cap 15, summary)
市场行情 (trading)
asset-group tabs: macro / 美股 / 加密 / 中港 / 商品外汇
时政观察 (politics:world)
merged single timeline, cap 15, summary, sports filtered
财经要点 (finance:news)
merged single timeline, cap 12, summary
社区讨论 (community)
Source tabs: V2EX / LinuxDo (cap 10 each)
Note: cn-community is registered under category=tech but rendered as its
own L1 panel — see TECH_MAIN_SUBS vs TECH_COMMUNITY_SUBS in render.ts
Scheduler integration (cross-platform)
scripts/install.mjs registers the daily trigger via the OS-native scheduler:
| OS | Mechanism | Wake-from-sleep |
|---|---|---|
| Windows | Task Scheduler "DailyBrief" (WakeToRun, AllowStartIfOnBatteries, StartWhenAvailable) + power-plan wake timers | ✓ wakes laptop |
| macOS | launchd plist ~/Library/LaunchAgents/com.daily-brief.plist | ✗ doesn't wake; configure pmset separately if needed |
| Linux | crontab entry tagged # daily-brief | ✗ cron doesn't fire while suspended — run skipped |
Common:
- Default trigger: 08:00 local time (
--at HH:MMto change) - Runs as current interactive user — required because claude CLI's OAuth token lives in user profile
- Execution timeout: 30 min (Windows only; macOS/Linux no built-in timeout)
- Set up:
node scripts/install.mjs [--at HH:MM] [--global] - Tear down:
node scripts/uninstall.mjs - Inspect:
Get-ScheduledTask DailyBrief | flortaskschd.mscGUI
What NOT to do
- Don't
console.logdebugging that won't survive — use structured logger or write tologs/ - Don't add
process.exit(1)deep in a fetcher; letdaily.ts's per-source try/catch handle it - Don't bypass
runLlm(lib/ai/llm.ts) by importing a specific backend directly — that defeats the LLM_BACKEND switch and pins call sites to one provider - Don't change the default backend silently; if user has Max subscription they almost certainly want
claude-clito keep using it. Switching to API costs them money - Don't put AI-generated digest fields (hero_headline, editor_note, keywords) back into the HTML view — they're intentionally hidden. Still generated and live in
<date>.jsonand<date>.mdfor archive - Don't add Playwright / Puppeteer dependencies casually — project uses curl + JSON APIs to stay light