"""生成早报 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