feat: generate 以 board_select 为唯一列表主人并写回 shown keys
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7,6 +7,7 @@ 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 = {
|
||||
@@ -55,25 +56,177 @@ def _skill_line(rank: int, item: dict[str, Any], *, badge: str = "") -> list[str
|
||||
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] = []
|
||||
for i, item in enumerate(items, 1):
|
||||
title = item.get("title", "?")
|
||||
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", "")
|
||||
pub_suffix = f" · {pub}" if pub else ""
|
||||
desc = (item.get("desc_short") or "").strip()
|
||||
pub_suffix = "" if merged else (f" · {pub}" if pub else "")
|
||||
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:
|
||||
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)
|
||||
if desc:
|
||||
lines.append(f" > {desc}")
|
||||
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):
|
||||
@@ -94,12 +247,45 @@ def _github_repo_lines(repos: list[dict[str, Any]], *, show_created: bool = Fals
|
||||
meta_parts.append(f"创建于 {created}")
|
||||
meta = f" · {' · '.join(meta_parts)}" if meta_parts else ""
|
||||
lines.append(f"{i}. [{name}]({url}){meta}")
|
||||
desc = repo.get("desc_short") or repo.get("description", "")
|
||||
desc = repo.get("wecom_desc") or repo.get("desc_short") or repo.get("description", "")
|
||||
if desc:
|
||||
lines.append(f" > {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)
|
||||
@@ -173,6 +359,7 @@ def _grouped_skill_to_wecom_item(
|
||||
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,
|
||||
@@ -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]:
|
||||
installs = int(move.get("installs") or 0)
|
||||
rank = move.get("rank", "?")
|
||||
board_badge = (move.get("badge") or "").strip()
|
||||
badge = f"[新入 #{rank}]"
|
||||
if board_badge:
|
||||
badge = f"{badge} {board_badge}"
|
||||
return {
|
||||
"title": move.get("title", "?"),
|
||||
"source": move.get("source", "?"),
|
||||
"installs_fmt": move.get("installs_fmt") or str(installs),
|
||||
"link": move.get("link", ""),
|
||||
"description": (move.get("description") or "").strip(),
|
||||
"badge": badge,
|
||||
}
|
||||
|
||||
|
||||
def _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:
|
||||
prepared = finalize_wecom_skill_groups([_move_to_wecom_skill_item(m) for m in trending_moves])
|
||||
items = [_grouped_skill_to_wecom_item(x) for x in prepared]
|
||||
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, badge=item.get("badge", "")))
|
||||
lines.extend(_skill_line(rank, item))
|
||||
sections.append("\n".join(lines))
|
||||
if hot_moves:
|
||||
prepared = finalize_wecom_skill_groups([_move_to_wecom_skill_item(m) for m in hot_moves])
|
||||
items = [_grouped_skill_to_wecom_item(x) for x in prepared]
|
||||
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, badge=item.get("badge", "")))
|
||||
lines.extend(_skill_line(rank, item))
|
||||
sections.append("\n".join(lines))
|
||||
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", ""),
|
||||
"description": move.get("description", ""),
|
||||
"desc_short": (move.get("description") or "").strip(),
|
||||
"badge": f"[新入 #{move.get('rank', '?')}]",
|
||||
}
|
||||
|
||||
|
||||
def build_github_delta_sections(movement: dict[str, Any], *, topic_name: str) -> str:
|
||||
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),
|
||||
@@ -259,13 +645,8 @@ def build_github_delta_sections(movement: dict[str, Any], *, topic_name: str) ->
|
||||
if not moves:
|
||||
continue
|
||||
lines = [f"{ICONS[icon_key]} **{label}**"]
|
||||
for i, move in enumerate(moves, 1):
|
||||
repo = _github_move_to_repo(move)
|
||||
badge = repo.pop("badge", "")
|
||||
chunk = _github_repo_lines([repo], show_created=show_created)
|
||||
if chunk:
|
||||
chunk[0] = f"{i}. {badge} " + chunk[0].split(". ", 1)[-1]
|
||||
lines.extend(chunk)
|
||||
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)
|
||||
|
||||
@@ -287,6 +668,105 @@ def _strip_board_sections(md: str) -> str:
|
||||
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,
|
||||
*,
|
||||
@@ -295,16 +775,56 @@ def replace_wecom_board_sections(
|
||||
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 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":
|
||||
t_moves, h_moves = partition_skill_moves_for_wecom(
|
||||
movement.get("skills_trending_moves") or [],
|
||||
movement.get("skills_hot_moves") or [],
|
||||
)
|
||||
skills_sec = build_skills_delta_sections(t_moves, h_moves)
|
||||
github_sec = build_github_delta_sections(movement, topic_name=topic_name)
|
||||
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:
|
||||
@@ -331,6 +851,21 @@ def replace_wecom_skill_sections(
|
||||
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(
|
||||
@@ -340,6 +875,21 @@ def replace_wecom_skill_sections(
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
@@ -378,6 +928,8 @@ def build_wecom_report(
|
||||
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 = "",
|
||||
@@ -396,6 +948,12 @@ def build_wecom_report(
|
||||
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))
|
||||
@@ -419,17 +977,17 @@ def build_wecom_report(
|
||||
|
||||
if 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("")
|
||||
|
||||
if 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("")
|
||||
|
||||
if 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(f"{ICONS['pick']} **今日首推**")
|
||||
|
||||
@@ -22,17 +22,30 @@ from daily.config import (
|
||||
LOG_DIR,
|
||||
OUTPUT_DIR,
|
||||
SNAPSHOT_FILE,
|
||||
board_pool_size,
|
||||
env,
|
||||
env_int,
|
||||
full_desc_limit,
|
||||
news_summary_limit,
|
||||
wecom_delta_pad,
|
||||
wecom_news_desc_limit,
|
||||
wecom_pad_pool_size,
|
||||
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 (
|
||||
build_wecom_report,
|
||||
finalize_wecom_skill_groups,
|
||||
replace_wecom_board_sections,
|
||||
replace_wecom_news_sections,
|
||||
replace_wecom_skill_sections,
|
||||
resolve_wecom_board_items,
|
||||
)
|
||||
from daily.agent_workflow import is_agent_mode, run_agent_workflow
|
||||
from daily.featured_pick import (
|
||||
@@ -56,10 +69,17 @@ from daily.news.fetch import (
|
||||
fetch_cn_ai_news,
|
||||
format_cn_news_section,
|
||||
format_news_section,
|
||||
finalize_wecom_news_items,
|
||||
prepare_wecom_cn_news_items,
|
||||
prepare_wecom_news_items,
|
||||
sync_wecom_news_rows,
|
||||
)
|
||||
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.report_data import (
|
||||
build_full_payload,
|
||||
@@ -68,7 +88,6 @@ from daily.report_data import (
|
||||
save_json,
|
||||
)
|
||||
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]]] = [
|
||||
("🎬", "AI 多媒体 / 视频", ["runcomfy", "remotion", "video", "seedance", "inpaint", "lipsync"]),
|
||||
@@ -140,7 +159,30 @@ def _localize_descriptions_in_place(
|
||||
seen_news.add(link)
|
||||
summary = (item.get("summary") or "").strip()
|
||||
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)
|
||||
if not zh_map and not jobs:
|
||||
@@ -166,6 +208,11 @@ def _localize_descriptions_in_place(
|
||||
key = f"news:{item.get('link', '')}"
|
||||
if key in mapping:
|
||||
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)
|
||||
|
||||
@@ -199,7 +246,29 @@ def _localize_descriptions_in_place(
|
||||
seen_news.add(link)
|
||||
summary = (item.get("summary") or "").strip()
|
||||
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:
|
||||
_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)}
|
||||
|
||||
|
||||
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:
|
||||
atom_url = f"https://github.com/{repo}/releases.atom"
|
||||
try:
|
||||
@@ -425,15 +515,17 @@ def generate_report() -> tuple[str, str, Path, Path]:
|
||||
skill_pool = max(10, env_int("DAILY_WECOM_SKILL_POOL", 200))
|
||||
wecom_trending = env_int("DAILY_WECOM_TRENDING", 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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
seen_repos.update(r["repo"] for r in github_emerging)
|
||||
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()
|
||||
cn_ai_news = fetch_cn_ai_news()
|
||||
|
||||
@@ -460,7 +571,56 @@ def generate_report() -> tuple[str, str, Path, Path]:
|
||||
"emerging": wecom_emerging,
|
||||
"topic": wecom_topic,
|
||||
"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(
|
||||
date_str=date_str,
|
||||
@@ -474,15 +634,22 @@ def generate_report() -> tuple[str, str, Path, Path]:
|
||||
ai_news=ai_news,
|
||||
cn_ai_news=cn_ai_news,
|
||||
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)
|
||||
movement = llm_input["movement"]
|
||||
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_cn = prepare_wecom_cn_news_items(cn_ai_news, date_str=date_str)
|
||||
push_gate = evaluate_push_gate(
|
||||
movement=movement,
|
||||
ai_news_items=wecom_ai,
|
||||
ai_news_items=wecom_news if news_merged else wecom_ai,
|
||||
cn_ai_news_items=wecom_cn,
|
||||
featured_pick=featured,
|
||||
)
|
||||
@@ -526,6 +693,17 @@ def generate_report() -> tuple[str, str, Path, Path]:
|
||||
_localize_descriptions_in_place(
|
||||
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)
|
||||
|
||||
@@ -534,7 +712,8 @@ def generate_report() -> tuple[str, str, Path, Path]:
|
||||
"",
|
||||
f"> 生成时间:{now.strftime('%Y-%m-%d %H:%M')} (UTC+8) ",
|
||||
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("")
|
||||
|
||||
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))
|
||||
section_no += 1
|
||||
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`*"])
|
||||
|
||||
markdown = "\n".join(lines)
|
||||
gt = group_skills_by_source(trending, limit=wecom_trending, pool_size=skill_pool)
|
||||
gh = group_skills_by_source(hot, limit=wecom_hot, pool_size=skill_pool)
|
||||
if agent_wecom:
|
||||
wecom_md = replace_wecom_skill_sections(
|
||||
agent_wecom,
|
||||
trending=gt,
|
||||
hot=gh,
|
||||
mode=eff_mode,
|
||||
movement=movement,
|
||||
topic_name=topic_name,
|
||||
gt = selected_trending
|
||||
gh = selected_hot
|
||||
gt_pad = board_select(
|
||||
board="skills_trending",
|
||||
items=trending,
|
||||
recent_keys=recent_shown["skills_trending"],
|
||||
limit=pad_pool,
|
||||
pool_size=pool,
|
||||
kind="skill",
|
||||
)
|
||||
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:
|
||||
wecom_md = build_wecom_report(
|
||||
date_str=date_str,
|
||||
@@ -635,8 +886,10 @@ def generate_report() -> tuple[str, str, Path, Path]:
|
||||
highlights=editorial_highlights
|
||||
or _build_highlights(trending, hot, github_trending, github_emerging, ai_news, cn_ai_news),
|
||||
theme_line=editorial_theme or _detect_theme_line(feed),
|
||||
ai_news=wecom_ai,
|
||||
cn_ai_news=wecom_cn,
|
||||
ai_news=wecom_ai if not news_merged else None,
|
||||
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=[
|
||||
_prepare_skill_item(item, prev_ids, r)
|
||||
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)
|
||||
for r, item in enumerate(finalize_wecom_skill_groups(gh), 1)
|
||||
],
|
||||
repos=[_prepare_github_item(item) for item in github_trending[:wecom_github]],
|
||||
emerging=[_prepare_github_item(item) for item in github_emerging[:wecom_emerging]],
|
||||
repos=wecom_github_items,
|
||||
emerging=wecom_emerging_items,
|
||||
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_why=pick_why,
|
||||
pick_title=pick_title,
|
||||
pick_url=pick_url,
|
||||
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,
|
||||
mode=eff_mode,
|
||||
movement=movement,
|
||||
trending=gt,
|
||||
hot=gh,
|
||||
topic_name=topic_name,
|
||||
ai_news=wecom_news if news_merged else wecom_ai,
|
||||
cn_ai_news=None if news_merged else wecom_cn,
|
||||
tech_ai_news=wecom_tech_news if news_merged else None,
|
||||
merged=news_merged,
|
||||
)
|
||||
|
||||
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")]
|
||||
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(
|
||||
data_json_path(date_str),
|
||||
build_full_payload(
|
||||
@@ -675,6 +939,7 @@ def generate_report() -> tuple[str, str, Path, Path]:
|
||||
meta={
|
||||
"generated_at": now.isoformat(),
|
||||
"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(),
|
||||
"featured_pick": featured.get("title") if featured else None,
|
||||
"effective_wecom_mode": eff_mode,
|
||||
|
||||
@@ -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.news.fetch import prepare_wecom_cn_news_items, prepare_wecom_news_items
|
||||
from daily.skills_group import group_skills_by_source
|
||||
from daily.text_utils import trim_brief
|
||||
|
||||
|
||||
def skill_id(item: dict[str, Any]) -> str:
|
||||
@@ -69,7 +70,10 @@ def _slim_news_items(
|
||||
"title": item.get("title", ""),
|
||||
"source_name": item.get("source_name", ""),
|
||||
"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:
|
||||
@@ -106,10 +110,46 @@ def build_llm_input(
|
||||
ai_news: dict[str, Any],
|
||||
cn_ai_news: dict[str, Any],
|
||||
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]:
|
||||
"""供 Cursor 编辑的精简 JSON(不含完整 markdown)。"""
|
||||
news_limit = wecom_limits.get("ai_news", 10)
|
||||
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()
|
||||
trend_cmp = trending[:depth]
|
||||
hot_cmp = hot[:depth]
|
||||
@@ -117,6 +157,13 @@ def build_llm_input(
|
||||
emerging_cmp = github_emerging[: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,
|
||||
limit=wecom_limits.get("trending", 10),
|
||||
@@ -163,12 +210,10 @@ def build_llm_input(
|
||||
"topic": topic_name,
|
||||
"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 [],
|
||||
"cn_ai_news": _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": ai_news_payload,
|
||||
"tech_ai_news": tech_news_payload,
|
||||
"cn_ai_news": cn_news_payload,
|
||||
"ai_news_mode": ai_news_mode,
|
||||
"movement": movement,
|
||||
"movement_baseline": movement_baseline,
|
||||
}
|
||||
|
||||
@@ -60,6 +60,33 @@ class ShownKeysTests(unittest.TestCase):
|
||||
)
|
||||
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__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user