Files
diary-news/backend/app/schemas/article.py
xiaji 07534eb144 feat(ingest): API Push 短新闻接口层
- POST /api/v1/ingest:鉴权(X-Ingest-Token) + 限速(每 token 2 篇/秒,
  Redis 滑动桶,INGEST_RATE_PER_SEC 可调) + 三层去重(L1 external_id /
  L2 content_hash / L3 DB UNIQUE 兜底,均带 reason)
- 写入字段:is_short_news=True、translation/format/image_ai_status='n/a'、
  classify_status=(有 tags?'ok':'pending')、commentary_{angel,meituan}_status='pending'、
  body_zh_text=body_text(走统一路径,前端/prompt 不用改)
- services/fetchers/api_push.py:compute_content_hash + synthesize_url +
  normalize_published_at + build_initial_status 纯函数
- schemas/ingest.py:IngestPayload(title 1-200/body 1-5000/tags 去重去空) +
  IngestResponse(article_id/content_hash/status/reason/matched_external_id)
- admin.py:POST/GET/DELETE /admin/sources/{id}/ingest-tokens — owner 生成
  (raw_token 仅一次性返回)、列出、撤销
- schemas/article.py:ArticleListItem 加 is_short_news/source_ref;
  ArticleDetail 加 is_short_news/source_ref/external_id
- main.py:挂 ingest router;config.py + .env.example:ingest_rate_per_sec 默认 2

短新闻由 commit 1 enrichment_loop 自动接管 classify + 双 provider commentary,
跳过 format/image。
2026-06-14 16:04:45 +08:00

119 lines
3.8 KiB
Python

"""Article schemas."""
from __future__ import annotations
from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field
class SourceBrief(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
name: str
slug: str
region: str | None = None
class ArticleListItem(BaseModel):
"""列表项:首页展示标题/译标/正文摘要/分类/插图,详细阅读进详情页。"""
model_config = ConfigDict(from_attributes=True)
id: int
source: SourceBrief
title: str
title_zh: str | None = None
# 翻译后的正文(纯文本);列表里截断显示,详情页展示完整
body_zh_text: str | None = None
summary_zh: str | None = None
lang_src: str | None = None
translation_status: str
category: str | None = None
published_at: datetime | None = None
fetched_at: datetime
image_url: str | None = None
# === 列表预览钩子:点击进详情前的"诱导点" ===
# 双 provider 评论:Angel(原字段) + 美团(meituan 字段),前端两条都展示
commentary: str | None = None # Angel 评论(列表里截断显示)
commentary_status: str | None = None # ok/failed/pending/n/a
commentary_meituan: str | None = None # 美团评论
commentary_meituan_status: str | None = None
commentary_engine: str | None = None # angel / meituan / "angel,meituan"
image_ai_url: str | None = None # AI 插图(列表里缩略图)
# === API Push 短新闻标识 ===
is_short_news: bool = False
source_ref: str | None = None
is_starred: bool = False
is_read: bool = False
class ArticleDetail(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
source: SourceBrief
url: str
title: str
body_html: str | None = None
body_text: str
title_zh: str | None = None
body_zh_html: str | None = None
body_zh_text: str | None = None
body_zh_formatted: str | None = None # LLM 排版后
summary_zh: str | None = None
lang_src: str | None = None
author: str | None = None
image_url: str | None = None
image_ai_url: str | None = None # LLM 生成的插图
translation_status: str
translation_engine: str | None = None
translated_at: datetime | None = None
# === LLM 增强状态 + 内容 ===
category: str | None = None
format_status: str | None = None # pending/ok/failed/n/a
classify_status: str | None = None
image_ai_status: str | None = None
# 双 provider 评论
commentary_status: str | None = None
commentary: str | None = None # Angel
commentary_engine: str | None = None
commentary_meituan_status: str | None = None
commentary_meituan: str | None = None
commentary_meituan_model: str | None = None
commentary_meituan_error: str | None = None
entities: dict | None = None
sentiment: float | None = None
duplicate_of: int | None = None
published_at: datetime | None = None
fetched_at: datetime
# === API Push 短新闻标识 ===
is_short_news: bool = False
source_ref: str | None = None
external_id: str | None = None # 调用方幂等 key
is_starred: bool = False
is_read: bool = False
class ArticleListResponse(BaseModel):
items: list[ArticleListItem]
# 页码分页
page: int = 1
page_size: int = 50
total: int
total_pages: int
class ArticleQuery(BaseModel):
"""用作 ?query= 解析参考(实际 FastAPI 直接用 Query)。"""
since: datetime | None = None
until: datetime | None = None
source: str | None = None # 逗号分隔 slug
category: str | None = None
q: str | None = None
lang: str = Field(default="both", pattern=r"^(src|zh|both)$")
page: int = Field(default=1, ge=1)
page_size: int = Field(default=50, ge=1, le=200)
starred_only: bool = False