"""企微早报排版。""" from __future__ import annotations import re from typing import Any from daily.config import env_bool, wecom_skill_desc_limit from daily.localize import LocalizeJob, localize_brief_descriptions, needs_chinese from daily.skills_group import group_skills_by_source from daily.text_utils import trim_brief ICONS = { "header": "📰", "highlights": "💡", "trending": "📈", "hot": "🔥", "github": "🐙", "emerging": "🌱", "topic": "🤖", "ainews": "🌍", "cnainews": "🇨🇳", "pick": "📦", "theme": "🎯", "file": "📄", } def _skill_line(rank: int, item: dict[str, Any], *, badge: str = "") -> list[str]: source = item.get("source", "?") installs = item.get("installs_fmt", "?") link = item.get("link", "") desc = item.get("desc_short", "") badge_prefix = f"{badge} " if badge else "" if item.get("cluster"): count = int(item.get("cluster_count") or 1) sample = item.get("cluster_titles") or item.get("title", "") label = f"**{source}** · {count} skills · **{installs}**" if link: head = f"{rank}. {badge_prefix}[{label}]({link})" else: head = f"{rank}. {badge_prefix}{label}" lines = [head] if sample or desc: hint = desc or sample lines.append(f" {hint}") return lines title = item.get("title", "?") if link: head = f"{rank}. {badge_prefix}[**{title}**]({link}) · `{source}` · **{installs}**" else: head = f"{rank}. {badge_prefix}**{title}** · `{source}` · **{installs}**" lines = [head] if desc: lines.append(f" {desc}") return lines def _ai_news_link_label(item: dict[str, Any], *, merged: bool = False) -> str: title = (item.get("title") or "?").strip() or "?" source = (item.get("source_name") or "").strip() if merged and source: return f"{source} - {title}" desc = (item.get("desc_short") or "").strip() if desc: return desc return title def _ai_news_lines(items: list[dict[str, Any]], *, merged: bool = False) -> list[str]: lines: list[str] = [] for i, item in enumerate(items, 1): label = _ai_news_link_label(item, merged=merged) link = item.get("link", "") source = item.get("source_name", "?") pub = item.get("published_fmt", "") desc = (item.get("desc_short") or "").strip() pub_suffix = "" if merged else (f" · {pub}" if pub else "") if link: if merged and desc: head = f"{i}. [{label}]({link}) — {desc}{pub_suffix}" elif merged: head = f"{i}. [{label}]({link}){pub_suffix}" else: head = f"{i}. [{label}]({link}) · `{source}`{pub_suffix}" elif merged and desc: head = f"{i}. {label} — {desc}{pub_suffix}" else: head = f"{i}. {label} · `{source}`{pub_suffix}" lines.append(head) return lines _NEWS_BLOCK_END = re.compile( r"\n\n(📈|🔥|🐙|🌱|🤖|📦|🎯|💡|🌍|🇨🇳|📰|🔧)", ) def _build_merged_news_block( items: list[dict[str, Any]], tech_items: list[dict[str, Any]] | None = None, ) -> str: combined = list(items or []) if tech_items: combined.extend(tech_items) if not combined: return "" parts = [f"📰 **AI 时讯精选 Top {len(combined)}**"] parts.extend(_ai_news_lines(combined, merged=True)) return "\n".join(parts) + "\n" def _replace_merged_news_block( md: str, items: list[dict[str, Any]], tech_items: list[dict[str, Any]] | None = None, ) -> str: if not items and not tech_items: return md block = _build_merged_news_block(items, tech_items) start_pat = re.compile( r"^📰 \*\*AI 时讯精选[^\n]*\*\*\s*$", re.MULTILINE, ) match = start_pat.search(md) if not match: anchor = re.search(r"^(💡|🎯).*$", md, re.MULTILINE) if anchor: insert_at = anchor.end() return md[:insert_at] + "\n\n" + block + md[insert_at:].lstrip("\n") anchor2 = re.search(r"^(🌍|🇨🇳|📈|🔧).*$", md, re.MULTILINE) if anchor2: insert_at = anchor2.start() return md[:insert_at] + block + md[insert_at:].lstrip("\n") return md.rstrip() + "\n\n" + block start = match.start() tail = md[match.end() :] end_rel = _NEWS_BLOCK_END.search(tail) end = match.end() + (end_rel.start() if end_rel else len(tail)) return md[:start] + block + md[end:].lstrip("\n") def _remove_news_blocks(md: str, icons: tuple[str, ...], *, label_must_contain: str = "") -> str: out = md for icon in icons: start_pat = re.compile(rf"^{re.escape(icon)} \*\*[^\n]+\*\*\s*$", re.MULTILINE) while True: match = None for candidate in start_pat.finditer(out): if label_must_contain and label_must_contain not in candidate.group(0): continue match = candidate break if not match: break start = match.start() tail = out[match.end() :] end_rel = _NEWS_BLOCK_END.search(tail) end = match.end() + (end_rel.start() if end_rel else len(tail)) out = out[:start] + out[end:].lstrip("\n") return out def _replace_news_block( md: str, icon: str, items: list[dict[str, Any]], label: str, *, merged: bool = False, ) -> str: if not items: return md if merged: start_pat = re.compile( rf"^{re.escape(icon)} \*\*{re.escape(label)}[^\n]*\*\*\s*$", re.MULTILINE, ) else: start_pat = re.compile(rf"^{re.escape(icon)} \*\*[^\n]+\*\*\s*$", re.MULTILINE) match = start_pat.search(md) block = ( f"{icon} **{label} Top {len(items)}**\n" + "\n".join(_ai_news_lines(items, merged=merged)) + "\n" ) if not match: anchor = re.search(r"^(💡|🎯).*$", md, re.MULTILINE) if anchor: insert_at = anchor.end() return md[:insert_at] + "\n\n" + block + md[insert_at:].lstrip("\n") # 插在 🌍/🇨🇳 原位置,或 Skills 区块前 anchor2 = re.search(r"^(🌍|🇨🇳|📈).*$", md, re.MULTILINE) if anchor2: insert_at = anchor2.start() return md[:insert_at] + block + md[insert_at:].lstrip("\n") return md.rstrip() + "\n\n" + block start = match.start() tail = md[match.end() :] end_rel = _NEWS_BLOCK_END.search(tail) end = match.end() + (end_rel.start() if end_rel else len(tail)) return md[:start] + block + md[end:].lstrip("\n") def replace_wecom_news_sections( md: str, *, ai_news: list[dict[str, Any]] | None = None, cn_ai_news: list[dict[str, Any]] | None = None, tech_ai_news: list[dict[str, Any]] | None = None, merged: bool = False, ) -> str: """用 Python 整理后的新闻列表替换 Agent/模板中的时讯区块。""" if merged and (ai_news or tech_ai_news): out = _remove_news_blocks(md, ("🌍", "🇨🇳")) out = _remove_news_blocks(out, ("📰",), label_must_contain="AI 时讯精选") out = _remove_news_blocks(out, ("🔧",), label_must_contain="技术类时讯") return _replace_merged_news_block(out, ai_news or [], tech_ai_news) out = md if cn_ai_news: out = _replace_news_block(out, "🇨🇳", cn_ai_news, "国内 AI 时讯") if ai_news: out = _replace_news_block(out, "🌍", ai_news, "国际 AI 时讯") return out WECOM_GITHUB_DESC_LIMIT = 40 def _github_repo_lines(repos: list[dict[str, Any]], *, show_created: bool = False) -> list[str]: lines: list[str] = [] for i, repo in enumerate(repos, 1): name = repo["repo"] url = repo["url"] lang = repo.get("language", "") stars_today = repo.get("stars_today_fmt", "") total = repo.get("total_stars_fmt", "") created = repo.get("created_at", "") meta_parts: list[str] = [] if lang: meta_parts.append(lang) if stars_today: meta_parts.append(f"+{stars_today} today") elif total: meta_parts.append(f"⭐{total}") if show_created and created: meta_parts.append(f"创建于 {created}") meta = f" · {' · '.join(meta_parts)}" if meta_parts else "" lines.append(f"{i}. [{name}]({url}){meta}") desc = repo.get("wecom_desc") or repo.get("desc_short") or repo.get("description", "") if desc: lines.append(f" {desc}") return lines def finalize_wecom_github_repos( items: list[dict[str, Any]], *, desc_limit: int | None = None, ) -> list[dict[str, Any]]: """为企微 GitHub 条目生成简短中文简介。""" if desc_limit is None: desc_limit = WECOM_GITHUB_DESC_LIMIT limit = desc_limit if desc_limit > 0 else 40 copies: list[tuple[str, dict[str, Any]]] = [] jobs: list[LocalizeJob] = [] for item in items: copy = dict(item) desc = (copy.get("description") or copy.get("desc_short") or "").strip() key = f"github:{copy.get('repo', '?')}" if needs_chinese(desc) or len(desc) > limit: jobs.append(LocalizeJob(key, desc, limit)) else: copy["wecom_desc"] = desc copies.append((key, copy)) zh_map = localize_brief_descriptions(jobs, archive=True) out: list[dict[str, Any]] = [] for key, copy in copies: if key in zh_map: copy["wecom_desc"] = zh_map[key] elif "wecom_desc" not in copy: fallback = (copy.get("description") or copy.get("desc_short") or "").strip() copy["wecom_desc"] = _brief_fallback_desc(fallback, limit) if fallback else "" out.append(copy) return out def _fallback_skill_desc(item: dict[str, Any]) -> str: if item.get("cluster"): count = int(item.get("cluster_count") or 1) source = item.get("source") or "unknown" sample = item.get("cluster_titles") or item.get("title") or "" return f"{count} agent skills from {source}, including {sample}" title = item.get("title") or "skill" source = item.get("source") or "unknown" return f"{title} skill from {source}" def _skill_group_key(item: dict[str, Any]) -> str: return str(item.get("id") or f"{item.get('source')}/{item.get('title')}") def finalize_wecom_skill_groups( items: list[dict[str, Any]], *, desc_limit: int | None = None, ) -> list[dict[str, Any]]: """为企微 Skills 榜生成一句简要中文简介(与完整版 .md 长描述分离)。""" if desc_limit is None: desc_limit = wecom_skill_desc_limit() limit = desc_limit if desc_limit > 0 else 48 copies: list[tuple[str, dict[str, Any]]] = [] jobs: list[LocalizeJob] = [] for item in items: copy = dict(item) desc = (copy.get("description") or "").strip() if not desc: desc = _fallback_skill_desc(copy) key = _skill_group_key(copy) job_key = f"wecom:{key}" if needs_chinese(desc) or len(desc) > limit: jobs.append(LocalizeJob(job_key, desc, limit)) else: copy["wecom_desc"] = desc copies.append((job_key, copy)) zh_map = localize_brief_descriptions(jobs, archive=True) out: list[dict[str, Any]] = [] for job_key, copy in copies: if job_key in zh_map: copy["wecom_desc"] = zh_map[job_key] elif "wecom_desc" not in copy: copy["wecom_desc"] = _brief_fallback_desc( (copy.get("description") or "").strip() or _fallback_skill_desc(copy), limit, ) out.append(copy) return out def _brief_fallback_desc(text: str, limit: int) -> str: return trim_brief(text, limit) def _grouped_skill_to_wecom_item( item: dict[str, Any], *, desc_limit: int | None = None, ) -> dict[str, Any]: if desc_limit is None: desc_limit = wecom_skill_desc_limit() limit = desc_limit if desc_limit > 0 else 48 installs = int(item.get("installs") or 0) installs_fmt = item.get("installs_fmt") or str(installs) desc = (item.get("wecom_desc") or item.get("description") or "").strip() if not desc and item.get("cluster"): desc = item.get("cluster_titles") or "" if not item.get("wecom_desc"): desc = _brief_fallback_desc(desc, limit) return { "id": str(item.get("id") or f"{item.get('source', '?')}/{item.get('title', '')}"), "title": item.get("title", ""), "source": item.get("source", "?"), "installs_fmt": installs_fmt, "link": item.get("link", ""), "desc_short": desc, "badge": item.get("badge", ""), "cluster": bool(item.get("cluster")), "cluster_count": item.get("cluster_count"), "cluster_titles": item.get("cluster_titles"), } def build_skills_board_section(icon_key: str, board_label: str, items: list[dict[str, Any]]) -> str: prepared = finalize_wecom_skill_groups(items) wecom_items = [_grouped_skill_to_wecom_item(x) for x in prepared] lines = [f"{ICONS[icon_key]} **{board_label} Top {len(wecom_items)}**"] for rank, item in enumerate(wecom_items, 1): lines.extend(_skill_line(rank, item)) return "\n".join(lines) def _move_to_wecom_skill_item(move: dict[str, Any]) -> dict[str, Any]: installs = int(move.get("installs") or 0) return { "title": move.get("title", "?"), "source": move.get("source", "?"), "installs_fmt": move.get("installs_fmt") or str(installs), "link": move.get("link", ""), "description": (move.get("description") or "").strip(), } def _move_to_skill_row(move: dict[str, Any]) -> dict[str, Any]: title = str(move.get("title") or "?") source = str(move.get("source") or "?") installs = int(move.get("installs") or 0) sid = str(move.get("id") or f"{source}/{title}") return { "id": sid, "title": title, "source": source, "installs": installs, "link": move.get("link", ""), "description": (move.get("description") or "").strip(), } def _flatten_skill_board_item(item: dict[str, Any]) -> list[dict[str, Any]]: if item.get("cluster"): source = str(item.get("source") or "?") installs = int(item.get("installs") or 0) titles = [str(t) for t in (item.get("cluster_skills") or []) if t] if not titles: titles = [str(item.get("title") or "?")] top_title = str(item.get("title") or titles[0]) rows: list[dict[str, Any]] = [] for title in titles: link = item.get("link", "") if title != top_title: link = f"https://www.skills.sh/{source}/{title}" rows.append( { "id": f"{source}/{title}", "title": title, "source": source, "installs": installs, "link": link, "description": (item.get("description") or "").strip(), } ) return rows title = str(item.get("title") or "?") source = str(item.get("source") or "?") return [ { "id": str(item.get("id") or f"{source}/{title}"), "title": title, "source": source, "installs": int(item.get("installs") or 0), "link": item.get("link", ""), "description": (item.get("description") or "").strip(), } ] def _skill_keys_in_board_item(item: dict[str, Any]) -> set[str]: if item.get("cluster"): source = str(item.get("source") or "?") titles = item.get("cluster_skills") or [item.get("title", "")] return {f"{source}/{t}" for t in titles if t} return {_skill_group_key(item)} def _prepare_grouped_wecom_skills( flat_rows: list[dict[str, Any]], *, limit: int, ) -> tuple[list[dict[str, Any]], set[str]]: if not flat_rows: return [], set() grouped = group_skills_by_source(flat_rows, limit=limit, pool_size=max(len(flat_rows), limit)) prepared = finalize_wecom_skill_groups(grouped) wecom_items = [_grouped_skill_to_wecom_item(x) for x in prepared] keys: set[str] = set() for item in grouped: keys.update(_skill_keys_in_board_item(item)) return wecom_items[:limit], keys def _normalize_skill_source_groups(items: list[dict[str, Any]]) -> list[dict[str, Any]]: """将条目规范为按 source 合并的榜单项(展示始终为合并态)。""" flat: list[dict[str, Any]] = [] for item in items: flat.extend(_flatten_skill_board_item(item)) if not flat: return [] return group_skills_by_source(flat, limit=len(flat), pool_size=len(flat)) def _skill_primary_id(item: dict[str, Any]) -> str: return str(item.get("id") or f"{item.get('source')}/{item.get('title')}" or "").strip() def _source_from_skill_key(key: str) -> str: from daily.skills_group import source_from_skill_key return source_from_skill_key(key) def expand_skill_recent_keys(keys: set[str] | None) -> set[str]: from daily.skills_group import expand_skill_recent_keys as _expand return _expand(keys) def _merge_skill_board_items( moves: list[dict[str, Any]], full_items: list[dict[str, Any]], limit: int, *, exclude_keys: set[str] | None = None, recent_keys: set[str] | None = None, ) -> tuple[list[dict[str, Any]], set[str]]: """异动优先,不足时用深池补满;按 source 合并态取条。 周去重按 source(含从 skill id 展开);同日避开其它榜时也按 source。 """ from daily.delta import skill_id as move_skill_id exclude = exclude_keys or set() recent = expand_skill_recent_keys(recent_keys) exclude_sources = {_source_from_skill_key(k) for k in exclude if k} exclude_sources.update(k for k in exclude if k) def _blocked(item: dict[str, Any]) -> bool: primary = _skill_primary_id(item) source = str(item.get("source") or "").strip() if primary and primary in recent: return True if source and source in recent: return True if primary and primary in exclude: return True if source and (source in exclude_sources or source in exclude): return True return False groups: list[dict[str, Any]] = [] seen_sources: set[str] = set() move_rows: list[dict[str, Any]] = [] seen_move_ids: set[str] = set() for move in moves: key = move_skill_id(move) if not key or key in seen_move_ids: continue move_source = str(move.get("source") or "").strip() if key in exclude or (move_source and move_source in exclude_sources): continue if key in recent or (move_source and move_source in recent): continue seen_move_ids.add(key) move_rows.append(_move_to_skill_row(move)) for group in _normalize_skill_source_groups(move_rows): source = str(group.get("source") or "?") if source in seen_sources or _blocked(group): continue seen_sources.add(source) groups.append(group) if len(groups) >= limit: break if len(groups) < limit: for group in _normalize_skill_source_groups(full_items): if len(groups) >= limit: break source = str(group.get("source") or "?") if source in seen_sources or _blocked(group): continue seen_sources.add(source) groups.append(group) if not groups: return [], set() prepared = finalize_wecom_skill_groups(groups[:limit]) wecom_items = [_grouped_skill_to_wecom_item(x) for x in prepared] # 供同日 Hot 排除:主键 + source keys: set[str] = set() for item in groups[:limit]: primary = _skill_primary_id(item) if primary: keys.add(primary) source = str(item.get("source") or "").strip() if source: keys.add(source) return wecom_items[:limit], keys def build_skills_delta_sections( trending_moves: list[dict[str, Any]], hot_moves: list[dict[str, Any]], *, trending_full: list[dict[str, Any]] | None = None, hot_full: list[dict[str, Any]] | None = None, trending_limit: int = 5, hot_limit: int = 5, pad: bool = False, recent_trending: set[str] | None = None, recent_hot: set[str] | None = None, ) -> str: sections: list[str] = [] trending_keys: set[str] = set() if pad: skill_recent = expand_skill_recent_keys( (recent_trending or set()) | (recent_hot or set()) ) t_items, trending_keys = _merge_skill_board_items( trending_moves, trending_full or [], trending_limit, recent_keys=skill_recent, ) if t_items: lines = [f"{ICONS['trending']} **Skills Trending Top {len(t_items)}**"] for rank, item in enumerate(t_items, 1): lines.extend(_skill_line(rank, item)) sections.append("\n".join(lines)) h_items, _ = _merge_skill_board_items( hot_moves, hot_full or [], hot_limit, exclude_keys=trending_keys, recent_keys=skill_recent, ) if h_items: lines = [f"{ICONS['hot']} **Skills Hot Top {len(h_items)}**"] for rank, item in enumerate(h_items, 1): lines.extend(_skill_line(rank, item)) sections.append("\n".join(lines)) return "\n\n".join(sections) if trending_moves: flat = [_move_to_skill_row(m) for m in trending_moves] items, _ = _prepare_grouped_wecom_skills(flat, limit=len(flat)) lines = [f"{ICONS['trending']} **Skills Trending 变化**"] for rank, item in enumerate(items, 1): lines.extend(_skill_line(rank, item)) sections.append("\n".join(lines)) if hot_moves: flat = [_move_to_skill_row(m) for m in hot_moves] items, _ = _prepare_grouped_wecom_skills(flat, limit=len(flat)) lines = [f"{ICONS['hot']} **Skills Hot 变化**"] for rank, item in enumerate(items, 1): lines.extend(_skill_line(rank, item)) sections.append("\n".join(lines)) return "\n\n".join(sections) def _github_move_to_repo(move: dict[str, Any]) -> dict[str, Any]: return { "repo": move.get("repo", "?"), "url": move.get("url", ""), "language": move.get("language", ""), "stars_today_fmt": move.get("stars_today_fmt", ""), "total_stars_fmt": move.get("total_stars_fmt", ""), "created_at": move.get("created_at", ""), "description": move.get("description", ""), "desc_short": (move.get("description") or "").strip(), } def _merge_github_board_items( moves: list[dict[str, Any]], full_repos: list[dict[str, Any]], limit: int, *, recent_repos: set[str] | None = None, ) -> list[dict[str, Any]]: recent = recent_repos or set() seen: set[str] = set() merged: list[dict[str, Any]] = [] for move in moves: repo = _github_move_to_repo(move) key = str(repo.get("repo") or "") if not key or key in seen or key in recent: continue seen.add(key) merged.append(repo) for repo in full_repos: if len(merged) >= limit: break key = str(repo.get("repo") or "") if not key or key in seen or key in recent: continue seen.add(key) merged.append(repo) return finalize_wecom_github_repos(merged)[:limit] def build_github_delta_sections( movement: dict[str, Any], *, topic_name: str, github_trending: list[dict[str, Any]] | None = None, github_emerging: list[dict[str, Any]] | None = None, github_topic: list[dict[str, Any]] | None = None, trending_limit: int = 5, emerging_limit: int = 5, topic_limit: int = 5, pad: bool = False, recent_board_keys: dict[str, set[str]] | None = None, ) -> str: sections: list[str] = [] recent = recent_board_keys or {} if pad: github_recent = ( (recent.get("github_trending") or set()) | (recent.get("github_emerging") or set()) | (recent.get("github_topic") or set()) ) mapping = [ ("github_trending_moves", "github_trending", github_trending or [], trending_limit, "github", "GitHub Trending", False), ("github_emerging_moves", "github_emerging", github_emerging or [], emerging_limit, "emerging", "GitHub 新兴", True), ("github_topic_moves", "github_topic", github_topic or [], topic_limit, "topic", f"Topic `{topic_name}`", False), ] for move_key, board_key, full_repos, limit, icon_key, label, show_created in mapping: repos = _merge_github_board_items( movement.get(move_key) or [], full_repos, limit, recent_repos=github_recent, ) github_recent |= {str(r.get("repo") or "") for r in repos if r.get("repo")} if not repos: continue lines = [f"{ICONS[icon_key]} **{label} Top {len(repos)}**"] lines.extend(_github_repo_lines(repos, show_created=show_created)) sections.append("\n".join(lines)) return "\n\n".join(sections) mapping = [ ("github_trending_moves", "github", "GitHub Trending 变化", False), ("github_emerging_moves", "emerging", "GitHub 新兴 变化", True), ("github_topic_moves", "topic", f"Topic `{topic_name}` 变化", False), ] for key, icon_key, label, show_created in mapping: moves = movement.get(key) or [] if not moves: continue lines = [f"{ICONS[icon_key]} **{label}**"] repos = finalize_wecom_github_repos([_github_move_to_repo(m) for m in moves]) lines.extend(_github_repo_lines(repos, show_created=show_created)) sections.append("\n".join(lines)) return "\n\n".join(sections) _SKILL_SECTIONS = re.compile( r"📈 \*\*Skills Trending.*?(?=🐙 \*\*GitHub Trending)", re.DOTALL, ) _SKILL_TRENDING_BLOCK = re.compile(r"📈 \*\*Skills Trending[^\n]*\n(?:.*?\n)*?(?=\n🔥 \*\*Skills Hot|\n🐙 |\n🌱 |\n🤖 |\Z)", re.DOTALL) _SKILL_HOT_BLOCK = re.compile(r"🔥 \*\*Skills Hot[^\n]*\n(?:.*?\n)*?(?=\n🐙 |\n🌱 |\n🤖 |\Z)", re.DOTALL) _GITHUB_SECTIONS = re.compile(r"🐙 \*\*GitHub Trending.*", re.DOTALL) def _strip_board_sections(md: str) -> str: md = _SKILL_TRENDING_BLOCK.sub("", md) md = _SKILL_HOT_BLOCK.sub("", md) if _GITHUB_SECTIONS.search(md): md = _GITHUB_SECTIONS.sub("", md) return re.sub(r"\n{3,}", "\n\n", md).rstrip() def resolve_wecom_board_items( *, mode: str, movement: dict[str, Any], trending: list[dict[str, Any]], hot: list[dict[str, Any]], topic_name: str, github_trending: list[dict[str, Any]] | None = None, github_emerging: list[dict[str, Any]] | None = None, github_topic: list[dict[str, Any]] | None = None, wecom_trending: int = 5, wecom_hot: int = 5, wecom_github: int = 5, wecom_emerging: int = 5, wecom_topic: int = 5, pad: bool = False, date_str: str | None = None, trending_pad: list[dict[str, Any]] | None = None, hot_pad: list[dict[str, Any]] | None = None, github_trending_pad: list[dict[str, Any]] | None = None, github_emerging_pad: list[dict[str, Any]] | None = None, github_topic_pad: list[dict[str, Any]] | None = None, ) -> dict[str, list[dict[str, Any]]]: """返回最终企微正文各榜 items(与 replace_wecom_board_sections 同源),供写回 shown。""" from daily.delta import load_recent_board_keys, partition_skill_moves_for_wecom if mode != "delta": return { "skills_trending": list(trending), "skills_hot": list(hot), "github_trending": list(github_trending or []), "github_emerging": list(github_emerging or []), "github_topic": list(github_topic or []), } recent_board_keys: dict[str, set[str]] = {} skill_recent: set[str] = set() github_recent: set[str] = set() if pad and date_str: recent_board_keys = load_recent_board_keys(date_str) # Trending / Hot 共用周去重:任一类出现过的 source 两边都不再展示 skill_recent = expand_skill_recent_keys( (recent_board_keys.get("skills_trending") or set()) | (recent_board_keys.get("skills_hot") or set()) ) # GitHub 三榜共用周去重:任一类出现过的 repo 各榜都不再展示 github_recent = ( (recent_board_keys.get("github_trending") or set()) | (recent_board_keys.get("github_emerging") or set()) | (recent_board_keys.get("github_topic") or set()) ) t_moves, h_moves = partition_skill_moves_for_wecom( movement.get("skills_trending_moves") or [], movement.get("skills_hot_moves") or [], ) if pad: t_items, trending_keys = _merge_skill_board_items( t_moves, trending_pad if trending_pad else trending, wecom_trending, recent_keys=skill_recent, ) h_items, _ = _merge_skill_board_items( h_moves, hot_pad if hot_pad else hot, wecom_hot, exclude_keys=trending_keys, recent_keys=skill_recent, ) gt_items = _merge_github_board_items( movement.get("github_trending_moves") or [], github_trending_pad if github_trending_pad else (github_trending or []), wecom_github, recent_repos=github_recent, ) github_recent |= {str(r.get("repo") or "") for r in gt_items if r.get("repo")} ge_items = _merge_github_board_items( movement.get("github_emerging_moves") or [], github_emerging_pad if github_emerging_pad else (github_emerging or []), wecom_emerging, recent_repos=github_recent, ) github_recent |= {str(r.get("repo") or "") for r in ge_items if r.get("repo")} gtopic_items = _merge_github_board_items( movement.get("github_topic_moves") or [], github_topic_pad if github_topic_pad else (github_topic or []), wecom_topic, recent_repos=github_recent, ) return { "skills_trending": t_items, "skills_hot": h_items, "github_trending": gt_items, "github_emerging": ge_items, "github_topic": gtopic_items, } t_flat = [_move_to_skill_row(m) for m in t_moves] h_flat = [_move_to_skill_row(m) for m in h_moves] t_items, _ = _prepare_grouped_wecom_skills(t_flat, limit=len(t_flat) or 1) if t_flat else ([], set()) h_items, _ = _prepare_grouped_wecom_skills(h_flat, limit=len(h_flat) or 1) if h_flat else ([], set()) return { "skills_trending": t_items, "skills_hot": h_items, "github_trending": finalize_wecom_github_repos( [_github_move_to_repo(m) for m in (movement.get("github_trending_moves") or [])] ), "github_emerging": finalize_wecom_github_repos( [_github_move_to_repo(m) for m in (movement.get("github_emerging_moves") or [])] ), "github_topic": finalize_wecom_github_repos( [_github_move_to_repo(m) for m in (movement.get("github_topic_moves") or [])] ), } def replace_wecom_board_sections( md: str, *, mode: str, movement: dict[str, Any], trending: list[dict[str, Any]], hot: list[dict[str, Any]], topic_name: str, github_trending: list[dict[str, Any]] | None = None, github_emerging: list[dict[str, Any]] | None = None, github_topic: list[dict[str, Any]] | None = None, wecom_trending: int = 5, wecom_hot: int = 5, wecom_github: int = 5, wecom_emerging: int = 5, wecom_topic: int = 5, pad: bool = False, date_str: str | None = None, trending_pad: list[dict[str, Any]] | None = None, hot_pad: list[dict[str, Any]] | None = None, github_trending_pad: list[dict[str, Any]] | None = None, github_emerging_pad: list[dict[str, Any]] | None = None, github_topic_pad: list[dict[str, Any]] | None = None, ) -> str: from daily.delta import load_recent_board_keys, partition_skill_moves_for_wecom recent_board_keys: dict[str, set[str]] = {} if pad and date_str: recent_board_keys = load_recent_board_keys(date_str) if mode == "delta": t_moves, h_moves = partition_skill_moves_for_wecom( movement.get("skills_trending_moves") or [], movement.get("skills_hot_moves") or [], ) skills_sec = build_skills_delta_sections( t_moves, h_moves, trending_full=trending_pad if pad and trending_pad else trending, hot_full=hot_pad if pad and hot_pad else hot, trending_limit=wecom_trending, hot_limit=wecom_hot, pad=pad, recent_trending=recent_board_keys.get("skills_trending"), recent_hot=recent_board_keys.get("skills_hot"), ) github_sec = build_github_delta_sections( movement, topic_name=topic_name, github_trending=github_trending_pad if pad and github_trending_pad else github_trending, github_emerging=github_emerging_pad if pad and github_emerging_pad else github_emerging, github_topic=github_topic_pad if pad and github_topic_pad else github_topic, trending_limit=wecom_github, emerging_limit=wecom_emerging, topic_limit=wecom_topic, pad=pad, recent_board_keys=recent_board_keys, ) board_block = "\n\n".join(x for x in [skills_sec, github_sec] if x) md = _strip_board_sections(md) if board_block: return md + "\n\n" + board_block + "\n" return md + "\n" trending_sec = build_skills_board_section("trending", "Skills Trending", trending) hot_sec = build_skills_board_section("hot", "Skills Hot", hot) replacement = f"{trending_sec}\n\n{hot_sec}\n\n" if _SKILL_SECTIONS.search(md): return _SKILL_SECTIONS.sub(replacement, md) github_marker = "🐙 **GitHub Trending" idx = md.find(github_marker) if idx >= 0: return md[:idx] + replacement + md[idx:] return md.rstrip() + "\n\n" + replacement def replace_wecom_skill_sections( md: str, *, trending: list[dict[str, Any]], hot: list[dict[str, Any]], mode: str = "full", movement: dict[str, Any] | None = None, topic_name: str = "llm", github_trending: list[dict[str, Any]] | None = None, github_emerging: list[dict[str, Any]] | None = None, github_topic: list[dict[str, Any]] | None = None, wecom_trending: int = 5, wecom_hot: int = 5, wecom_github: int = 5, wecom_emerging: int = 5, wecom_topic: int = 5, pad: bool = False, date_str: str | None = None, trending_pad: list[dict[str, Any]] | None = None, hot_pad: list[dict[str, Any]] | None = None, github_trending_pad: list[dict[str, Any]] | None = None, github_emerging_pad: list[dict[str, Any]] | None = None, github_topic_pad: list[dict[str, Any]] | None = None, ) -> str: """用 Python 合并后的 Skills 榜替换或插入 Agent 早报中的对应区块。""" return replace_wecom_board_sections( md, mode=mode, movement=movement or {}, trending=trending, hot=hot, topic_name=topic_name, github_trending=github_trending, github_emerging=github_emerging, github_topic=github_topic, wecom_trending=wecom_trending, wecom_hot=wecom_hot, wecom_github=wecom_github, wecom_emerging=wecom_emerging, wecom_topic=wecom_topic, pad=pad, date_str=date_str, trending_pad=trending_pad, hot_pad=hot_pad, github_trending_pad=github_trending_pad, github_emerging_pad=github_emerging_pad, github_topic_pad=github_topic_pad, ) def _format_pick_link(pick_command: str, *, title: str = "", url: str = "") -> str: cmd = pick_command.strip() if not cmd: return "" label = title.strip() if cmd.startswith("http://") or cmd.startswith("https://"): if not label: m = re.match(r"https?://github\.com/([^/\s#?]+/[^/\s#?]+)", cmd) label = m.group(1) if m else cmd return f"[{label}]({cmd})" m = re.match(r"npx skills add (\S+)", cmd) if m: skill_path = m.group(1) if not label: label = skill_path.split("/")[-1] href = url.strip() or f"https://skills.sh/{skill_path}" return f"[{label}]({href})" return f"`{cmd}`" def build_wecom_report( *, date_str: str, time_str: str, updated: str, highlights: list[str], theme_line: str, trending: list[dict[str, Any]], hot: list[dict[str, Any]], repos: list[dict[str, Any]], emerging: list[dict[str, Any]], topic_name: str, topic_repos: list[dict[str, Any]], ai_news: list[dict[str, Any]] | None = None, cn_ai_news: list[dict[str, Any]] | None = None, merged_ai_news: list[dict[str, Any]] | None = None, merged_tech_ai_news: list[dict[str, Any]] | None = None, pick_command: str, pick_why: str = "", pick_title: str = "", pick_url: str = "", include_boards: bool = True, ) -> str: lines = [ f"{ICONS['header']} **早报 · {date_str}**", f"> ⏱ {time_str} · 数据截至 {updated}", "", f"{ICONS['highlights']} **今日速览**", ] for point in highlights[:3]: lines.append(f"> {point}") lines.append("") lines.append(f"{ICONS['theme']} {theme_line}") lines.append("") if merged_ai_news or merged_tech_ai_news: block = _build_merged_news_block(merged_ai_news or [], merged_tech_ai_news) if block: lines.append(block.rstrip()) lines.append("") else: if ai_news: lines.append(f"{ICONS['ainews']} **国际 AI 时讯 Top {len(ai_news)}**") lines.extend(_ai_news_lines(ai_news)) lines.append("") if cn_ai_news: lines.append(f"{ICONS['cnainews']} **国内 AI 时讯 Top {len(cn_ai_news)}**") lines.extend(_ai_news_lines(cn_ai_news)) lines.append("") if include_boards: lines.append(f"{ICONS['trending']} **Skills Trending Top {len(trending)}**") for rank, item in enumerate(trending, 1): lines.extend(_skill_line(rank, item, badge=item.get("badge", ""))) lines.append("") lines.append(f"{ICONS['hot']} **Skills Hot Top {len(hot)}**") for rank, item in enumerate(hot, 1): lines.extend(_skill_line(rank, item, badge=item.get("badge", ""))) lines.append("") if repos: lines.append(f"{ICONS['github']} **GitHub Trending Top {len(repos)}**") lines.extend(_github_repo_lines(finalize_wecom_github_repos(repos))) lines.append("") if emerging: lines.append(f"{ICONS['emerging']} **新兴项目 Top {len(emerging)}**") lines.extend(_github_repo_lines(finalize_wecom_github_repos(emerging), show_created=True)) lines.append("") if topic_repos: lines.append(f"{ICONS['topic']} **Topic `{topic_name}` Top {len(topic_repos)}**") lines.extend(_github_repo_lines(finalize_wecom_github_repos(topic_repos))) lines.append("") lines.append(f"{ICONS['pick']} **今日首推**") lines.append(_format_pick_link(pick_command, title=pick_title, url=pick_url)) # 首推「为什么值得点开」一句理由; DAILY_FEATURED_REASON=0 关闭, 空则省略 if pick_why and env_bool("DAILY_FEATURED_REASON", True): lines.append(f"> {pick_why}") return "\n".join(lines)