Compare commits
8 Commits
6ea2a4e4c6
...
d66f2c716c
| Author | SHA1 | Date | |
|---|---|---|---|
| d66f2c716c | |||
| d9aef4b340 | |||
| d7992e7a7a | |||
| 3321a307a0 | |||
| 33bfed0e79 | |||
| ea8de9fe61 | |||
| cd92644be4 | |||
| 92e0ed9a27 |
@@ -8,6 +8,10 @@ DAILY_GITHUB_TRENDING_LIMIT=10
|
|||||||
DAILY_GITHUB_EMERGING_LIMIT=10
|
DAILY_GITHUB_EMERGING_LIMIT=10
|
||||||
DAILY_GITHUB_TOPIC_LIMIT=10
|
DAILY_GITHUB_TOPIC_LIMIT=10
|
||||||
|
|
||||||
|
# 企微版可选行(默认均开)
|
||||||
|
# DAILY_WECOM_TOP_LINE=1 # 顶部「今日主题」行,无评分命中时回退主题名
|
||||||
|
# DAILY_FEATURED_REASON=1 # 首推下「为什么值得点开」理由行
|
||||||
|
|
||||||
# GitHub Trending
|
# GitHub Trending
|
||||||
GITHUB_TRENDING_SINCE=daily
|
GITHUB_TRENDING_SINCE=daily
|
||||||
# GITHUB_TRENDING_LANGUAGE=python
|
# GITHUB_TRENDING_LANGUAGE=python
|
||||||
|
|||||||
@@ -124,7 +124,8 @@ def warm_cursor_bridge(force: bool = False) -> None:
|
|||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
discovery = _read_discovery_polling(process)
|
discovery = _read_discovery_polling(process)
|
||||||
except Exception:
|
except (RuntimeError, OSError, ValueError) as exc:
|
||||||
|
logger.warning("Cursor bridge discovery 失败,终止子进程:%s", exc)
|
||||||
process.kill()
|
process.kill()
|
||||||
process.wait(timeout=5)
|
process.wait(timeout=5)
|
||||||
raise
|
raise
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|||||||
import re
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from daily.config import wecom_skill_desc_limit
|
from daily.config import env_bool, 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
|
from daily.skills_group import group_skills_by_source
|
||||||
from daily.text_utils import trim_brief
|
from daily.text_utils import trim_brief
|
||||||
@@ -1096,7 +1096,8 @@ def build_wecom_report(
|
|||||||
|
|
||||||
lines.append(f"{ICONS['pick']} **今日首推**")
|
lines.append(f"{ICONS['pick']} **今日首推**")
|
||||||
lines.append(_format_pick_link(pick_command, title=pick_title, url=pick_url))
|
lines.append(_format_pick_link(pick_command, title=pick_title, url=pick_url))
|
||||||
if pick_why:
|
# 首推「为什么值得点开」一句理由; DAILY_FEATURED_REASON=0 关闭, 空则省略
|
||||||
|
if pick_why and env_bool("DAILY_FEATURED_REASON", True):
|
||||||
lines.append(f"> {pick_why}")
|
lines.append(f"> {pick_why}")
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ from daily.config import (
|
|||||||
SNAPSHOT_FILE,
|
SNAPSHOT_FILE,
|
||||||
board_pool_size,
|
board_pool_size,
|
||||||
env,
|
env,
|
||||||
|
env_bool,
|
||||||
env_int,
|
env_int,
|
||||||
full_desc_limit,
|
full_desc_limit,
|
||||||
news_summary_limit,
|
news_summary_limit,
|
||||||
@@ -65,6 +66,7 @@ from daily.github.auth import github_html_headers
|
|||||||
from daily.github.search import fetch_emerging_repos, fetch_topic_hot_repos
|
from daily.github.search import fetch_emerging_repos, fetch_topic_hot_repos
|
||||||
from daily.github.trending import fetch_github_trending, trending_data_source_note
|
from daily.github.trending import fetch_github_trending, trending_data_source_note
|
||||||
from daily.localize import LocalizeJob, localize_descriptions, needs_chinese
|
from daily.localize import LocalizeJob, localize_descriptions, needs_chinese
|
||||||
|
from daily.narrative_axis import theme_clusters, theme_names
|
||||||
from daily.news.fetch import (
|
from daily.news.fetch import (
|
||||||
fetch_ai_news,
|
fetch_ai_news,
|
||||||
fetch_cn_ai_news,
|
fetch_cn_ai_news,
|
||||||
@@ -344,6 +346,33 @@ def _detect_theme_line(feed: dict[str, Any]) -> str:
|
|||||||
return f"**今日主题**:{max(scores.items(), key=lambda x: x[1])[0]}"
|
return f"**今日主题**:{max(scores.items(), key=lambda x: x[1])[0]}"
|
||||||
|
|
||||||
|
|
||||||
|
def _top_line(feed: dict[str, Any]) -> str:
|
||||||
|
"""今日看点行: 优先评分最高的主题, 回退 _theme_clusters 的主题名(非 markdown 示例)。
|
||||||
|
|
||||||
|
DAILY_WECOM_TOP_LINE=0 关闭时退回 _detect_theme_line 原逻辑。
|
||||||
|
与原 _detect_theme_line 的差别仅在「无评分命中」时的兜底文案:
|
||||||
|
用 theme_names 的真实主题取代硬编码「Agent Skills 生态持续活跃」。
|
||||||
|
"""
|
||||||
|
if not env_bool("DAILY_WECOM_TOP_LINE", True):
|
||||||
|
return _detect_theme_line(feed)
|
||||||
|
scores: dict[str, int] = defaultdict(int)
|
||||||
|
for board in ("topTrending", "topHot"):
|
||||||
|
for rank, item in enumerate(feed.get(board, [])[:10], 1):
|
||||||
|
haystack = " ".join(
|
||||||
|
[item.get("title", ""), item.get("source", ""), item.get("description", "")]
|
||||||
|
).lower()
|
||||||
|
for _icon, label, keywords in THEME_RULES:
|
||||||
|
if any(k in haystack for k in keywords):
|
||||||
|
scores[label] += max(1, 11 - rank)
|
||||||
|
break
|
||||||
|
if scores:
|
||||||
|
return f"**今日主题**:{max(scores.items(), key=lambda x: x[1])[0]}"
|
||||||
|
names = theme_names(feed, theme_rules=THEME_RULES, skill_id_fn=_skill_id)
|
||||||
|
if names:
|
||||||
|
return f"**今日主题**:{' · '.join(names)}"
|
||||||
|
return "**今日主题**:Agent Skills 生态持续活跃"
|
||||||
|
|
||||||
|
|
||||||
def _build_highlights(
|
def _build_highlights(
|
||||||
trending: list[dict[str, Any]],
|
trending: list[dict[str, Any]],
|
||||||
hot: list[dict[str, Any]],
|
hot: list[dict[str, Any]],
|
||||||
@@ -444,31 +473,11 @@ def _fetch_latest_release_title(repo: str) -> str | None:
|
|||||||
return None
|
return None
|
||||||
title = entry.find("a:title", ns)
|
title = entry.find("a:title", ns)
|
||||||
return title.text.strip() if title is not None and title.text else None
|
return title.text.strip() if title is not None and title.text else None
|
||||||
except Exception:
|
except (httpx.HTTPError, ET.ParseError, OSError) as exc:
|
||||||
|
logger.warning("读取 %s release 失败:%s", atom_url, exc)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _theme_clusters(feed: dict[str, Any], limit: int = 5) -> list[tuple[str, list[str]]]:
|
|
||||||
buckets: dict[str, list[str]] = defaultdict(list)
|
|
||||||
seen: set[str] = set()
|
|
||||||
for board in ("topTrending", "topHot"):
|
|
||||||
for item in feed.get(board, [])[:20]:
|
|
||||||
item_id = _skill_id(item)
|
|
||||||
if item_id in seen:
|
|
||||||
continue
|
|
||||||
seen.add(item_id)
|
|
||||||
haystack = " ".join(
|
|
||||||
[item.get("title", ""), item.get("source", ""), item.get("description", "")]
|
|
||||||
).lower()
|
|
||||||
for _icon, theme, keywords in THEME_RULES:
|
|
||||||
if any(k in haystack for k in keywords):
|
|
||||||
label = f"**{item.get('title')}** (`{item.get('source')}`)"
|
|
||||||
if label not in buckets[theme]:
|
|
||||||
buckets[theme].append(label)
|
|
||||||
break
|
|
||||||
return [(theme, examples[:limit]) for theme, examples in buckets.items() if examples]
|
|
||||||
|
|
||||||
|
|
||||||
def _format_github_repo_section(repos: list[dict[str, Any]], *, show_created: bool = False) -> list[str]:
|
def _format_github_repo_section(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):
|
||||||
@@ -509,7 +518,11 @@ def _format_skill_section(items: list[dict[str, Any]], *, hot: bool = False) ->
|
|||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
|
||||||
def generate_report() -> tuple[str, str, Path, Path]:
|
def _collect(date_str: str) -> dict[str, Any]:
|
||||||
|
"""抓取 skills/github/news 数据并归一化,计算 wecom 限额与周去重 recent keys。
|
||||||
|
|
||||||
|
无副作用(不写快照/不记已推)。返回供 _select/_render 消费的 bundle。
|
||||||
|
"""
|
||||||
# Hot/Trending 前排同 source 极密,需更深抓取才能凑够展示用的唯一 source
|
# Hot/Trending 前排同 source 极密,需更深抓取才能凑够展示用的唯一 source
|
||||||
trending_n = env_int("DAILY_TRENDING_LIMIT", 400)
|
trending_n = env_int("DAILY_TRENDING_LIMIT", 400)
|
||||||
hot_n = max(env_int("DAILY_HOT_LIMIT", 400), compare_depth())
|
hot_n = max(env_int("DAILY_HOT_LIMIT", 400), compare_depth())
|
||||||
@@ -590,11 +603,79 @@ def generate_report() -> tuple[str, str, Path, Path]:
|
|||||||
| recent_shown["github_emerging"]
|
| recent_shown["github_emerging"]
|
||||||
| recent_shown["github_topic"]
|
| recent_shown["github_topic"]
|
||||||
)
|
)
|
||||||
|
return {
|
||||||
|
"trending_n": trending_n,
|
||||||
|
"hot_n": hot_n,
|
||||||
|
"github_limit": github_limit,
|
||||||
|
"emerging_limit": emerging_limit,
|
||||||
|
"topic_limit": topic_limit,
|
||||||
|
"wecom_trending": wecom_trending,
|
||||||
|
"wecom_hot": wecom_hot,
|
||||||
|
"wecom_github": wecom_github,
|
||||||
|
"wecom_emerging": wecom_emerging,
|
||||||
|
"wecom_topic": wecom_topic,
|
||||||
|
"wecom_limits": wecom_limits,
|
||||||
|
"pool": pool,
|
||||||
|
"pad_pool": pad_pool,
|
||||||
|
"feed": feed,
|
||||||
|
"prev_ids": prev_ids,
|
||||||
|
"now": now,
|
||||||
|
"date_str": date_str,
|
||||||
|
"time_str": time_str,
|
||||||
|
"updated": updated,
|
||||||
|
"trending": trending,
|
||||||
|
"hot": hot,
|
||||||
|
"github_trending": github_trending,
|
||||||
|
"github_emerging": github_emerging,
|
||||||
|
"github_topic": github_topic,
|
||||||
|
"topic_name": topic_name,
|
||||||
|
"news_merged": news_merged,
|
||||||
|
"ai_news": ai_news,
|
||||||
|
"cn_ai_news": cn_ai_news,
|
||||||
|
"ai_news_research": ai_news_research,
|
||||||
|
"wecom_news": wecom_news,
|
||||||
|
"wecom_tech_news": wecom_tech_news,
|
||||||
|
"recent_shown": recent_shown,
|
||||||
|
"skill_recent": skill_recent,
|
||||||
|
"github_recent": github_recent,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _select(c: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""全部「选择 + 数据突变」:9 处 board_select + featured_pick + LLM 步骤 + pad。
|
||||||
|
|
||||||
|
时序约束内聚本段:featured pick 依赖 deep-pool;_localize 在 featured 后、
|
||||||
|
pad 前执行;写回 shown keys/llm_input 也在此。_render 不再做任何选择。
|
||||||
|
副作用: apply_featured_pick 写回 shown keys(保持原时序)。
|
||||||
|
"""
|
||||||
|
date_str = c["date_str"]
|
||||||
|
feed = c["feed"]
|
||||||
|
prev_ids = c["prev_ids"]
|
||||||
|
updated = c["updated"]
|
||||||
|
time_str = c["time_str"]
|
||||||
|
trending = c["trending"]
|
||||||
|
hot = c["hot"]
|
||||||
|
github_trending = c["github_trending"]
|
||||||
|
github_emerging = c["github_emerging"]
|
||||||
|
github_topic = c["github_topic"]
|
||||||
|
topic_name = c["topic_name"]
|
||||||
|
news_merged = c["news_merged"]
|
||||||
|
ai_news = c["ai_news"]
|
||||||
|
cn_ai_news = c["cn_ai_news"]
|
||||||
|
wecom_news = c["wecom_news"]
|
||||||
|
wecom_tech_news = c["wecom_tech_news"]
|
||||||
|
wecom_limits = c["wecom_limits"]
|
||||||
|
pool = c["pool"]
|
||||||
|
pad_pool = c["pad_pool"]
|
||||||
|
recent_shown = c["recent_shown"]
|
||||||
|
skill_recent = c["skill_recent"]
|
||||||
|
github_recent = c["github_recent"]
|
||||||
|
|
||||||
selected_trending = board_select(
|
selected_trending = board_select(
|
||||||
board="skills_trending",
|
board="skills_trending",
|
||||||
items=trending,
|
items=trending,
|
||||||
recent_keys=skill_recent,
|
recent_keys=skill_recent,
|
||||||
limit=wecom_trending,
|
limit=c["wecom_trending"],
|
||||||
pool_size=pool,
|
pool_size=pool,
|
||||||
kind="skill",
|
kind="skill",
|
||||||
)
|
)
|
||||||
@@ -602,7 +683,7 @@ def generate_report() -> tuple[str, str, Path, Path]:
|
|||||||
board="skills_hot",
|
board="skills_hot",
|
||||||
items=hot,
|
items=hot,
|
||||||
recent_keys=skill_recent,
|
recent_keys=skill_recent,
|
||||||
limit=wecom_hot,
|
limit=c["wecom_hot"],
|
||||||
pool_size=pool,
|
pool_size=pool,
|
||||||
kind="skill",
|
kind="skill",
|
||||||
)
|
)
|
||||||
@@ -610,7 +691,7 @@ def generate_report() -> tuple[str, str, Path, Path]:
|
|||||||
board="github_trending",
|
board="github_trending",
|
||||||
items=github_trending,
|
items=github_trending,
|
||||||
recent_keys=github_recent,
|
recent_keys=github_recent,
|
||||||
limit=wecom_github,
|
limit=c["wecom_github"],
|
||||||
pool_size=pool,
|
pool_size=pool,
|
||||||
kind="github",
|
kind="github",
|
||||||
)
|
)
|
||||||
@@ -619,7 +700,7 @@ def generate_report() -> tuple[str, str, Path, Path]:
|
|||||||
board="github_emerging",
|
board="github_emerging",
|
||||||
items=github_emerging,
|
items=github_emerging,
|
||||||
recent_keys=github_recent,
|
recent_keys=github_recent,
|
||||||
limit=wecom_emerging,
|
limit=c["wecom_emerging"],
|
||||||
pool_size=pool,
|
pool_size=pool,
|
||||||
kind="github",
|
kind="github",
|
||||||
)
|
)
|
||||||
@@ -628,7 +709,7 @@ def generate_report() -> tuple[str, str, Path, Path]:
|
|||||||
board="github_topic",
|
board="github_topic",
|
||||||
items=github_topic,
|
items=github_topic,
|
||||||
recent_keys=github_recent,
|
recent_keys=github_recent,
|
||||||
limit=wecom_topic,
|
limit=c["wecom_topic"],
|
||||||
pool_size=pool,
|
pool_size=pool,
|
||||||
kind="github",
|
kind="github",
|
||||||
)
|
)
|
||||||
@@ -759,7 +840,179 @@ def generate_report() -> tuple[str, str, Path, Path]:
|
|||||||
github_topic=github_topic,
|
github_topic=github_topic,
|
||||||
)
|
)
|
||||||
|
|
||||||
themes = _theme_clusters(feed)
|
themes = theme_clusters(feed, theme_rules=THEME_RULES, skill_id_fn=_skill_id)
|
||||||
|
|
||||||
|
pick_src = trending[0].get("source", "") if trending else ""
|
||||||
|
pick_name = trending[0].get("title", "") if trending else ""
|
||||||
|
pick_command = pick_command_from_featured(featured) or (
|
||||||
|
f"npx skills add {pick_src}/{pick_name}"
|
||||||
|
if pick_src and pick_name
|
||||||
|
else "npx skills add vercel-labs/skills/find-skills"
|
||||||
|
)
|
||||||
|
pick_why = pick_why_from_featured(featured) or ""
|
||||||
|
pick_title = str((featured or {}).get("title") or pick_name or "").strip()
|
||||||
|
pick_url = str((featured or {}).get("url") or "").strip()
|
||||||
|
|
||||||
|
gt = selected_trending
|
||||||
|
gh = selected_hot
|
||||||
|
gt_pad = board_select(
|
||||||
|
board="skills_trending",
|
||||||
|
items=trending,
|
||||||
|
recent_keys=skill_recent,
|
||||||
|
limit=pad_pool,
|
||||||
|
pool_size=pool,
|
||||||
|
kind="skill",
|
||||||
|
)
|
||||||
|
gh_pad = board_select(
|
||||||
|
board="skills_hot",
|
||||||
|
items=hot,
|
||||||
|
recent_keys=skill_recent,
|
||||||
|
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]
|
||||||
|
github_pad_recent = (
|
||||||
|
recent_shown["github_trending"]
|
||||||
|
| recent_shown["github_emerging"]
|
||||||
|
| recent_shown["github_topic"]
|
||||||
|
)
|
||||||
|
wecom_github_pad = [
|
||||||
|
_prepare_github_item(item)
|
||||||
|
for item in board_select(
|
||||||
|
board="github_trending",
|
||||||
|
items=github_trending,
|
||||||
|
recent_keys=github_pad_recent,
|
||||||
|
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=github_pad_recent,
|
||||||
|
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=github_pad_recent,
|
||||||
|
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": c["wecom_trending"],
|
||||||
|
"wecom_hot": c["wecom_hot"],
|
||||||
|
"wecom_github": c["wecom_github"],
|
||||||
|
"wecom_emerging": c["wecom_emerging"],
|
||||||
|
"wecom_topic": c["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,
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"selected_trending": selected_trending,
|
||||||
|
"selected_hot": selected_hot,
|
||||||
|
"boards_for_wecom": boards_for_wecom,
|
||||||
|
"llm_input": llm_input,
|
||||||
|
"featured": featured,
|
||||||
|
"movement": movement,
|
||||||
|
"eff_mode": eff_mode,
|
||||||
|
"push_gate": push_gate,
|
||||||
|
"wecom_ai": wecom_ai,
|
||||||
|
"wecom_cn": wecom_cn,
|
||||||
|
"agent_wecom": agent_wecom,
|
||||||
|
"editorial_theme": editorial_theme,
|
||||||
|
"editorial_highlights": editorial_highlights,
|
||||||
|
"themes": themes,
|
||||||
|
"pick_command": pick_command,
|
||||||
|
"pick_why": pick_why,
|
||||||
|
"pick_title": pick_title,
|
||||||
|
"pick_url": pick_url,
|
||||||
|
"board_kwargs": board_kwargs,
|
||||||
|
"prev_ids": prev_ids,
|
||||||
|
"gt": gt,
|
||||||
|
"gh": gh,
|
||||||
|
"wecom_github_items": wecom_github_items,
|
||||||
|
"wecom_emerging_items": wecom_emerging_items,
|
||||||
|
"wecom_topic_items": wecom_topic_items,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _render(c: dict[str, Any], s: dict[str, Any]) -> tuple[str, str, Path, Path]:
|
||||||
|
"""纯拼装: 拼完整版 markdown + 企微 wecom_md + 写盘。
|
||||||
|
|
||||||
|
零选择逻辑。副作用: record_pushed_links + _save_snapshot + save_json + 写文件,
|
||||||
|
全部保持原时序(在拼装完成后执行)。
|
||||||
|
"""
|
||||||
|
feed = c["feed"]
|
||||||
|
now = c["now"]
|
||||||
|
date_str = c["date_str"]
|
||||||
|
time_str = c["time_str"]
|
||||||
|
updated = c["updated"]
|
||||||
|
trending = c["trending"]
|
||||||
|
hot = c["hot"]
|
||||||
|
github_trending = c["github_trending"]
|
||||||
|
github_emerging = c["github_emerging"]
|
||||||
|
github_topic = c["github_topic"]
|
||||||
|
topic_name = c["topic_name"]
|
||||||
|
news_merged = c["news_merged"]
|
||||||
|
ai_news = c["ai_news"]
|
||||||
|
cn_ai_news = c["cn_ai_news"]
|
||||||
|
ai_news_research = c["ai_news_research"]
|
||||||
|
wecom_news = c["wecom_news"]
|
||||||
|
wecom_tech_news = c["wecom_tech_news"]
|
||||||
|
trending_n = c["trending_n"]
|
||||||
|
hot_n = c["hot_n"]
|
||||||
|
github_limit = c["github_limit"]
|
||||||
|
emerging_limit = c["emerging_limit"]
|
||||||
|
topic_limit = c["topic_limit"]
|
||||||
|
prev_ids = s["prev_ids"]
|
||||||
|
llm_input = s["llm_input"]
|
||||||
|
featured = s["featured"]
|
||||||
|
eff_mode = s["eff_mode"]
|
||||||
|
push_gate = s["push_gate"]
|
||||||
|
wecom_ai = s["wecom_ai"]
|
||||||
|
wecom_cn = s["wecom_cn"]
|
||||||
|
agent_wecom = s["agent_wecom"]
|
||||||
|
editorial_theme = s["editorial_theme"]
|
||||||
|
editorial_highlights = s["editorial_highlights"]
|
||||||
|
themes = s["themes"]
|
||||||
|
pick_command = s["pick_command"]
|
||||||
|
pick_why = s["pick_why"]
|
||||||
|
pick_title = s["pick_title"]
|
||||||
|
pick_url = s["pick_url"]
|
||||||
|
board_kwargs = s["board_kwargs"]
|
||||||
|
gt = s["gt"]
|
||||||
|
gh = s["gh"]
|
||||||
|
wecom_github_items = s["wecom_github_items"]
|
||||||
|
wecom_emerging_items = s["wecom_emerging_items"]
|
||||||
|
wecom_topic_items = s["wecom_topic_items"]
|
||||||
|
|
||||||
lines = [
|
lines = [
|
||||||
f"# 早报 · {date_str}",
|
f"# 早报 · {date_str}",
|
||||||
@@ -834,17 +1087,6 @@ def generate_report() -> tuple[str, str, Path, Path]:
|
|||||||
lines.append(f"- {ex}")
|
lines.append(f"- {ex}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
pick_src = trending[0].get("source", "") if trending else ""
|
|
||||||
pick_name = trending[0].get("title", "") if trending else ""
|
|
||||||
pick_command = pick_command_from_featured(featured) or (
|
|
||||||
f"npx skills add {pick_src}/{pick_name}"
|
|
||||||
if pick_src and pick_name
|
|
||||||
else "npx skills add vercel-labs/skills/find-skills"
|
|
||||||
)
|
|
||||||
pick_why = pick_why_from_featured(featured) or ""
|
|
||||||
pick_title = str((featured or {}).get("title") or pick_name or "").strip()
|
|
||||||
pick_url = str((featured or {}).get("url") or "").strip()
|
|
||||||
|
|
||||||
lines.extend(["---", "", "## 安装示例", "", "```bash"])
|
lines.extend(["---", "", "## 安装示例", "", "```bash"])
|
||||||
for item in trending[:4]:
|
for item in trending[:4]:
|
||||||
src, name = item.get("source", ""), item.get("title", "")
|
src, name = item.get("source", ""), item.get("title", "")
|
||||||
@@ -853,88 +1095,7 @@ 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 = selected_trending
|
|
||||||
gh = selected_hot
|
|
||||||
gt_pad = board_select(
|
|
||||||
board="skills_trending",
|
|
||||||
items=trending,
|
|
||||||
recent_keys=skill_recent,
|
|
||||||
limit=pad_pool,
|
|
||||||
pool_size=pool,
|
|
||||||
kind="skill",
|
|
||||||
)
|
|
||||||
gh_pad = board_select(
|
|
||||||
board="skills_hot",
|
|
||||||
items=hot,
|
|
||||||
recent_keys=skill_recent,
|
|
||||||
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]
|
|
||||||
github_pad_recent = (
|
|
||||||
recent_shown["github_trending"]
|
|
||||||
| recent_shown["github_emerging"]
|
|
||||||
| recent_shown["github_topic"]
|
|
||||||
)
|
|
||||||
wecom_github_pad = [
|
|
||||||
_prepare_github_item(item)
|
|
||||||
for item in board_select(
|
|
||||||
board="github_trending",
|
|
||||||
items=github_trending,
|
|
||||||
recent_keys=github_pad_recent,
|
|
||||||
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=github_pad_recent,
|
|
||||||
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=github_pad_recent,
|
|
||||||
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:
|
if agent_wecom:
|
||||||
wecom_md = replace_wecom_skill_sections(agent_wecom, **board_kwargs)
|
wecom_md = replace_wecom_skill_sections(agent_wecom, **board_kwargs)
|
||||||
else:
|
else:
|
||||||
@@ -944,7 +1105,7 @@ def generate_report() -> tuple[str, str, Path, Path]:
|
|||||||
updated=updated,
|
updated=updated,
|
||||||
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 _top_line(feed),
|
||||||
ai_news=wecom_ai if not news_merged else None,
|
ai_news=wecom_ai if not news_merged else None,
|
||||||
cn_ai_news=wecom_cn 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_ai_news=wecom_news if news_merged else None,
|
||||||
@@ -1020,6 +1181,13 @@ def generate_report() -> tuple[str, str, Path, Path]:
|
|||||||
return markdown, wecom_md, out_md, out_wecom
|
return markdown, wecom_md, out_md, out_wecom
|
||||||
|
|
||||||
|
|
||||||
|
def generate_report() -> tuple[str, str, Path, Path]:
|
||||||
|
"""编排三段: 抓取(_collect) → 选择(_select) → 拼装(_render)。"""
|
||||||
|
collected = _collect(_now_cst().strftime("%Y-%m-%d"))
|
||||||
|
selected = _select(collected)
|
||||||
|
return _render(collected, selected)
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
log_file = LOG_DIR / f"{_now_cst():%Y-%m-%d}.log"
|
log_file = LOG_DIR / f"{_now_cst():%Y-%m-%d}.log"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
|
from collections import defaultdict
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -101,3 +102,48 @@ def enforce_narrative_axis(trends: dict[str, Any], axis: str) -> dict[str, Any]:
|
|||||||
out = dict(trends)
|
out = dict(trends)
|
||||||
out["narrative_axis"] = axis
|
out["narrative_axis"] = axis
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def theme_clusters(
|
||||||
|
feed: dict[str, Any],
|
||||||
|
*,
|
||||||
|
limit: int = 5,
|
||||||
|
theme_rules: list[tuple[str, str, list[str]]],
|
||||||
|
skill_id_fn: Any,
|
||||||
|
) -> list[tuple[str, list[str]]]:
|
||||||
|
"""按 THEME_RULES 把 feed 的 topTrending/topHot 聚成 (主题, 示例列表)。
|
||||||
|
|
||||||
|
从 generate.py 迁入(原私有 _theme_clusters)。theme_rules 与 skill_id_fn
|
||||||
|
由调用方注入,避免对 generate.py 的反向依赖(防循环 import)。
|
||||||
|
"""
|
||||||
|
buckets: dict[str, list[str]] = defaultdict(list)
|
||||||
|
seen: set[str] = set()
|
||||||
|
for board in ("topTrending", "topHot"):
|
||||||
|
for item in feed.get(board, [])[:20]:
|
||||||
|
item_id = skill_id_fn(item)
|
||||||
|
if item_id in seen:
|
||||||
|
continue
|
||||||
|
seen.add(item_id)
|
||||||
|
haystack = " ".join(
|
||||||
|
[item.get("title", ""), item.get("source", ""), item.get("description", "")]
|
||||||
|
).lower()
|
||||||
|
for _icon, theme, keywords in theme_rules:
|
||||||
|
if any(k in haystack for k in keywords):
|
||||||
|
label = f"**{item.get('title')}** (`{item.get('source')}`)"
|
||||||
|
if label not in buckets[theme]:
|
||||||
|
buckets[theme].append(label)
|
||||||
|
break
|
||||||
|
return [(theme, examples[:limit]) for theme, examples in buckets.items() if examples]
|
||||||
|
|
||||||
|
|
||||||
|
def theme_names(
|
||||||
|
feed: dict[str, Any],
|
||||||
|
*,
|
||||||
|
theme_rules: list[tuple[str, str, list[str]]],
|
||||||
|
skill_id_fn: Any,
|
||||||
|
limit: int = 3,
|
||||||
|
) -> list[str]:
|
||||||
|
"""仅取主题名(不含 markdown 示例),供「今日看点/theme_line」回退文案。"""
|
||||||
|
return [theme for theme, _ in theme_clusters(
|
||||||
|
feed, theme_rules=theme_rules, skill_id_fn=skill_id_fn
|
||||||
|
)][:limit]
|
||||||
|
|||||||
@@ -2,11 +2,14 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from daily.config import force_push, skip_push_when_silent
|
from daily.config import force_push, skip_push_when_silent
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PushGateResult:
|
class PushGateResult:
|
||||||
@@ -34,6 +37,7 @@ def evaluate_push_gate(
|
|||||||
featured_pick: dict[str, Any] | None,
|
featured_pick: dict[str, Any] | None,
|
||||||
) -> PushGateResult:
|
) -> PushGateResult:
|
||||||
if force_push():
|
if force_push():
|
||||||
|
logger.info("push_gate: force_push=on, 强制推送")
|
||||||
return PushGateResult(should_push=True, reasons=["force_push"], silent=False)
|
return PushGateResult(should_push=True, reasons=["force_push"], silent=False)
|
||||||
|
|
||||||
reasons: list[str] = []
|
reasons: list[str] = []
|
||||||
@@ -48,4 +52,10 @@ def evaluate_push_gate(
|
|||||||
|
|
||||||
should = bool(reasons)
|
should = bool(reasons)
|
||||||
silent = not should and skip_push_when_silent()
|
silent = not should and skip_push_when_silent()
|
||||||
|
if should:
|
||||||
|
logger.info("push_gate: 推送 (原因: %s)", ",".join(reasons))
|
||||||
|
elif silent:
|
||||||
|
logger.info("push_gate: 静默日, 跳过推送 (无任何更新信号)")
|
||||||
|
else:
|
||||||
|
logger.info("push_gate: 无更新但 skip_push_when_silent=off, 仍推送")
|
||||||
return PushGateResult(should_push=should, reasons=reasons, silent=silent)
|
return PushGateResult(should_push=should, reasons=reasons, silent=silent)
|
||||||
|
|||||||
55
tests/test_featured_reason.py
Normal file
55
tests/test_featured_reason.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# tests/test_featured_reason.py
|
||||||
|
"""T6: 首推理由行(pick_why + DAILY_FEATURED_REASON 开关)测试。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import daily.format_wecom as fw
|
||||||
|
|
||||||
|
|
||||||
|
def _build(pick_why: str) -> str:
|
||||||
|
return fw.build_wecom_report(
|
||||||
|
date_str="2026-07-18",
|
||||||
|
time_str="08:50 (UTC+8)",
|
||||||
|
updated="2026-07-18",
|
||||||
|
highlights=[],
|
||||||
|
theme_line="**今日主题**:测试",
|
||||||
|
ai_news=None,
|
||||||
|
cn_ai_news=None,
|
||||||
|
merged_ai_news=None,
|
||||||
|
merged_tech_ai_news=None,
|
||||||
|
trending=[],
|
||||||
|
hot=[],
|
||||||
|
repos=[],
|
||||||
|
emerging=[],
|
||||||
|
topic_name="t",
|
||||||
|
topic_repos=[],
|
||||||
|
pick_command="npx skills add src/alpha",
|
||||||
|
pick_why=pick_why,
|
||||||
|
pick_title="alpha",
|
||||||
|
pick_url="https://x/a",
|
||||||
|
include_boards=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FeaturedReasonTests(unittest.TestCase):
|
||||||
|
def test_reason_shown_when_present_and_enabled(self):
|
||||||
|
with mock.patch.object(fw, "env_bool", return_value=True):
|
||||||
|
md = _build("昨日 star 增速第一")
|
||||||
|
self.assertIn("> 昨日 star 增速第一", md)
|
||||||
|
|
||||||
|
def test_reason_hidden_when_switch_off(self):
|
||||||
|
with mock.patch.object(fw, "env_bool", return_value=False):
|
||||||
|
md = _build("昨日 star 增速第一")
|
||||||
|
self.assertNotIn("> 昨日 star 增速第一", md)
|
||||||
|
|
||||||
|
def test_reason_omitted_when_empty(self):
|
||||||
|
with mock.patch.object(fw, "env_bool", return_value=True):
|
||||||
|
md = _build("")
|
||||||
|
self.assertIn("今日首推", md)
|
||||||
|
self.assertNotIn("> \n", md)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
103
tests/test_generate_golden.py
Normal file
103
tests/test_generate_golden.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
# tests/test_generate_golden.py
|
||||||
|
"""黄金文件/确定性回归: 冻结时间、mock 网络与 LLM, 验证 generate_report
|
||||||
|
在同一输入下产出逐字节一致(拆分 _collect/_select/_render 不得改变行为)。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import daily.generate as g
|
||||||
|
|
||||||
|
FIXED_NOW = datetime(2026, 7, 18, 8, 50, tzinfo=timezone(timedelta(hours=8)))
|
||||||
|
|
||||||
|
|
||||||
|
def _feed() -> dict:
|
||||||
|
return {
|
||||||
|
"updatedAt": "2026-07-18T08:00:00",
|
||||||
|
"topTrending": [
|
||||||
|
{"id": "a", "source": "src", "title": "alpha", "description": "desc a",
|
||||||
|
"installs": 100, "link": "https://x/a"},
|
||||||
|
{"id": "b", "source": "src", "title": "beta", "description": "desc b",
|
||||||
|
"installs": 90, "link": "https://x/b"},
|
||||||
|
],
|
||||||
|
"topHot": [
|
||||||
|
{"id": "c", "source": "src", "title": "gamma", "description": "desc c",
|
||||||
|
"installs": 80, "link": "https://x/c"},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _patches(tmp: Path):
|
||||||
|
"""集中 mock 所有外部边界: 网络/文件/时间/LLM。返回 patcher 列表。"""
|
||||||
|
return [
|
||||||
|
mock.patch.object(g, "_now_cst", return_value=FIXED_NOW),
|
||||||
|
mock.patch.object(g, "load_feed", return_value=_feed()),
|
||||||
|
mock.patch.object(g, "_load_snapshot", return_value=set()),
|
||||||
|
mock.patch.object(g, "_save_snapshot", lambda *a, **k: None),
|
||||||
|
mock.patch.object(g, "load_boards", return_value=(
|
||||||
|
_feed()["topTrending"], _feed()["topHot"])),
|
||||||
|
mock.patch.object(g, "fetch_github_trending", return_value=[]),
|
||||||
|
mock.patch.object(g, "fetch_emerging_repos", return_value=[]),
|
||||||
|
mock.patch.object(g, "fetch_topic_hot_repos", return_value=("topic", [])),
|
||||||
|
mock.patch.object(g, "fetch_ai_news", return_value={
|
||||||
|
"enabled": False, "categories": [], "flat": [], "stats": {}}),
|
||||||
|
mock.patch.object(g, "fetch_cn_ai_news", return_value={
|
||||||
|
"enabled": False, "categories": [], "flat": [], "stats": {}}),
|
||||||
|
mock.patch.object(g, "is_research_mode", return_value=False),
|
||||||
|
mock.patch.object(g, "is_agent_mode", return_value=False),
|
||||||
|
mock.patch.object(g, "run_editorial", return_value=None),
|
||||||
|
mock.patch.object(g, "cursor_editor_enabled", return_value=False),
|
||||||
|
mock.patch.object(g, "record_pushed_links", lambda *a, **k: None),
|
||||||
|
mock.patch.object(g, "save_json", lambda *a, **k: None),
|
||||||
|
mock.patch.object(g, "load_recent_shown_keys", return_value={
|
||||||
|
"skills_trending": set(), "skills_hot": set(),
|
||||||
|
"github_trending": set(), "github_emerging": set(),
|
||||||
|
"github_topic": set()}),
|
||||||
|
mock.patch.object(g, "OUTPUT_DIR", tmp),
|
||||||
|
mock.patch.object(g, "_localize_descriptions_in_place", lambda *a, **k: None),
|
||||||
|
# featured_pick / push_gate 有独立测试;此处冻结以免触网(LLM)与读盘。
|
||||||
|
mock.patch.object(g, "apply_featured_pick", return_value={
|
||||||
|
"title": "alpha", "url": "https://x/a",
|
||||||
|
"command": "npx skills add src/alpha", "why": "昨日 star 增速第一"}),
|
||||||
|
mock.patch.object(g, "evaluate_push_gate", return_value=mock.Mock(
|
||||||
|
should_push=False, silent=True, reasons=["golden-mock"])),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class GenerateGoldenTests(unittest.TestCase):
|
||||||
|
def _run(self, tmp: Path) -> tuple[str, str]:
|
||||||
|
patches = _patches(tmp)
|
||||||
|
for p in patches:
|
||||||
|
p.start()
|
||||||
|
try:
|
||||||
|
markdown, wecom_md, _md, _we = g.generate_report()
|
||||||
|
return markdown, wecom_md
|
||||||
|
finally:
|
||||||
|
for p in patches:
|
||||||
|
try:
|
||||||
|
p.stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_deterministic_same_input_same_output(self):
|
||||||
|
import tempfile
|
||||||
|
with tempfile.TemporaryDirectory() as d1, tempfile.TemporaryDirectory() as d2:
|
||||||
|
md1, we1 = self._run(Path(d1))
|
||||||
|
md2, we2 = self._run(Path(d2))
|
||||||
|
self.assertEqual(md1, md2, "完整版 markdown 在相同输入下必须逐字节一致")
|
||||||
|
self.assertEqual(we1, we2, "企微 wecom_md 在相同输入下必须逐字节一致")
|
||||||
|
|
||||||
|
def test_output_contains_core_sections(self):
|
||||||
|
import tempfile
|
||||||
|
with tempfile.TemporaryDirectory() as d:
|
||||||
|
markdown, wecom_md = self._run(Path(d))
|
||||||
|
self.assertIn("# 早报 · 2026-07-18", markdown)
|
||||||
|
self.assertIn("主题聚类", markdown)
|
||||||
|
self.assertIsInstance(wecom_md, str)
|
||||||
|
self.assertTrue(len(wecom_md) > 0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
60
tests/test_top_line.py
Normal file
60
tests/test_top_line.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# tests/test_top_line.py
|
||||||
|
"""T5: _top_line 看点行(theme_line 取数升级)测试。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import daily.generate as g
|
||||||
|
|
||||||
|
FEED_HIT = {
|
||||||
|
"topTrending": [
|
||||||
|
{"title": "remotion-video", "source": "src", "description": "video tool"},
|
||||||
|
],
|
||||||
|
"topHot": [],
|
||||||
|
}
|
||||||
|
FEED_MISS = {
|
||||||
|
"topTrending": [
|
||||||
|
{"title": "zzz-nomatch", "source": "src", "description": "nothing"},
|
||||||
|
],
|
||||||
|
"topHot": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TopLineTests(unittest.TestCase):
|
||||||
|
def test_scores_hit_returns_theme(self):
|
||||||
|
# 命中 THEME_RULES(video) -> 评分最高主题
|
||||||
|
with mock.patch.object(g, "env_bool", return_value=True):
|
||||||
|
line = g._top_line(FEED_HIT)
|
||||||
|
self.assertIn("今日主题", line)
|
||||||
|
self.assertIn("AI 多媒体", line)
|
||||||
|
|
||||||
|
def test_fallback_to_theme_names_when_no_score(self):
|
||||||
|
# 无评分命中但 theme_clusters 能聚类 -> 用主题名
|
||||||
|
feed = {
|
||||||
|
"topTrending": [
|
||||||
|
{"id": "x", "title": "runcomfy-x", "source": "s", "description": "video x"},
|
||||||
|
],
|
||||||
|
"topHot": [],
|
||||||
|
}
|
||||||
|
with mock.patch.object(g, "env_bool", return_value=True):
|
||||||
|
# 让评分落空(前 10 无命中)但 clusters(前 20)命中
|
||||||
|
line = g._top_line(feed)
|
||||||
|
self.assertIn("今日主题", line)
|
||||||
|
self.assertIn("AI 多媒体", line)
|
||||||
|
|
||||||
|
def test_switch_off_uses_legacy_detect(self):
|
||||||
|
# DAILY_WECOM_TOP_LINE=0 -> 退回 _detect_theme_line
|
||||||
|
with mock.patch.object(g, "env_bool", return_value=False):
|
||||||
|
line = g._top_line(FEED_MISS)
|
||||||
|
self.assertEqual(line, g._detect_theme_line(FEED_MISS))
|
||||||
|
|
||||||
|
def test_empty_feed_hardcoded_fallback(self):
|
||||||
|
# 完全无数据 -> 硬编码兜底, 不抛异常
|
||||||
|
with mock.patch.object(g, "env_bool", return_value=True):
|
||||||
|
line = g._top_line({"topTrending": [], "topHot": []})
|
||||||
|
self.assertIn("今日主题", line)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import re
|
|
||||||
|
|
||||||
import certifi
|
|
||||||
import httpx
|
|
||||||
|
|
||||||
url = "https://skills.sh/vercel-labs/skills/find-skills"
|
|
||||||
r = httpx.get(url, headers={"User-Agent": "Mozilla/5.0"}, timeout=20, verify=certifi.where())
|
|
||||||
chunks = re.findall(r'self\.__next_f\.push\(\[1,"(.*?)"\]\)', r.text, re.DOTALL)
|
|
||||||
blob = "\n".join(chunks).encode("utf-8").decode("unicode_escape", errors="ignore")
|
|
||||||
for needle in ("description", "Helps users", "SKILL.md", "summary"):
|
|
||||||
print(needle, blob.count(needle))
|
|
||||||
idx = blob.find("Helps users")
|
|
||||||
if idx >= 0:
|
|
||||||
print(blob[idx : idx + 300])
|
|
||||||
Binary file not shown.
Reference in New Issue
Block a user