Skip to content

Instantly share code, notes, and snippets.

@neltseng
Last active August 19, 2026 03:20
Show Gist options
  • Select an option

  • Save neltseng/7fa844b8ef81108c53076ddc989d9c2b to your computer and use it in GitHub Desktop.

Select an option

Save neltseng/7fa844b8ef81108c53076ddc989d9c2b to your computer and use it in GitHub Desktop.
hz daily digest feeds (single source of truth for local skill + cloud routine)
{
"note": "抓取層黑名單。標題命中任一 regex 就整則丟棄,不進 latest.json,也不會出現在日報的『已過濾』清單。只擋與 nel 完全無關的類別(股市籌碼、財報、影音直播),有判斷價值的(詐騙警示、法規、費率)留給篩選層。",
"title": [
"股價|漲停|跌停|當沖|沖銷|本益比|股價淨值比|除權|除息|填息|殖利率",
"法人|外資|投信|自營|融資|融券|分點|籌碼|買超|賣超|目標價|概念股",
"營收|財報|季報|年報|EPS|每股盈餘|股東會|法說會|承銷|掛牌|興櫃",
"盤中|盤後|收盤|開盤|台股|美股|大盤|加權指數",
"同學會|存股|飆股|抱緊處理",
"- YouTube$||LIVE|直播",
"^Nightly$|^trunk-snapshot$"
]
}
[
{
"name": "WordPress News",
"url": "https://wordpress.org/news/feed/",
"cat": "wordpress"
},
{
"name": "WP Tavern",
"url": "https://wptavern.com/feed",
"cat": "wordpress"
},
{
"name": "WordPress Developer Blog",
"url": "https://developer.wordpress.org/news/feed/",
"cat": "wordpress"
},
{
"name": "WooCommerce Developer Blog",
"url": "https://developer.woocommerce.com/feed/",
"cat": "woocommerce"
},
{
"name": "Practical Ecommerce",
"url": "https://www.practicalecommerce.com/feed",
"cat": "ecommerce"
},
{
"name": "Modern Retail",
"url": "https://www.modernretail.co/feed/",
"cat": "ecommerce"
},
{
"name": "Retail Dive",
"url": "https://www.retaildive.com/feeds/news/",
"cat": "ecommerce"
},
{
"name": "Search Engine Land",
"url": "https://searchengineland.com/feed",
"cat": "ads"
},
{
"name": "Search Engine Journal PPC",
"url": "https://www.searchenginejournal.com/category/pay-per-click/feed/",
"cat": "ads"
},
{
"name": "Google Ads & Commerce",
"url": "https://blog.google/products/ads-commerce/rss/",
"cat": "ads"
},
{
"name": "PPC Land",
"url": "https://ppc.land/rss/",
"cat": "ads"
},
{
"name": "Google Search Central",
"url": "https://developers.google.com/search/blog/feed.xml",
"cat": "seo"
},
{
"name": "Search Engine Roundtable",
"url": "https://www.seroundtable.com/index.rdf",
"cat": "seo"
},
{
"name": "Search Engine Journal SEO",
"url": "https://www.searchenginejournal.com/category/seo/feed/",
"cat": "seo"
},
{
"name": "Ahrefs Blog",
"url": "https://ahrefs.com/blog/feed/",
"cat": "seo"
},
{
"name": "Semrush Blog",
"url": "https://www.semrush.com/blog/feed/",
"cat": "seo"
},
{
"name": "Moz Blog",
"url": "https://moz.com/posts/rss/blog",
"cat": "seo"
},
{
"name": "Stripe Blog",
"url": "https://stripe.com/blog/feed.rss",
"cat": "payment"
},
{
"name": "Stripe PHP Releases",
"url": "https://github.com/stripe/stripe-php/releases.atom",
"cat": "payment"
},
{
"name": "LINE Bot SDK Releases",
"url": "https://github.com/line/line-bot-sdk-php/releases.atom",
"cat": "payment"
},
{
"name": "台灣第三方支付(GNews)",
"url": "https://news.google.com/rss/search?q=%E7%B6%A0%E7%95%8C+OR+%E8%97%8D%E6%96%B0+OR+%E7%AC%AC%E4%B8%89%E6%96%B9%E6%94%AF%E4%BB%98&hl=zh-TW&gl=TW&ceid=TW:zh-Hant",
"cat": "payment"
},
{
"name": "LINE Pay(GNews)",
"url": "https://news.google.com/rss/search?q=LINE+Pay+OR+LINE+%E9%8C%A2%E5%8C%85&hl=zh-TW&gl=TW&ceid=TW:zh-Hant",
"cat": "payment"
},
{
"name": "OpenAI News",
"url": "https://openai.com/news/rss.xml",
"cat": "ai"
},
{
"name": "Anthropic(GNews)",
"url": "https://news.google.com/rss/search?q=Anthropic+Claude&hl=en-US&gl=US&ceid=US:en",
"cat": "ai"
},
{
"name": "xAI Grok(GNews)",
"url": "https://news.google.com/rss/search?q=xAI+Grok&hl=en-US&gl=US&ceid=US:en",
"cat": "ai"
},
{
"name": "Claude Code Releases",
"url": "https://github.com/anthropics/claude-code/releases.atom",
"cat": "ai"
},
{
"name": "Shopify Dev Changelog",
"url": "https://shopify.dev/changelog/feed.xml",
"cat": "shopify"
},
{
"name": "Shopify Changelog",
"url": "https://changelog.shopify.com/feed",
"cat": "shopify"
},
{
"name": "Cloudflare Blog",
"url": "https://blog.cloudflare.com/rss/",
"cat": "stack"
},
{
"name": "Vercel",
"url": "https://vercel.com/atom",
"cat": "stack"
},
{
"name": "Tailwind CSS Blog",
"url": "https://tailwindcss.com/feeds/feed.xml",
"cat": "stack"
},
{
"name": "Tailwind CSS Releases",
"url": "https://github.com/tailwindlabs/tailwindcss/releases.atom",
"cat": "stack"
},
{
"name": "WooCommerce Releases",
"url": "https://github.com/woocommerce/woocommerce/releases.atom",
"cat": "release"
},
{
"name": "WordPress Releases",
"url": "https://github.com/WordPress/WordPress/releases.atom",
"cat": "release"
},
{
"name": "WP-CLI Releases",
"url": "https://github.com/wp-cli/wp-cli/releases.atom",
"cat": "release"
},
{
"name": "Medusa Releases",
"url": "https://github.com/medusajs/medusa/releases.atom",
"cat": "release"
},
{
"name": "Saleor Releases",
"url": "https://github.com/saleor/saleor/releases.atom",
"cat": "release"
},
{
"name": "80 Level",
"cat": "gamedev",
"url": "https://80.lv/feed/"
},
{
"name": "Game Developer",
"cat": "gamedev",
"url": "https://www.gamedeveloper.com/rss.xml"
},
{
"name": "three.js Releases",
"cat": "gamedev",
"url": "https://github.com/mrdoob/three.js/releases.atom"
},
{
"name": "React Three Fiber Releases",
"cat": "gamedev",
"url": "https://github.com/pmndrs/react-three-fiber/releases.atom"
},
{
"name": "ComfyUI Releases",
"cat": "genai",
"url": "https://github.com/comfyanonymous/ComfyUI/releases.atom"
},
{
"name": "Hugging Face Blog",
"cat": "genai",
"url": "https://huggingface.co/blog/feed.xml"
},
{
"name": "Google DeepMind",
"cat": "genai",
"url": "https://deepmind.google/blog/rss.xml"
},
{
"name": "Google Labs",
"cat": "genai",
"url": "https://blog.google/technology/google-labs/rss/"
},
{
"name": "AI 生成 3D(GNews)",
"cat": "genai",
"url": "https://news.google.com/rss/search?q=%22text-to-3D%22+OR+%22image-to-3D%22+OR+%22video-to-3D%22&hl=en-US&gl=US&ceid=US:en"
},
{
"name": "AI 生成音效音樂(GNews)",
"cat": "genai",
"url": "https://news.google.com/rss/search?q=%22AI+sound+effects%22+OR+%22AI+music+generation%22+OR+%22text-to-audio%22&hl=en-US&gl=US&ceid=US:en"
},
{
"name": "AI 做遊戲(GNews)",
"cat": "gamedev",
"url": "https://news.google.com/rss/search?q=%22AI+game+development%22+OR+%22generative+game+assets%22&hl=en-US&gl=US&ceid=US:en"
},
{
"name": "Codrops",
"cat": "ui",
"url": "https://tympanus.net/codrops/feed/"
},
{
"name": "shadcn/ui Releases",
"cat": "ui",
"url": "https://github.com/shadcn-ui/ui/releases.atom"
},
{
"name": "Preline Releases",
"cat": "ui",
"url": "https://github.com/htmlstreamofficial/preline/releases.atom"
},
{
"name": "Material Web Releases",
"cat": "ui",
"url": "https://github.com/material-components/material-web/releases.atom"
},
{
"name": "GSAP Releases",
"cat": "ui",
"url": "https://github.com/greensock/GSAP/releases.atom"
},
{
"name": "Motion Releases",
"cat": "ui",
"url": "https://github.com/motiondivision/motion/releases.atom"
},
{
"name": "免費資源網路社群",
"cat": "tools",
"url": "https://free.com.tw/feed/"
}
]
"""Fetch configured RSS/Atom feeds and emit recent items as JSON. Stdlib only.
The feed list lives in a gist so the local skill and the cloud routine read the
same source of truth. Edit feeds.json, then: gh gist edit 7fa844b8ef81108c53076ddc989d9c2b -a feeds.json
"""
import json
import re
import sys
import time
import urllib.request
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timedelta, timezone
from email.utils import parsedate_to_datetime
from html.entities import name2codepoint
from xml.etree import ElementTree
GIST = "https://gist.githubusercontent.com/neltseng/7fa844b8ef81108c53076ddc989d9c2b/raw"
FEEDS_URL = f"{GIST}/feeds.json"
DENY_URL = f"{GIST}/deny.json"
UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
NS = {"atom": "http://www.w3.org/2005/Atom", "rss": "http://purl.org/rss/1.0/"}
TAGS = re.compile(r"<[^>]+>")
# Some WP feeds emit HTML entities (&nbsp;) that a strict XML parser rejects.
ENTITY = re.compile(rb"&(?!#)([a-zA-Z][a-zA-Z0-9]*);")
# Shopify's changelog embeds raw control chars (VT) in code samples; XML 1.0 forbids them.
CTRL = re.compile(rb"[\x00-\x08\x0b\x0c\x0e-\x1f]")
def xml_safe(raw):
def to_numeric(match):
code = name2codepoint.get(match.group(1).decode())
return f"&#{code};".encode() if code else match.group(0)
return CTRL.sub(b" ", ENTITY.sub(to_numeric, raw))
def parse_date(raw):
raw = raw.strip()
try:
return parsedate_to_datetime(raw)
except (TypeError, ValueError):
pass
try:
return datetime.fromisoformat(raw.replace("Z", "+00:00"))
except ValueError:
return None
def text_of(node, *paths):
for path in paths:
found = node.find(path, NS)
if found is not None:
value = found.text or found.get("href") or ""
if value.strip():
return value.strip()
return ""
def entries(root):
for tag in ("channel/item", "item", "atom:entry", "rss:item"):
found = root.findall(tag, NS)
if found:
return found
return []
def fetch(feed, cutoff, deny):
req = urllib.request.Request(feed["url"], headers={"User-Agent": UA})
try:
with urllib.request.urlopen(req, timeout=20) as resp:
root = ElementTree.fromstring(xml_safe(resp.read()))
except Exception as exc: # noqa: BLE001 - report and skip dead feeds
print(f" ! {feed['name']}: {exc}", file=sys.stderr)
return []
out, blocked = [], 0
for item in entries(root):
raw_date = text_of(item, "pubDate", "atom:updated", "atom:published", "{http://purl.org/dc/elements/1.1/}date")
published = parse_date(raw_date) if raw_date else None
if published and published.tzinfo is None:
published = published.replace(tzinfo=timezone.utc)
if published and published < cutoff:
continue
title = TAGS.sub("", text_of(item, "title", "atom:title")).strip()
if deny and deny.search(title):
blocked += 1
continue
body = text_of(item, "description", "atom:summary", "atom:content", "{http://purl.org/rss/1.0/modules/content/}encoded")
out.append({
"src": feed["name"],
"cat": feed["cat"],
"title": title,
"url": text_of(item, "link", "atom:link"),
"published": published.isoformat() if published else "",
"content": TAGS.sub(" ", body)[:600].strip(),
})
print(f" · {feed['name']}: {len(out)}" + (f"(擋 {blocked})" if blocked else ""), file=sys.stderr)
return out
def load_json(url):
# GitHub raw caches aggressively; bust it so a gist edit takes effect immediately.
req = urllib.request.Request(f"{url}?t={int(time.time())}", headers={"User-Agent": UA, "Cache-Control": "no-cache"})
with urllib.request.urlopen(req, timeout=20) as resp:
return json.loads(resp.read())
def load_deny():
try:
return re.compile("|".join(load_json(DENY_URL)["title"]))
except Exception as exc: # noqa: BLE001 - a missing deny list must not stop the fetch
print(f" ! deny.json: {exc}", file=sys.stderr)
return None
def main():
hours = int(sys.argv[1]) if len(sys.argv) > 1 else 24
feeds = load_json(FEEDS_URL)
deny = load_deny()
cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
with ThreadPoolExecutor(max_workers=8) as pool:
results = pool.map(lambda f: fetch(f, cutoff, deny), feeds)
items = [i for group in results for i in group]
items.sort(key=lambda i: i["published"], reverse=True)
print(f"\n{len(items)} items / {hours}h", file=sys.stderr)
json.dump(items, sys.stdout, ensure_ascii=False, indent=1)
if __name__ == "__main__":
main()
"""Render digest.json into the cloudwp Mailchimp-style HTML and send via Mailgun.
Usage: MAILGUN_KEY=<key> python3 mail.py [digest.json]
SMTP is unusable from the Claude cloud sandbox (its egress proxy rejects raw TCP
on non-443 ports), so this goes over Mailgun's REST API on 443. The template
mirrors .doc/cloudwp/mailchimp/email-template-announcement.html.
"""
import base64
import html
import json
import os
import re
import sys
import urllib.request
from pathlib import Path
DOMAIN = "cloudwp.pro"
TO = "nel@tseng.cc"
COLORS = {
"直接影響你手上的東西": "#ef4444",
"電商": "#f97316",
"廣告投放": "#eab308",
"SEO": "#22c55e",
"技術棧與金流": "#3b82f6",
"AI": "#a855f7",
"遊戲與生成式素材": "#ec4899",
"UI 與設計系統": "#14b8a6",
}
def render_item(item):
time = (
f'<p style="font-size: 13px; color: #64748b; margin: 5px 0 0 0;">{html.escape(item["time"])}</p>'
if item.get("time") else ""
)
return f"""
<div style="margin: 0 0 24px 0; padding: 0 0 24px 0; border-bottom: 1px solid #e2e8f0;">
<a href="{html.escape(item['url'])}" style="font-size: 17px; font-weight: 600; color: #0f172a; line-height: 1.45; text-decoration: none;">{html.escape(item['title'])}</a>
{time}
<p style="font-size: 15px; line-height: 1.75; color: #1e293b; margin: 10px 0 12px 0;">{html.escape(item['fact'])}</p>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color: #eef2f7; border-radius: 6px;">
<tr>
<td style="padding: 15px 0 14px 16px; vertical-align: top; width: 34px;">
<img src="https://raw.githubusercontent.com/neltseng/hz-assets/main/info.png" width="18" height="18" alt="" style="display: block;">
</td>
<td style="padding: 14px 16px 14px 0; vertical-align: top;">
<p style="font-size: 15px; line-height: 1.65; color: #1e293b; margin: 0;">{html.escape(item['action'])}</p>
</td>
</tr>
</table>
</div>"""
def tag_of(sec):
# Older digests prefixed the title with an emoji; the template renders a colored tag, so strip it.
return re.sub(r"^\W+", "", sec["title"]).strip()
def render_section(sec):
tag = tag_of(sec)
color = COLORS.get(tag, "#94a3b8")
items = "".join(render_item(i) for i in sec["items"])
return f"""
<table role="presentation" cellpadding="0" cellspacing="0" style="margin: 36px 0 18px 0;">
<tr><td style="background-color: {color}; border-radius: 4px; padding: 5px 12px;">
<span style="font-size: 14px; color: #ffffff; font-weight: 600; letter-spacing: 0.02em;">{html.escape(tag)}</span>
</td></tr>
</table>
{items}"""
def filtered_title(f):
title = html.escape(f["title"])
if not f.get("url"):
return title
return (
f'<a href="{html.escape(f["url"])}" style="color: #334155; text-decoration: none;">{title}</a>'
)
def render_filtered(filtered):
if not filtered:
return ""
rows = "".join(
f'<li style="font-size: 15px; line-height: 1.6; color: #334155; margin-bottom: 12px;">'
f'{filtered_title(f)}'
f'<span style="display: block; font-size: 14px; color: #64748b; margin-top: 2px;">'
f'{html.escape(f.get("zh", ""))}'
f'{" " if f.get("zh") else ""}<span style="color: #94a3b8;">{html.escape(f["src"])}</span></span></li>'
for f in filtered
)
return f"""
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin: 40px 0 0 0; border-top: 1px solid #e2e8f0;">
<tr><td style="padding: 24px 0 0 0;">
<p style="font-size: 16px; font-weight: 600; color: #0f172a; margin: 0 0 14px 0;">已過濾({len(filtered)} 則)</p>
<ul style="margin: 0; padding-left: 20px;">{rows}</ul>
</td></tr>
</table>"""
def render(d):
s = d["stats"]
sections = "".join(render_section(x) for x in d["sections"])
note = (
f'<p style="font-size: 14px; line-height: 1.7; color: #334155; margin: 28px 0 0 0; '
f'padding: 14px 16px; background-color: #fffbeb; border-left: 3px solid #eab308;">{html.escape(d["note"])}</p>'
if d.get("note") else ""
)
return f"""<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>黑閃日報 {d['date']}</title>
<style type="text/css">
body, table, td {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; }}
body {{ margin: 0; padding: 0; background-color: #f4f5f7; -webkit-text-size-adjust: 100%; }}
table {{ border-collapse: collapse; }}
img {{ border: 0; outline: none; text-decoration: none; }}
a {{ color: #2962FF; text-decoration: none; }}
a:hover {{ text-decoration: underline; }}
@media only screen and (max-width: 600px) {{
.container {{ width: 100% !important; }}
.content {{ padding: 24px 20px !important; }}
.header {{ padding: 20px !important; }}
.footer-content {{ padding: 20px !important; }}
}}
</style>
</head>
<body>
<div style="display:none;font-size:1px;color:#f4f5f7;line-height:1px;max-height:0;max-width:0;opacity:0;overflow:hidden;">
抓 {s['fetched']} 則,保留 {s['kept']} 則
</div>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color: #f4f5f7;">
<tr>
<td align="center" style="padding: 32px 16px;">
<table role="presentation" class="container" width="100%" cellpadding="0" cellspacing="0" style="max-width: 600px; background-color: #ffffff;">
<tr>
<td class="header" style="padding: 28px 35px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td style="text-align: left;">
<img src="https://cloudwp.pro/assets/images/logo-dark.svg" alt="cloudwp" width="120" style="display: inline-block; vertical-align: middle;">
</td>
<td style="text-align: right;">
<span style="font-size: 13px; color: #94a3b8;">{d['date']}</span>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td class="content" style="padding: 0 35px 40px;">
<h1 style="font-size: 24px; line-height: 1.4; color: #0f172a; margin: 0 0 6px 0; font-weight: 700;">黑閃日報</h1>
<p style="font-size: 13px; color: #64748b; margin: 0;">抓 {s['fetched']} 則 保留 {s['kept']} 則 過濾 {s['filtered']} 則</p>
{sections}
{render_filtered(d.get('filtered', []))}
{note}
</td>
</tr>
<tr>
<td style="border-top: 1px solid #e2e8f0; background-color: #f8fafc;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td class="footer-content" style="padding: 28px 35px; text-align: center;">
<table role="presentation" cellpadding="0" cellspacing="0" style="margin: 0 auto;">
<tr>
<td style="vertical-align: middle; padding-right: 12px;">
<a href="https://woocommerce.com/development-services/cloudwp/250740886/">
<img src="https://cloudwp.pro/assets/images/badge/woo-partner-badge-light.png" alt="WooCommerce Agency Partner" height="28" style="display: block;">
</a>
</td>
<td style="vertical-align: middle; padding-right: 12px;">
<a href="https://www.facebook.com/CloudWP">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="#94a3b8"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>
</a>
</td>
<td style="vertical-align: middle;">
<a href="https://github.com/orgs/LazyProApp/repositories">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="#94a3b8"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
</a>
</td>
</tr>
</table>
<p style="font-size: 13px; line-height: 1.8; color: #94a3b8; margin: 16px 0 0 0;">
cloudwp &copy; 2026 黑閃實驗室 | 統編 93067949<br>
台灣 WooCommerce 商家的技術夥伴
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>"""
def to_text(d):
lines = [f"黑閃日報 {d['date']}", ""]
for sec in d["sections"]:
lines.append(tag_of(sec))
for i in sec["items"]:
lines += ["", i["title"], i.get("time", ""), i["fact"], f"→ {i['action']}", i["url"]]
lines.append("")
if d.get("filtered"):
lines.append(f"已過濾({len(d['filtered'])} 則)")
lines += [
f"{f['title']}\n {f.get('zh', '')}({f['src']})" + (f"\n {f['url']}" if f.get("url") else "")
for f in d["filtered"]
]
return "\n".join(lines)
def norm_title(t):
"""標題常帶 HTML entity,或被摘要層加上「(昨天已報過)」之類的尾註,比對前先剝掉。"""
return re.sub(r"([^()]*)\s*$", "", html.unescape(t)).strip().lower()
def backfill_urls(d, here):
"""filtered 漏填 url 時,用 latest.json 裡同標題的那則補回來。"""
path = here / "latest.json"
if not path.exists():
return
by_title = {
norm_title(i["title"]): i["url"]
for i in json.loads(path.read_text(encoding="utf-8"))
if i.get("url")
}
for f in d.get("filtered", []):
if not f.get("url"):
f["url"] = by_title.get(norm_title(f["title"]), "")
def multipart(fields, inline):
"""Build a multipart/form-data body. `inline` is a list of (filename, bytes) sent as cid: images."""
boundary = "----hz" + base64.urlsafe_b64encode(os.urandom(12)).decode().strip("=")
out = bytearray()
for name, value in fields.items():
out += f'--{boundary}\r\nContent-Disposition: form-data; name="{name}"\r\n\r\n'.encode()
out += value.encode() + b"\r\n"
for filename, blob in inline:
out += f'--{boundary}\r\nContent-Disposition: form-data; name="inline"; filename="{filename}"\r\n'.encode()
out += b"Content-Type: image/png\r\n\r\n" + blob + b"\r\n"
out += f"--{boundary}--\r\n".encode()
return bytes(out), f"multipart/form-data; boundary={boundary}"
def main():
key = os.environ.get("MAILGUN_KEY")
if not key:
sys.exit("MAILGUN_KEY 未設定")
here = Path(__file__).parent
path = sys.argv[1] if len(sys.argv) > 1 else str(here / "digest.json")
d = json.loads(Path(path).read_text(encoding="utf-8"))
backfill_urls(d, here)
icon = here / "sparkles.png"
body, content_type = multipart(
{
"from": f"黑閃日報 <noreply@{DOMAIN}>",
"to": TO,
"subject": f"黑閃日報 {d['date']}",
"text": to_text(d),
"html": render(d),
},
[("sparkles.png", icon.read_bytes())] if icon.exists() else [],
)
req = urllib.request.Request(f"https://api.mailgun.net/v3/{DOMAIN}/messages", data=body)
req.add_header("Content-Type", content_type)
req.add_header("Authorization", "Basic " + base64.b64encode(f"api:{key}".encode()).decode())
with urllib.request.urlopen(req, timeout=30) as resp:
print(resp.read().decode())
if __name__ == "__main__":
main()

