Files
daily-robots/daily/generate.py
yumao ce04d5a342
Some checks failed
test / pytest (push) Failing after 3s
daily / report (push) Failing after 1s
refactor: Phase 3 拆分 generate 流水线并补全测试
提取 collect/formatters/themes 等 pipeline 模块,新增 wecom 分条、RSS、delta、Agent 工作流测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-03 14:59:12 +08:00

271 lines
9.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""生成早报 Markdown完整版 + 企微短版)。"""
from __future__ import annotations
import logging
import sys
from datetime import datetime, timezone, timedelta
from pathlib import Path
from daily.agent_workflow import is_agent_mode, run_agent_workflow
from daily.config import LOG_DIR, OUTPUT_DIR, env
from daily.cursor_editor import (
apply_descriptions,
is_enabled as cursor_editor_enabled,
run_editorial,
theme_line_from_editorial,
)
from daily.format_wecom import build_wecom_report, finalize_wecom_skill_groups, replace_wecom_skill_sections
from daily.github.trending import trending_data_source_note
from daily.news.fetch import (
format_cn_news_section,
format_news_section,
prepare_wecom_cn_news_items,
prepare_wecom_news_items,
)
from daily.pipeline.collect import collect_report_context
from daily.pipeline.formatters import (
build_highlights,
fetch_latest_release_title,
format_github_repo_section,
format_skill_section,
prepare_github_item,
prepare_skill_item,
)
from daily.pipeline.localize import localize_descriptions_in_place
from daily.pipeline.snapshot import load_snapshot, save_snapshot
from daily.pipeline.themes import detect_theme_line, theme_clusters
from daily.report_data import build_full_payload, data_json_path, save_json
from daily.skills_group import group_skills_by_source
logger = logging.getLogger(__name__)
def _now_cst() -> datetime:
return datetime.now(timezone(timedelta(hours=8)))
def generate_report() -> tuple[str, str, Path, Path]:
ctx = collect_report_context(_now_cst())
now = _now_cst()
prev_ids = load_snapshot()
save_json(
data_json_path(ctx.date_str),
build_full_payload(
ctx.llm_input,
meta={
"generated_at": now.isoformat(),
"report_mode": "agent" if is_agent_mode() else "classic",
"cursor_editor": cursor_editor_enabled() and not is_agent_mode(),
},
),
)
agent_wecom: str | None = None
if is_agent_mode():
agent_wecom = run_agent_workflow(
ctx.llm_input,
date_str=ctx.date_str,
time_str=ctx.time_str,
updated=ctx.updated,
)
if not agent_wecom:
logger.warning("Agent 工作流失败,回退 classic 模式")
editorial_theme: str | None = None
editorial_highlights: list[str] | None = None
if agent_wecom is None:
editorial = run_editorial(ctx.llm_input, date_str=ctx.date_str)
if editorial:
apply_descriptions(
trending=ctx.trending,
hot=ctx.hot,
github_trending=ctx.github_trending,
github_emerging=ctx.github_emerging,
github_topic=ctx.github_topic,
ai_news=ctx.ai_news,
cn_ai_news=ctx.cn_ai_news,
descriptions=editorial.get("descriptions") or {},
)
editorial_theme = theme_line_from_editorial(editorial) or None
hl = editorial.get("highlights") or []
editorial_highlights = hl if hl else None
localize_descriptions_in_place(
ctx.trending,
ctx.hot,
ctx.github_trending,
ctx.github_emerging,
ctx.github_topic,
ctx.ai_news,
ctx.cn_ai_news,
)
limits = ctx.limits
themes = theme_clusters(ctx.feed)
lines = [
f"# 早报 · {ctx.date_str}",
"",
f"> 生成时间:{now.strftime('%Y-%m-%d %H:%M')} (UTC+8) ",
f"> skills 数据更新:{ctx.updated} ",
"> 数据来源:[skills.sh/trending](https://skills.sh/trending) · [skills.sh/hot](https://skills.sh/hot) · 国际/国内 AI RSS",
"",
"---",
"",
f"## 一、Skills Trending Top {limits.trending_n}",
"",
*format_skill_section(ctx.trending),
"---",
"",
f"## 二、Skills Hot Top {limits.hot_n}",
"",
*format_skill_section(ctx.hot, hot=True),
"",
"---",
"",
f"## 三、GitHub Trending Top {limits.github_limit}",
"",
trending_data_source_note(),
"",
]
if ctx.github_trending:
lines.extend(format_github_repo_section(ctx.github_trending))
else:
lines.append("*GitHub Trending 获取失败,请检查网络或配置 GITHUB_TOKEN。*")
lines.append("")
lines.extend(
["---", "", f"## 四、新兴项目 Top {limits.emerging_limit}", "", "> 数据来源GitHub Search API需 `GITHUB_TOKEN`", ""]
)
if ctx.github_emerging:
lines.extend(format_github_repo_section(ctx.github_emerging, show_created=True))
else:
lines.append("*新兴项目获取失败或未配置 GITHUB_TOKEN。*")
lines.append("")
lines.extend(
["---", "", f"## 五、Topic `{ctx.topic_name}` Top {limits.topic_limit}", "", "> 数据来源GitHub Search API需 `GITHUB_TOKEN`", ""]
)
if ctx.github_topic:
lines.extend(format_github_repo_section(ctx.github_topic))
else:
lines.append(f"*Topic `{ctx.topic_name}` 热点获取失败或未配置 GITHUB_TOKEN。*")
lines.append("")
section_no = 6
lines.extend(format_news_section(ctx.ai_news, section_no=section_no))
section_no += 1
lines.extend(format_cn_news_section(ctx.cn_ai_news, section_no=section_no))
section_no += 1
watch = (env("GITHUB_REPOS") or "").strip()
if watch:
lines.extend(["---", "", f"## {section_no}、关注仓库 Release", ""])
section_no += 1
for repo in [r.strip() for r in watch.split(",") if r.strip()]:
release = fetch_latest_release_title(repo)
lines.append(f"- **{repo}**{release or '暂无 release'}")
lines.append("")
lines.extend(["---", "", f"## {section_no}、主题聚类", ""])
for theme, examples in themes:
lines.append(f"### {theme}")
for ex in examples:
lines.append(f"- {ex}")
lines.append("")
pick_src = ctx.trending[0].get("source", "") if ctx.trending else ""
pick_name = ctx.trending[0].get("title", "") if ctx.trending else ""
pick_command = (
f"npx skills add {pick_src}/{pick_name}"
if pick_src and pick_name
else "npx skills add vercel-labs/skills/find-skills"
)
lines.extend(["---", "", "## 安装示例", "", "```bash"])
for item in ctx.trending[:4]:
src, name = item.get("source", ""), item.get("title", "")
if src and name:
lines.append(f"npx skills add {src}/{name}")
lines.extend(["```", "", f"*企微短版见 `output/{ctx.date_str}.wecom.md`*"])
markdown = "\n".join(lines)
if agent_wecom:
gt = group_skills_by_source(
ctx.trending, limit=limits.wecom_trending, pool_size=limits.skill_pool
)
gh = group_skills_by_source(ctx.hot, limit=limits.wecom_hot, pool_size=limits.skill_pool)
wecom_md = replace_wecom_skill_sections(agent_wecom, trending=gt, hot=gh)
else:
wecom_md = build_wecom_report(
date_str=ctx.date_str,
time_str=ctx.time_str,
updated=ctx.updated,
highlights=editorial_highlights
or build_highlights(
ctx.trending,
ctx.hot,
ctx.github_trending,
ctx.github_emerging,
ctx.ai_news,
ctx.cn_ai_news,
),
theme_line=editorial_theme or detect_theme_line(ctx.feed),
ai_news=prepare_wecom_news_items(ctx.ai_news),
cn_ai_news=prepare_wecom_cn_news_items(ctx.cn_ai_news),
trending=[
prepare_skill_item(item, prev_ids, r)
for r, item in enumerate(
finalize_wecom_skill_groups(
group_skills_by_source(
ctx.trending, limit=limits.wecom_trending, pool_size=limits.skill_pool
)
),
1,
)
],
hot=[
prepare_skill_item(item, prev_ids, r)
for r, item in enumerate(
finalize_wecom_skill_groups(
group_skills_by_source(
ctx.hot, limit=limits.wecom_hot, pool_size=limits.skill_pool
)
),
1,
)
],
repos=[prepare_github_item(item) for item in ctx.github_trending[: limits.wecom_github]],
emerging=[prepare_github_item(item) for item in ctx.github_emerging[: limits.wecom_emerging]],
topic_name=ctx.topic_name,
topic_repos=[prepare_github_item(item) for item in ctx.github_topic[: limits.wecom_topic]],
pick_command=pick_command,
)
save_snapshot(ctx.feed, ctx.date_str)
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
out_md = OUTPUT_DIR / f"{ctx.date_str}.md"
out_wecom = OUTPUT_DIR / f"{ctx.date_str}.wecom.md"
out_md.write_text(markdown, encoding="utf-8")
out_wecom.write_text(wecom_md, encoding="utf-8")
return markdown, wecom_md, out_md, out_wecom
def main() -> int:
LOG_DIR.mkdir(parents=True, exist_ok=True)
log_file = LOG_DIR / f"{_now_cst():%Y-%m-%d}.log"
try:
_, wecom_md, out_md, out_wecom = generate_report()
nbytes = len(wecom_md.encode("utf-8"))
msg = f"[{_now_cst():%H:%M:%S}] OK -> {out_md}, {out_wecom} ({nbytes} bytes)\n"
log_file.write_text(msg, encoding="utf-8")
print(msg.strip())
return 0
except Exception as exc:
msg = f"[{_now_cst():%H:%M:%S}] FAIL: {exc}\n"
log_file.write_text(msg, encoding="utf-8")
print(msg.strip(), file=sys.stderr)
return 1