"""LLM 设置相关 Pydantic schemas。""" from __future__ import annotations from datetime import datetime from pydantic import BaseModel, ConfigDict, Field class LlmSettingOut(BaseModel): model_config = ConfigDict(from_attributes=True) format_prompt: str | None = None classify_prompt: str | None = None commentary_prompt: str | None = None image_prompt_template: str | None = None image_size: str = "768x512" chat_model: str = "agnes-2.0-flash" image_model: str = "agnes-image-2.1-flash" interval_sec: float = 2.0 enabled: bool = True # 全局屏蔽分类标签;与 sources.blocklist_tags 合并后注入 classify prompt blocklist_tags: list[str] = [] # === Angel(Agnes)凭据 — DB 存,优先于 .env === # 安全:不回传明文,只回 agnes_api_key_set (True = DB 已有 key) agnes_api_key_set: bool = False agnes_base_url_override: str = "" # 留空 = 用 .env # 美团大模型(LongCat,OpenAI 兼容)双 provider 评论 # 安全:不回传 api_key 真值,只回传 meituan_api_key_set 表示"是否已配置" meituan_api_key_set: bool = False meituan_base_url: str = "https://api.longcat.chat/openai/v1" meituan_chat_model: str = "LongCat-2.0-Preview" meituan_interval_sec: float = 2.0 meituan_enabled: bool = True meituan_commentary_prompt: str | None = None # 美团"无可奉告"主题清单(命中则不调美团 API,直接写固定文案) meituan_blocked_topics: list[str] = [] meituan_blocked_keywords: list[str] = [] meituan_no_comment_text: str = "无可奉告" updated_at: datetime | None = None @classmethod def from_row(cls, row) -> "LlmSettingOut": """从 LlmSetting 构造,key 字段转 bool。""" return cls( format_prompt=row.format_prompt, classify_prompt=row.classify_prompt, commentary_prompt=row.commentary_prompt, image_prompt_template=row.image_prompt_template, image_size=row.image_size, chat_model=row.chat_model, image_model=row.image_model, interval_sec=row.interval_sec, enabled=row.enabled, blocklist_tags=row.blocklist_tags or [], # === key 安全转换 === agnes_api_key_set=bool(row.agnes_api_key), agnes_base_url_override=row.agnes_base_url_override or "", meituan_api_key_set=bool(row.meituan_api_key), meituan_base_url=row.meituan_base_url, meituan_chat_model=row.meituan_chat_model, meituan_interval_sec=row.meituan_interval_sec, meituan_enabled=row.meituan_enabled, meituan_commentary_prompt=row.meituan_commentary_prompt, meituan_blocked_topics=row.meituan_blocked_topics or [], meituan_blocked_keywords=row.meituan_blocked_keywords or [], meituan_no_comment_text=row.meituan_no_comment_text or "无可奉告", updated_at=row.updated_at, ) class LlmSettingUpdate(BaseModel): """PATCH — 全部字段 optional,只更新传入的。""" format_prompt: str | None = None classify_prompt: str | None = None commentary_prompt: str | None = None image_prompt_template: str | None = None image_size: str | None = Field(default=None, pattern=r"^\d{2,4}x\d{2,4}$") chat_model: str | None = Field(default=None, min_length=1, max_length=64) image_model: str | None = Field(default=None, min_length=1, max_length=64) interval_sec: float | None = Field(default=None, ge=0.0, le=60.0) enabled: bool | None = None blocklist_tags: list[str] | None = None # === Angel provider 字段 === agnes_api_key: str | None = Field(default=None, max_length=512) agnes_base_url_override: str | None = Field(default=None, max_length=255) # 美团 provider 字段(api_key 可更新;None/空 = 不修改;显式传空字符串 = 清空) meituan_api_key: str | None = Field(default=None, max_length=512) meituan_base_url: str | None = Field(default=None, max_length=255) meituan_chat_model: str | None = Field(default=None, max_length=64) meituan_interval_sec: float | None = Field(default=None, ge=0.0, le=60.0) meituan_enabled: bool | None = None meituan_commentary_prompt: str | None = None meituan_blocked_topics: list[str] | None = None meituan_blocked_keywords: list[str] | None = None meituan_no_comment_text: str | None = None # === 默认提示词(模板,用户可改)=== DEFAULT_PROMPTS = { "format_prompt": ( "你是中文新闻排版助手。请将以下译文改写为适合网页阅读的版式,要求:\n" "1. 保留所有事实信息,不要增删内容\n" "2. 按段落拆分(2-4 句一段),段间空行\n" "3. 关键人物/机构/数字用 **加粗**\n" "4. 如有并列要点,转为编号列表(1. 2. 3.)\n" "5. 不要使用 # 标题,不要外层 markdown 代码块\n" "6. 直接输出排版后的纯文本\n\n" "原文:\n{body}\n" ), "classify_prompt": ( "你是新闻分类助手。请阅读以下新闻,返回 2-5 个最相关的分类标签(多标签)。\n" "可选标签(可自由组合,不限于此): 时政 / 经济 / 科技 / 军事 / 社会 / 国际 / 体育 / 文化 / 环境 / 健康 / 金融 / 能源 / 气候\n" "黑名单分类(若新闻属于或主要围绕这些领域,务必将 drop 设为 true): {blocklist}\n" "严格要求:只返回 JSON,形如 {\"categories\": [\"时政\", \"国际\", \"经济\"], \"drop\": false}," "若新闻属于或主要围绕黑名单中的任何分类,将 drop 设为 true 并把该分类放入 categories。" "不要其他内容。\n\n" "标题:{title}\n摘要:{summary}\n正文(节选):{body}\n" ), "commentary_prompt": ( "你是资深新闻评论员。请基于以下新闻写一段 100-200 字的中文点评。\n" "要求:客观、有深度、避免空洞套话,给出具体观察或背景。\n\n" "标题:{title}\n正文:{body}\n" ), "image_prompt_template": ( "Editorial news illustration inspired by: {body}. " "Cinematic, professional journalism style, soft natural lighting, " "no text, no logos, no watermark." ), } def get_default_prompts() -> dict[str, str]: return dict(DEFAULT_PROMPTS)