nel 的服務與技術棧(日報解讀依據)

篩選與寫 action 時以此檔為準。判斷不出某則跟下列哪一項有關 → 它就不重要,丟進 filtered。 不要泛泛寫「值得關注」「可以考慮」;寫得出「動到哪條產品線的哪個部分」才留。

三條產品線

abt — AlphaBolt(AlphaBolt/README.md

WooCommerce 商家的店務工作台,wp-admin 內管理既有訂單/商品/庫存/會員/優惠券 + 營運看板 + CRM 分析。 不做建置型功能(新增商品、新增訂單、深層設定)——那些留給 WooCommerce 原生頁。

  • AlphaBolt:WordPress.org 上架外掛(slug / text domain alphabolt,Author BlackFlash Labs
  • AlphaBolt Connect:alphabolt.app 下載的訂閱 add-on(slug abt-connect),串接、同步、進階功能
  • ABT Admin:alphabolt.app 內部營運後台
  • 收費:方案 / 訂單量計費(billing 走預儲值點數制)

csq — CloudSquad(.doc/CloudSquad/訂閱外掛計劃/README.md

台灣 WooCommerce 商家的本地功能外掛,依類別訂閱,每類 399/月:金流、物流、電子發票、通知。定期定額是金流類別底下的付款方式,不是獨立類別。 主外掛 csq-hq(免費,授權驗證 + when-if-then 規則引擎)+ 四支類別外掛。 一個訂閱綁一個網址;不走 AlphaBolt 訂單量計費;與舊無限方案兩線並存、不遷移。

cwp — CloudWP(.doc/cloudwp/INDEX.md

公司主線/官網 cloudwp.pro。兩塊:

  • 舊外掛產品線:25 個已上市的台灣金流/物流/發票外掛(cw-ecpay-*cw-ezpay-*cw-linepaycw-p2gei-*cw-cherri-*cw-allpay-*、tappay、omg-sms、apple pay、google pay),授權走 auth.woocloud.io,無限方案,存量訂閱有分潤
  • 服務線:WooCommerce 官方認證合作夥伴(Woo Partner)。網站維護訂閱 6,000 / 16,000 / 26,000 月費三方案(顧問方案含主機診斷、安全檢測、效能分析、SEO 與流量分析、外掛風險評估)+ 客戶站健檢報告系統(site-audit)+ 客戶文件中心

if — Infinity Flip(InfinityFlip/INDEX.md

MCP 放置型多人 RPG,已上線 https://infinityflip.app 。玩家的 AI agent 透過 MCP 連上,伺服器 tick 自動 farming、五行屬性克制、多人共打王。

  • 技術:Cloudflare Workers + Durable Objects + D1 + KV,成本硬上限 $20/月(超量才切 VPS hybrid)
  • 美術:RO 風 2.5D,three.js + React Three Fiber
  • 素材生產:AI 生圖(render-manifest.json 109 張 render 清單、Codex CLI runner),內容庫 104 小怪 + 7 宗罪王
  • 收入模式:未定(PM 未裁示)

算數的:AI 生圖 / 生影片 / 影片轉 3D / text-to-3D / 生音效 / 生音樂的新模型與工具(能直接拿來產遊戲素材的)、AI 輔助遊戲開發的工具與流程、three.js / R3F 破壞性變更、Cloudflare DO / D1 / KV 的定價與限制(直接撞成本上限)、MCP 協定變更。 不算數的:遊戲產業新聞(發行、銷量、裁員、主機戰爭)、單一遊戲的評測與 meme、模型跑分排行。

UI 與設計系統(abt settings-ui、csq、cwp 官網、if 都會用)

算數的

  • Tailwind CSS v4、Tailwind Plus / @tailwindplus/elements
  • shadcn/ui、Preline、Material Design 3 / material-web 的版本與破壞性變更
  • dashboard / admin UI 的做法與元件模式
  • 頁面動態 hero 特效(WebGL / GSAP / Motion / CSS)的新技法與實作教學——nel 想用在官網與產品頁
  • AI 生成 UI / 前端程式碼的工具

不算數的:純作品集展示、得獎名單、設計師人事。

技術棧(動到這些 = 直接影響)

東西 用在哪 什麼樣的新聞算數
WooCommerce core abt / csq / cwp 全部外掛的宿主 HPOS、Blocks / Store API、Cart & Checkout Blocks、Gateway API、Shipping API、Subscriptions API、REST API 破壞性變更、版本時程、外掛相容性要求
WordPress core 同上 版本時程、PHP 最低版本、REST / block editor 破壞性變更
WordPress.org AlphaBolt 上架通路 Plugin Directory 審查規則、readme / 素材規範、外掛下架事件、i18n 機制
Cloudflare Workers + D1 abt 與 csq 的授權後端、provider 代理(api.cloudwp.pro);csq 還用 Service Bindings 呼叫四支類別 Worker Workers / D1 定價異動、限制調整、破壞性變更、VPC / Dedicated Egress IP(csq relay 的方案 B)
Turso / Neon / Queue abt workers 資料層與備援 定價、限制、停止服務
GCP Compute Engine csq relay.cloudwp.pro 正式上線後的固定出口 IP(e2-micro) 定價、e2-micro 政策
Tailwind CSS v4 + @tailwindplus/elements abt settings-ui v4 破壞性變更、Tailwind Plus 授權 / 定價
Vite 7 abt settings-ui build 破壞性變更
Chart.js 4 abt 營運看板 破壞性變更
Playwright abt e2e 影響測試的破壞性變更
Mailgun 寄件(cloudwp.pro) 定價、信譽政策、API 變更
three.js + React Three Fiber if 的 2.5D 美術 破壞性變更、效能相關新功能
Cloudflare DO / KV if 的王戰與狀態 定價、限制(撞 $20 成本上限)
MCP if 的玩家接入協定 協定版本、SDK 破壞性變更

依賴的第三方 provider(動到這些 = 直接影響)

  • 台灣金流:綠界 ECPay、藍新 ezPay / 智付通、紅陽 P2GEI、TapPay、LINE Pay、街口、Apple Pay / Google Pay → 算數的:API 版本異動、參數 / 回傳欄位變更、憑證 / 加密方式變更、費率、IP 白名單、sandbox 政策、法規(電子支付管理條例、代理收付) → 不算數的:股價、營收、財報、法人籌碼(deny.json 已擋)
  • 電子發票:財政部電子發票平台、綠界 / 藍新發票 API → 開立規則、欄位、法規變更
  • 物流:超商取貨 / 宅配 provider API、C2C / B2C 介接變更
  • 通知:SMS(omg-sms)、LINE 通知

通路與行銷(nel 自己在做)

  • Google Ads:自己投(產品線與服務線)→ 出價機制、政策、追蹤(GTM / GA4)的實質變更算數
  • SEO:顧 cloudwp.pro / alphabolt.app 與客戶站 → 演算法、Search Console 功能、索引行為算數
  • 結構化資料 / Merchant Center / AI 購物協定:影響電商站與客戶站 → 算數
  • 競品情報:Shopify 平台與 API 的實質變更(可借鏡的做法)算數

寫 action 的準則

  1. 指名產品線:說「動到 csq 的金流類別外掛」,不要說「對電商開發者有影響」。
  2. 指名要改什麼:哪個檔、哪個設定、哪個 API 呼叫。不知道就寫「要查 X 是否受影響」,不要編。
  3. 給時間軸:對方有 deadline(WooCommerce 版本、法規上路日、API 停用日)就寫出來;沒有就不要假裝有。
  4. 不重要就說不重要:留在日報裡但寫「知道就好,不用動」比硬掰一個 action 好。
  5. 沒有事實根據的推論不要寫。不確定它影響哪條線 → 那它就該被 filtered。

常見誤判

  • 把「WooCommerce 生態系的一般新聞」當成「破壞性變更」→ 只有 API / hook / 資料結構 / 版本時程才算。
  • 把「AI 公司發模型」當成「要行動」→ nel 只在意能不能拿來做開發或改善產品,不是模型排行榜。
  • 把「Shopify 的行銷活動」當成競品動向 → 只有平台與 API 的實質變更算。
  • 把「台灣支付商的公司新聞」當成金流變更 → 只有 API / 費率 / 法規算。
  • 把「AI 訂閱方案的用量額度 / rate limit 調整」當成技術棧變更 → 那是 nel 自己用工具的節奏,不動到任何產品線,一律 filtered。真正算數的是 API 定價與 API 破壞性變更。
  • 把「GNews 剛 index 到的轉述文」當成新消息 → 先看內文講的事件本身是哪天發生的,事件超過兩週的一律 filtered。
name rss
version 1.0.0
description 黑閃日報。抓 55 個來源(電商 / WooCommerce / WordPress / 廣告 / SEO / 技術棧 / 支付 / AI / 遊戲與生成式素材 / UI 設計系統), 依 profile.md 篩選並解讀對 nel 四條產品線(abt / csq / cwp / if)的影響,寄 Email 給 nel。 帶參數時展開某一則的完整內容。 Use when asked to "/rss", "今日摘要", "黑閃日報", "展開 <標題>".
allowed-tools
Bash
Read
WebFetch

/rss

模式 A:無參數 → 產日報並寄信

0. 先同步腳本(雲端必做)

雲端 routine 跑的是建立當下的 skill 快照,fetch.py / mail.py / profile.md 不會自動更新。 每次執行先從 gist(唯一真實來源)拉最新版覆蓋,快照多舊都沒差:

cd /Users/neltseng/Dropbox/Works/NT/.claude/skills/rss
G=https://gist.githubusercontent.com/neltseng/7fa844b8ef81108c53076ddc989d9c2b/raw
for f in fetch.py mail.py profile.md; do curl -s "$G/$f?t=$(date +%s)" > "$f"; done

1. 抓取

cd /Users/neltseng/Dropbox/Works/NT/.claude/skills/rss && python3 fetch.py 24 > latest.json

2. 讀取並篩選

先 Read profile.md(必讀)。那是 nel 四條產品線、技術棧、依賴的第三方 provider 的實際清單, 留不留、action 怎麼寫,一律以它為依據。沒讀它就寫不出準確的解讀,只會產生泛泛的評論。

再 Read latest.json(全部讀完,不要抽樣)。

保留(依序):

  • WooCommerce / WordPress core 的破壞性變更、API 變更、版本時程、新機制(外掛開發者必須知道的)
  • Shopify 平台與 API 的實質變更(競品動向、可借鏡的做法)
  • Google / OpenAI 對電商的結構化資料、Merchant Center、AI 購物協定變更(可直接動手做的)
  • Google Ads 出價機制、政策、追蹤(GTM / GA4)的實質變更
  • SEO 演算法、Search Console 功能、索引行為的實質變更
  • 技術棧(Cloudflare / Vercel / Tailwind CSS / three.js)的破壞性變更、新功能、定價異動
  • 金流(Stripe / LINE Pay / 台灣第三方支付)的 API 變更、法規、費率
  • AI 模型與工具(Claude / OpenAI / Grok)的重大發布
  • AI 生圖 / 生影片 / 影片轉 3D / 生音效的新模型與工具(能拿來產 Infinity Flip 遊戲素材的)
  • UI 與設計系統(Tailwind、shadcn/ui、Preline、Material Design 3)、dashboard 做法、頁面動態 hero 特效技法
  • 電商經營的可行動洞察(不是財報,是做法)

丟掉

  • 美國零售業八卦(品牌人事、財報、商標訴訟、聯名活動、明星代言)
  • 廣告業界人事與併購(除非工具本身要收掉或改價)
  • 收視率、音樂榜、體育賽事流量
  • 純推銷的 listicle(「2026 最佳 N 個工具」)
  • AI 公司的融資、估值、人事、口水戰(要的是產品與 API 變更)
  • 遊戲產業新聞(發行、銷量、裁員、主機戰爭、單一遊戲評測與 meme)
  • 設計作品集展示、得獎名單
  • 同一事件的重複報導 → 合併成一則,保留資訊最完整的來源

篩完通常剩 10–20%。寧可少不可濫。

3. 寫 digest.json

digest.json(mail.py 只讀這支,不讀 txt / md):

{
  "date": "YYYY-MM-DD",
  "stats": {"fetched": 0, "kept": 0, "filtered": 0},
  "sections": [
    {"title": "直接影響你手上的東西", "items": [
      {"title": "一句話標題", "time": "YYYY-MM-DD HH:MM 來源名", "fact": "一到兩句事實",
       "action": "所以你要做什麼 / 為什麼跟你有關", "url": "原文連結"}
    ]}
  ],
  "filtered": [
    {"title": "原標題", "zh": "中譯", "src": "來源名", "url": "原文連結"}
  ],
  "note": "選填,整份的補充"
}

title 是 tag 名稱,純文字不加 emoji(信件模板把它渲染成有色 tag,顏色查 mail.pyCOLORS)。 只放有東西的區,title 必須是下列其中一個,否則 tag 會變灰底:

直接影響你手上的東西 / 電商 / 廣告投放 / SEO / 技術棧與金流 / AI / 遊戲與生成式素材 / UI 與設計系統

每則的 actionprofile.md 的「寫 action 的準則」寫:指名哪條產品線(abt / csq / cwp / if)、指名要改什麼、有 deadline 就寫。 判斷不出它動到哪條線 → 它就該被丟進 filtered,不要硬掰。

filtered 把丟掉的每一則都列出來,url 必填(標題在信裡是可點的原文連結,讓 nel 直接檢查有沒有錯殺)。不寫理由。這區不可省略。 url 一律從 latest.json 同一則的 url 原樣帶過來,不要自己拼。

4. 寄 Email

cd /Users/neltseng/Dropbox/Works/NT/.claude/skills/rss && MAILGUN_KEY=<key> python3 mail.py

走 Mailgun REST API(sandbox 擋 SMTP 的非 443 埠)。寄件 noreply@cloudwp.pronel@tseng.ccalphabolt.apptseng.cc 未 onboard 發信,From 只能用 cloudwp.pro

模式 B:帶參數 → 展開該則

/rss <標題關鍵字>

grep -i "<關鍵字>" /Users/neltseng/Dropbox/Works/NT/.claude/skills/rss/latest.json

取出該則的 url → WebFetch 抓全文 → 展開說明:

  1. 事實:實際講了什麼(不是標題重述)
  2. 影響:對 nel 的外掛 / 電商站 / 廣告帳戶具體是什麼影響
  3. 動作:要不要做、什麼時候之前要做完、怎麼做
  4. 不確定的地方直接說,不要編

grep 不到時,直接把 nel 貼的標題拿去 WebSearch 找原文。

模式 C:加減來源

gist 是唯一真實來源,本機 skill 和雲端 routine 都讀它(雲端沒有本機檔案系統,沒推 gist 就等於沒改): https://gist.githubusercontent.com/neltseng/7fa844b8ef81108c53076ddc989d9c2b/raw/<檔名> 內含 feeds.json(來源)、deny.json(噪音黑名單)、profile.md(解讀依據)、fetch.pymail.py

改法(三步,缺一不可):

# 1. 驗新來源是 RSS/Atom 不是 HTML(開頭要有 <rss 或 <feed 或 <?xml)
curl -sL -A 'Mozilla/5.0' <新的feed網址> | head -c 300

# 2. 編 /Users/neltseng/Dropbox/Works/NT/.claude/skills/rss/feeds.json

# 3. 推上 gist —— 沒推就等於沒改
cd /Users/neltseng/Dropbox/Works/NT/.claude/skills/rss && gh gist edit 7fa844b8ef81108c53076ddc989d9c2b -a feeds.json

推完跑 python3 fetch.py 24 確認新來源有抓到東西。雲端 routine 下次執行時自動生效,不必另外改 routine。

新網域還要加進雲端 allowlist(Claude routines → 黑閃日報 → 鉛筆 → 環境齒輪 → Allowed domains),否則雲端抓不到,只有本機抓得到。

模式 D:擋噪音

GNews 那幾條搜尋 feed(綠界 / 藍新 / 第三方支付、LINE Pay、Anthropic、xAI)會把股市討論區、法人籌碼、財報、YouTube 直播一起 index 進來。 改 query 加 -site: / -關鍵字 沒用——實測 GNews 會因此擴散到更不相干的結果。

改抓取層黑名單 deny.json:標題命中任一 regex 就整則丟棄,不進 latest.json,也不會出現在日報的「已過濾」清單。

# 1. 編 deny.json 的 title 陣列(regex,對標題比對)
# 2. 回測:確認擋到該擋的、沒錯殺
cd /Users/neltseng/Dropbox/Works/NT/.claude/skills/rss && python3 -c "
import fetch, json
d = fetch.load_deny()
for i in json.load(open('week.json')):
    if d.search(i['title']): print('擋', i['title'][:60])
"
# 3. 推上 gist —— 沒推就等於沒改
gh gist edit 7fa844b8ef81108c53076ddc989d9c2b -a deny.json

只擋與 nel 完全無關的類別(股市籌碼、財報、影音直播)。詐騙警示、法規、費率有判斷價值,留給篩選層決定,不在這層擋。

已知限制

  • Anthropic 官方新聞、xAI、LINE Developers、台灣支付商都沒有 RSS(404 或 403)→ 改用 Google News 搜尋 RSS 代替,噪音較多,靠篩選層處理
  • Reddit 在本機被 403,沒放進來
  • Metorik 沒有任何 RSS
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment