Files
daily-robots/daily/cursor_editor.py
2026-07-02 11:31:16 +08:00

145 lines
4.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Cursor 编辑层JSON 数据 → 主题 / 速览 / 中文描述。"""
from __future__ import annotations
import json
import logging
from pathlib import Path
from typing import Any
from daily.config import ROOT, env
from daily.llm_client import extract_json_object, has_llm_configured, llm_chat
from daily.text_utils import clip_text
from daily.report_data import editorial_json_path, save_json, skill_id
logger = logging.getLogger(__name__)
_SKILL_DIR = ROOT / "skills" / "daily-editor"
def is_enabled() -> bool:
raw = (env("DAILY_CURSOR_EDITOR") or "").strip().lower()
if raw in {"1", "true", "yes", "on"}:
return has_llm_configured()
if raw in {"0", "false", "no", "off"}:
return False
return False
def _load_skill_prompt() -> str:
skill_path = _SKILL_DIR / "SKILL.md"
if skill_path.exists():
return skill_path.read_text(encoding="utf-8").strip()
return "你是技术早报编辑。根据输入 JSON 输出编辑结果 JSON。"
def _build_system_prompt() -> str:
skill = _load_skill_prompt()
return (
f"{skill}\n\n"
"再次强调:只输出 JSON 对象,包含 theme_line、highlights3条、descriptions。"
)
def run_editorial(llm_input: dict[str, Any], *, date_str: str) -> dict[str, Any] | None:
"""调用 LLM 生成 editorial失败返回 None。"""
if not is_enabled():
return None
system = _build_system_prompt()
user = json.dumps(llm_input, ensure_ascii=False, indent=2)
try:
raw = llm_chat(system, user)
except Exception as exc:
logger.warning("Cursor 编辑失败,回退规则模式:%s", exc)
return None
if not raw:
logger.warning("Cursor 编辑无响应,回退规则模式")
return None
parsed = extract_json_object(raw)
if not parsed.get("theme_line") and not parsed.get("descriptions"):
logger.warning("Cursor 编辑 JSON 无效,回退规则模式")
return None
editorial = _normalize_editorial(parsed)
save_json(editorial_json_path(date_str), editorial)
return editorial
def _normalize_editorial(raw: dict[str, Any]) -> dict[str, Any]:
theme = str(raw.get("theme_line") or "").strip()
highlights_raw = raw.get("highlights") or []
highlights: list[str] = []
if isinstance(highlights_raw, list):
for item in highlights_raw:
if isinstance(item, str) and item.strip():
highlights.append(item.strip())
descriptions_raw = raw.get("descriptions") or {}
descriptions: dict[str, str] = {}
if isinstance(descriptions_raw, dict):
for key, value in descriptions_raw.items():
if isinstance(value, str) and value.strip():
limit = 40 if str(key).startswith("github:") else 36
descriptions[str(key)] = clip_text(value, limit)
return {
"theme_line": theme,
"highlights": highlights[:3],
"descriptions": descriptions,
}
def theme_line_from_editorial(editorial: dict[str, Any]) -> str:
theme = editorial.get("theme_line", "")
if not theme:
return ""
if "今日主题" in theme:
return theme if theme.startswith("**") else f"**{theme}**"
return f"**今日主题**{theme}"
def apply_descriptions(
*,
trending: list[dict[str, Any]],
hot: list[dict[str, Any]],
github_trending: list[dict[str, Any]],
github_emerging: list[dict[str, Any]],
github_topic: list[dict[str, Any]],
ai_news: dict[str, Any],
descriptions: dict[str, str],
) -> None:
if not descriptions:
return
for item in trending + hot:
key = f"skill:{skill_id(item)}"
if key in descriptions:
item["description"] = descriptions[key]
for repo_list in (github_trending, github_emerging, github_topic):
for item in repo_list:
key = f"github:{item.get('repo', '')}"
if key in descriptions:
item["description"] = descriptions[key]
if not ai_news.get("enabled"):
return
def _apply_news_item(item: dict[str, Any]) -> None:
key = f"news:{item.get('link', '')}"
if key in descriptions:
item["summary"] = descriptions[key]
for cat in ai_news.get("categories") or []:
for item in cat.get("items") or []:
_apply_news_item(item)
for item in ai_news.get("flat") or []:
_apply_news_item(item)
def load_cached_editorial(date_str: str) -> dict[str, Any] | None:
path = editorial_json_path(date_str)
if not path.exists():
return None
try:
return _normalize_editorial(json.loads(path.read_text(encoding="utf-8")))
except (OSError, json.JSONDecodeError):
return None