项目初始化

This commit is contained in:
2026-07-02 11:31:16 +08:00
commit eef4c76e3f
64 changed files with 13369 additions and 0 deletions

144
daily/cursor_editor.py Normal file
View File

@@ -0,0 +1,144 @@
"""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