feat(meituan): 政治类文章拦截 + 写'无可奉告' + Angel 并发 3→1

- llm_settings 加 meituan_blocked_topics / blocked_keywords / no_comment_text
- alembic 0006 迁移,默认 topics=[时政/国际/军事/政治/战争/冲突/制裁/选举], 默认文案='无可奉告'
- enrichment._is_meituan_blocked 预检:category 命中 topic 或 关键词 → 直接写'无可奉告',不调美团 API
- 命中后 commentary_meituan_model='policy-block' 标识非真实生成
- enrichment_loop Semaphore(3)→(1),Agnes 免费 plan 不再 429
- 前端 AdminLlmSettings 美团卡片加 3 字段 UI(主题/关键词/固定文案)
This commit is contained in:
xiaji
2026-06-12 22:44:00 +08:00
parent aaf728f3f4
commit 16536fe3a0
6 changed files with 185 additions and 2 deletions

View File

@@ -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")

View File

@@ -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(

View File

@@ -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
# === 默认提示词(模板,用户可改)===

View File

@@ -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:

View File

@@ -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
}

View File

@@ -30,6 +30,9 @@ const setting = ref<LlmSetting>({
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"
/>
<NDivider style="margin: 12px 0" />
<NText strong>🚧 主题拦截("无可奉告")</NText>
<NText depth="3" style="font-size: 12px; margin-top: 4px; display: block">
美团 LongCat 对战争/政治/敏感新闻会触发内容安全审核 400<br />
命中下述"主题""关键词"的文章,直接写固定文案(<NCode>无可奉告</NCode>),不调美团 API
</NText>
<NSpace vertical style="margin-top: 8px">
<NSpace align="start">
<NText style="min-width: 80px; padding-top: 4px">主题(topic):</NText>
<NInput
:value="(setting.meituan_blocked_topics || []).join(' / ')"
type="textarea"
:autosize="{ minRows: 2, maxRows: 4 }"
placeholder="时政 / 国际 / 军事"
style="width: 380px"
@update:value="(v: string) => setting.meituan_blocked_topics = v.split(/[,,;\n]/).map(s => s.trim()).filter(Boolean)"
/>
</NSpace>
<NSpace align="start">
<NText style="min-width: 80px; padding-top: 4px">关键词:</NText>
<NInput
:value="(setting.meituan_blocked_keywords || []).join(' / ')"
type="textarea"
:autosize="{ minRows: 2, maxRows: 4 }"
placeholder="(留空) — 命中标题或正文任一关键词即触发"
style="width: 380px"
@update:value="(v: string) => setting.meituan_blocked_keywords = v.split(/[,,;\n]/).map(s => s.trim()).filter(Boolean)"
/>
</NSpace>
<NSpace align="center">
<NText style="min-width: 80px">固定文案:</NText>
<NInput
v-model:value="setting.meituan_no_comment_text"
placeholder="无可奉告"
style="width: 200px"
/>
<NText depth="3" style="font-size: 12px">(命中后写入 commentary_meituan 字段的内容)</NText>
</NSpace>
</NSpace>
</NCard>
<NCard title="排版提示词" style="margin-top: 16px">