diff --git a/backend/alembic/versions/0006_meituan_blocked_topics.py b/backend/alembic/versions/0006_meituan_blocked_topics.py new file mode 100644 index 0000000..9a1709b --- /dev/null +++ b/backend/alembic/versions/0006_meituan_blocked_topics.py @@ -0,0 +1,64 @@ +"""美团"无可奉告"主题清单 — 命中则不调美团 API,直接写固定文案 + +- llm_settings.meituan_blocked_topics JSONB (默认 ["时政", "国际", "军事", "政治", "战争", "冲突", "制裁", "选举"]) +- llm_settings.meituan_blocked_keywords JSONB (默认 []) +- llm_settings.meituan_no_comment_text TEXT (默认 "无可奉告") + +Revision ID: 0006 +Revises: 0005 +Create Date: 2026-06-12 +""" +from __future__ import annotations + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects.postgresql import JSONB + +revision: str = "0006" +down_revision: Union[str, None] = "0005" +branch_labels = None +depends_on = None + + +DEFAULT_TOPICS = ["时政", "国际", "军事", "政治", "战争", "冲突", "制裁", "选举"] + + +def upgrade() -> None: + import json + default_topics_json = sa.text(repr(json.dumps(DEFAULT_TOPICS, ensure_ascii=False))) + + op.add_column( + "llm_settings", + sa.Column( + "meituan_blocked_topics", + JSONB, + nullable=False, + server_default=default_topics_json, + ), + ) + op.add_column( + "llm_settings", + sa.Column( + "meituan_blocked_keywords", + JSONB, + nullable=False, + server_default=sa.text("'[]'::jsonb"), + ), + ) + op.add_column( + "llm_settings", + sa.Column( + "meituan_no_comment_text", + sa.Text, + nullable=False, + server_default="无可奉告", + ), + ) + + +def downgrade() -> None: + op.drop_column("llm_settings", "meituan_no_comment_text") + op.drop_column("llm_settings", "meituan_blocked_keywords") + op.drop_column("llm_settings", "meituan_blocked_topics") diff --git a/backend/app/models/llm_setting.py b/backend/app/models/llm_setting.py index b9946cf..5760976 100644 --- a/backend/app/models/llm_setting.py +++ b/backend/app/models/llm_setting.py @@ -71,6 +71,20 @@ class LlmSetting(Base): meituan_interval_sec: Mapped[float] = mapped_column(default=2.0, nullable=False) meituan_enabled: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) meituan_commentary_prompt: Mapped[str | None] = mapped_column(Text) # 留空用默认 + # 美团"无可奉告"主题清单 — 命中这些 topic 的文章,直接写"无可奉告" + 不调美团 API + # (避免 LongCat security_audit_fail 400 循环重试,污染评论失败率) + # 匹配规则:article.category 含任一项 OR 标题/正文命中关键词(宽松匹配) + meituan_blocked_topics: Mapped[list[str]] = mapped_column( + JSONB, nullable=False, default=list, server_default="[]" + ) + meituan_blocked_keywords: Mapped[list[str]] = mapped_column( + JSONB, nullable=False, default=list, server_default="[]" + ) + meituan_no_comment_text: Mapped[str] = mapped_column( + Text, + default="无可奉告", + nullable=False, + ) # === 时间 === updated_at: Mapped[datetime] = mapped_column( diff --git a/backend/app/schemas/llm.py b/backend/app/schemas/llm.py index 172dc25..3bd7010 100644 --- a/backend/app/schemas/llm.py +++ b/backend/app/schemas/llm.py @@ -32,6 +32,10 @@ class LlmSettingOut(BaseModel): 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 @@ -57,6 +61,9 @@ class LlmSettingOut(BaseModel): 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, ) @@ -84,6 +91,9 @@ class LlmSettingUpdate(BaseModel): 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 # === 默认提示词(模板,用户可改)=== diff --git a/backend/app/services/llm/enrichment.py b/backend/app/services/llm/enrichment.py index 857ed78..c2dc8e5 100644 --- a/backend/app/services/llm/enrichment.py +++ b/backend/app/services/llm/enrichment.py @@ -320,10 +320,55 @@ async def _enrich_commentary_angel( article.commentary_engine = ",".join(sorted(engines)) +# === 美团"无可奉告"预检 === +# 美团(LongCat)对战争/政治/敏感新闻会触发 security_audit_fail 400, +# 死循环重试不解决问题。命中"政治类"文章直接写固定文案,不调美团 API。 +# 命中规则: +# 1) article.category(LLM 已分类,逗号分隔)中含 meituan_blocked_topics 任一项 +# 2) 标题/正文命中 meituan_blocked_keywords 任一关键词(宽松包含) +def _is_meituan_blocked(article: Article, setting: LlmSetting) -> bool: + """返回 True 表示这篇应该被"无可奉告"处理(不调美团 API)。""" + topics = set(setting.meituan_blocked_topics or []) + keywords = [k for k in (setting.meituan_blocked_keywords or []) if k] + if topics: + cats = {c.strip() for c in (article.category or "").split(",") if c.strip()} + if cats & topics: + return True + if keywords: + text = " ".join( + (article.title_zh or ""), (article.title or ""), + (article.body_zh_text or "")[:1500], + ) + for kw in keywords: + if kw and kw in text: + return True + return False + + async def _enrich_commentary_meituan( article: Article, setting: LlmSetting, client: LlmClient ) -> None: - """美团评论 — 写入 commentary_meituan 等新字段。""" + """美团评论 — 写入 commentary_meituan 等新字段。 + + 预检:命中"政治类"主题的文章直接写"无可奉告"固定文案, + 不调美团 API(避免 LongCat security_audit_fail 400 循环)。 + """ + # === 预检:政治类文章写"无可奉告" === + if _is_meituan_blocked(article, setting): + no_text = setting.meituan_no_comment_text or "无可奉告" + article.commentary_meituan = no_text + article.commentary_meituan_status = "ok" + article.commentary_meituan_error = None + article.commentary_meituan_model = "policy-block" # 标识非真实生成 + engines = set(filter(None, (article.commentary_engine or "").split(","))) + engines.add(PROVIDER_MEITUAN) + article.commentary_engine = ",".join(sorted(engines)) + logger.info( + "commentary_meituan policy-block for article %s (category=%s)", + article.id, article.category, + ) + return + # 优先用 setting.meituan_commentary_prompt,留空用默认 template = setting.meituan_commentary_prompt or _default_commentary_prompt() prompt = _safe_format( @@ -548,7 +593,10 @@ async def enrichment_loop() -> None: # 并发 enrich 多篇(LlmClient 内部 interval_sec 已经做了限速,这里只并发不限并发上限) # 但为了不让 LLM API 同时打太多,加一层并发上限 - sem = asyncio.Semaphore(3) + # 2026-06-12 调:3→1。Agnes 免费 plan 在并发 3 时会偶发 429 + # (每篇 enrich 内部 Angel 跑 4 chat + 1 image,3 路并发 = 12+3 短时突发), + # 降到 1 路串行,稳定不超限,且不浪费 429 重试窗口。 + sem = asyncio.Semaphore(1) async def _run_one(aid: int) -> None: async with sem: try: diff --git a/frontend/src/api/articles.ts b/frontend/src/api/articles.ts index 6df94d5..670609c 100644 --- a/frontend/src/api/articles.ts +++ b/frontend/src/api/articles.ts @@ -102,6 +102,10 @@ export interface LlmSetting { meituan_interval_sec?: number meituan_enabled?: boolean meituan_commentary_prompt?: string | null + // 命中"无可奉告"主题清单(政治/战争/敏感新闻,直接写固定文案,不调美团 API) + meituan_blocked_topics?: string[] + meituan_blocked_keywords?: string[] + meituan_no_comment_text?: string updated_at?: string | null } diff --git a/frontend/src/views/AdminLlmSettings.vue b/frontend/src/views/AdminLlmSettings.vue index 3c642d6..7c637d9 100644 --- a/frontend/src/views/AdminLlmSettings.vue +++ b/frontend/src/views/AdminLlmSettings.vue @@ -30,6 +30,9 @@ const setting = ref({ meituan_interval_sec: 2.0, meituan_enabled: true, meituan_commentary_prompt: '', + meituan_blocked_topics: ['时政', '国际', '军事', '政治', '战争', '冲突', '制裁', '选举'], + meituan_blocked_keywords: [], + meituan_no_comment_text: '无可奉告', }) // === Angel api_key 编辑(不回显真值) === @@ -324,6 +327,46 @@ onMounted(load) placeholder="留空用默认" style="margin-top: 8px" /> + + + 🚧 主题拦截("无可奉告") + + 美团 LongCat 对战争/政治/敏感新闻会触发内容安全审核 400。
+ 命中下述"主题"或"关键词"的文章,直接写固定文案(无可奉告),不调美团 API。 +
+ + + 主题(topic): + + + + 关键词: + + + + 固定文案: + + (命中后写入 commentary_meituan 字段的内容) + +