Add #rss command family with LLM tags and summaries (v0.20.0)
CI / build + unit (pull_request) Successful in 1m15s
CI / build + unit (pull_request) Successful in 1m15s
#rss subcommands on the command channel: help / add <url> <name> / remove <name> / tags / sources / get <name|#tag>. Sources are stored in a new rss_sources table in the existing MESH_DB_PATH file (second node:sqlite connection, case-insensitive names); feeds parsed with fast-xml-parser (RSS 2.0 + Atom); AI tags and headline summaries via injected LlmService.chat() with bounded maxTokens (50/200). CommandService gains prefix registration so one handler owns the whole #rss family while #ping stays an exact match; wired in CommandModule's factory — still no constructor-level cycle, no forwardRef. One dead feed does not sink a tag summary.
This commit is contained in:
@@ -1,5 +1,14 @@
|
||||
# Changelog
|
||||
|
||||
## v0.20.0 — 2026-09-25 18:40
|
||||
|
||||
- New `#rss` command family on the command channel (ch2): `#rss help`, `#rss add <url> <name>` (validates URL + fetches with a 15 s timeout, requires ≥1 item with a non-empty title, rejects duplicate names case-insensitively; tags are generated by the LLM from up to 8 latest titles — on LLM failure the source is still added with empty tags and the reply says so), `#rss remove <name>`, `#rss tags` (all distinct tags), `#rss sources`, `#rss get <name>` and `#rss get #<tag>` (LLM headline summary, maxTokens 200; tag form collects items across ALL sources carrying the tag — one dead feed does not sink the summary). Bare `#rss` / unknown subcommand → help. All replies ride the existing respond() path like `#ping`.
|
||||
- Storage: new `rss_sources(name TEXT PRIMARY KEY, url, tags, created_at)` table in the EXISTING MESH_DB_PATH file (default ./data/mesh-bot.db) via a second node:sqlite connection opened by `RssModule`'s factory — no new DB file. Names are matched case-insensitively everywhere (`COLLATE NOCASE`).
|
||||
- Feed parsing: new dependency `fast-xml-parser` (the only one added for this feature) — RSS 2.0 (`<item><title>/<link>`) and Atom (`<entry><title>`, link as `href` attribute); CDATA titles supported; malformed input or no usable items → "not a valid feed".
|
||||
- LLM: `RssService` injects `LlmService` (plain DI, one-way — `RssModule` imports `LlmModule`) and uses `chat()` directly with explicit maxTokens (50 for tags, 200 for summaries) to keep mesh replies short. Tag output is sanitized (lowercase, no #, whitespace → '-', deduped).
|
||||
- Dispatch: `CommandService` gains prefix registration (`registerPrefix(name, fn)`) alongside exact matches — one handler owns the whole `#rss` family while `#ping` stays an untouched exact match; exact names win over prefixes, and a name merely starting with the prefix (e.g. `#rssfoo`) does not dispatch. Wired in `CommandModule`'s factory (`inject: [MeshService, RssService]`) — still no constructor-level cycle, no forwardRef.
|
||||
- Tests: 128 → 153 specs — store CRUD against a real temp SQLite file (incl. second-connection persistence), feed parser fixtures (RSS 2.0 / Atom / malformed / empty titles), handler behavior with mocked LLM + fetch (help, add success/LLM-fail/invalid-url/duplicate/non-2xx/unparseable, remove found/missing, tags/sources empty+populated, get by name/by tag across sources/dead-feed resilience/unknown name+tag), and `#rss` prefix dispatch through respond().
|
||||
|
||||
## v0.19.0 — 2026-09-25 18:03
|
||||
|
||||
- New LLM client module (`src/llm`): `LlmService` speaks the OpenAI-compatible chat-completions API of the local server at `LLM_API_BASE` (default `http://192.168.1.70:1111/v1`, no auth) using native fetch only — no new dependencies. Plain feature module (`LlmModule`), wired into `app.module.ts`; no Mesh/Schedule dependency.
|
||||
|
||||
Reference in New Issue
Block a user