"""企微早报排版。""" from __future__ import annotations import re from typing import Any from daily.config import wecom_skill_desc_limit from daily.localize import LocalizeJob, localize_brief_descriptions, needs_chinese 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_lines(items: list[dict[str, Any]]) -> list[str]: lines: list[str] = [] for i, item in enumerate(items, 1): title = item.get("title", "?") link = item.get("link", "") source = item.get("source_name", "?") pub = item.get("published_fmt", "") desc = item.get("desc_short", "") pub_suffix = f" · {pub}" if pub else "" if link: head = f"{i}. [**{title}**]({link}) · `{source}`{pub_suffix}" else: head = f"{i}. **{title}** · `{source}`{pub_suffix}" lines.append(head) if desc: lines.append(f" > {desc}") return lines 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("desc_short") or repo.get("description", "") if desc: lines.append(f" > {desc}") return lines 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 { "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) rank = move.get("rank", "?") board_badge = (move.get("badge") or "").strip() badge = f"[新入 #{rank}]" if board_badge: badge = f"{badge} {board_badge}" 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(), "badge": badge, } def build_skills_delta_sections( trending_moves: list[dict[str, Any]], hot_moves: list[dict[str, Any]], ) -> str: sections: list[str] = [] if trending_moves: prepared = finalize_wecom_skill_groups([_move_to_wecom_skill_item(m) for m in trending_moves]) items = [_grouped_skill_to_wecom_item(x) for x in prepared] lines = [f"{ICONS['trending']} **Skills Trending 变化**"] for rank, item in enumerate(items, 1): lines.extend(_skill_line(rank, item, badge=item.get("badge", ""))) sections.append("\n".join(lines)) if hot_moves: prepared = finalize_wecom_skill_groups([_move_to_wecom_skill_item(m) for m in hot_moves]) items = [_grouped_skill_to_wecom_item(x) for x in prepared] lines = [f"{ICONS['hot']} **Skills Hot 变化**"] for rank, item in enumerate(items, 1): lines.extend(_skill_line(rank, item, badge=item.get("badge", ""))) 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(), "badge": f"[新入 #{move.get('rank', '?')}]", } def build_github_delta_sections(movement: dict[str, Any], *, topic_name: str) -> str: sections: list[str] = [] 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}**"] for i, move in enumerate(moves, 1): repo = _github_move_to_repo(move) badge = repo.pop("badge", "") chunk = _github_repo_lines([repo], show_created=show_created) if chunk: chunk[0] = f"{i}. {badge} " + chunk[0].split(". ", 1)[-1] lines.extend(chunk) 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 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, ) -> str: from daily.delta import partition_skill_moves_for_wecom 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) github_sec = build_github_delta_sections(movement, topic_name=topic_name) 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", ) -> str: """用 Python 合并后的 Skills 榜替换或插入 Agent 早报中的对应区块。""" return replace_wecom_board_sections( md, mode=mode, movement=movement or {}, trending=trending, hot=hot, topic_name=topic_name, ) 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, 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 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(repos)) lines.append("") if emerging: lines.append(f"{ICONS['emerging']} **新兴项目 Top {len(emerging)}**") lines.extend(_github_repo_lines(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(topic_repos)) lines.append("") lines.append(f"{ICONS['pick']} **今日首推**") lines.append(_format_pick_link(pick_command, title=pick_title, url=pick_url)) if pick_why: lines.append(f"> {pick_why}") return "\n".join(lines)