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.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:
|
||||||
|
head = f"{i}. [{label}]({link}) · `{source}`{pub_suffix}"
|
||||||
|
elif merged and desc:
|
||||||
|
head = f"{i}. {label} — {desc}{pub_suffix}"
|
||||||
else:
|
else:
|
||||||
head = f"{i}. **{title}** · `{source}`{pub_suffix}"
|
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,15 +948,21 @@ def build_wecom_report(
|
|||||||
lines.append(f"{ICONS['theme']} {theme_line}")
|
lines.append(f"{ICONS['theme']} {theme_line}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
if ai_news:
|
if merged_ai_news or merged_tech_ai_news:
|
||||||
lines.append(f"{ICONS['ainews']} **国际 AI 时讯 Top {len(ai_news)}**")
|
block = _build_merged_news_block(merged_ai_news or [], merged_tech_ai_news)
|
||||||
lines.extend(_ai_news_lines(ai_news))
|
if block:
|
||||||
lines.append("")
|
lines.append(block.rstrip())
|
||||||
|
lines.append("")
|
||||||
|
else:
|
||||||
|
if ai_news:
|
||||||
|
lines.append(f"{ICONS['ainews']} **国际 AI 时讯 Top {len(ai_news)}**")
|
||||||
|
lines.extend(_ai_news_lines(ai_news))
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
if cn_ai_news:
|
if cn_ai_news:
|
||||||
lines.append(f"{ICONS['cnainews']} **国内 AI 时讯 Top {len(cn_ai_news)}**")
|
lines.append(f"{ICONS['cnainews']} **国内 AI 时讯 Top {len(cn_ai_news)}**")
|
||||||
lines.extend(_ai_news_lines(cn_ai_news))
|
lines.extend(_ai_news_lines(cn_ai_news))
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
if include_boards:
|
if include_boards:
|
||||||
lines.append(f"{ICONS['trending']} **Skills Trending Top {len(trending)}**")
|
lines.append(f"{ICONS['trending']} **Skills Trending Top {len(trending)}**")
|
||||||
@@ -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']} **今日首推**")
|
||||||
|
|||||||
@@ -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,8 +540,27 @@ 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)
|
||||||
ai_news = fetch_ai_news()
|
|
||||||
cn_ai_news = fetch_cn_ai_news()
|
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()
|
||||||
|
|
||||||
wecom_limits = {
|
wecom_limits = {
|
||||||
"trending": wecom_trending,
|
"trending": wecom_trending,
|
||||||
@@ -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"]
|
||||||
wecom_ai = prepare_wecom_news_items(ai_news, date_str=date_str)
|
if news_merged:
|
||||||
wecom_cn = prepare_wecom_cn_news_items(cn_ai_news, date_str=date_str)
|
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(
|
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,10 +755,14 @@ def generate_report() -> tuple[str, str, Path, Path]:
|
|||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
section_no = 6
|
section_no = 6
|
||||||
lines.extend(format_news_section(ai_news, section_no=section_no))
|
if news_merged and ai_news_research is not None:
|
||||||
section_no += 1
|
lines.extend(format_research_news_section(ai_news_research, section_no=section_no))
|
||||||
lines.extend(format_cn_news_section(cn_ai_news, section_no=section_no))
|
section_no += 1
|
||||||
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))
|
||||||
|
section_no += 1
|
||||||
|
|
||||||
watch = (env("GITHUB_REPOS") or "").strip()
|
watch = (env("GITHUB_REPOS") or "").strip()
|
||||||
if watch:
|
if watch:
|
||||||
@@ -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,
|
|
||||||
mode=eff_mode,
|
wecom_md = replace_wecom_news_sections(
|
||||||
movement=movement,
|
wecom_md,
|
||||||
trending=gt,
|
ai_news=wecom_news if news_merged else wecom_ai,
|
||||||
hot=gh,
|
cn_ai_news=None if news_merged else wecom_cn,
|
||||||
topic_name=topic_name,
|
tech_ai_news=wecom_tech_news if news_merged else None,
|
||||||
)
|
merged=news_merged,
|
||||||
|
)
|
||||||
|
|
||||||
if push_gate.should_push:
|
if push_gate.should_push:
|
||||||
links = [x["link"] for x in wecom_ai + wecom_cn if x.get("link")]
|
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)
|
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,
|
||||||
|
|||||||
@@ -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,19 +157,26 @@ def build_llm_input(
|
|||||||
emerging_cmp = github_emerging[:depth]
|
emerging_cmp = github_emerging[:depth]
|
||||||
topic_cmp = github_topic[:depth]
|
topic_cmp = github_topic[:depth]
|
||||||
|
|
||||||
trending_slice = group_skills_by_source(
|
if boards_for_wecom:
|
||||||
trending,
|
trending_slice = boards_for_wecom.get("skills_trending") or []
|
||||||
limit=wecom_limits.get("trending", 10),
|
hot_slice = boards_for_wecom.get("skills_hot") or []
|
||||||
pool_size=wecom_limits.get("trending_pool", _wecom_skill_pool()),
|
github_slice = boards_for_wecom.get("github_trending") or []
|
||||||
)
|
emerging_slice = boards_for_wecom.get("github_emerging") or []
|
||||||
hot_slice = group_skills_by_source(
|
topic_slice = boards_for_wecom.get("github_topic") or []
|
||||||
hot,
|
else:
|
||||||
limit=wecom_limits.get("hot", 10),
|
trending_slice = group_skills_by_source(
|
||||||
pool_size=wecom_limits.get("hot_pool", _wecom_skill_pool()),
|
trending,
|
||||||
)
|
limit=wecom_limits.get("trending", 10),
|
||||||
github_slice = github_trending[: wecom_limits.get("github", 5)]
|
pool_size=wecom_limits.get("trending_pool", _wecom_skill_pool()),
|
||||||
emerging_slice = github_emerging[: wecom_limits.get("emerging", 3)]
|
)
|
||||||
topic_slice = github_topic[: wecom_limits.get("topic", 3)]
|
hot_slice = group_skills_by_source(
|
||||||
|
hot,
|
||||||
|
limit=wecom_limits.get("hot", 10),
|
||||||
|
pool_size=wecom_limits.get("hot_pool", _wecom_skill_pool()),
|
||||||
|
)
|
||||||
|
github_slice = github_trending[: wecom_limits.get("github", 5)]
|
||||||
|
emerging_slice = github_emerging[: wecom_limits.get("emerging", 3)]
|
||||||
|
topic_slice = github_topic[: wecom_limits.get("topic", 3)]
|
||||||
|
|
||||||
movement = build_movement_context(
|
movement = build_movement_context(
|
||||||
date_str=date_str,
|
date_str=date_str,
|
||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user