# πŸ“° dsh-rss-daily > One line: every morning, dsh quietly prepares an editor-grade news digest for you β€” curated from 46 RSS sources by the model you already configured, delivered to your IM, and broadcast in the chat like a regular reply (zero context cost). [δΈ­ζ–‡θ―΄ζ˜Ž](README.md) [![npm](https://img.shields.io/npm/v/dsh-rss-daily)](https://www.npmjs.com/package/dsh-rss-daily) [![npm downloads](https://img.shields.io/npm/dw/dsh-rss-daily)](https://www.npmjs.com/package/dsh-rss-daily) [![awesome Β· DSH plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com) [![CI](https://github.com/shangjian2023/dsh-rss-daily/actions/workflows/ci.yml/badge.svg)](https://github.com/shangjian2023/dsh-rss-daily/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE) ## What it looks like The digest shows up **inside your conversation, styled exactly like a model reply** β€” plain markdown, same typography, same width: ![In-chat digest](docs/in-chat.png) It's frontend-only: displayed in the chat, but **never written to the session log and never sent to the model**. Your context stays clean. ## Why you'll like it - **It reads like an editor picked it, not an aggregator.** ~20 candidates are scored (source tier Γ— recency Γ— signal words), deduplicated (Jaccard + 14-day MD5 history), cross-checked against a multi-source event graph, then handed to **your own dsh model** for the final edit: merge same-event reports, drop PR filler, balance topics (max 2 per tag), and compress each item into one concrete fact line (≀45 chars). - **Zero extra API keys.** The editorial pass goes through `ctx.llm` β€” whatever model your dsh already runs. No new credentials, no extra bill. - **It never misses a day.** If the model call fails, the digest degrades to a deterministic rule-based selection automatically. If dsh was down at the scheduled time, it catches up on next boot (within 12 h). - **It comes to you.** Webhook delivery to ServerChan / PushDeer / WeCom / Telegram / Bark / gotify / any custom endpoint. At least one success counts as delivered. - **Production-grade pipeline.** Ported from a personal script that has run daily since June 2026 through 9 revisions: per-source health tracking with adaptive timeouts, feed mojibake repair, full-text enrichment for thin summaries, a 420 s hard budget (SIGALRM on POSIX; on Windows via per-request timeouts plus soft budget checks), and idempotent two-phase send (fetch β†’ outbox β†’ deliver β†’ confirm). ## Quick start ```sh # from npm (recommended) dsh plugin --profile web add dsh-rss-daily # or from GitHub dsh plugin --profile web add github:shangjian2023/dsh-rss-daily ``` Restart `dsh web`. Every conversation header gets a **πŸ“°** button, and the daily schedule is live. **30-second setup:** 1. Click the **πŸ“°** button in any conversation β†’ **Settings** tab 2. Pick a time and add a delivery target (ServerChan / PushDeer / WeCom / Telegram / Bark / gotify / custom webhook) 3. Hit **Get today's digest** β€” the digest is generated right now, delivered to your webhook, and appears in the chat No YAML editing required β€” everything (schedule, targets, sources, digest size, LLM provider) is editable in the panel and takes effect immediately. ## Highlights - πŸ–₯ **Three UIs, zero context cost**: the in-chat broadcast, the πŸ“° panel (digest / sources / settings), and the plugin settings card β€” all rendered client-side, never entering the session log - πŸ“ **The broadcast is a real-looking reply**: the digest body renders through the host's own `MarkdownText` component (the exact component behind genuine assistant messages), streams in with a typing cadence, and hides its action row until done β€” it looks like the model answering, yet costs zero context tokens - 🧰 **46 curated sources** across AI / tech / science / world / finance / dev / humanities and 15 categories in total, reachability-tested from mainland China; add or disable your own in the sources tab (sources failing 3 times in a row auto-degrade for 24 h; recovery needs 2 consecutive successes) - πŸ€– **`rss_daily` agent tool** β€” `run` / `status` / `redo` / `deliver`, so you can also just ask the agent *"generate today's news digest"* - πŸ”Œ **Headless mode** β€” `py/daily.py` runs standalone with any OpenAI-compatible endpoint, no dsh required: ```sh RSS_LLM_ENDPOINT=https://api.deepseek.com/v1 RSS_LLM_KEY=sk-... \ python py/daily.py --state-dir ~/.rss-daily ``` ## Configuration Defaults are sensible; the panel covers everything. The main knobs: | Key | Default | Meaning | |---|---|---| | `time` | `08:00` | Local HH:MM to run daily (boot catch-up if missed <12 h). The digest title date is computed in Beijing time (UTC+8) | | `targets[]` | `[]` | Delivery targets; delivered = at least one succeeded | | `digestItems` | `8` | Max items per digest | | `llmMode` | `harness` | `harness` (use dsh's model) or `none` (rule-based only) | | `broadcast` | `true` | Show the in-chat digest card | | `stateDir` | `~/.dsh/rss-daily` | Sources / dedup history / outbox live here | Power users can also overlay the config row in the profile's `cordis.patch.yml`; see the [HTTP API and pipeline stages](README.md#http-apiθΏ›ι˜Ά) docs for the full surface. ## How it works ``` 46 sources ──▢ fetch (health, timeouts, dedup) ──▢ ~20 candidates β”‚ ctx.llm editorial pass (rule fallback) β”‚ digest ──▢ webhook targets ──▢ confirm β”‚ in-chat broadcast (frontend-only) ``` `py/daily.py` is independently scriptable (`--stage fetch|finalize|confirm|status`, single-line JSON on stdout); the plugin drives exactly these stages. Never confirms before at least one delivery succeeds β€” confirmed items enter the 14-day dedup window permanently. The editorial prompt suite is distilled from [AIHOT](https://github.com/KKKKhazix/AIHOT) (an open-source self-curating hot-news framework): five-axis private scoring with a noise-suppression table, answer-first one-liners, self-contained titles, anti-hallucination rules, an optional day `lead`, and per-event corroboration badges (✚N outlets). Selection may fall back to rules, but the digest language never does: a translate-only salvage pass runs before raw titles are shown (and those carry an explicit "raw titles" note). **DSH Desktop compatible**: the default model follows the host's `agentDefaultModel` selection; empty replies (thinking models burning the token budget on reasoning) retry once at doubled budget; every paid call is receipted with a daily budget breaker; the panel ships its own opaque light/dark design system (host CSS variables renamed in 0.2.0-rc once left it transparent); without a host settings service, panel config persists to `stateDir/panel-config.json`. ## MCP (for other agents) `mcp/server.py` exposes the same pipeline as [MCP](https://modelcontextprotocol.io) tools, so Claude Code, Codex, opencode, Cursor β€” any MCP client β€” can drive it: `rss_status`, `rss_fetch`, `rss_finalize`, `rss_confirm`. The host agent acts as both the editor (pick & rewrite per the returned prompt) and the delivery channel (show the digest to the user). The state directory is shared with the dsh plugin by default (idempotency and fetch locking honored across both); scheduled delivery stays with the dsh plugin or cron β€” MCP is for interactive use. One-liner for Claude Code (user scope, all projects): ```bash claude mcp add --scope user rss-daily -- python /path/to/dsh-rss-daily/mcp/server.py ``` See [`mcp/README.md`](mcp/README.md) for Codex / opencode configs and the long-task polling convention. ## Requirements - dsh with the `web` (or any long-running) profile - Python 3.9+ with `feedparser` (`pip install feedparser`); add `pip install mcp` for the MCP server (both mcp 1.x and 2.x work) - Node.js β‰₯ 22.19 (matches the dsh host's own Node requirement; the plain-JS layers stay 18-compatible, but the plugin only runs inside dsh) ## License MIT