Files
daily-robots/daily/format_wecom.py
yumao 3321a307a0 feat: 首推理由行加 DAILY_FEATURED_REASON 开关
pick_why 本已渲染,现加开关默认开,空则省略(T6)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 16:53:19 +08:00

1104 lines
40 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 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)