
Gzh Ai Feed
- 233 installs
- 316 repo stars
- Updated August 4, 2026
- redfox-data/redfox-community
Use gzh-ai-feed for development tasks
About
gzh-ai-feed: A skill for development. This provides functionality for development workflows.
- gzh-ai-feed
Gzh Ai Feed by the numbers
- 233 all-time installs (skills.sh)
- +16 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #1,692 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/redfox-data/redfox-community --skill gzh-ai-feedAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 233 |
|---|---|
| repo stars | ★ 316 |
| Last updated | August 4, 2026 |
| Repository | redfox-data/redfox-community ↗ |
What it does
Use gzh-ai-feed for development tasks
Files
AI公众号信息源
每日自动扫描全网 AI 公众号,按阅读量找出最火的爆款内容,智能聚类后生成精美 HTML 日报。
API 请求均携带 AI公众号信息源-SkillHub 标识。需先配置 API Key,通过环境变量 REDFOX_API_KEY 或 --api-key 参数传入。---
能力概述
- 爆款发现:从 200+ 篇 AI 公众号文章中,按阅读量筛选最火的热门内容
- 智能聚类:自动从当天内容中发现话题方向(Agent、大模型、AI绘画...),每天的分类由内容决定
- 终端表格:分类 + 标题 + 作者 + 阅读/点赞/评论数,一目了然
- 可视化日报:深色主题 HTML,封面图、互动数据、文章直链、日期导航
- 全库搜索:日报页面内置搜索框,通过本地代理服务实时检索全量文章库(模糊匹配,与日期无关)
- 一键订阅:
--subscribe开启每日自动产出,日报自动攒在本地文件夹
---
使用方式
# 生成今日爆款日报
python3 "$SKILL_PATH/assets/daily_report.py"
# 自定义关注方向
python3 "$SKILL_PATH/assets/daily_report.py" --keywords "AI Agent,RAG,LangChain,Prompt"
# 查看历史某天
python3 "$SKILL_PATH/assets/daily_report.py" --date 2026-05-26
# 订阅 / 取消订阅
python3 "$SKILL_PATH/assets/daily_report.py" --subscribe
python3 "$SKILL_PATH/assets/daily_report.py" --unsubscribe生成的 HTML 日报保存在 ~/Downloads/QoderReports/,自动浏览器打开。终端同步输出分类文章表格。
---
首次使用
先配置 API Key,然后运行:
# 方式一:环境变量(推荐)
export REDFOX_API_KEY=ak_你的密钥
# 方式二:配置文件
mkdir -p ~/.qoder/apis
echo '{"api_key":"ak_你的密钥"}' > ~/.qoder/apis/redfox.json
# 然后运行
python3 "$SKILL_PATH/assets/daily_report.py"注册获取 Key:redfox.hk
---
后续使用
前往 redfox.hk 注册获取个人 API Token:
| 方式 | 命令 |
|---|---|
| 环境变量(推荐) | export REDFOX_API_KEY=ak_你的密钥 |
| 命令行参数 | --api-key ak_你的密钥 |
| 配置文件 | echo '{"api_key":"ak_你的密钥"}' > ~/.qoder/apis/redfox.json |
---
参数说明
| 参数 | 说明 | 默认值 |
|---|---|---|
--keywords | 关注的话题方向,逗号分隔 | AI,人工智能,大模型,GPT,Agent,AI绘画 |
--count | 扫描文章数量 | 200 |
--date | 指定日期 YYYY-MM-DD | 今天 |
--output-dir | 输出目录 | ~/Downloads/QoderReports |
--api-key | 指定 API Key | — |
--subscribe | 开启每日订阅 | — |
--unsubscribe | 关闭每日订阅 | — |
--no-open | 不自动打开浏览器 | — |
---
依赖
pip3 install requests---
常见问题
Q:日报里的分类是怎么来的? A:完全由当天内容决定。从文章话题、分类标签和标题关键词中自动识别聚类,每天的热点方向不同。
Q:怎么看到更多文章? A:用 --count 300 扩大扫描范围,或通过 --keywords 添加更多关注方向。
Q:HTML 搜索怎么用? A:日报页面内置搜索框,脚本启动时会自动拉起本地代理服务(端口 8765),在浏览器中直接搜索全量文章库。
Q:搜索和日报是什么关系? A:日报是特定日期的聚类展示;搜索是实时查询全库内容(模糊匹配,与日期无关),适合找特定话题的任意日期文章。
Q:订阅后日报存在哪? A:默认 ~/Downloads/QoderReports/,文件名格式 AI日报_2026-05-27.html。
Q:额度用完怎么办? A:前往 redfox.hk 注册获取 Token。
#!/usr/bin/env python3
"""
AI公众号信息源 — 每日热门内容聚类
====================================
每天扫描 AI 公众号热门文章,自动聚类后生成 HTML 日报。
Usage:
python3 daily_report.py
python3 daily_report.py --keywords "AI Agent,RAG,LangChain"
python3 daily_report.py --subscribe
"""
import argparse
import json
import os
import re
import subprocess
import sys
import time
from collections import Counter, defaultdict
from datetime import datetime
from http.server import HTTPServer, SimpleHTTPRequestHandler
from pathlib import Path
from urllib.parse import urlparse, parse_qs
try:
import requests
HAS_REQUESTS = True
except ImportError:
HAS_REQUESTS = False
# ─── 配置 ─────────────────────────────────────────────────────────────────────────
API_URL = "https://redfox.hk/story/api/parseWork/queryAiMsgs"
CONFIG_DIR = Path.home() / ".qoder" / "apis"
CONFIG_FILE = CONFIG_DIR / "redfox.json"
ENV_KEY = "REDFOX_API_KEY"
SOURCE = "AI公众号信息源-GitHub"
DEFAULT_KEYWORDS = ["AI", "人工智能", "大模型", "GPT", "Agent", "AI绘画"]
DEFAULT_OUTPUT_DIR = Path.home() / "Downloads" / "QoderReports"
PAGES_PER_KEYWORD = 5 # 首关键词最大翻页数(后续关键词仅翻 2 页)
PAGE_SIZE = 20
PLIST_LABEL = "com.qoder.gzh-ai-feed"
PLIST_DIR = Path.home() / "Library" / "LaunchAgents"
# ─── 终端颜色 ──────────────────────────────────────────────────────────────────────
GREEN = "\033[92m"
YELLOW = "\033[93m"
RED = "\033[91m"
CYAN = "\033[96m"
BOLD = "\033[1m"
RESET = "\033[0m"
def info(msg):
print(f"{GREEN}[✓]{RESET} {msg}")
def warn(msg):
print(f"{YELLOW}[!]{RESET} {msg}")
def error(msg):
print(f"{RED}[✗]{RESET} {msg}")
def step(msg):
print(f"{CYAN}[→]{RESET} {msg}")
# ─── API Key 管理 ──────────────────────────────────────────────────────────────────
def get_api_key(cli_key=None):
"""Get API key: CLI arg > env var > config file."""
if cli_key:
return cli_key
env_key = os.environ.get(ENV_KEY)
if env_key:
return env_key
if CONFIG_FILE.exists():
try:
data = json.loads(CONFIG_FILE.read_text())
key = data.get("api_key")
if key:
return key
except (json.JSONDecodeError, OSError):
pass
return None
# ─── 数据获取 ──────────────────────────────────────────────────────────────────────
def fetch_page(session, keyword, page_num):
"""获取单页文章数据"""
payload = {
"keyword": keyword,
"pageNum": page_num,
"pageSize": PAGE_SIZE,
"source": SOURCE,
}
try:
resp = session.post(API_URL, json=payload, timeout=15)
result = resp.json()
except Exception as e:
warn(f"请求失败 (keyword={keyword}, page={page_num}): {e}")
return []
code = result.get("code")
if code == 3108:
warn("限频,等待 5s...")
time.sleep(5)
try:
resp = session.post(API_URL, json=payload, timeout=15)
result = resp.json()
code = result.get("code")
except Exception:
return []
if code not in (200, 2000):
if code in (3106, 3107):
error(f"API Key 错误 (code {code}): {result.get('msg', '')}")
return []
data = result.get("data", {})
return data.get("list", [])
def fetch_articles(session, keywords, target_count):
"""多关键词分页抓取,去重后返回文章列表。
首关键词为主力深翻(最多 5 页),其余关键词仅浅尝(2 页)作补充,
避免 6 关键词 × N 页的指数级 API 调用。"""
articles = []
seen_ids = set()
for i, kw in enumerate(keywords):
if len(articles) >= target_count:
break
# 首关键词深入翻页,后续关键词只浅查
max_pages = PAGES_PER_KEYWORD if i == 0 else 2
for page in range(1, max_pages + 1):
if len(articles) >= target_count:
break
page_articles = fetch_page(session, kw, page)
if not page_articles:
if page == 1 and i == 0:
warn(f"关键词 \"{kw}\" 暂无内容(当前仅搜索 AI 相关公众号,更多内容请访问 https://redfox.hk/settings/api-keys?source=github)")
break
new_count = 0
for article in page_articles:
pid = article.get("photoId", "")
if pid and pid not in seen_ids:
seen_ids.add(pid)
articles.append(article)
new_count += 1
print(f"\r {CYAN}[→]{RESET} 扫描: {kw} (第{page}页) "
f"新增{new_count}条, 累计{len(articles)}条", end="", flush=True)
# 如果当前页重复率过高(>70%),换下一个关键词
if len(page_articles) > 0 and new_count / len(page_articles) < 0.3:
break
time.sleep(0.5)
# 已接近目标(75%+),跳过剩余关键词
if len(articles) >= target_count * 0.75:
break
print()
return articles
# ─── 自动聚类 ──────────────────────────────────────────────────────────────────────
STOP_WORDS = set("的了是在和与及或但对于从到被将把让给用有这那个也都还又不没"
"就才能会要可以怎么什么为什么怎样如何哪些多少一个一些这些那些"
"已经正在可能应该必须需要通过进行使用利用根据关于对于由于因为所以"
"虽然但是然而因此所以如果那么只要只有无论不管即使不仅而且")
# 过于宽泛的标签,聚类时跳过
GENERIC_TAGS = {"#AI", "#人工智能", "#ai", "AI", "人工智能", "#科技", "#技术",
"#人工智能应用", "#智能", "科技", "技术"}
def extract_keywords(title):
"""从标题中提取中文关键词片段"""
if not title:
return []
# 移除标点和特殊字符
cleaned = re.sub(r'[^\u4e00-\u9fff\w]', ' ', title)
# 提取 2-4 字中文片段
segments = re.findall(r'[\u4e00-\u9fff]{2,4}', cleaned)
# 过滤停用词
keywords = [s for s in segments if not all(c in STOP_WORDS for c in s)]
return keywords[:5]
def get_article_tags(article):
"""提取文章的所有有效标签(去除泛标签),优先 type 再 topic"""
tags = []
# type 字段通常更细致(如 #AI热点、#AI教程、#AI大模型)
atype = (article.get("type") or "").strip()
if atype:
for t in re.split(r'[,,]+', atype):
t = t.strip()
if t and t not in GENERIC_TAGS:
tags.append(t)
# topic 字段作为补充(跳过泛标签)
topic = (article.get("topic") or "").strip()
if topic:
for t in re.split(r'[,,\s]+', topic):
t = t.strip()
if t and t not in GENERIC_TAGS and t not in tags:
tags.append(t)
return tags
def cluster_articles(articles):
"""基于 type + topic 标签自动聚类,确保分类细致且至少 5 个"""
# 第一步:为每篇文章提取标签,按首个有效标签分组
topic_groups = defaultdict(list)
for article in articles:
tags = get_article_tags(article)
if tags:
# 使用第一个非泛标签作为主分类
topic_groups[tags[0]].append(article)
else:
topic_groups["其他"].append(article)
# 第二步:如果大组过大(>20%文章),尝试拆分
total = len(articles)
split_threshold = max(total * 0.2, 25)
groups_to_split = {}
for topic, arts in list(topic_groups.items()):
if len(arts) > split_threshold and topic != "其他":
groups_to_split[topic] = arts
for topic, arts in groups_to_split.items():
del topic_groups[topic]
# 用文章的第二标签进行二次拆分
for article in arts:
tags = get_article_tags(article)
if len(tags) >= 2:
topic_groups[tags[1]].append(article)
else:
topic_groups[topic].append(article)
# 第三步:合并小组(< 3 篇)
final_groups = {}
small_articles = []
for topic, arts in topic_groups.items():
if len(arts) >= 3:
final_groups[topic] = arts
else:
small_articles.extend(arts)
# 小组文章尝试用标签匹配到已有大组
still_orphan = []
for article in small_articles:
tags = get_article_tags(article)
placed = False
for tag in tags:
if tag in final_groups:
final_groups[tag].append(article)
placed = True
break
if not placed:
still_orphan.append(article)
if still_orphan:
if "其他" in final_groups:
final_groups["其他"].extend(still_orphan)
else:
final_groups["其他"] = still_orphan
# 第四步:对过大的组用标题关键词进一步拆分
MAX_GROUP_SIZE = max(total * 0.3, 40)
for _ in range(3): # 最多拆 3 轮
oversized = [(t, a) for t, a in final_groups.items() if len(a) > MAX_GROUP_SIZE]
if not oversized:
break
for topic, arts in oversized:
# 用标题中的高频关键词拆分
kw_counter = Counter()
article_kw_map = {}
for article in arts:
title = article.get("title", "")
kws = extract_keywords(title)
article_kw_map[id(article)] = kws
for kw in kws:
kw_counter[kw] += 1
# 找出频次够高的关键词作为子分类
common_kws = [kw for kw, cnt in kw_counter.most_common(5)
if cnt >= 5 and kw not in topic
and f"#{kw}" not in GENERIC_TAGS
and kw not in ("人工智能", "智能", "模型", "技术", "应用")]
if not common_kws:
continue
# 用第一个高频词拆出子组
split_kw = common_kws[0]
new_group = []
remaining = []
for article in arts:
kws = article_kw_map.get(id(article), [])
if split_kw in kws:
new_group.append(article)
else:
remaining.append(article)
if len(new_group) >= 5:
final_groups[f"#{split_kw}"] = new_group
final_groups[topic] = remaining
# 第五步:确保至少 5 个分类(如果不够,对最大组继续拆分)
while len(final_groups) < 5 and final_groups:
largest_topic = max(final_groups, key=lambda k: len(final_groups[k]))
largest_arts = final_groups[largest_topic]
if len(largest_arts) < 6:
break # 最大组也太小了,无法再拆
# 从最大组中按第二标签拆出子组
sub_groups = defaultdict(list)
remain = []
for article in largest_arts:
tags = get_article_tags(article)
second_tag = None
for t in tags:
if t != largest_topic:
second_tag = t
break
if second_tag:
sub_groups[second_tag].append(article)
else:
remain.append(article)
# 找出最大的子组拆出来
if sub_groups:
best_sub = max(sub_groups, key=lambda k: len(sub_groups[k]))
if len(sub_groups[best_sub]) >= 3:
final_groups[best_sub] = sub_groups[best_sub]
# 更新原组
new_arts = remain
for k, v in sub_groups.items():
if k != best_sub:
new_arts.extend(v)
final_groups[largest_topic] = new_arts
continue
break # 无法继续拆分
# 第六步:构建输出,按条数降序
clusters = []
for category, arts in sorted(final_groups.items(), key=lambda x: -len(x[1])):
# 按阅读量排序取 top 5
sorted_arts = sorted(arts, key=lambda a: (a.get("readCount") or 0), reverse=True)
clusters.append({
"category": category,
"count": len(arts),
"articles": sorted_arts[:5],
})
return clusters
# ─── HTML 报告生成 ──────────────────────────────────────────────────────────────────
def compute_stats(articles):
"""计算统计数据"""
total = len(articles)
if total == 0:
return {"total": 0, "avg_reads": 0, "top_author": "-", "total_likes": 0}
reads = [a.get("readCount") or 0 for a in articles]
avg_reads = sum(reads) // total if total > 0 else 0
author_counter = Counter(a.get("userName", "未知") for a in articles)
top_author = author_counter.most_common(1)[0][0] if author_counter else "-"
total_likes = sum(a.get("likeCount") or 0 for a in articles)
return {
"total": total,
"avg_reads": avg_reads,
"top_author": top_author,
"total_likes": total_likes,
}
def format_number(n):
"""格式化数字: 1234 -> 1.2k"""
if n is None:
return "0"
if n >= 10000:
return f"{n/10000:.1f}w"
if n >= 1000:
return f"{n/1000:.1f}k"
return str(n)
def print_article_table(clusters):
"""在终端打印分类文章表格"""
print(f"\n{BOLD}{'='*78}{RESET}")
print(f"{BOLD} AI公众号信息源 · 分类文章一览{RESET}")
print(f"{BOLD}{'='*78}{RESET}\n")
for i, cluster in enumerate(clusters, 1):
category = cluster["category"]
arts = cluster["articles"]
# 分类标题
print(f" {CYAN}{BOLD}【{category}】{RESET} "
f"共 {len(arts)} 篇展示 / {cluster['count']} 篇总计")
# 表头
header = (f" {'序号':<4}{'标题':<36}{'作者':<14}"
f"{'阅读':>8}{'点赞':>8}{'评论':>8}")
print(f" {YELLOW}{'─'*76}{RESET}")
print(f" {YELLOW}{header}{RESET}")
print(f" {YELLOW}{'─'*76}{RESET}")
for j, article in enumerate(arts, 1):
title = article.get("title", "无标题")
author = article.get("userName", "-")
reads = format_number(article.get("readCount"))
likes = format_number(article.get("likeCount"))
comments = format_number(article.get("commentCount"))
# 截断过长的标题和作者
display_title = title[:34] + ".." if len(title) > 36 else title
display_author = author[:12] + ".." if len(author) > 14 else author
print(f" {j:<4}{display_title:<36}{display_author:<14}"
f"{reads:>8}{likes:>8}{comments:>8}")
print() # 分类之间空行
def generate_category_cards(clusters):
"""生成分类卡片 HTML"""
cards_html = ""
for i, cluster in enumerate(clusters, 1):
articles_html = ""
for article in cluster["articles"]:
title = article.get("title", "无标题")
url = article.get("url") or "#"
author = article.get("userName", "")
cover = article.get("coverUrl") or ""
likes = format_number(article.get("likeCount"))
reads = format_number(article.get("readCount"))
comments = format_number(article.get("commentCount"))
cover_html = ""
if cover:
cover_html = f'<img class="article-cover" src="{cover}" alt="" loading="lazy" referrerpolicy="no-referrer">'
articles_html += f'''
<div class="article-item">
{cover_html}
<div class="article-info">
<a href="{url}" target="_blank" class="article-title">{title}</a>
<div class="article-meta">
<span class="author">{author}</span>
<span class="metrics">
<span class="metric">👁 {reads}</span>
<span class="metric">👍 {likes}</span>
<span class="metric">💬 {comments}</span>
</span>
</div>
</div>
</div>'''
cards_html += f'''
<div class="category-card reveal">
<div class="card-header">
<span class="card-number">{i:02d}</span>
<h3 class="card-category">{cluster["category"]}</h3>
<span class="card-count">{cluster["count"]} 篇</span>
</div>
<div class="card-body">{articles_html}
</div>
</div>'''
return cards_html
def generate_report(clusters, articles, date_str, api_key=None):
"""生成完整 HTML 报告"""
stats = compute_stats(articles)
topic_count = len(clusters)
# 尝试从模板文件读取
template_path = Path(__file__).parent / "report_template.html"
if template_path.exists():
template = template_path.read_text(encoding="utf-8")
else:
warn("模板文件未找到,使用内置模板")
template = get_fallback_template()
# 生成日期显示
try:
dt = datetime.strptime(date_str, "%Y-%m-%d")
weekdays = ["一", "二", "三", "四", "五", "六", "日"]
date_cn = f"{dt.year}年{dt.month}月{dt.day}日 星期{weekdays[dt.weekday()]}"
except ValueError:
date_cn = date_str
category_cards = generate_category_cards(clusters)
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
html = template
html = html.replace("{{DATE}}", date_str)
html = html.replace("{{DATE_CN}}", date_cn)
html = html.replace("{{TOTAL_COUNT}}", str(stats["total"]))
html = html.replace("{{TOPIC_COUNT}}", str(topic_count))
html = html.replace("{{TOP_AUTHOR}}", stats["top_author"])
html = html.replace("{{AVG_READS}}", format_number(stats["avg_reads"]))
html = html.replace("{{TOTAL_LIKES}}", format_number(stats["total_likes"]))
html = html.replace("{{CATEGORY_CARDS}}", category_cards)
html = html.replace("{{TIMESTAMP}}", timestamp)
html = html.replace("{{API_KEY}}", api_key or "")
html = html.replace("{{SOURCE}}", SOURCE)
return html
def get_fallback_template():
"""内置最小 HTML 模板(当模板文件缺失时使用)"""
return '''<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI公众号信息源 - {{DATE}}</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, sans-serif; background: #1a1a1a; color: #e8e4df; padding: 2rem; }
.header { text-align: center; padding: 2rem 0; }
.header h1 { font-size: 2rem; color: #FF5722; }
.header p { color: #9a9590; margin-top: 0.5rem; }
.stats { display: flex; justify-content: center; gap: 2rem; padding: 1rem; margin: 1rem 0; }
.stat-item { text-align: center; }
.stat-value { font-size: 1.5rem; font-weight: bold; color: #FF5722; }
.stat-label { font-size: 0.8rem; color: #9a9590; }
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); gap: 1.5rem; max-width: 1200px; margin: 2rem auto; }
.category-card { background: #2d2d2d; border-radius: 12px; padding: 1.5rem; }
.card-header { display: flex; align-items: center; gap: 0.8rem; margin-bottom: 1rem; padding-bottom: 0.8rem; border-bottom: 1px solid #3d3d3d; }
.card-number { font-size: 1.5rem; font-weight: bold; color: #FF5722; }
.card-category { flex: 1; font-size: 1.1rem; }
.card-count { color: #9a9590; font-size: 0.9rem; }
.article-item { padding: 0.6rem 0; border-bottom: 1px solid #3d3d3d; display: flex; gap: 0.8rem; }
.article-item:last-child { border-bottom: none; }
.article-cover { width: 60px; height: 60px; border-radius: 6px; object-fit: cover; flex-shrink: 0; }
.article-info { flex: 1; min-width: 0; }
.article-title { color: #e8e4df; text-decoration: none; font-size: 0.9rem; line-height: 1.4; display: block; }
.article-title:hover { color: #FF5722; }
.article-meta { display: flex; justify-content: space-between; margin-top: 0.3rem; font-size: 0.75rem; color: #9a9590; }
.metrics { display: flex; gap: 0.8rem; }
.footer { text-align: center; padding: 2rem; color: #666; font-size: 0.8rem; }
</style>
</head>
<body>
<div class="header">
<h1>AI公众号信息源</h1>
<p>{{DATE_CN}} | 共 {{TOTAL_COUNT}} 篇热门文章</p>
</div>
<div class="stats">
<div class="stat-item"><div class="stat-value">{{TOPIC_COUNT}}</div><div class="stat-label">分类</div></div>
<div class="stat-item"><div class="stat-value">{{TOTAL_COUNT}}</div><div class="stat-label">文章</div></div>
<div class="stat-item"><div class="stat-value">{{AVG_READS}}</div><div class="stat-label">平均阅读</div></div>
<div class="stat-item"><div class="stat-value">{{TOTAL_LIKES}}</div><div class="stat-label">总点赞</div></div>
</div>
<div class="cards">{{CATEGORY_CARDS}}</div>
<div class="footer">Generated at {{TIMESTAMP}} by AI公众号信息源 Skill</div>
</body>
</html>'''
# ─── 订阅机制 ──────────────────────────────────────────────────────────────────────
def install_subscription():
"""安装定时任务,每天自动生成日报"""
if sys.platform == "darwin":
PLIST_DIR.mkdir(parents=True, exist_ok=True)
plist_path = PLIST_DIR / f"{PLIST_LABEL}.plist"
script_path = os.path.abspath(__file__)
log_path = str(Path.home() / "Library" / "Logs" / "qoder-ai-hot-articles.log")
# 传递 API Key 环境变量
env_section = ""
api_key = os.environ.get(ENV_KEY)
if api_key:
env_section = f"""
<key>EnvironmentVariables</key>
<dict>
<key>{ENV_KEY}</key>
<string>{api_key}</string>
</dict>"""
plist_content = f'''<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>{PLIST_LABEL}</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/python3</string>
<string>{script_path}</string>
<string>--no-open</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>9</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<key>StandardOutPath</key>
<string>{log_path}</string>
<key>StandardErrorPath</key>
<string>{log_path}</string>
<key>RunAtLoad</key>
<false/>{env_section}
</dict>
</plist>'''
plist_path.write_text(plist_content, encoding="utf-8")
try:
subprocess.run(["launchctl", "load", str(plist_path)], check=True, capture_output=True)
info("订阅成功! 每天 09:00 自动生成爆款日报")
info(f"日报目录: ~/Downloads/QoderReports/")
info(f"日志: {log_path}")
return True
except subprocess.CalledProcessError as e:
error(f"订阅安装失败: {e.stderr.decode()}")
return False
else:
# Linux / Windows: 使用 crontab
script_path = os.path.abspath(__file__)
cron_line = f"0 9 * * * /usr/bin/python3 {script_path} --no-open"
try:
subprocess.run(
f'(crontab -l 2>/dev/null; echo "{cron_line}") | crontab -',
shell=True, check=True, capture_output=True
)
info("订阅成功! 每天 09:00 自动生成爆款日报 (crontab)")
info(f"日报目录: ~/Downloads/QoderReports/")
return True
except subprocess.CalledProcessError:
warn("自动配置 crontab 失败,请手动添加:")
print(f" {cron_line}")
return False
def remove_subscription():
"""卸载定时任务"""
if sys.platform == "darwin":
plist_path = PLIST_DIR / f"{PLIST_LABEL}.plist"
if not plist_path.exists():
warn("未找到订阅配置,无需取消")
return False
try:
subprocess.run(["launchctl", "unload", str(plist_path)], check=True, capture_output=True)
except subprocess.CalledProcessError:
pass
plist_path.unlink(missing_ok=True)
info("已取消订阅,定时任务已移除")
return True
else:
script_path = os.path.abspath(__file__)
try:
subprocess.run(
f'crontab -l 2>/dev/null | grep -v "{script_path}" | crontab -',
shell=True, check=True, capture_output=True
)
info("已取消订阅,crontab 任务已移除")
return True
except subprocess.CalledProcessError:
warn("自动移除 crontab 失败,请手动执行: crontab -e")
return False
# ─── API 代理 HTTP 服务 ─────────────────────────────────────────────────────────────
class ProxyHTTPHandler(SimpleHTTPRequestHandler):
"""静态文件服务 + /api/search 代理到 redfox.hk"""
api_key = None
search_url = API_URL
def do_GET(self):
parsed = urlparse(self.path)
if parsed.path == "/api/search":
self._handle_search(parsed)
else:
super().do_GET()
def _handle_search(self, parsed):
params = parse_qs(parsed.query)
keyword = params.get("keyword", [""])[0]
if not keyword:
self._send_json({"code": -1, "msg": "missing keyword"})
return
payload = {
"keyword": keyword,
"pageNum": 1,
"pageSize": 20,
"source": SOURCE,
}
try:
resp = requests.post(
self.search_url,
json=payload,
headers={
"Content-Type": "application/json",
"X-API-KEY": self.api_key,
},
timeout=10,
)
self._send_json(resp.json())
except Exception as e:
self._send_json({"code": -1, "msg": str(e)})
def _send_json(self, data):
body = json.dumps(data, ensure_ascii=False).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, format, *args):
pass # 静默日志
def start_server(output_dir, api_key, port=8765):
"""启动内置 HTTP 服务(静态文件 + API 代理)"""
import threading
ProxyHTTPHandler.api_key = api_key
os.chdir(str(output_dir))
server = HTTPServer(("127.0.0.1", port), ProxyHTTPHandler)
t = threading.Thread(target=server.serve_forever, daemon=False)
t.start()
info(f"本地服务已启动: http://127.0.0.1:{port}")
return server
# ─── 主流程 ────────────────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(
description="AI公众号信息源 — 每日热门内容聚类日报",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python3 daily_report.py
python3 daily_report.py --keywords "AI Agent,RAG,LangChain"
python3 daily_report.py --subscribe
python3 daily_report.py --unsubscribe
""",
)
parser.add_argument("--keywords", default=",".join(DEFAULT_KEYWORDS),
help="搜索关键词,逗号分隔 (默认: AI,人工智能,大模型,GPT,Agent,AI绘画)")
parser.add_argument("--count", type=int, default=200, help="目标文章数 (默认: 200)")
parser.add_argument("--date", default=datetime.now().strftime("%Y-%m-%d"),
help="指定日期 YYYY-MM-DD (默认: 今天)")
parser.add_argument("--output-dir", help=f"输出目录 (默认: ~/Downloads/QoderReports)")
parser.add_argument("--api-key", help="API Key (不传则读取环境变量或内置公共 Key)")
parser.add_argument("--subscribe", action="store_true", help="安装每日定时任务 (09:00)")
parser.add_argument("--unsubscribe", action="store_true", help="卸载定时任务")
parser.add_argument("--no-open", action="store_true", help="不自动打开浏览器")
args = parser.parse_args()
# ── Banner ──
banner = f"""{CYAN}{BOLD}
╔══════════════════════════════════════╗
║ AI公众号信息源 · 日报生成 ║
║ 每日热门内容聚类 · 爆款一网打尽 ║
╚══════════════════════════════════════╝{RESET}
"""
print(banner)
# ── 订阅/取消 ──
if args.subscribe:
install_subscription()
return
if args.unsubscribe:
remove_subscription()
return
# ── 检查依赖 ──
if not HAS_REQUESTS:
error("缺少 requests 库,请安装: pip3 install requests")
sys.exit(1)
# ── API Key ──
api_key = get_api_key(cli_key=args.api_key)
if not api_key:
print(f"{RED}╔══════════════════════════════════════════════════╗{RESET}")
print(f"{RED}║ 未配置 API Key,请通过以下方式之一配置: ║{RESET}")
print(f"{RED}║ ║{RESET}")
print(f"{RED}║ export REDFOX_API_KEY=ak_你的密钥 ║{RESET}")
print(f"{RED}║ python3 daily_report.py --api-key ak_你的密钥 ║{RESET}")
print(RED + "║ echo '{\"api_key\":\"ak_你的密钥\"}' > ~/.qoder/apis/redfox.json ║" + RESET)
print(f"{RED}║ ║{RESET}")
print(f"{RED}║ 注册获取 Key: https://redfox.hk/settings/api-keys ║{RESET}")
print(f"{RED}╚══════════════════════════════════════════════════╝{RESET}")
sys.exit(1)
# ── Session ──
session = requests.Session()
session.verify = True
session.headers.update({
"Content-Type": "application/json",
"X-API-KEY": api_key,
})
# ── 获取文章 ──
keywords = [k.strip() for k in args.keywords.split(",") if k.strip()]
step(f"扫描热门内容,关键词: {keywords}")
step(f"目标: {args.count} 条, 日期: {args.date}")
print()
articles = fetch_articles(session, keywords, args.count)
if not articles:
error("未获取到任何文章")
print(f"\n{YELLOW} 提示:当前仅搜索 AI 相关公众号作品。{RESET}")
print(f"{YELLOW} 如需搜索全量公众号内容,请访问 https://redfox.hk/settings/api-keys?source=github 获取公众号搜索 Skill{RESET}")
sys.exit(1)
info(f"扫描完成: {len(articles)} 篇热门文章")
# ── 自动聚类 ──
step("正在自动聚类...")
clusters = cluster_articles(articles)
info(f"聚类完成: 发现 {len(clusters)} 个分类")
for c in clusters[:10]:
print(f" {c['category']}: {c['count']} 篇")
# ── 终端表格展示 ──
print_article_table(clusters)
# ── 生成报告 ──
step("生成 HTML 日报...")
html_content = generate_report(clusters, articles, args.date, api_key=api_key)
# ── 保存文件 ──
output_dir = Path(args.output_dir) if args.output_dir else DEFAULT_OUTPUT_DIR
output_dir.mkdir(parents=True, exist_ok=True)
filename = f"AI日报_{args.date}.html"
output_path = output_dir / filename
output_path.write_text(html_content, encoding="utf-8")
info(f"日报已生成: {output_path}")
# ── 启动内置服务 + 打开浏览器 ──
if not args.no_open:
server = start_server(output_dir, api_key)
url = f"http://127.0.0.1:8765/{filename}"
if sys.platform == "darwin":
subprocess.run(["open", url], check=False)
elif sys.platform == "linux":
subprocess.run(["xdg-open", url], check=False)
info(f"浏览器已打开: {url}")
print(f"\n{GREEN}{BOLD}✓ 完成!{RESET}")
print(f" 文件: {output_path}")
print(f" 分类: {len(clusters)} 个")
print(f" 文章: {len(articles)} 篇")
if not args.no_open:
print(f" 搜索功能: 已就绪(通过内置 API 代理)")
print(f" {YELLOW}提示:关闭终端后服务自动停止,HTML 文件可随时离线查阅{RESET}")
if __name__ == "__main__":
main()
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI公众号信息源 - {{DATE}}</title>
<!-- Fonts: Archivo Black (display) + Space Grotesk (body) -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=Space+Grotesk:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
/* ═══════════════════════════════════════════════
CSS CUSTOM PROPERTIES — BOLD SIGNAL PRESET
═══════════════════════════════════════════════ */
:root {
--bg-primary: #0f0f0f;
--bg-secondary: #1a1a1a;
--bg-card: #1e1e1e;
--bg-card-hover: #252525;
--text-primary: #f0ece6;
--text-secondary: #9a9590;
--text-muted: #666;
--accent: #FF5722;
--accent-light: #FF7043;
--accent-glow: rgba(255, 87, 34, 0.15);
--accent-border: rgba(255, 87, 34, 0.3);
--border: rgba(255, 255, 255, 0.06);
--font-display: 'Archivo Black', Impact, sans-serif;
--font-body: 'Space Grotesk', -apple-system, sans-serif;
--title-size: clamp(1.8rem, 4vw, 3.5rem);
--h2-size: clamp(1.2rem, 2.5vw, 1.8rem);
--h3-size: clamp(1rem, 1.8vw, 1.3rem);
--body-size: clamp(0.8rem, 1.2vw, 0.95rem);
--small-size: clamp(0.7rem, 0.9vw, 0.8rem);
--radius: 12px;
--radius-sm: 8px;
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
}
/* ═══════════════════════════════════════════════
RESET & BASE
═══════════════════════════════════════════════ */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
font-family: var(--font-body);
font-size: var(--body-size);
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
min-height: 100vh;
padding: clamp(1rem, 3vw, 3rem);
}
/* ═══════════════════════════════════════════════
HEADER — DATE PROMINENT
═══════════════════════════════════════════════ */
.report-header {
text-align: center;
padding: clamp(2rem, 5vw, 4rem) 0 clamp(1rem, 2vw, 1.5rem);
max-width: 900px;
margin: 0 auto;
}
.report-header h1 {
font-family: var(--font-display);
font-size: var(--title-size);
color: var(--accent);
letter-spacing: -0.02em;
margin-bottom: 0.3rem;
}
.report-header .subtitle {
font-size: clamp(0.9rem, 1.5vw, 1.1rem);
color: var(--text-secondary);
font-weight: 300;
margin-bottom: 1.2rem;
}
/* 大日期显示 */
.date-display {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
margin: 1.2rem 0;
}
.date-display .date-text {
font-family: var(--font-display);
font-size: clamp(1.4rem, 3vw, 2.4rem);
color: var(--text-primary);
letter-spacing: 0.02em;
}
.date-nav-btn {
background: var(--bg-secondary);
border: 1px solid var(--border);
color: var(--text-secondary);
width: 36px;
height: 36px;
border-radius: 50%;
cursor: pointer;
font-size: 1.1rem;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.date-nav-btn:hover {
border-color: var(--accent-border);
color: var(--accent);
background: var(--accent-glow);
}
.article-count-badge {
display: inline-block;
margin-top: 0.6rem;
padding: 0.3rem 1rem;
background: var(--accent-glow);
border: 1px solid var(--accent-border);
border-radius: 20px;
font-size: var(--small-size);
color: var(--accent-light);
font-weight: 500;
}
/* ═══════════════════════════════════════════════
SEARCH BAR
═══════════════════════════════════════════════ */
.search-bar {
max-width: 500px;
margin: 1.5rem auto 0;
position: relative;
}
.search-bar input {
width: 100%;
padding: 0.7rem 1.2rem 0.7rem 2.8rem;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 24px;
color: var(--text-primary);
font-size: var(--body-size);
font-family: var(--font-body);
outline: none;
transition: border-color 0.2s ease;
}
.search-bar input::placeholder {
color: var(--text-muted);
}
.search-bar input:focus {
border-color: var(--accent-border);
}
.search-bar .search-icon {
position: absolute;
left: 1rem;
top: 50%;
transform: translateY(-50%);
color: var(--text-muted);
font-size: 1rem;
pointer-events: none;
}
/* ═══════════════════════════════════════════════
STATS BAR
═══════════════════════════════════════════════ */
.stats-bar {
display: flex;
justify-content: center;
gap: clamp(1.5rem, 4vw, 3rem);
padding: clamp(1rem, 2vw, 1.5rem) clamp(1rem, 3vw, 2rem);
background: var(--bg-secondary);
border-radius: var(--radius);
border: 1px solid var(--border);
max-width: 700px;
margin: clamp(1.5rem, 3vw, 2rem) auto clamp(2rem, 4vw, 3rem);
flex-wrap: wrap;
}
.stat-item {
text-align: center;
min-width: 80px;
}
.stat-value {
font-family: var(--font-display);
font-size: var(--h2-size);
color: var(--accent);
line-height: 1.2;
}
.stat-label {
font-size: var(--small-size);
color: var(--text-secondary);
margin-top: 0.2rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* ═══════════════════════════════════════════════
CATEGORY CARDS GRID
═══════════════════════════════════════════════ */
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(100%, 380px), 1fr));
gap: clamp(1rem, 2vw, 1.5rem);
max-width: 1200px;
margin: 0 auto;
}
.category-card {
background: var(--bg-card);
border-radius: var(--radius);
border: 1px solid var(--border);
overflow: hidden;
transition: transform 0.3s var(--ease-out-expo), border-color 0.3s ease;
}
.category-card:hover {
transform: translateY(-2px);
border-color: var(--accent-border);
}
.category-card.hidden {
display: none;
}
.card-header {
display: flex;
align-items: center;
gap: 0.8rem;
padding: clamp(1rem, 2vw, 1.3rem) clamp(1rem, 2vw, 1.5rem);
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-light) 100%);
}
.card-number {
font-family: var(--font-display);
font-size: var(--h2-size);
color: rgba(0, 0, 0, 0.3);
line-height: 1;
}
.card-category {
flex: 1;
font-family: var(--font-body);
font-size: var(--h3-size);
font-weight: 700;
color: #fff;
}
.card-count {
font-size: var(--small-size);
color: rgba(255, 255, 255, 0.8);
font-weight: 500;
background: rgba(0, 0, 0, 0.2);
padding: 0.2rem 0.6rem;
border-radius: 10px;
}
.card-body {
padding: clamp(0.8rem, 1.5vw, 1.2rem) clamp(1rem, 2vw, 1.5rem);
}
/* ═══════════════════════════════════════════════
ARTICLE ITEMS
═══════════════════════════════════════════════ */
.article-item {
padding: 0.7rem 0;
border-bottom: 1px solid var(--border);
display: flex;
gap: 0.8rem;
align-items: flex-start;
}
.article-item:last-child {
border-bottom: none;
padding-bottom: 0;
}
.article-cover {
width: 72px;
height: 72px;
border-radius: var(--radius-sm);
object-fit: cover;
flex-shrink: 0;
background: var(--bg-secondary);
}
.article-info {
flex: 1;
min-width: 0;
}
.article-title {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
color: var(--text-primary);
text-decoration: none;
font-size: var(--body-size);
font-weight: 500;
line-height: 1.5;
transition: color 0.2s ease;
}
.article-title:hover {
color: var(--accent);
}
.article-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 0.3rem;
font-size: var(--small-size);
color: var(--text-muted);
flex-wrap: wrap;
gap: 0.3rem;
}
.author {
color: var(--text-secondary);
}
.metrics {
display: flex;
gap: 0.8rem;
}
.metric {
white-space: nowrap;
}
/* ═══════════════════════════════════════════════
NO RESULTS / SEARCH RESULTS
═══════════════════════════════════════════════ */
.no-results {
text-align: center;
padding: 3rem;
color: var(--text-muted);
font-size: 1.1rem;
display: none;
}
.no-results.visible {
display: block;
}
/* Search results overlay */
.search-results {
display: none;
max-width: 800px;
margin: 2rem auto;
}
.search-results.active {
display: block;
}
.search-results-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1.5rem;
padding-bottom: 0.8rem;
border-bottom: 1px solid var(--accent-border);
}
.search-results-header h3 {
font-family: var(--font-display);
font-size: var(--h3-size);
color: var(--accent-light);
}
.search-results-count {
font-size: var(--small-size);
color: var(--text-secondary);
}
.search-result-item {
display: flex;
gap: 0.8rem;
padding: 0.8rem 0;
border-bottom: 1px solid var(--border);
align-items: flex-start;
transition: background 0.2s ease;
}
.search-result-item:hover {
background: var(--bg-card-hover);
}
.search-result-item .article-cover {
width: 72px;
height: 72px;
border-radius: var(--radius-sm);
object-fit: cover;
flex-shrink: 0;
background: var(--bg-secondary);
}
.search-result-info {
flex: 1;
min-width: 0;
}
.search-result-title {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
color: var(--text-primary);
text-decoration: none;
font-size: var(--body-size);
font-weight: 500;
line-height: 1.5;
transition: color 0.2s ease;
}
.search-result-title:hover {
color: var(--accent);
}
.search-result-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 0.3rem;
font-size: var(--small-size);
color: var(--text-muted);
flex-wrap: wrap;
gap: 0.3rem;
}
.search-loading {
text-align: center;
padding: 2rem;
color: var(--text-secondary);
display: none;
}
.search-loading.visible {
display: block;
}
.search-error {
text-align: center;
padding: 2rem;
color: var(--accent);
display: none;
}
.search-error.visible {
display: block;
}
/* ═══════════════════════════════════════════════
FOOTER
═══════════════════════════════════════════════ */
.report-footer {
text-align: center;
padding: clamp(2rem, 4vw, 3rem) 0 1rem;
color: var(--text-muted);
font-size: var(--small-size);
}
.report-footer .powered {
margin-top: 0.5rem;
color: var(--text-secondary);
}
/* ═══════════════════════════════════════════════
ANIMATIONS
═══════════════════════════════════════════════ */
.reveal {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.6s var(--ease-out-expo), transform 0.6s var(--ease-out-expo);
}
.reveal.visible {
opacity: 1;
transform: translateY(0);
}
.category-card:nth-child(1) { transition-delay: 0.05s; }
.category-card:nth-child(2) { transition-delay: 0.1s; }
.category-card:nth-child(3) { transition-delay: 0.15s; }
.category-card:nth-child(4) { transition-delay: 0.2s; }
.category-card:nth-child(5) { transition-delay: 0.25s; }
.category-card:nth-child(6) { transition-delay: 0.3s; }
.category-card:nth-child(7) { transition-delay: 0.35s; }
.category-card:nth-child(8) { transition-delay: 0.4s; }
/* ═══════════════════════════════════════════════
RESPONSIVE
═══════════════════════════════════════════════ */
@media (max-width: 600px) {
.stats-bar {
gap: 1rem;
}
.stat-item {
min-width: 60px;
}
.cards-grid {
grid-template-columns: 1fr;
}
.date-display .date-text {
font-size: 1.3rem;
}
.article-cover {
width: 56px;
height: 56px;
}
}
@media (prefers-reduced-motion: reduce) {
.reveal {
opacity: 1;
transform: none;
transition: none;
}
}
</style>
</head>
<body>
<!-- HEADER -->
<header class="report-header">
<h1>AI公众号信息源</h1>
<p class="subtitle">每日热门内容聚类 · 阅读量靠前的 AI 爆款一网打尽</p>
<!-- 大日期 + 前后切换 -->
<div class="date-display">
<button class="date-nav-btn" id="prevDay" title="前一天">←</button>
<span class="date-text">{{DATE_CN}}</span>
<button class="date-nav-btn" id="nextDay" title="后一天">→</button>
</div>
<span class="article-count-badge">共 {{TOTAL_COUNT}} 篇热门文章</span>
<!-- 搜索框 -->
<div class="search-bar">
<span class="search-icon">🔍</span>
<input type="text" id="searchInput" placeholder="搜索文章标题、作者或分类...">
</div>
</header>
<!-- STATS BAR -->
<div class="stats-bar">
<div class="stat-item">
<div class="stat-value">{{TOPIC_COUNT}}</div>
<div class="stat-label">分类</div>
</div>
<div class="stat-item">
<div class="stat-value">{{TOTAL_COUNT}}</div>
<div class="stat-label">文章</div>
</div>
<div class="stat-item">
<div class="stat-value">{{AVG_READS}}</div>
<div class="stat-label">平均阅读</div>
</div>
<div class="stat-item">
<div class="stat-value">{{TOTAL_LIKES}}</div>
<div class="stat-label">总点赞</div>
</div>
</div>
<!-- CATEGORY CARDS -->
<div class="cards-grid" id="cardsGrid">
{{CATEGORY_CARDS}}
</div>
<!-- NO RESULTS -->
<div class="no-results" id="noResults">
没有找到匹配的文章
</div>
<!-- SEARCH RESULTS (API-based, full library search) -->
<div class="search-results" id="searchResults">
<div class="search-results-header">
<h3>搜索结果</h3>
<span class="search-results-count" id="searchCount"></span>
</div>
<div class="search-loading" id="searchLoading">搜索中...</div>
<div class="search-error" id="searchError"></div>
<div id="searchResultList"></div>
</div>
<!-- FOOTER -->
<footer class="report-footer">
<p>Generated at {{TIMESTAMP}}</p>
<p class="powered">Powered by AI公众号信息源 Skill</p>
</footer>
<!-- SCRIPTS -->
<script>
(function() {
// ── 配置 ──
var SEARCH_URL = '/api/search';
var SEARCH_PAGE_SIZE = 20;
// ── Scroll reveal animation ──
var observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
}
});
}, { threshold: 0.1, rootMargin: '0px 0px -50px 0px' });
document.querySelectorAll('.reveal').forEach(function(el) {
observer.observe(el);
});
// ── DOM refs ──
var searchInput = document.getElementById('searchInput');
var cardsGrid = document.getElementById('cardsGrid');
var noResults = document.getElementById('noResults');
var searchResults = document.getElementById('searchResults');
var searchLoading = document.getElementById('searchLoading');
var searchError = document.getElementById('searchError');
var searchResultList = document.getElementById('searchResultList');
var searchCount = document.getElementById('searchCount');
var statsBar = document.querySelector('.stats-bar');
var articleBadge = document.querySelector('.article-count-badge');
var searchTimer = null;
var DEBOUNCE_MS = 500;
// ── Format number helper ──
function fmtNum(n) {
if (!n) return '0';
if (n >= 10000) return (n / 10000).toFixed(1) + 'w';
if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
return String(n);
}
// ── Format time ──
function fmtTime(dt) {
if (!dt) return '';
try {
var d = new Date(dt.replace(/-/g, '/'));
var m = String(d.getMonth() + 1).padStart(2, '0');
var day = String(d.getDate()).padStart(2, '0');
return m + '-' + day;
} catch(e) {
return dt.substring(0, 10);
}
}
// ── Show daily report view ──
function showReportView() {
cardsGrid.style.display = '';
statsBar.style.display = '';
if (articleBadge) articleBadge.style.display = '';
noResults.classList.remove('visible');
searchResults.classList.remove('active');
searchResultList.innerHTML = '';
}
// ── Show search results view ──
function showSearchView() {
cardsGrid.style.display = 'none';
statsBar.style.display = 'none';
if (articleBadge) articleBadge.style.display = 'none';
noResults.classList.remove('visible');
searchResults.classList.add('active');
}
// ── Search API call ──
function doSearch(query) {
showSearchView();
// 检测 file:// 协议
if (window.location.protocol === 'file:') {
searchLoading.classList.remove('visible');
searchError.innerHTML = '当前为离线模式。<br>请运行 <code>python3 daily_report.py</code> 启动搜索服务';
searchError.classList.add('visible');
return;
}
searchLoading.classList.add('visible');
searchError.classList.remove('visible');
searchResultList.innerHTML = '';
fetch(SEARCH_URL + '?keyword=' + encodeURIComponent(query))
.then(function(resp) {
if (!resp.ok) throw new Error('HTTP ' + resp.status);
return resp.json();
})
.then(function(data) {
searchLoading.classList.remove('visible');
var code = data.code || 0;
if (code !== 200 && code !== 2000) {
searchError.textContent = data.msg || '搜索失败,请稍后重试';
searchError.classList.add('visible');
return;
}
var articles = (data.data && data.data.list) || [];
if (articles.length === 0) {
searchError.textContent = '没有找到相关文章,试试换个关键词';
searchError.classList.add('visible');
searchCount.textContent = '';
return;
}
searchCount.textContent = '找到 ' + articles.length + ' 篇相关文章';
var html = '';
articles.forEach(function(a) {
var title = a.title || '无标题';
var url = a.url || '#';
var author = a.userName || '';
var cover = a.coverUrl || '';
var reads = fmtNum(a.readCount);
var likes = fmtNum(a.likeCount);
var time = fmtTime(a.gmtCreate);
var coverHtml = cover
? '<img class="article-cover" src="' + cover + '" alt="" loading="lazy" referrerpolicy="no-referrer">'
: '';
html += '<div class="search-result-item">'
+ coverHtml
+ '<div class="search-result-info">'
+ '<a href="' + url + '" target="_blank" class="search-result-title">' + title + '</a>'
+ '<div class="search-result-meta">'
+ '<span class="author">' + author + '</span>'
+ '<span class="metrics">'
+ '<span class="metric">👁 ' + reads + '</span>'
+ '<span class="metric">👍 ' + likes + '</span>'
+ (time ? '<span class="metric">📅 ' + time + '</span>' : '')
+ '</span></div></div></div>';
});
searchResultList.innerHTML = html;
})
.catch(function(err) {
searchLoading.classList.remove('visible');
if (window.location.protocol === 'file:') {
searchError.textContent = '离线模式下无法搜索。请用 python3 daily_report.py 启动服务';
} else {
searchError.textContent = '搜索服务未启动。请在终端运行 python3 daily_report.py 后重试';
}
searchError.classList.add('visible');
});
}
// ── Search input handler with debounce ──
searchInput.addEventListener('input', function() {
var query = this.value.trim();
if (!query) {
// Empty search → show daily report
clearTimeout(searchTimer);
showReportView();
return;
}
clearTimeout(searchTimer);
searchTimer = setTimeout(function() {
doSearch(query);
}, DEBOUNCE_MS);
});
// ── Date navigation ──
var currentDate = '{{DATE}}';
function navigateDate(offset) {
var parts = currentDate.split('-');
var d = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]));
d.setDate(d.getDate() + offset);
var y = d.getFullYear();
var m = String(d.getMonth() + 1).padStart(2, '0');
var day = String(d.getDate()).padStart(2, '0');
var newDate = y + '-' + m + '-' + day;
var newFile = 'AI日报_' + newDate + '.html';
window.location.href = newFile;
}
document.getElementById('prevDay').addEventListener('click', function() {
navigateDate(-1);
});
document.getElementById('nextDay').addEventListener('click', function() {
navigateDate(1);
});
})();
</script>
</body>
</html>
AI WeChat Feed / gzh-ai-feed
---
Overview
Daily automatic scanning of AI WeChat public account articles, finding the hottest content by read count, auto-clustering into topics, and generating a styled HTML daily report.
Core Value
- Hot Content Discovery: Scans 200+ AI public account articles, ranked by read count
- Smart Clustering: Auto-discovers topic directions from daily content (Agent, LLM, AI Art...)
- Visual Report: Dark-themed HTML with cover images, metrics, article links, and date navigation
- One-Click Subscription: Enable daily auto-generation, reports saved locally
Target Users
- 📊 AI Professionals — Stay on top of AI WeChat content trends daily
- 📝 Content Creators — Discover trending topics and writing angles in the AI space
- 🔍 Industry Researchers — Track AI topic trends and discussion shifts
---
Features
Core Features
- Daily Hot Scan: Auto-retrieves 200+ AI public account articles, sorted by reads
- Smart Topic Clustering: Content-driven topic discovery, not fixed categories
- Terminal Table Output: Category + title + author + reads/likes/comments at a glance
- Visual HTML Report: Dark theme with cover images, metrics, date nav, full-database search
- Keyword Filtering: Custom focus directions for precise content filtering
- Daily Subscription: One-click cron setup for automated daily report generation
---
API Key Acquisition & Security
- The skill uses the environment variable:
REDFOX_API_KEY. REDFOX_API_KEYis provided by RedFoxHub (https://redfox.hk).- Visit RedFoxHub to register and obtain your
REDFOX_API_KEY. - Configure the device environment variable
REDFOX_API_KEYbefore using this skill. - Before providing your key, verify its origin, scope, validity period, and whether reset/revocation is supported.
- Never hardcode or expose the key in code, prompts, logs, or output files.
---
Usage
Describe your needs in natural language.
Quick Reference
| Intent | Example | Result |
|---|---|---|
| Today's report | "Generate today's AI public account daily report" | Auto-fetches hot articles, generates HTML report |
| Custom focus | "Focus on Agent and RAG articles" | Filters by specified keywords |
| Historical view | "Show me the AI report for May 26" | Retrieves hot content for a specific date |
| Subscribe | "Subscribe to daily AI public account reports" | Installs cron job for daily auto-generation |
---
Use Cases
| Scenario | Role | Example Query | Benefit |
|---|---|---|---|
| Daily briefing | AI professional | "What's hot in AI today?" | 5-minute overview of AI content trends |
| Topic inspiration | Content writer | "What topics are trending in AI public accounts?" | Discover trending angles aligned with the buzz |
| Competitor monitoring | Content ops | "What AI content are competitors publishing?" | Track competitive dynamics, adjust strategy |
| Trend research | Industry analyst | "How have AI topics shifted this week?" | Track topic evolution, produce research reports |
AI公众号信息源 / gzh-ai-feed
---
简介
每日自动扫描全网 AI 公众号爆款文章,按阅读量找出最火内容,智能聚类后生成精美 HTML 日报。
核心价值
- 爆款发现:从 200+ 篇 AI 公众号文章中,按阅读量筛选最火的热门内容
- 智能聚类:自动从当天内容中发现话题方向(Agent、大模型、AI绘画…),每天的分类由内容决定
- 可视化日报:深色主题 HTML,封面图、互动数据、文章直链、日期导航
- 一键订阅:开启订阅后每日自动产出,日报自动攒在本地文件夹
适用对象
- 📊 AI 从业者 — 每日掌握 AI 领域公众号内容风向
- 📝 内容创作者 — 发现 AI 赛道爆款选题与写作角度
- 🔍 行业研究员 — 追踪 AI 话题热度变化与讨论趋势
---
功能特性
核心功能
- 每日爆款扫描:自动检索 200+ AI 公众号文章,按阅读量排序
- 智能话题聚类:基于当天内容自动识别话题方向,非固定分类
- 终端表格输出:分类 + 标题 + 作者 + 阅读/点赞/评论数,一目了然
- 可视化 HTML 日报:深色主题,含封面图、数据指标、日期导航、全库搜索
- 关键词过滤:支持自定义关注方向,精准筛选感兴趣的内容
- 每日订阅:一键开启定时任务,日报自动产出保存
---
密钥获取与安全说明
- 本技能需要使用环境变量:
REDFOX_API_KEY。 REDFOX_API_KEY由 红狐 hub (https://redfox.hk)提供。- 请前往 红狐 hub 注册账号,获取
REDFOX_API_KEY。 - 配置设备环境变量
REDFOX_API_KEY后使用本技能。 - 在提供密钥前,请先确认密钥来源、可用范围、有效期及是否支持重置/撤销。
- 禁止在代码、提示词、日志或输出文件中硬编码/明文暴露密钥。
---
使用指南
直接用自然语言描述需求即可。
常用说法速查
| 意图 | 示例话术 | 效果 |
|---|---|---|
| 生成今日日报 | 「帮我生成今天的 AI 公众号日报」 | 自动拉取当日热门文章,生成 HTML 日报 |
| 自定义方向 | 「关注 Agent 和 RAG 方向的公众号文章」 | 按指定关键词筛选相关内容 |
| 查看历史 | 「看看 5 月 26 号的 AI 日报」 | 回溯指定日期的热门内容 |
| 开启订阅 | 「订阅 AI 公众号日报,每天推送」 | 安装定时任务,每日自动产出 |
---
使用场景
| 场景 | 角色 | 示例问法 | 收益 |
|---|---|---|---|
| 每日资讯速览 | AI 从业者 | 「今天 AI 圈有什么热点?」 | 5 分钟掌握当天 AI 内容风向 |
| 选题灵感搜集 | 公众号作者 | 「最近 AI 公众号什么选题火?」 | 发现爆款选题,对齐赛道热度 |
| 竞品内容监控 | 内容运营 | 「看看同行最近发了什么 AI 内容」 | 掌握竞品动态,调整内容策略 |
| 趋势研究分析 | 行业分析师 | 「这周 AI 话题有什么变化?」 | 追踪话题趋势,产出研究报告 |