
Ak Rss Digest
- 380 installs
- 928 repo stars
- Updated July 25, 2026
- rookie-ricardo/erduo-skills
ak-rss-digest is an agent skill that fetches a bundled OPML feed list, scores articles on a 10-point rubric, and outputs a Simplified Chinese curated digest for developers tracking AI agents and frontier technology writi
About
ak-rss-digest is an erduo-skills workflow that runs scripts/fetch_today_feed_items.py against references/feeds.opml to collect RSS and Atom entries, defaulting to the most recent 7 days in Asia/Shanghai timezone. The agent scores each candidate on relevance, originality, readability, and usefulness, rejecting papers, changelogs, and vendor fluff, then outputs only items scoring strictly above 7.0 in a concise Chinese daily-brief format with title, score, recommendation, two-sentence summary, and link. Developers reach for ak-rss-digest when Codex or another agent needs a weekly AI-agent reading list, a single-day pull with --date, or a JSON feed for downstream automation. Feed failures are non-fatal; the skill continues with successful sources and reports coverage gaps only when material.
- Feed aggregation
- Summarized digests
- Source deduplication
- Trend spotting
- Publishing cadence
Ak Rss Digest by the numbers
- 380 all-time installs (skills.sh)
- +4 installs in the week ending Jul 26, 2026 (Skillselion tracking)
- Ranked #456 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/rookie-ricardo/erduo-skills --skill ak-rss-digestAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 380 |
|---|---|
| repo stars | ★ 928 |
| Last updated | July 25, 2026 |
| Repository | rookie-ricardo/erduo-skills ↗ |
How do you curate RSS feeds into ranked digests?
Aggregate RSS feeds into concise digests highlighting new posts, trends, and follow-up actions for newsletters, research, or team briefings.
Who is it for?
Developers who want an agent to filter a fixed RSS bundle for high-signal AI agent and frontier tech essays in Chinese brief format.
Skip if: Skip ak-rss-digest when you need English output, custom feed lists without editing feeds.opml, or real-time push notifications instead of on-demand digest runs.
When should I use this skill?
User asks for a weekly RSS reading digest, AI agent news roundup, or to run fetch_today_feed_items.py with scoring above 7.0 in Chinese.
What you get
Simplified Chinese digest listing scored articles above 7.0 with title, recommendation, two-sentence summary, and canonical URL per item.
- Scored Chinese reading digest
- JSON feed item export
By the numbers
- Scores articles on a 10-point scale and outputs only items above 7.0
- Defaults to fetching the most recent 7 days of RSS entries
- Bundles feeds.opml and fetch_today_feed_items.py script
Files
AK RSS Digest
Overview
Use this skill to build a current reading list from the feed bundle in references/feeds.opml. Default to the most recent 7 days ending on the current date in Asia/Shanghai, and narrow to a single day only when the user explicitly asks for it.
Workflow
1. Run python3 scripts/fetch_today_feed_items.py --format json to collect entries from the configured feeds. This defaults to the most recent 7 days. 2. Treat feed-level network failures as non-fatal. Continue with the feeds that succeeded and mention major failures only when they materially reduce coverage. 3. Read the structured output and discard obvious mismatches before opening article pages. Reject items that are clearly raw research papers, release notes, changelogs, benchmark dumps, or narrowly technical implementation logs without broader implications. 4. Open the remaining candidate links when the feed summary is too thin to judge the article well. Skim for thesis, novelty, readability, and whether the piece offers strong perspective rather than just information. 5. Score every serious candidate on the rubric below. Output only items with a score strictly greater than 7.0. 6. If nothing clears the threshold, say so directly instead of padding the output with mediocre picks.
Selection Heuristics
Prefer articles with at least one of these traits:
- Fresh thinking about AI agents, agent tooling, agent UX, multi-agent workflows, evaluation, deployment, or failure modes.
- Strong interviews or conversations with operators, founders, researchers, or engineers who reveal how frontier work is actually being done.
- Essays that synthesize a new direction, new constraint, or strategic implication in AI, software, or adjacent technology.
- Pieces that are readable and idea-dense for a general technical audience, not just specialists in one subfield.
Penalize heavily or reject:
- Pure technical papers and paper summaries with little interpretive value.
- Vendor marketing, launch fluff, SEO writing, or obvious news rewrites.
- Narrow implementation diaries that do not connect to broader product, research, or ecosystem questions.
- Dry reference material that is correct but not worth a strong recommendation.
Scoring Rubric
9-10: Exceptional fit. Strong signal, strong writing, original insight, and clearly valuable for someone tracking AI agents or adjacent frontier shifts.8-8.9: Good recommendation. Worth reading, clear point of view, and relevant enough to the target taste profile.7-7.9: Borderline. Useful but not compelling enough for the final digest. Do not output it.5-6.9: Competent but dry, derivative, too narrow, or not aligned with the target taste profile.<5: Irrelevant, low-signal, or actively unsuitable.
When scoring, weigh these dimensions:
- Relevance to AI agents, frontier AI, deep operator insight, or adjacent strategic technology discussion.
- Originality of the article's argument or reporting.
- Readability and ability to hold attention.
- Practical usefulness for someone trying to keep up with meaningful new directions.
Output Format
Write the final answer in Simplified Chinese. For each article that scores above 7, include exactly these elements with Chinese labels:
标题: original article title.评分:x/10, use one decimal place when helpful.推荐语: one or two sentences explaining why this is worth reading.摘要: exactly two sentences summarizing the article.链接: canonical article URL.
Use a concise tone that reads like a curated daily brief, not a formal report:
- Prefer short, direct sentences over explanatory padding.
- Lead with why the article is worth the user's time.
- Keep each item compact and scannable.
- Avoid English field names such as
Title,Score, orRecommendation.
Use this structure for the final answer:
本期从最近一周的 RSS 里筛出几篇值得看的文章,重点偏 AI agent、前沿判断和不太枯燥的深度内容。
- 标题:文章标题
评分:8.7/10
推荐语:1-2 句话,先说为什么值得看。
摘要:严格两句话,讲清核心观点和价值。
链接:文章链接If nothing qualifies, say so directly in Chinese, for example:
这周没有筛到真正值得推荐的文章。现有更新要么偏技术细节,要么信息密度不够,没有过 7 分线。Resources
scripts/fetch_today_feed_items.py
Use this script to fetch the configured feeds and return recent entries as structured JSON or Markdown.
references/feeds.opml
Use this as the source of truth for the feed bundle. Keep the workflow anchored to this file unless the user explicitly asks to change the feed list.
Command Examples
Fetch the latest week of entries in Shanghai time:
python3 scripts/fetch_today_feed_items.py --format jsonFetch a single day explicitly:
python3 scripts/fetch_today_feed_items.py --date 2026-03-17 --days 1 --timezone Asia/Shanghai --format jsonFetch the latest posts from the past week:
python3 scripts/fetch_today_feed_items.py --days 7 --limit 30 --format jsonInspect a quick Markdown view instead of JSON:
python3 scripts/fetch_today_feed_items.py --format markdowninterface:
display_name: "RSS Agent Digest"
short_description: "Daily RSS picks for AI agent readers"
default_prompt: "Use $ak-rss-digest to pull the latest week's configured RSS posts, score them, and return only the strongest recommendations in Chinese."
policy:
allow_implicit_invocation: true
<?xml version="1.0" encoding="UTF-8"?>
<opml version="2.0">
<head>
<title>Blog Feeds</title>
</head>
<body>
<outline text="Blogs" title="Blogs">
<outline type="rss" text="simonwillison.net" title="simonwillison.net" xmlUrl="https://simonwillison.net/atom/everything/" htmlUrl="https://simonwillison.net"/>
<outline type="rss" text="jeffgeerling.com" title="jeffgeerling.com" xmlUrl="https://www.jeffgeerling.com/blog.xml" htmlUrl="https://jeffgeerling.com"/>
<outline type="rss" text="seangoedecke.com" title="seangoedecke.com" xmlUrl="https://www.seangoedecke.com/rss.xml" htmlUrl="https://seangoedecke.com"/>
<outline type="rss" text="krebsonsecurity.com" title="krebsonsecurity.com" xmlUrl="https://krebsonsecurity.com/feed/" htmlUrl="https://krebsonsecurity.com"/>
<outline type="rss" text="daringfireball.net" title="daringfireball.net" xmlUrl="https://daringfireball.net/feeds/main" htmlUrl="https://daringfireball.net"/>
<outline type="rss" text="ericmigi.com" title="ericmigi.com" xmlUrl="https://ericmigi.com/rss.xml" htmlUrl="https://ericmigi.com"/>
<outline type="rss" text="antirez.com" title="antirez.com" xmlUrl="http://antirez.com/rss" htmlUrl="http://antirez.com"/>
<outline type="rss" text="idiallo.com" title="idiallo.com" xmlUrl="https://idiallo.com/feed.rss" htmlUrl="https://idiallo.com"/>
<outline type="rss" text="maurycyz.com" title="maurycyz.com" xmlUrl="https://maurycyz.com/index.xml" htmlUrl="https://maurycyz.com"/>
<outline type="rss" text="pluralistic.net" title="pluralistic.net" xmlUrl="https://pluralistic.net/feed/" htmlUrl="https://pluralistic.net"/>
<outline type="rss" text="shkspr.mobi" title="shkspr.mobi" xmlUrl="https://shkspr.mobi/blog/feed/" htmlUrl="https://shkspr.mobi"/>
<outline type="rss" text="lcamtuf.substack.com" title="lcamtuf.substack.com" xmlUrl="https://lcamtuf.substack.com/feed" htmlUrl="https://lcamtuf.substack.com"/>
<outline type="rss" text="mitchellh.com" title="mitchellh.com" xmlUrl="https://mitchellh.com/feed.xml" htmlUrl="https://mitchellh.com"/>
<outline type="rss" text="dynomight.net" title="dynomight.net" xmlUrl="https://dynomight.net/feed.xml" htmlUrl="https://dynomight.net"/>
<outline type="rss" text="utcc.utoronto.ca/~cks" title="utcc.utoronto.ca/~cks" xmlUrl="https://utcc.utoronto.ca/~cks/space/blog/?atom" htmlUrl="https://utcc.utoronto.ca/~cks"/>
<outline type="rss" text="xeiaso.net" title="xeiaso.net" xmlUrl="https://xeiaso.net/blog.rss" htmlUrl="https://xeiaso.net"/>
<outline type="rss" text="devblogs.microsoft.com/oldnewthing" title="devblogs.microsoft.com/oldnewthing" xmlUrl="https://devblogs.microsoft.com/oldnewthing/feed" htmlUrl="https://devblogs.microsoft.com/oldnewthing"/>
<outline type="rss" text="righto.com" title="righto.com" xmlUrl="https://www.righto.com/feeds/posts/default" htmlUrl="https://righto.com"/>
<outline type="rss" text="lucumr.pocoo.org" title="lucumr.pocoo.org" xmlUrl="https://lucumr.pocoo.org/feed.atom" htmlUrl="https://lucumr.pocoo.org"/>
<outline type="rss" text="skyfall.dev" title="skyfall.dev" xmlUrl="https://skyfall.dev/rss.xml" htmlUrl="https://skyfall.dev"/>
<outline type="rss" text="garymarcus.substack.com" title="garymarcus.substack.com" xmlUrl="https://garymarcus.substack.com/feed" htmlUrl="https://garymarcus.substack.com"/>
<outline type="rss" text="overreacted.io" title="overreacted.io" xmlUrl="https://overreacted.io/rss.xml" htmlUrl="https://overreacted.io"/>
<outline type="rss" text="timsh.org" title="timsh.org" xmlUrl="https://timsh.org/rss/" htmlUrl="https://timsh.org"/>
<outline type="rss" text="johndcook.com" title="johndcook.com" xmlUrl="https://www.johndcook.com/blog/feed/" htmlUrl="https://johndcook.com"/>
<outline type="rss" text="gilesthomas.com" title="gilesthomas.com" xmlUrl="https://gilesthomas.com/feed/rss.xml" htmlUrl="https://gilesthomas.com"/>
<outline type="rss" text="matklad.github.io" title="matklad.github.io" xmlUrl="https://matklad.github.io/feed.xml" htmlUrl="https://matklad.github.io"/>
<outline type="rss" text="derekthompson.org" title="derekthompson.org" xmlUrl="https://www.theatlantic.com/feed/author/derek-thompson/" htmlUrl="https://derekthompson.org"/>
<outline type="rss" text="evanhahn.com" title="evanhahn.com" xmlUrl="https://evanhahn.com/feed.xml" htmlUrl="https://evanhahn.com"/>
<outline type="rss" text="terriblesoftware.org" title="terriblesoftware.org" xmlUrl="https://terriblesoftware.org/feed/" htmlUrl="https://terriblesoftware.org"/>
<outline type="rss" text="rakhim.exotext.com" title="rakhim.exotext.com" xmlUrl="https://rakhim.exotext.com/rss.xml" htmlUrl="https://rakhim.exotext.com"/>
<outline type="rss" text="joanwestenberg.com" title="joanwestenberg.com" xmlUrl="https://joanwestenberg.com/rss" htmlUrl="https://joanwestenberg.com"/>
<outline type="rss" text="xania.org" title="xania.org" xmlUrl="https://xania.org/feed" htmlUrl="https://xania.org"/>
<outline type="rss" text="micahflee.com" title="micahflee.com" xmlUrl="https://micahflee.com/feed/" htmlUrl="https://micahflee.com"/>
<outline type="rss" text="nesbitt.io" title="nesbitt.io" xmlUrl="https://nesbitt.io/feed.xml" htmlUrl="https://nesbitt.io"/>
<outline type="rss" text="construction-physics.com" title="construction-physics.com" xmlUrl="https://www.construction-physics.com/feed" htmlUrl="https://construction-physics.com"/>
<outline type="rss" text="tedium.co" title="tedium.co" xmlUrl="https://feed.tedium.co/" htmlUrl="https://tedium.co"/>
<outline type="rss" text="susam.net" title="susam.net" xmlUrl="https://susam.net/feed.xml" htmlUrl="https://susam.net"/>
<outline type="rss" text="entropicthoughts.com" title="entropicthoughts.com" xmlUrl="https://entropicthoughts.com/feed.xml" htmlUrl="https://entropicthoughts.com"/>
<outline type="rss" text="buttondown.com/hillelwayne" title="buttondown.com/hillelwayne" xmlUrl="https://buttondown.com/hillelwayne/rss" htmlUrl="https://buttondown.com/hillelwayne"/>
<outline type="rss" text="dwarkesh.com" title="dwarkesh.com" xmlUrl="https://www.dwarkeshpatel.com/feed" htmlUrl="https://dwarkesh.com"/>
<outline type="rss" text="borretti.me" title="borretti.me" xmlUrl="https://borretti.me/feed.xml" htmlUrl="https://borretti.me"/>
<outline type="rss" text="wheresyoured.at" title="wheresyoured.at" xmlUrl="https://www.wheresyoured.at/rss/" htmlUrl="https://wheresyoured.at"/>
<outline type="rss" text="jayd.ml" title="jayd.ml" xmlUrl="https://jayd.ml/feed.xml" htmlUrl="https://jayd.ml"/>
<outline type="rss" text="minimaxir.com" title="minimaxir.com" xmlUrl="https://minimaxir.com/index.xml" htmlUrl="https://minimaxir.com"/>
<outline type="rss" text="geohot.github.io" title="geohot.github.io" xmlUrl="https://geohot.github.io/blog/feed.xml" htmlUrl="https://geohot.github.io"/>
<outline type="rss" text="paulgraham.com" title="paulgraham.com" xmlUrl="http://www.aaronsw.com/2002/feeds/pgessays.rss" htmlUrl="https://paulgraham.com"/>
<outline type="rss" text="filfre.net" title="filfre.net" xmlUrl="https://www.filfre.net/feed/" htmlUrl="https://filfre.net"/>
<outline type="rss" text="blog.jim-nielsen.com" title="blog.jim-nielsen.com" xmlUrl="https://blog.jim-nielsen.com/feed.xml" htmlUrl="https://blog.jim-nielsen.com"/>
<outline type="rss" text="jyn.dev" title="jyn.dev" xmlUrl="https://jyn.dev/atom.xml" htmlUrl="https://jyn.dev"/>
<outline type="rss" text="geoffreylitt.com" title="geoffreylitt.com" xmlUrl="https://www.geoffreylitt.com/feed.xml" htmlUrl="https://geoffreylitt.com"/>
<outline type="rss" text="downtowndougbrown.com" title="downtowndougbrown.com" xmlUrl="https://www.downtowndougbrown.com/feed/" htmlUrl="https://downtowndougbrown.com"/>
<outline type="rss" text="brutecat.com" title="brutecat.com" xmlUrl="https://brutecat.com/rss.xml" htmlUrl="https://brutecat.com"/>
<outline type="rss" text="eli.thegreenplace.net" title="eli.thegreenplace.net" xmlUrl="https://eli.thegreenplace.net/feeds/all.atom.xml" htmlUrl="https://eli.thegreenplace.net"/>
<outline type="rss" text="abortretry.fail" title="abortretry.fail" xmlUrl="https://www.abortretry.fail/feed" htmlUrl="https://abortretry.fail"/>
<outline type="rss" text="fabiensanglard.net" title="fabiensanglard.net" xmlUrl="https://fabiensanglard.net/rss.xml" htmlUrl="https://fabiensanglard.net"/>
<outline type="rss" text="oldvcr.blogspot.com" title="oldvcr.blogspot.com" xmlUrl="https://oldvcr.blogspot.com/feeds/posts/default" htmlUrl="https://oldvcr.blogspot.com"/>
<outline type="rss" text="bogdanthegeek.github.io" title="bogdanthegeek.github.io" xmlUrl="https://bogdanthegeek.github.io/blog/index.xml" htmlUrl="https://bogdanthegeek.github.io"/>
<outline type="rss" text="hugotunius.se" title="hugotunius.se" xmlUrl="https://hugotunius.se/feed.xml" htmlUrl="https://hugotunius.se"/>
<outline type="rss" text="gwern.net" title="gwern.net" xmlUrl="https://gwern.substack.com/feed" htmlUrl="https://gwern.net"/>
<outline type="rss" text="berthub.eu" title="berthub.eu" xmlUrl="https://berthub.eu/articles/index.xml" htmlUrl="https://berthub.eu"/>
<outline type="rss" text="chadnauseam.com" title="chadnauseam.com" xmlUrl="https://chadnauseam.com/rss.xml" htmlUrl="https://chadnauseam.com"/>
<outline type="rss" text="simone.org" title="simone.org" xmlUrl="https://simone.org/feed/" htmlUrl="https://simone.org"/>
<outline type="rss" text="it-notes.dragas.net" title="it-notes.dragas.net" xmlUrl="https://it-notes.dragas.net/feed/" htmlUrl="https://it-notes.dragas.net"/>
<outline type="rss" text="beej.us" title="beej.us" xmlUrl="https://beej.us/blog/rss.xml" htmlUrl="https://beej.us"/>
<outline type="rss" text="hey.paris" title="hey.paris" xmlUrl="https://hey.paris/index.xml" htmlUrl="https://hey.paris"/>
<outline type="rss" text="danielwirtz.com" title="danielwirtz.com" xmlUrl="https://danielwirtz.com/rss.xml" htmlUrl="https://danielwirtz.com"/>
<outline type="rss" text="matduggan.com" title="matduggan.com" xmlUrl="https://matduggan.com/rss/" htmlUrl="https://matduggan.com"/>
<outline type="rss" text="refactoringenglish.com" title="refactoringenglish.com" xmlUrl="https://refactoringenglish.com/index.xml" htmlUrl="https://refactoringenglish.com"/>
<outline type="rss" text="worksonmymachine.substack.com" title="worksonmymachine.substack.com" xmlUrl="https://worksonmymachine.substack.com/feed" htmlUrl="https://worksonmymachine.substack.com"/>
<outline type="rss" text="philiplaine.com" title="philiplaine.com" xmlUrl="https://philiplaine.com/index.xml" htmlUrl="https://philiplaine.com"/>
<outline type="rss" text="steveblank.com" title="steveblank.com" xmlUrl="https://steveblank.com/feed/" htmlUrl="https://steveblank.com"/>
<outline type="rss" text="bernsteinbear.com" title="bernsteinbear.com" xmlUrl="https://bernsteinbear.com/feed.xml" htmlUrl="https://bernsteinbear.com"/>
<outline type="rss" text="danieldelaney.net" title="danieldelaney.net" xmlUrl="https://danieldelaney.net/feed" htmlUrl="https://danieldelaney.net"/>
<outline type="rss" text="troyhunt.com" title="troyhunt.com" xmlUrl="https://www.troyhunt.com/rss/" htmlUrl="https://troyhunt.com"/>
<outline type="rss" text="herman.bearblog.dev" title="herman.bearblog.dev" xmlUrl="https://herman.bearblog.dev/feed/" htmlUrl="https://herman.bearblog.dev"/>
<outline type="rss" text="tomrenner.com" title="tomrenner.com" xmlUrl="https://tomrenner.com/index.xml" htmlUrl="https://tomrenner.com"/>
<outline type="rss" text="blog.pixelmelt.dev" title="blog.pixelmelt.dev" xmlUrl="https://blog.pixelmelt.dev/rss/" htmlUrl="https://blog.pixelmelt.dev"/>
<outline type="rss" text="martinalderson.com" title="martinalderson.com" xmlUrl="https://martinalderson.com/feed.xml" htmlUrl="https://martinalderson.com"/>
<outline type="rss" text="danielchasehooper.com" title="danielchasehooper.com" xmlUrl="https://danielchasehooper.com/feed.xml" htmlUrl="https://danielchasehooper.com"/>
<outline type="rss" text="chiark.greenend.org.uk/~sgtatham" title="chiark.greenend.org.uk/~sgtatham" xmlUrl="https://www.chiark.greenend.org.uk/~sgtatham/quasiblog/feed.xml" htmlUrl="https://chiark.greenend.org.uk/~sgtatham"/>
<outline type="rss" text="grantslatton.com" title="grantslatton.com" xmlUrl="https://grantslatton.com/rss.xml" htmlUrl="https://grantslatton.com"/>
<outline type="rss" text="experimental-history.com" title="experimental-history.com" xmlUrl="https://www.experimental-history.com/feed" htmlUrl="https://experimental-history.com"/>
<outline type="rss" text="anildash.com" title="anildash.com" xmlUrl="https://anildash.com/feed.xml" htmlUrl="https://anildash.com"/>
<outline type="rss" text="aresluna.org" title="aresluna.org" xmlUrl="https://aresluna.org/main.rss" htmlUrl="https://aresluna.org"/>
<outline type="rss" text="michael.stapelberg.ch" title="michael.stapelberg.ch" xmlUrl="https://michael.stapelberg.ch/feed.xml" htmlUrl="https://michael.stapelberg.ch"/>
<outline type="rss" text="miguelgrinberg.com" title="miguelgrinberg.com" xmlUrl="https://blog.miguelgrinberg.com/feed" htmlUrl="https://miguelgrinberg.com"/>
<outline type="rss" text="keygen.sh" title="keygen.sh" xmlUrl="https://keygen.sh/blog/feed.xml" htmlUrl="https://keygen.sh"/>
<outline type="rss" text="mjg59.dreamwidth.org" title="mjg59.dreamwidth.org" xmlUrl="https://mjg59.dreamwidth.org/data/rss" htmlUrl="https://mjg59.dreamwidth.org"/>
<outline type="rss" text="computer.rip" title="computer.rip" xmlUrl="https://computer.rip/rss.xml" htmlUrl="https://computer.rip"/>
</outline>
</body>
</opml>
#!/usr/bin/env python3
import argparse
import concurrent.futures
import datetime as dt
import html
import json
import re
import ssl
import sys
import urllib.error
import urllib.request
import xml.etree.ElementTree as ET
from email.utils import parsedate_to_datetime
from pathlib import Path
USER_AGENT = "Mozilla/5.0 (compatible; ak-rss-digest/1.0; +https://openai.com)"
ACCEPT = "application/rss+xml, application/atom+xml, application/xml, text/xml, */*;q=0.8"
DEFAULT_TIMEOUT = 15
DEFAULT_TZ = "Asia/Shanghai"
DEFAULT_DAYS = 7
def local_name(tag):
return tag.rsplit("}", 1)[-1]
def strip_html(raw):
if not raw:
return ""
text = re.sub(r"<[^>]+>", " ", raw)
text = html.unescape(text)
return re.sub(r"\s+", " ", text).strip()
def first_text(parent, names):
for child in parent:
if local_name(child.tag) in names:
text = "".join(child.itertext()).strip()
if text:
return text
return ""
def first_link(entry):
for child in entry:
if local_name(child.tag) != "link":
continue
href = child.attrib.get("href")
rel = child.attrib.get("rel", "alternate")
if href and rel == "alternate":
return href
text = "".join(child.itertext()).strip()
if text:
return text
if href:
return href
return ""
def parse_datetime(raw):
if not raw:
return None
value = raw.strip()
try:
parsed = parsedate_to_datetime(value)
if parsed.tzinfo is None:
return parsed.replace(tzinfo=dt.timezone.utc)
return parsed
except (TypeError, ValueError, IndexError):
pass
normalized = value.replace("Z", "+00:00")
try:
parsed = dt.datetime.fromisoformat(normalized)
except ValueError:
return None
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=dt.timezone.utc)
return parsed
def load_feeds(opml_path):
root = ET.parse(opml_path).getroot()
feeds = []
for outline in root.findall(".//outline[@type='rss']"):
feeds.append(
{
"name": outline.attrib.get("text") or outline.attrib.get("title") or "",
"xml_url": outline.attrib["xmlUrl"],
"html_url": outline.attrib.get("htmlUrl", ""),
}
)
return feeds
def fetch_url(url, timeout):
req = urllib.request.Request(
url,
headers={
"User-Agent": USER_AGENT,
"Accept": ACCEPT,
},
)
with urllib.request.urlopen(req, timeout=timeout, context=ssl.create_default_context()) as resp:
return resp.read(), resp.geturl(), resp.headers.get("Content-Type", "")
def parse_atom(root, feed_meta):
feed_title = first_text(root, {"title"}) or feed_meta["name"]
entries = []
for entry in root:
if local_name(entry.tag) != "entry":
continue
published_raw = first_text(entry, {"published", "updated", "issued", "created"})
entries.append(
{
"feed_name": feed_title,
"feed_url": feed_meta["xml_url"],
"site_url": feed_meta["html_url"],
"title": first_text(entry, {"title"}) or "(untitled)",
"link": first_link(entry),
"published_raw": published_raw,
"published_at": parse_datetime(published_raw),
"summary": strip_html(first_text(entry, {"summary", "content"})),
}
)
return entries
def parse_rss(root, feed_meta):
channel = None
if local_name(root.tag) == "rss":
for child in root:
if local_name(child.tag) == "channel":
channel = child
break
elif local_name(root.tag) in {"RDF", "rdf"}:
channel = root
else:
channel = root
feed_title = first_text(channel, {"title"}) or feed_meta["name"]
entries = []
for item in channel.iter():
if local_name(item.tag) != "item":
continue
published_raw = first_text(item, {"pubDate", "published", "date", "updated"})
entries.append(
{
"feed_name": feed_title,
"feed_url": feed_meta["xml_url"],
"site_url": feed_meta["html_url"],
"title": first_text(item, {"title"}) or "(untitled)",
"link": first_link(item) or first_text(item, {"guid"}),
"published_raw": published_raw,
"published_at": parse_datetime(published_raw),
"summary": strip_html(first_text(item, {"description", "encoded", "content", "summary"})),
}
)
return entries
def parse_feed(content, feed_meta):
root = ET.fromstring(content)
tag = local_name(root.tag)
if tag == "feed":
return parse_atom(root, feed_meta)
if tag in {"rss", "RDF", "rdf"}:
return parse_rss(root, feed_meta)
raise ValueError(f"Unsupported feed root tag: {root.tag}")
def fetch_feed(feed_meta, timeout):
try:
content, final_url, content_type = fetch_url(feed_meta["xml_url"], timeout)
entries = parse_feed(content, feed_meta)
return {
"feed": feed_meta,
"status": "ok",
"final_url": final_url,
"content_type": content_type,
"entries": entries,
}
except Exception as exc:
return {
"feed": feed_meta,
"status": "error",
"error": f"{type(exc).__name__}: {exc}",
"entries": [],
}
def serialize_item(item, target_tz):
published_at = item["published_at"]
published_local = published_at.astimezone(target_tz) if published_at else None
return {
"feed_name": item["feed_name"],
"feed_url": item["feed_url"],
"site_url": item["site_url"],
"title": item["title"],
"link": item["link"],
"published_raw": item["published_raw"],
"published_at": published_at.isoformat() if published_at else None,
"published_local": published_local.isoformat() if published_local else None,
"summary": item["summary"],
}
def format_markdown(payload):
target_label = payload.get("target_date")
if payload.get("days", 1) > 1:
target_label = f"{payload['target_date']} minus {payload['days'] - 1} day(s)"
lines = [
f"# RSS items for {target_label} ({payload['timezone']})",
"",
f"- Feeds checked: {payload['feed_count']}",
f"- Feeds failed: {len(payload['errors'])}",
f"- Matching items: {len(payload['items'])}",
"",
]
if payload["errors"]:
lines.extend(["## Feed errors", ""])
for error in payload["errors"]:
lines.append(f"- {error['feed_name']}: {error['error']}")
lines.append("")
if payload["items"]:
lines.extend(["## Items", ""])
for item in payload["items"]:
lines.append(f"### {item['title']}")
lines.append(f"- Feed: {item['feed_name']}")
lines.append(f"- Published: {item['published_local'] or item['published_raw'] or 'unknown'}")
lines.append(f"- Link: {item['link']}")
if item["summary"]:
lines.append(f"- Summary: {item['summary']}")
lines.append("")
return "\n".join(lines).rstrip() + "\n"
def main():
skill_dir = Path(__file__).resolve().parent.parent
parser = argparse.ArgumentParser(description="Fetch recent items from the configured RSS bundle.")
parser.add_argument("--feeds-file", default=str(skill_dir / "references" / "feeds.opml"))
parser.add_argument("--date", help="Target end date in YYYY-MM-DD. Default: current date in target timezone.")
parser.add_argument(
"--days",
type=int,
default=DEFAULT_DAYS,
help=f"Number of days to include ending on --date. Default: {DEFAULT_DAYS}.",
)
parser.add_argument("--limit", type=int, help="Optional maximum number of items to return after sorting.")
parser.add_argument("--timezone", default=DEFAULT_TZ)
parser.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT)
parser.add_argument("--workers", type=int, default=10)
parser.add_argument("--format", choices=("json", "markdown"), default="json")
args = parser.parse_args()
if args.days < 1:
raise SystemExit("--days must be at least 1")
if args.limit is not None and args.limit < 1:
raise SystemExit("--limit must be at least 1")
try:
target_tz = dt.ZoneInfo(args.timezone)
except Exception as exc:
raise SystemExit(f"Invalid timezone '{args.timezone}': {exc}")
if args.date:
target_date = dt.date.fromisoformat(args.date)
else:
target_date = dt.datetime.now(target_tz).date()
start_date = target_date - dt.timedelta(days=args.days - 1)
feeds = load_feeds(args.feeds_file)
results = []
with concurrent.futures.ThreadPoolExecutor(max_workers=args.workers) as executor:
futures = [executor.submit(fetch_feed, feed, args.timeout) for feed in feeds]
for future in concurrent.futures.as_completed(futures):
results.append(future.result())
items = []
errors = []
for result in results:
if result["status"] != "ok":
errors.append(
{
"feed_name": result["feed"]["name"],
"feed_url": result["feed"]["xml_url"],
"error": result["error"],
}
)
continue
for entry in result["entries"]:
published_at = entry["published_at"]
if not published_at:
continue
published_local = published_at.astimezone(target_tz)
published_date = published_local.date()
if published_date < start_date or published_date > target_date:
continue
if not entry["link"]:
continue
items.append(serialize_item(entry, target_tz))
items.sort(
key=lambda item: (
item["published_local"] or "",
item["feed_name"].lower(),
item["title"].lower(),
),
reverse=True,
)
errors.sort(key=lambda err: err["feed_name"].lower())
if args.limit is not None:
items = items[: args.limit]
payload = {
"start_date": start_date.isoformat(),
"target_date": target_date.isoformat(),
"days": args.days,
"timezone": args.timezone,
"feed_count": len(feeds),
"items": items,
"errors": errors,
}
if args.format == "markdown":
sys.stdout.write(format_markdown(payload))
else:
json.dump(payload, sys.stdout, ensure_ascii=True, indent=2)
sys.stdout.write("\n")
if __name__ == "__main__":
if not hasattr(dt, "ZoneInfo"):
from zoneinfo import ZoneInfo # type: ignore
dt.ZoneInfo = ZoneInfo # type: ignore[attr-defined]
main()
Related skills
FAQ
What score threshold does ak-rss-digest use?
ak-rss-digest scores every serious candidate on a 10-point rubric and outputs only articles strictly above 7.0. Borderline 7.0–7.9 items are excluded; if nothing qualifies, the skill reports that directly in Chinese.
How does ak-rss-digest fetch feed entries?
ak-rss-digest runs python3 scripts/fetch_today_feed_items.py against references/feeds.opml. The default command returns the most recent 7 days in Asia/Shanghai timezone as JSON or Markdown.
What content does ak-rss-digest prioritize?
ak-rss-digest prioritizes AI agents, multi-agent workflows, operator interviews, and frontier synthesis essays readable to a general technical audience. It penalizes raw papers, release notes, SEO rewrites, and vendor launch fluff.