Files
daily-robots/daily/format_wecom.py

999 lines
36 KiB
Python
Raw 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.

"""企微早报排版。"""
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.skills_group import group_skills_by_source, skill_id as board_skill_id
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 _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]]:
from daily.delta import skill_id as move_skill_id
exclude = exclude_keys or set()
recent = recent_keys or set()
seen: set[str] = set()
flat: list[dict[str, Any]] = []
for move in moves:
key = move_skill_id(move)
if not key or key in seen or key in exclude:
continue
seen.add(key)
flat.append(_move_to_skill_row(move))
pad_pool = max(limit * 5, len(flat), 50)
for item in full_items:
if len(flat) >= pad_pool:
break
for row in _flatten_skill_board_item(item):
key = board_skill_id(row)
if not key or key in seen or key in exclude or key in recent:
continue
seen.add(key)
flat.append(row)
return _prepare_grouped_wecom_skills(flat, limit=limit)
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 = 10,
hot_limit: int = 10,
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:
t_items, trending_keys = _merge_skill_board_items(
trending_moves,
trending_full or [],
trending_limit,
recent_keys=recent_trending,
)
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=recent_hot,
)
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:
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 = 10,
emerging_limit: int = 10,
topic_limit: int = 10,
pad: bool = False,
recent_board_keys: dict[str, set[str]] | None = None,
) -> str:
sections: list[str] = []
recent = recent_board_keys or {}
if pad:
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=recent.get(board_key),
)
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 = 10,
wecom_hot: int = 10,
wecom_github: int = 10,
wecom_emerging: int = 10,
wecom_topic: int = 10,
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]] = {}
if pad and date_str:
recent_board_keys = load_recent_board_keys(date_str)
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=recent_board_keys.get("skills_trending"),
)
h_items, _ = _merge_skill_board_items(
h_moves,
hot_pad if hot_pad else hot,
wecom_hot,
exclude_keys=trending_keys,
recent_keys=recent_board_keys.get("skills_hot"),
)
return {
"skills_trending": t_items,
"skills_hot": h_items,
"github_trending": _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=recent_board_keys.get("github_trending"),
),
"github_emerging": _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=recent_board_keys.get("github_emerging"),
),
"github_topic": _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=recent_board_keys.get("github_topic"),
),
}
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 = 10,
wecom_hot: int = 10,
wecom_github: int = 10,
wecom_emerging: int = 10,
wecom_topic: int = 10,
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 = 10,
wecom_hot: int = 10,
wecom_github: int = 10,
wecom_emerging: int = 10,
wecom_topic: int = 10,
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))
if pick_why:
lines.append(f"> {pick_why}")
return "\n".join(lines)