feat: generate 以 board_select 为唯一列表主人并写回 shown keys

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-14 10:36:53 +08:00
parent 0c324f9ace
commit d448002e7a
4 changed files with 1001 additions and 106 deletions

View File

@@ -7,6 +7,7 @@ from typing import Any
from daily.config import wecom_skill_desc_limit from daily.config import wecom_skill_desc_limit
from daily.localize import LocalizeJob, localize_brief_descriptions, needs_chinese 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 from daily.text_utils import trim_brief
ICONS = { ICONS = {
@@ -55,25 +56,177 @@ def _skill_line(rank: int, item: dict[str, Any], *, badge: str = "") -> list[str
return lines return lines
def _ai_news_lines(items: list[dict[str, Any]]) -> list[str]: 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] = [] lines: list[str] = []
for i, item in enumerate(items, 1): for i, item in enumerate(items, 1):
title = item.get("title", "?") label = _ai_news_link_label(item, merged=merged)
link = item.get("link", "") link = item.get("link", "")
source = item.get("source_name", "?") source = item.get("source_name", "?")
pub = item.get("published_fmt", "") pub = item.get("published_fmt", "")
desc = item.get("desc_short", "") desc = (item.get("desc_short") or "").strip()
pub_suffix = f" · {pub}" if pub else "" pub_suffix = "" if merged else (f" · {pub}" if pub else "")
if link: if link:
head = f"{i}. [**{title}**]({link}) · `{source}`{pub_suffix}" if merged and desc:
head = f"{i}. [{label}]({link}) — {desc}{pub_suffix}"
elif merged:
head = f"{i}. [{label}]({link}){pub_suffix}"
else: else:
head = f"{i}. **{title}** · `{source}`{pub_suffix}" 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) lines.append(head)
if desc:
lines.append(f" > {desc}")
return lines 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]: def _github_repo_lines(repos: list[dict[str, Any]], *, show_created: bool = False) -> list[str]:
lines: list[str] = [] lines: list[str] = []
for i, repo in enumerate(repos, 1): for i, repo in enumerate(repos, 1):
@@ -94,12 +247,45 @@ def _github_repo_lines(repos: list[dict[str, Any]], *, show_created: bool = Fals
meta_parts.append(f"创建于 {created}") meta_parts.append(f"创建于 {created}")
meta = f" · {' · '.join(meta_parts)}" if meta_parts else "" meta = f" · {' · '.join(meta_parts)}" if meta_parts else ""
lines.append(f"{i}. [{name}]({url}){meta}") lines.append(f"{i}. [{name}]({url}){meta}")
desc = repo.get("desc_short") or repo.get("description", "") desc = repo.get("wecom_desc") or repo.get("desc_short") or repo.get("description", "")
if desc: if desc:
lines.append(f" > {desc}") lines.append(f" {desc}")
return lines 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: def _fallback_skill_desc(item: dict[str, Any]) -> str:
if item.get("cluster"): if item.get("cluster"):
count = int(item.get("cluster_count") or 1) count = int(item.get("cluster_count") or 1)
@@ -173,6 +359,7 @@ def _grouped_skill_to_wecom_item(
if not item.get("wecom_desc"): if not item.get("wecom_desc"):
desc = _brief_fallback_desc(desc, limit) desc = _brief_fallback_desc(desc, limit)
return { return {
"id": str(item.get("id") or f"{item.get('source', '?')}/{item.get('title', '')}"),
"title": item.get("title", ""), "title": item.get("title", ""),
"source": item.get("source", "?"), "source": item.get("source", "?"),
"installs_fmt": installs_fmt, "installs_fmt": installs_fmt,
@@ -196,39 +383,178 @@ def build_skills_board_section(icon_key: str, board_label: str, items: list[dict
def _move_to_wecom_skill_item(move: dict[str, Any]) -> dict[str, Any]: def _move_to_wecom_skill_item(move: dict[str, Any]) -> dict[str, Any]:
installs = int(move.get("installs") or 0) 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 { return {
"title": move.get("title", "?"), "title": move.get("title", "?"),
"source": move.get("source", "?"), "source": move.get("source", "?"),
"installs_fmt": move.get("installs_fmt") or str(installs), "installs_fmt": move.get("installs_fmt") or str(installs),
"link": move.get("link", ""), "link": move.get("link", ""),
"description": (move.get("description") or "").strip(), "description": (move.get("description") or "").strip(),
"badge": badge,
} }
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( def build_skills_delta_sections(
trending_moves: list[dict[str, Any]], trending_moves: list[dict[str, Any]],
hot_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: ) -> str:
sections: list[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: if trending_moves:
prepared = finalize_wecom_skill_groups([_move_to_wecom_skill_item(m) for m in trending_moves]) flat = [_move_to_skill_row(m) for m in trending_moves]
items = [_grouped_skill_to_wecom_item(x) for x in prepared] items, _ = _prepare_grouped_wecom_skills(flat, limit=len(flat))
lines = [f"{ICONS['trending']} **Skills Trending 变化**"] lines = [f"{ICONS['trending']} **Skills Trending 变化**"]
for rank, item in enumerate(items, 1): for rank, item in enumerate(items, 1):
lines.extend(_skill_line(rank, item, badge=item.get("badge", ""))) lines.extend(_skill_line(rank, item))
sections.append("\n".join(lines)) sections.append("\n".join(lines))
if hot_moves: if hot_moves:
prepared = finalize_wecom_skill_groups([_move_to_wecom_skill_item(m) for m in hot_moves]) flat = [_move_to_skill_row(m) for m in hot_moves]
items = [_grouped_skill_to_wecom_item(x) for x in prepared] items, _ = _prepare_grouped_wecom_skills(flat, limit=len(flat))
lines = [f"{ICONS['hot']} **Skills Hot 变化**"] lines = [f"{ICONS['hot']} **Skills Hot 变化**"]
for rank, item in enumerate(items, 1): for rank, item in enumerate(items, 1):
lines.extend(_skill_line(rank, item, badge=item.get("badge", ""))) lines.extend(_skill_line(rank, item))
sections.append("\n".join(lines)) sections.append("\n".join(lines))
return "\n\n".join(sections) return "\n\n".join(sections)
@@ -243,12 +569,72 @@ def _github_move_to_repo(move: dict[str, Any]) -> dict[str, Any]:
"created_at": move.get("created_at", ""), "created_at": move.get("created_at", ""),
"description": move.get("description", ""), "description": move.get("description", ""),
"desc_short": (move.get("description") or "").strip(), "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: 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] = [] 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 = [ mapping = [
("github_trending_moves", "github", "GitHub Trending 变化", False), ("github_trending_moves", "github", "GitHub Trending 变化", False),
("github_emerging_moves", "emerging", "GitHub 新兴 变化", True), ("github_emerging_moves", "emerging", "GitHub 新兴 变化", True),
@@ -259,13 +645,8 @@ def build_github_delta_sections(movement: dict[str, Any], *, topic_name: str) ->
if not moves: if not moves:
continue continue
lines = [f"{ICONS[icon_key]} **{label}**"] lines = [f"{ICONS[icon_key]} **{label}**"]
for i, move in enumerate(moves, 1): repos = finalize_wecom_github_repos([_github_move_to_repo(m) for m in moves])
repo = _github_move_to_repo(move) lines.extend(_github_repo_lines(repos, show_created=show_created))
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)) sections.append("\n".join(lines))
return "\n\n".join(sections) return "\n\n".join(sections)
@@ -287,6 +668,105 @@ def _strip_board_sections(md: str) -> str:
return re.sub(r"\n{3,}", "\n\n", md).rstrip() 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( def replace_wecom_board_sections(
md: str, md: str,
*, *,
@@ -295,16 +775,56 @@ def replace_wecom_board_sections(
trending: list[dict[str, Any]], trending: list[dict[str, Any]],
hot: list[dict[str, Any]], hot: list[dict[str, Any]],
topic_name: str, 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: ) -> str:
from daily.delta import partition_skill_moves_for_wecom 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": if mode == "delta":
t_moves, h_moves = partition_skill_moves_for_wecom( t_moves, h_moves = partition_skill_moves_for_wecom(
movement.get("skills_trending_moves") or [], movement.get("skills_trending_moves") or [],
movement.get("skills_hot_moves") or [], movement.get("skills_hot_moves") or [],
) )
skills_sec = build_skills_delta_sections(t_moves, h_moves) skills_sec = build_skills_delta_sections(
github_sec = build_github_delta_sections(movement, topic_name=topic_name) 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) board_block = "\n\n".join(x for x in [skills_sec, github_sec] if x)
md = _strip_board_sections(md) md = _strip_board_sections(md)
if board_block: if board_block:
@@ -331,6 +851,21 @@ def replace_wecom_skill_sections(
mode: str = "full", mode: str = "full",
movement: dict[str, Any] | None = None, movement: dict[str, Any] | None = None,
topic_name: str = "llm", 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: ) -> str:
"""用 Python 合并后的 Skills 榜替换或插入 Agent 早报中的对应区块。""" """用 Python 合并后的 Skills 榜替换或插入 Agent 早报中的对应区块。"""
return replace_wecom_board_sections( return replace_wecom_board_sections(
@@ -340,6 +875,21 @@ def replace_wecom_skill_sections(
trending=trending, trending=trending,
hot=hot, hot=hot,
topic_name=topic_name, 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,
) )
@@ -378,6 +928,8 @@ def build_wecom_report(
topic_repos: list[dict[str, Any]], topic_repos: list[dict[str, Any]],
ai_news: list[dict[str, Any]] | None = None, ai_news: list[dict[str, Any]] | None = None,
cn_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_command: str,
pick_why: str = "", pick_why: str = "",
pick_title: str = "", pick_title: str = "",
@@ -396,6 +948,12 @@ def build_wecom_report(
lines.append(f"{ICONS['theme']} {theme_line}") lines.append(f"{ICONS['theme']} {theme_line}")
lines.append("") 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: if ai_news:
lines.append(f"{ICONS['ainews']} **国际 AI 时讯 Top {len(ai_news)}**") lines.append(f"{ICONS['ainews']} **国际 AI 时讯 Top {len(ai_news)}**")
lines.extend(_ai_news_lines(ai_news)) lines.extend(_ai_news_lines(ai_news))
@@ -419,17 +977,17 @@ def build_wecom_report(
if repos: if repos:
lines.append(f"{ICONS['github']} **GitHub Trending Top {len(repos)}**") lines.append(f"{ICONS['github']} **GitHub Trending Top {len(repos)}**")
lines.extend(_github_repo_lines(repos)) lines.extend(_github_repo_lines(finalize_wecom_github_repos(repos)))
lines.append("") lines.append("")
if emerging: if emerging:
lines.append(f"{ICONS['emerging']} **新兴项目 Top {len(emerging)}**") lines.append(f"{ICONS['emerging']} **新兴项目 Top {len(emerging)}**")
lines.extend(_github_repo_lines(emerging, show_created=True)) lines.extend(_github_repo_lines(finalize_wecom_github_repos(emerging), show_created=True))
lines.append("") lines.append("")
if topic_repos: if topic_repos:
lines.append(f"{ICONS['topic']} **Topic `{topic_name}` Top {len(topic_repos)}**") lines.append(f"{ICONS['topic']} **Topic `{topic_name}` Top {len(topic_repos)}**")
lines.extend(_github_repo_lines(topic_repos)) lines.extend(_github_repo_lines(finalize_wecom_github_repos(topic_repos)))
lines.append("") lines.append("")
lines.append(f"{ICONS['pick']} **今日首推**") lines.append(f"{ICONS['pick']} **今日首推**")

View File

@@ -22,17 +22,30 @@ from daily.config import (
LOG_DIR, LOG_DIR,
OUTPUT_DIR, OUTPUT_DIR,
SNAPSHOT_FILE, SNAPSHOT_FILE,
board_pool_size,
env, env,
env_int, env_int,
full_desc_limit, full_desc_limit,
news_summary_limit, news_summary_limit,
wecom_delta_pad,
wecom_news_desc_limit,
wecom_pad_pool_size,
wecom_skill_desc_limit, wecom_skill_desc_limit,
) )
from daily.board_history import (
BOARD_KEYS,
extract_shown_keys,
load_recent_shown_keys,
merge_wecom_shown_into_data,
)
from daily.board_select import board_select
from daily.format_wecom import ( from daily.format_wecom import (
build_wecom_report, build_wecom_report,
finalize_wecom_skill_groups, finalize_wecom_skill_groups,
replace_wecom_board_sections, replace_wecom_board_sections,
replace_wecom_news_sections,
replace_wecom_skill_sections, replace_wecom_skill_sections,
resolve_wecom_board_items,
) )
from daily.agent_workflow import is_agent_mode, run_agent_workflow from daily.agent_workflow import is_agent_mode, run_agent_workflow
from daily.featured_pick import ( from daily.featured_pick import (
@@ -56,10 +69,17 @@ from daily.news.fetch import (
fetch_cn_ai_news, fetch_cn_ai_news,
format_cn_news_section, format_cn_news_section,
format_news_section, format_news_section,
finalize_wecom_news_items,
prepare_wecom_cn_news_items, prepare_wecom_cn_news_items,
prepare_wecom_news_items, prepare_wecom_news_items,
sync_wecom_news_rows,
) )
from daily.news.pushed_links import record_pushed_links from daily.news.pushed_links import record_pushed_links
from daily.news.research import (
fetch_ai_news_research,
format_research_news_section,
is_research_mode,
)
from daily.push_gate import evaluate_push_gate from daily.push_gate import evaluate_push_gate
from daily.report_data import ( from daily.report_data import (
build_full_payload, build_full_payload,
@@ -68,7 +88,6 @@ from daily.report_data import (
save_json, save_json,
) )
from daily.skills_board import format_installs, load_boards, load_feed from daily.skills_board import format_installs, load_boards, load_feed
from daily.skills_group import group_skills_by_source
THEME_RULES: list[tuple[str, str, list[str]]] = [ THEME_RULES: list[tuple[str, str, list[str]]] = [
("🎬", "AI 多媒体 / 视频", ["runcomfy", "remotion", "video", "seedance", "inpaint", "lipsync"]), ("🎬", "AI 多媒体 / 视频", ["runcomfy", "remotion", "video", "seedance", "inpaint", "lipsync"]),
@@ -140,7 +159,30 @@ def _localize_descriptions_in_place(
seen_news.add(link) seen_news.add(link)
summary = (item.get("summary") or "").strip() summary = (item.get("summary") or "").strip()
if summary: if summary:
jobs.append(LocalizeJob(f"news:{link}", summary, news_limit)) jobs.append(
LocalizeJob(
f"news:{link}",
summary,
news_limit if news_limit > 0 else wecom_news_desc_limit(),
)
)
if cn_ai_news and cn_ai_news.get("enabled"):
seen_cn: set[str] = set()
for item in cn_ai_news.get("flat") or []:
link = item.get("link", "")
if not link or link in seen_cn:
continue
seen_cn.add(link)
summary = (item.get("summary") or "").strip()
if summary and needs_chinese(summary):
jobs.append(
LocalizeJob(
f"news:{link}",
summary,
news_limit if news_limit > 0 else wecom_news_desc_limit(),
)
)
zh_map = localize_descriptions(jobs, archive=True) zh_map = localize_descriptions(jobs, archive=True)
if not zh_map and not jobs: if not zh_map and not jobs:
@@ -166,6 +208,11 @@ def _localize_descriptions_in_place(
key = f"news:{item.get('link', '')}" key = f"news:{item.get('link', '')}"
if key in mapping: if key in mapping:
item["summary"] = mapping[key] item["summary"] = mapping[key]
if cn_ai_news and cn_ai_news.get("enabled"):
for item in cn_ai_news.get("flat") or []:
key = f"news:{item.get('link', '')}"
if key in mapping:
item["summary"] = mapping[key]
_apply_zh(zh_map) _apply_zh(zh_map)
@@ -199,7 +246,29 @@ def _localize_descriptions_in_place(
seen_news.add(link) seen_news.add(link)
summary = (item.get("summary") or "").strip() summary = (item.get("summary") or "").strip()
if needs_chinese(summary): if needs_chinese(summary):
retry_jobs.append(LocalizeJob(f"news:{link}", summary, news_limit)) retry_jobs.append(
LocalizeJob(
f"news:{link}",
summary,
news_limit if news_limit > 0 else wecom_news_desc_limit(),
)
)
if cn_ai_news and cn_ai_news.get("enabled"):
seen_cn: set[str] = set()
for item in cn_ai_news.get("flat") or []:
link = item.get("link", "")
if not link or link in seen_cn:
continue
seen_cn.add(link)
summary = (item.get("summary") or "").strip()
if needs_chinese(summary):
retry_jobs.append(
LocalizeJob(
f"news:{link}",
summary,
news_limit if news_limit > 0 else wecom_news_desc_limit(),
)
)
if retry_jobs: if retry_jobs:
_apply_zh(localize_descriptions(retry_jobs, archive=True)) _apply_zh(localize_descriptions(retry_jobs, archive=True))
@@ -334,6 +403,27 @@ def _prepare_github_item(item: dict[str, Any]) -> dict[str, Any]:
return {**item, "desc_short": _wecom_desc(item.get("description", ""), 40)} return {**item, "desc_short": _wecom_desc(item.get("description", ""), 40)}
def _sync_movement_github_descriptions(
movement: dict[str, Any],
*,
github_trending: list[dict[str, Any]],
github_emerging: list[dict[str, Any]],
github_topic: list[dict[str, Any]],
) -> None:
"""将已中文化的 GitHub 描述同步到 movement 新入榜条目(供 Delta 企微列表使用)。"""
by_repo: dict[str, str] = {}
for item in github_trending + github_emerging + github_topic:
repo = str(item.get("repo") or "")
desc = (item.get("description") or "").strip()
if repo and desc:
by_repo[repo] = desc
for key in ("github_trending_moves", "github_emerging_moves", "github_topic_moves"):
for move in movement.get(key) or []:
repo = str(move.get("repo") or "")
if repo in by_repo:
move["description"] = by_repo[repo]
def _fetch_latest_release_title(repo: str) -> str | None: def _fetch_latest_release_title(repo: str) -> str | None:
atom_url = f"https://github.com/{repo}/releases.atom" atom_url = f"https://github.com/{repo}/releases.atom"
try: try:
@@ -425,15 +515,17 @@ def generate_report() -> tuple[str, str, Path, Path]:
skill_pool = max(10, env_int("DAILY_WECOM_SKILL_POOL", 200)) skill_pool = max(10, env_int("DAILY_WECOM_SKILL_POOL", 200))
wecom_trending = env_int("DAILY_WECOM_TRENDING", 10) wecom_trending = env_int("DAILY_WECOM_TRENDING", 10)
wecom_hot = env_int("DAILY_WECOM_HOT", 10) wecom_hot = env_int("DAILY_WECOM_HOT", 10)
pad_pool = wecom_pad_pool_size(max(wecom_trending, wecom_hot, 10))
skill_pool = max(skill_pool, pad_pool)
github_limit = env_int("DAILY_GITHUB_TRENDING_LIMIT", 10) github_limit = env_int("DAILY_GITHUB_TRENDING_LIMIT", 10)
wecom_github = env_int("DAILY_WECOM_GITHUB_TRENDING", env_int("DAILY_WECOM_REPOS", 10)) wecom_github = env_int("DAILY_WECOM_GITHUB_TRENDING", env_int("DAILY_WECOM_REPOS", 10))
github_fetch_n = max(github_limit, compare_n, wecom_github) github_fetch_n = max(github_limit, compare_n, wecom_github, pad_pool)
emerging_limit = env_int("DAILY_GITHUB_EMERGING_LIMIT", 10) emerging_limit = env_int("DAILY_GITHUB_EMERGING_LIMIT", 10)
wecom_emerging = env_int("DAILY_WECOM_GITHUB_EMERGING", 10) wecom_emerging = env_int("DAILY_WECOM_GITHUB_EMERGING", 10)
emerging_fetch_n = max(emerging_limit, compare_n, wecom_emerging) emerging_fetch_n = max(emerging_limit, compare_n, wecom_emerging, pad_pool)
topic_limit = env_int("DAILY_GITHUB_TOPIC_LIMIT", 10) topic_limit = env_int("DAILY_GITHUB_TOPIC_LIMIT", 10)
wecom_topic = env_int("DAILY_WECOM_GITHUB_TOPIC", 10) wecom_topic = env_int("DAILY_WECOM_GITHUB_TOPIC", 10)
topic_fetch_n = max(topic_limit, compare_n, wecom_topic) topic_fetch_n = max(topic_limit, compare_n, wecom_topic, pad_pool)
feed = load_feed(force=True) feed = load_feed(force=True)
prev_ids = _load_snapshot() prev_ids = _load_snapshot()
@@ -448,6 +540,25 @@ def generate_report() -> tuple[str, str, Path, Path]:
github_emerging = fetch_emerging_repos(emerging_fetch_n, exclude=seen_repos) github_emerging = fetch_emerging_repos(emerging_fetch_n, exclude=seen_repos)
seen_repos.update(r["repo"] for r in github_emerging) seen_repos.update(r["repo"] for r in github_emerging)
topic_name, github_topic = fetch_topic_hot_repos(topic_fetch_n, exclude=seen_repos) topic_name, github_topic = fetch_topic_hot_repos(topic_fetch_n, exclude=seen_repos)
news_merged = is_research_mode()
ai_news_research: dict[str, Any] | None = None
wecom_news: list[dict[str, Any]] = []
wecom_tech_news: list[dict[str, Any]] = []
if news_merged:
ai_news_research = fetch_ai_news_research(date_str=date_str)
wecom_news = list(ai_news_research.get("items") or [])
wecom_tech_news = list(ai_news_research.get("tech_items") or [])
ai_news = {
"enabled": ai_news_research.get("enabled", False),
"mode": "research",
"hours": ai_news_research.get("hours", 24),
"flat": ai_news_research.get("flat") or [],
"categories": [],
"stats": ai_news_research.get("stats") or {},
}
cn_ai_news = {"enabled": False, "categories": [], "flat": [], "stats": {}}
else:
ai_news = fetch_ai_news() ai_news = fetch_ai_news()
cn_ai_news = fetch_cn_ai_news() cn_ai_news = fetch_cn_ai_news()
@@ -460,7 +571,56 @@ def generate_report() -> tuple[str, str, Path, Path]:
"emerging": wecom_emerging, "emerging": wecom_emerging,
"topic": wecom_topic, "topic": wecom_topic,
"ai_news": env_int("DAILY_WECOM_AI_NEWS", 10), "ai_news": env_int("DAILY_WECOM_AI_NEWS", 10),
"cn_ai_news": env_int("DAILY_WECOM_CN_AI_NEWS", 8), "cn_ai_news": env_int("DAILY_WECOM_CN_AI_NEWS", 10),
}
pool = max(board_pool_size(), skill_pool, pad_pool)
recent_shown = load_recent_shown_keys(date_str)
selected_trending = board_select(
board="skills_trending",
items=trending,
recent_keys=recent_shown["skills_trending"],
limit=wecom_trending,
pool_size=pool,
kind="skill",
)
selected_hot = board_select(
board="skills_hot",
items=hot,
recent_keys=recent_shown["skills_hot"],
limit=wecom_hot,
pool_size=pool,
kind="skill",
)
selected_github = board_select(
board="github_trending",
items=github_trending,
recent_keys=recent_shown["github_trending"],
limit=wecom_github,
pool_size=pool,
kind="github",
)
selected_emerging = board_select(
board="github_emerging",
items=github_emerging,
recent_keys=recent_shown["github_emerging"],
limit=wecom_emerging,
pool_size=pool,
kind="github",
)
selected_topic = board_select(
board="github_topic",
items=github_topic,
recent_keys=recent_shown["github_topic"],
limit=wecom_topic,
pool_size=pool,
kind="github",
)
boards_for_wecom = {
"skills_trending": selected_trending,
"skills_hot": selected_hot,
"github_trending": selected_github,
"github_emerging": selected_emerging,
"github_topic": selected_topic,
} }
llm_input = build_llm_input( llm_input = build_llm_input(
date_str=date_str, date_str=date_str,
@@ -474,15 +634,22 @@ def generate_report() -> tuple[str, str, Path, Path]:
ai_news=ai_news, ai_news=ai_news,
cn_ai_news=cn_ai_news, cn_ai_news=cn_ai_news,
wecom_limits=wecom_limits, wecom_limits=wecom_limits,
research_items=wecom_news if news_merged else None,
research_tech_items=wecom_tech_news if news_merged else None,
boards_for_wecom=boards_for_wecom,
) )
featured = apply_featured_pick(llm_input, date_str=date_str) featured = apply_featured_pick(llm_input, date_str=date_str)
movement = llm_input["movement"] movement = llm_input["movement"]
eff_mode = llm_input["effective_wecom_mode"] eff_mode = llm_input["effective_wecom_mode"]
if news_merged:
wecom_ai: list[dict[str, Any]] = []
wecom_cn: list[dict[str, Any]] = []
else:
wecom_ai = prepare_wecom_news_items(ai_news, date_str=date_str) wecom_ai = prepare_wecom_news_items(ai_news, date_str=date_str)
wecom_cn = prepare_wecom_cn_news_items(cn_ai_news, date_str=date_str) wecom_cn = prepare_wecom_cn_news_items(cn_ai_news, date_str=date_str)
push_gate = evaluate_push_gate( push_gate = evaluate_push_gate(
movement=movement, movement=movement,
ai_news_items=wecom_ai, ai_news_items=wecom_news if news_merged else wecom_ai,
cn_ai_news_items=wecom_cn, cn_ai_news_items=wecom_cn,
featured_pick=featured, featured_pick=featured,
) )
@@ -526,6 +693,17 @@ def generate_report() -> tuple[str, str, Path, Path]:
_localize_descriptions_in_place( _localize_descriptions_in_place(
trending, hot, github_trending, github_emerging, github_topic, ai_news, cn_ai_news trending, hot, github_trending, github_emerging, github_topic, ai_news, cn_ai_news
) )
if not news_merged:
sync_wecom_news_rows(wecom_ai, ai_news.get("flat") or [])
sync_wecom_news_rows(wecom_cn, cn_ai_news.get("flat") or [])
finalize_wecom_news_items(wecom_ai, force_chinese=True)
finalize_wecom_news_items(wecom_cn, force_chinese=False)
_sync_movement_github_descriptions(
movement,
github_trending=github_trending,
github_emerging=github_emerging,
github_topic=github_topic,
)
themes = _theme_clusters(feed) themes = _theme_clusters(feed)
@@ -534,7 +712,8 @@ def generate_report() -> tuple[str, str, Path, Path]:
"", "",
f"> 生成时间:{now.strftime('%Y-%m-%d %H:%M')} (UTC+8) ", f"> 生成时间:{now.strftime('%Y-%m-%d %H:%M')} (UTC+8) ",
f"> skills 数据更新:{updated} ", f"> skills 数据更新:{updated} ",
"> 数据来源:[skills.sh/trending](https://skills.sh/trending) · [skills.sh/hot](https://skills.sh/hot) · 国际/国内 AI RSS", "> 数据来源:[skills.sh/trending](https://skills.sh/trending) · [skills.sh/hot](https://skills.sh/hot)"
+ (" · AI 时讯 Deep Research" if news_merged else " · 国际/国内 AI RSS"),
"", "",
"---", "---",
"", "",
@@ -576,6 +755,10 @@ def generate_report() -> tuple[str, str, Path, Path]:
lines.append("") lines.append("")
section_no = 6 section_no = 6
if news_merged and ai_news_research is not None:
lines.extend(format_research_news_section(ai_news_research, section_no=section_no))
section_no += 1
else:
lines.extend(format_news_section(ai_news, section_no=section_no)) lines.extend(format_news_section(ai_news, section_no=section_no))
section_no += 1 section_no += 1
lines.extend(format_cn_news_section(cn_ai_news, section_no=section_no)) lines.extend(format_cn_news_section(cn_ai_news, section_no=section_no))
@@ -616,17 +799,85 @@ def generate_report() -> tuple[str, str, Path, Path]:
lines.extend(["```", "", f"*企微短版见 `output/{date_str}.wecom.md`*"]) lines.extend(["```", "", f"*企微短版见 `output/{date_str}.wecom.md`*"])
markdown = "\n".join(lines) markdown = "\n".join(lines)
gt = group_skills_by_source(trending, limit=wecom_trending, pool_size=skill_pool) gt = selected_trending
gh = group_skills_by_source(hot, limit=wecom_hot, pool_size=skill_pool) gh = selected_hot
if agent_wecom: gt_pad = board_select(
wecom_md = replace_wecom_skill_sections( board="skills_trending",
agent_wecom, items=trending,
trending=gt, recent_keys=recent_shown["skills_trending"],
hot=gh, limit=pad_pool,
mode=eff_mode, pool_size=pool,
movement=movement, kind="skill",
topic_name=topic_name,
) )
gh_pad = board_select(
board="skills_hot",
items=hot,
recent_keys=recent_shown["skills_hot"],
limit=pad_pool,
pool_size=pool,
kind="skill",
)
wecom_github_items = [_prepare_github_item(item) for item in selected_github]
wecom_emerging_items = [_prepare_github_item(item) for item in selected_emerging]
wecom_topic_items = [_prepare_github_item(item) for item in selected_topic]
wecom_github_pad = [
_prepare_github_item(item)
for item in board_select(
board="github_trending",
items=github_trending,
recent_keys=recent_shown["github_trending"],
limit=pad_pool,
pool_size=pool,
kind="github",
)
]
wecom_emerging_pad = [
_prepare_github_item(item)
for item in board_select(
board="github_emerging",
items=github_emerging,
recent_keys=recent_shown["github_emerging"],
limit=pad_pool,
pool_size=pool,
kind="github",
)
]
wecom_topic_pad = [
_prepare_github_item(item)
for item in board_select(
board="github_topic",
items=github_topic,
recent_keys=recent_shown["github_topic"],
limit=pad_pool,
pool_size=pool,
kind="github",
)
]
delta_pad = eff_mode == "delta" and wecom_delta_pad()
board_kwargs = {
"mode": eff_mode,
"movement": movement,
"trending": gt,
"hot": gh,
"topic_name": topic_name,
"github_trending": wecom_github_items,
"github_emerging": wecom_emerging_items,
"github_topic": wecom_topic_items,
"wecom_trending": wecom_trending,
"wecom_hot": wecom_hot,
"wecom_github": wecom_github,
"wecom_emerging": wecom_emerging,
"wecom_topic": wecom_topic,
"pad": delta_pad,
"date_str": date_str,
"trending_pad": gt_pad,
"hot_pad": gh_pad,
"github_trending_pad": wecom_github_pad,
"github_emerging_pad": wecom_emerging_pad,
"github_topic_pad": wecom_topic_pad,
}
if agent_wecom:
wecom_md = replace_wecom_skill_sections(agent_wecom, **board_kwargs)
else: else:
wecom_md = build_wecom_report( wecom_md = build_wecom_report(
date_str=date_str, date_str=date_str,
@@ -635,8 +886,10 @@ def generate_report() -> tuple[str, str, Path, Path]:
highlights=editorial_highlights highlights=editorial_highlights
or _build_highlights(trending, hot, github_trending, github_emerging, ai_news, cn_ai_news), or _build_highlights(trending, hot, github_trending, github_emerging, ai_news, cn_ai_news),
theme_line=editorial_theme or _detect_theme_line(feed), theme_line=editorial_theme or _detect_theme_line(feed),
ai_news=wecom_ai, ai_news=wecom_ai if not news_merged else None,
cn_ai_news=wecom_cn, cn_ai_news=wecom_cn if not news_merged else None,
merged_ai_news=wecom_news if news_merged else None,
merged_tech_ai_news=wecom_tech_news if news_merged else None,
trending=[ trending=[
_prepare_skill_item(item, prev_ids, r) _prepare_skill_item(item, prev_ids, r)
for r, item in enumerate(finalize_wecom_skill_groups(gt), 1) for r, item in enumerate(finalize_wecom_skill_groups(gt), 1)
@@ -645,29 +898,40 @@ def generate_report() -> tuple[str, str, Path, Path]:
_prepare_skill_item(item, prev_ids, r) _prepare_skill_item(item, prev_ids, r)
for r, item in enumerate(finalize_wecom_skill_groups(gh), 1) for r, item in enumerate(finalize_wecom_skill_groups(gh), 1)
], ],
repos=[_prepare_github_item(item) for item in github_trending[:wecom_github]], repos=wecom_github_items,
emerging=[_prepare_github_item(item) for item in github_emerging[:wecom_emerging]], emerging=wecom_emerging_items,
topic_name=topic_name, topic_name=topic_name,
topic_repos=[_prepare_github_item(item) for item in github_topic[:wecom_topic]], topic_repos=wecom_topic_items,
pick_command=pick_command, pick_command=pick_command,
pick_why=pick_why, pick_why=pick_why,
pick_title=pick_title, pick_title=pick_title,
pick_url=pick_url, pick_url=pick_url,
include_boards=(eff_mode == "full"), include_boards=(eff_mode == "full"),
) )
wecom_md = replace_wecom_board_sections( wecom_md = replace_wecom_board_sections(wecom_md, **board_kwargs)
wecom_md = replace_wecom_news_sections(
wecom_md, wecom_md,
mode=eff_mode, ai_news=wecom_news if news_merged else wecom_ai,
movement=movement, cn_ai_news=None if news_merged else wecom_cn,
trending=gt, tech_ai_news=wecom_tech_news if news_merged else None,
hot=gh, merged=news_merged,
topic_name=topic_name,
) )
if push_gate.should_push: if push_gate.should_push:
if news_merged:
links = [x["link"] for x in wecom_news + wecom_tech_news if x.get("link")]
else:
links = [x["link"] for x in wecom_ai + wecom_cn if x.get("link")] links = [x["link"] for x in wecom_ai + wecom_cn if x.get("link")]
record_pushed_links(date_str, links) record_pushed_links(date_str, links)
final_boards = resolve_wecom_board_items(**board_kwargs)
shown_keys = {
board: extract_shown_keys(board, final_boards.get(board) or [])
for board in BOARD_KEYS
}
llm_input = merge_wecom_shown_into_data(llm_input, shown_keys)
save_json( save_json(
data_json_path(date_str), data_json_path(date_str),
build_full_payload( build_full_payload(
@@ -675,6 +939,7 @@ def generate_report() -> tuple[str, str, Path, Path]:
meta={ meta={
"generated_at": now.isoformat(), "generated_at": now.isoformat(),
"report_mode": "agent" if is_agent_mode() else "classic", "report_mode": "agent" if is_agent_mode() else "classic",
"ai_news_mode": "research" if news_merged else "rss",
"cursor_editor": cursor_editor_enabled() and not is_agent_mode(), "cursor_editor": cursor_editor_enabled() and not is_agent_mode(),
"featured_pick": featured.get("title") if featured else None, "featured_pick": featured.get("title") if featured else None,
"effective_wecom_mode": eff_mode, "effective_wecom_mode": eff_mode,

View File

@@ -10,6 +10,7 @@ from daily.config import OUTPUT_DIR, env_int, wecom_mode
from daily.delta import build_movement_baseline, build_movement_context, compare_depth, effective_wecom_mode from daily.delta import build_movement_baseline, build_movement_context, compare_depth, effective_wecom_mode
from daily.news.fetch import prepare_wecom_cn_news_items, prepare_wecom_news_items from daily.news.fetch import prepare_wecom_cn_news_items, prepare_wecom_news_items
from daily.skills_group import group_skills_by_source from daily.skills_group import group_skills_by_source
from daily.text_utils import trim_brief
def skill_id(item: dict[str, Any]) -> str: def skill_id(item: dict[str, Any]) -> str:
@@ -69,7 +70,10 @@ def _slim_news_items(
"title": item.get("title", ""), "title": item.get("title", ""),
"source_name": item.get("source_name", ""), "source_name": item.get("source_name", ""),
"published_fmt": item.get("published_fmt", ""), "published_fmt": item.get("published_fmt", ""),
"summary": item.get("desc_short") or "", "summary": trim_brief(
item.get("summary_plain") or item.get("desc_short") or "",
120,
),
} }
) )
if len(items) >= limit: if len(items) >= limit:
@@ -106,10 +110,46 @@ def build_llm_input(
ai_news: dict[str, Any], ai_news: dict[str, Any],
cn_ai_news: dict[str, Any], cn_ai_news: dict[str, Any],
wecom_limits: dict[str, int], wecom_limits: dict[str, int],
research_items: list[dict[str, Any]] | None = None,
research_tech_items: list[dict[str, Any]] | None = None,
boards_for_wecom: dict[str, list[dict[str, Any]]] | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""供 Cursor 编辑的精简 JSON不含完整 markdown""" """供 Cursor 编辑的精简 JSON不含完整 markdown"""
news_limit = wecom_limits.get("ai_news", 10) news_limit = wecom_limits.get("ai_news", 10)
cn_news_limit = wecom_limits.get("cn_ai_news", 8) cn_news_limit = wecom_limits.get("cn_ai_news", 8)
if research_items is not None:
slim_research = [
{
"link": item.get("link", ""),
"title": item.get("title", ""),
"source_name": item.get("source_name", ""),
"published_fmt": item.get("published_fmt", ""),
"summary": trim_brief(item.get("desc_short") or "", 120),
}
for item in research_items[:news_limit]
]
ai_news_payload = slim_research
tech_news_payload = [
{
"link": item.get("link", ""),
"title": item.get("title", ""),
"source_name": item.get("source_name", ""),
"published_fmt": item.get("published_fmt", ""),
"summary": trim_brief(item.get("desc_short") or "", 120),
}
for item in (research_tech_items or [])
]
cn_news_payload: list[dict[str, Any]] = []
ai_news_mode = "research"
else:
ai_news_payload = _slim_news_items(ai_news, news_limit, date_str=date_str) if ai_news.get("enabled") else []
cn_news_payload = (
_slim_news_items(cn_ai_news, cn_news_limit, prepare=prepare_wecom_cn_news_items, date_str=date_str)
if cn_ai_news.get("enabled")
else []
)
ai_news_mode = "rss"
tech_news_payload: list[dict[str, Any]] = []
depth = compare_depth() depth = compare_depth()
trend_cmp = trending[:depth] trend_cmp = trending[:depth]
hot_cmp = hot[:depth] hot_cmp = hot[:depth]
@@ -117,6 +157,13 @@ def build_llm_input(
emerging_cmp = github_emerging[:depth] emerging_cmp = github_emerging[:depth]
topic_cmp = github_topic[:depth] topic_cmp = github_topic[:depth]
if boards_for_wecom:
trending_slice = boards_for_wecom.get("skills_trending") or []
hot_slice = boards_for_wecom.get("skills_hot") or []
github_slice = boards_for_wecom.get("github_trending") or []
emerging_slice = boards_for_wecom.get("github_emerging") or []
topic_slice = boards_for_wecom.get("github_topic") or []
else:
trending_slice = group_skills_by_source( trending_slice = group_skills_by_source(
trending, trending,
limit=wecom_limits.get("trending", 10), limit=wecom_limits.get("trending", 10),
@@ -163,12 +210,10 @@ def build_llm_input(
"topic": topic_name, "topic": topic_name,
"repos": [_slim_github(x) for x in topic_slice], "repos": [_slim_github(x) for x in topic_slice],
}, },
"ai_news": _slim_news_items(ai_news, news_limit, date_str=date_str) if ai_news.get("enabled") else [], "ai_news": ai_news_payload,
"cn_ai_news": _slim_news_items( "tech_ai_news": tech_news_payload,
cn_ai_news, cn_news_limit, prepare=prepare_wecom_cn_news_items, date_str=date_str "cn_ai_news": cn_news_payload,
) "ai_news_mode": ai_news_mode,
if cn_ai_news.get("enabled")
else [],
"movement": movement, "movement": movement,
"movement_baseline": movement_baseline, "movement_baseline": movement_baseline,
} }

View File

@@ -60,6 +60,33 @@ class ShownKeysTests(unittest.TestCase):
) )
self.assertEqual(merged["wecom_shown_keys"]["github_trending"], ["shown/one"]) self.assertEqual(merged["wecom_shown_keys"]["github_trending"], ["shown/one"])
def test_persist_shown_keys_differs_from_baseline_keys(self):
from daily.board_select import board_select
raw = [{"repo": f"o/r{i}"} for i in range(10)]
recent = {f"o/r{i}" for i in range(3)}
selected = board_select(
board="github_trending",
items=raw,
recent_keys=recent,
limit=5,
pool_size=50,
kind="github",
)
baseline_keys = [x["repo"] for x in raw[:5]]
shown = extract_shown_keys("github_trending", selected)
data = {
"movement_baseline": {
"github_trending": [{"repo": k} for k in baseline_keys],
},
}
merged = merge_wecom_shown_into_data(data, {"github_trending": shown})
self.assertNotEqual(
set(merged["wecom_shown_keys"]["github_trending"]),
{x["repo"] for x in merged["movement_baseline"]["github_trending"]},
)
self.assertEqual(shown, ["o/r3", "o/r4", "o/r5", "o/r6", "o/r7"])
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()