diff --git a/daily/generate.py b/daily/generate.py index dfca5ba..61c3199 100644 --- a/daily/generate.py +++ b/daily/generate.py @@ -22,15 +22,24 @@ from daily.config import ( LOG_DIR, OUTPUT_DIR, SNAPSHOT_FILE, - ensure_bot_on_path, env, env_int, full_desc_limit, news_summary_limit, wecom_skill_desc_limit, ) -from daily.format_wecom import build_wecom_report, finalize_wecom_skill_groups, replace_wecom_skill_sections +from daily.format_wecom import ( + build_wecom_report, + finalize_wecom_skill_groups, + replace_wecom_board_sections, + replace_wecom_skill_sections, +) from daily.agent_workflow import is_agent_mode, run_agent_workflow +from daily.featured_pick import ( + apply_featured_pick, + pick_command_from_featured, + pick_why_from_featured, +) from daily.delta import compare_depth from daily.cursor_editor import ( apply_descriptions, @@ -50,18 +59,17 @@ from daily.news.fetch import ( prepare_wecom_cn_news_items, prepare_wecom_news_items, ) +from daily.news.pushed_links import record_pushed_links +from daily.push_gate import evaluate_push_gate from daily.report_data import ( build_full_payload, build_llm_input, data_json_path, save_json, ) -from daily.skills_board import load_boards +from daily.skills_board import format_installs, load_boards, load_feed from daily.skills_group import group_skills_by_source -ensure_bot_on_path() -from skills_service import _format_installs, load_feed # noqa: E402 - THEME_RULES: list[tuple[str, str, list[str]]] = [ ("🎬", "AI 多媒体 / 视频", ["runcomfy", "remotion", "video", "seedance", "inpaint", "lipsync"]), ("🔧", "工程协作 / Skill 元能力", ["grill", "tdd", "architecture", "find-skills", "to-issues"]), @@ -232,7 +240,7 @@ def _prepare_skill_item(item: dict[str, Any], prev_ids: set[str], rank: int) -> badge = "🆕" elif rank == 1: badge = "👑" - installs_fmt = item.get("installs_fmt") or _format_installs(item.get("installs", 0)) + installs_fmt = item.get("installs_fmt") or format_installs(item.get("installs", 0)) title = item.get("source", "?") if item.get("cluster") else item.get("title", "?") desc = item.get("wecom_desc") or item.get("description") or item.get("cluster_titles") or "" limit = wecom_skill_desc_limit() @@ -303,7 +311,7 @@ def _build_highlights( ) if trending: t0 = trending[0] - points.append(f"📈 Skills 榜首 **{t0.get('title')}**({_format_installs(t0.get('installs', 0))})") + points.append(f"📈 Skills 榜首 **{t0.get('title')}**({format_installs(t0.get('installs', 0))})") if github_trending: g0 = github_trending[0] stars = g0.get("stars_today_fmt", "") @@ -315,7 +323,7 @@ def _build_highlights( points.append(f"🌱 新兴 [{e0['repo']}]({e0['url']})(⭐ {e0.get('total_stars_fmt', '?')})") elif hot: h0 = hot[0] - points.append(f"🔥 Skills Hot 榜首 **{h0.get('title')}**(1H {_format_installs(h0.get('installs', 0))})") + points.append(f"🔥 Skills Hot 榜首 **{h0.get('title')}**(1H {format_installs(h0.get('installs', 0))})") while len(points) < 3 and len(trending) > len(points): item = trending[len(points)] points.append(f"✨ **{item.get('title')}** · `{item.get('source')}`") @@ -397,7 +405,7 @@ def _format_skill_section(items: list[dict[str, Any]], *, hot: bool = False) -> for i, item in enumerate(items, 1): skill_id = item.get("id") or f"{item.get('source', '?')}/{item.get('title', '?')}" link = item.get("link", "") - installs = _format_installs(item.get("installs", 0)) + installs = format_installs(item.get("installs", 0)) meta = f"1H {installs}" if hot else f"总安装 {installs}" if link: lines.append(f"{i}. **[{skill_id}]({link})** · {meta}") @@ -467,17 +475,22 @@ def generate_report() -> tuple[str, str, Path, Path]: cn_ai_news=cn_ai_news, wecom_limits=wecom_limits, ) - save_json( - data_json_path(date_str), - build_full_payload( - 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(), - }, - ), + featured = apply_featured_pick(llm_input, date_str=date_str) + movement = llm_input["movement"] + eff_mode = llm_input["effective_wecom_mode"] + wecom_ai = prepare_wecom_news_items(ai_news, date_str=date_str) + wecom_cn = prepare_wecom_cn_news_items(cn_ai_news, date_str=date_str) + push_gate = evaluate_push_gate( + movement=movement, + ai_news_items=wecom_ai, + cn_ai_news_items=wecom_cn, + featured_pick=featured, ) + llm_input["push_gate"] = { + "should_push": push_gate.should_push, + "silent": push_gate.silent, + "reasons": push_gate.reasons, + } agent_wecom: str | None = None if is_agent_mode(): @@ -586,11 +599,14 @@ def generate_report() -> tuple[str, str, Path, Path]: pick_src = trending[0].get("source", "") if trending else "" pick_name = trending[0].get("title", "") if trending else "" - pick_command = ( + pick_command = pick_command_from_featured(featured) or ( f"npx skills add {pick_src}/{pick_name}" if pick_src and pick_name else "npx skills add vercel-labs/skills/find-skills" ) + pick_why = pick_why_from_featured(featured) or "" + pick_title = str((featured or {}).get("title") or pick_name or "").strip() + pick_url = str((featured or {}).get("url") or "").strip() lines.extend(["---", "", "## 安装示例", "", "```bash"]) for item in trending[:4]: @@ -600,10 +616,17 @@ def generate_report() -> tuple[str, str, Path, Path]: lines.extend(["```", "", f"*企微短版见 `output/{date_str}.wecom.md`*"]) markdown = "\n".join(lines) + gt = group_skills_by_source(trending, limit=wecom_trending, pool_size=skill_pool) + gh = group_skills_by_source(hot, limit=wecom_hot, pool_size=skill_pool) if agent_wecom: - gt = group_skills_by_source(trending, limit=wecom_trending, pool_size=skill_pool) - gh = group_skills_by_source(hot, limit=wecom_hot, pool_size=skill_pool) - wecom_md = replace_wecom_skill_sections(agent_wecom, trending=gt, hot=gh) + wecom_md = replace_wecom_skill_sections( + agent_wecom, + trending=gt, + hot=gh, + mode=eff_mode, + movement=movement, + topic_name=topic_name, + ) else: wecom_md = build_wecom_report( date_str=date_str, @@ -612,32 +635,57 @@ def generate_report() -> tuple[str, str, Path, Path]: highlights=editorial_highlights or _build_highlights(trending, hot, github_trending, github_emerging, ai_news, cn_ai_news), theme_line=editorial_theme or _detect_theme_line(feed), - ai_news=prepare_wecom_news_items(ai_news), - cn_ai_news=prepare_wecom_cn_news_items(cn_ai_news), + ai_news=wecom_ai, + cn_ai_news=wecom_cn, trending=[ _prepare_skill_item(item, prev_ids, r) - for r, item in enumerate( - finalize_wecom_skill_groups( - group_skills_by_source(trending, limit=wecom_trending, pool_size=skill_pool) - ), - 1, - ) + for r, item in enumerate(finalize_wecom_skill_groups(gt), 1) ], hot=[ _prepare_skill_item(item, prev_ids, r) - for r, item in enumerate( - finalize_wecom_skill_groups( - group_skills_by_source(hot, limit=wecom_hot, pool_size=skill_pool) - ), - 1, - ) + for r, item in enumerate(finalize_wecom_skill_groups(gh), 1) ], repos=[_prepare_github_item(item) for item in github_trending[:wecom_github]], emerging=[_prepare_github_item(item) for item in github_emerging[:wecom_emerging]], topic_name=topic_name, topic_repos=[_prepare_github_item(item) for item in github_topic[:wecom_topic]], pick_command=pick_command, + pick_why=pick_why, + pick_title=pick_title, + pick_url=pick_url, + include_boards=(eff_mode == "full"), ) + wecom_md = replace_wecom_board_sections( + wecom_md, + mode=eff_mode, + movement=movement, + trending=gt, + hot=gh, + topic_name=topic_name, + ) + + if push_gate.should_push: + links = [x["link"] for x in wecom_ai + wecom_cn if x.get("link")] + record_pushed_links(date_str, links) + + save_json( + data_json_path(date_str), + build_full_payload( + 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(), + "featured_pick": featured.get("title") if featured else None, + "effective_wecom_mode": eff_mode, + "push_gate": { + "should_push": push_gate.should_push, + "silent": push_gate.silent, + "reasons": push_gate.reasons, + }, + }, + ), + ) _save_snapshot(feed, date_str) OUTPUT_DIR.mkdir(parents=True, exist_ok=True) diff --git a/daily/report_data.py b/daily/report_data.py index ea8f2ed..c2d3799 100644 --- a/daily/report_data.py +++ b/daily/report_data.py @@ -6,8 +6,8 @@ import json from pathlib import Path from typing import Any -from daily.config import OUTPUT_DIR, env_int -from daily.delta import build_movement_baseline, build_movement_context, compare_depth +from daily.config import OUTPUT_DIR, env_int, wecom_mode +from daily.delta import build_movement_baseline, build_movement_context, compare_depth, effective_wecom_mode from daily.news.fetch import prepare_wecom_cn_news_items, prepare_wecom_news_items from daily.skills_group import group_skills_by_source @@ -59,9 +59,10 @@ def _slim_news_items( limit: int, *, prepare=prepare_wecom_news_items, + date_str: str | None = None, ) -> list[dict[str, Any]]: items: list[dict[str, Any]] = [] - for item in prepare(ai_news): + for item in prepare(ai_news, date_str=date_str): items.append( { "link": item.get("link", ""), @@ -147,10 +148,13 @@ def build_llm_input( github_topic=[_slim_github(x) for x in topic_cmp], depth=depth, ) + eff_mode = effective_wecom_mode(date_str=date_str) return { "date": date_str, "data_updated": updated, + "wecom_mode": wecom_mode(), + "effective_wecom_mode": eff_mode, "skills_trending": [_slim_skill(x) for x in trending_slice], "skills_hot": [_slim_skill(x) for x in hot_slice], "github_trending": [_slim_github(x) for x in github_slice], @@ -159,9 +163,9 @@ def build_llm_input( "topic": topic_name, "repos": [_slim_github(x) for x in topic_slice], }, - "ai_news": _slim_news_items(ai_news, news_limit) if ai_news.get("enabled") else [], + "ai_news": _slim_news_items(ai_news, news_limit, date_str=date_str) if ai_news.get("enabled") else [], "cn_ai_news": _slim_news_items( - cn_ai_news, cn_news_limit, prepare=prepare_wecom_cn_news_items + cn_ai_news, cn_news_limit, prepare=prepare_wecom_cn_news_items, date_str=date_str ) if cn_ai_news.get("enabled") else [], @@ -186,6 +190,10 @@ def editorial_json_path(date_str: str) -> Path: return OUTPUT_DIR / f"{date_str}.editorial.json" +def featured_json_path(date_str: str) -> Path: + return OUTPUT_DIR / f"{date_str}.featured.json" + + def save_json(path: Path, data: dict[str, Any]) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")