From ba2298da0a083e588d2c56abfb6d0a42de9ad717 Mon Sep 17 00:00:00 2001 From: Mavis Date: Mon, 8 Jun 2026 14:24:23 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E9=9B=86=E6=88=90=20LLM=20=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=20=E2=80=94=20config/main/articles=20schema/workers?= =?UTF-8?q?=20+=20.env.example=20=E5=8A=A0=20Agnes=20=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 10 ++++++++++ backend/app/api/articles.py | 6 ++++++ backend/app/config.py | 9 +++++++++ backend/app/main.py | 3 ++- backend/app/models/__init__.py | 2 ++ backend/app/schemas/article.py | 7 +++++++ backend/app/workers/__main__.py | 18 ++++++++++++------ 7 files changed, 48 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index 56bb65e..0e9c7e8 100644 --- a/.env.example +++ b/.env.example @@ -56,3 +56,13 @@ FETCH_MAX_RETRIES=2 DOMAIN= # 邮箱(Let's Encrypt 用) ACME_EMAIL=you@example.com + +# ===== Agnes LLM(翻译后智能增强)===== +# 留空 = 不启用 LLM 增强(只走翻译) +# Agnes 控制台申请:https://platform.agnes-ai.com/ +AGNES_API_KEY=your_agnes_api_key +AGNES_BASE_URL=https://apihub.agnes-ai.com/v1 +AGNES_CHAT_MODEL=agnes-2.0-flash +AGNES_IMAGE_MODEL=agnes-image-2.1-flash +# LLM 调用间隔(秒,避免被限流;chat + image 各 1 个串行) +LLM_INTERVAL_SEC=2.0 diff --git a/backend/app/api/articles.py b/backend/app/api/articles.py index 700d516..5ee78f3 100644 --- a/backend/app/api/articles.py +++ b/backend/app/api/articles.py @@ -169,14 +169,20 @@ async def get_article( title_zh=article.title_zh, body_zh_html=article.body_zh_html, body_zh_text=article.body_zh_text, + body_zh_formatted=article.body_zh_formatted, summary_zh=article.summary_zh, lang_src=article.lang_src, author=article.author, image_url=article.image_url, + image_ai_url=article.image_ai_url, translation_status=article.translation_status, translation_engine=article.translation_engine, translated_at=article.translated_at, category=article.category, + format_status=article.format_status, + classify_status=article.classify_status, + image_ai_status=article.image_ai_status, + commentary_status=article.commentary_status, commentary=article.commentary, entities=article.entities, sentiment=article.sentiment, diff --git a/backend/app/config.py b/backend/app/config.py index 42f8ed8..a1c3a77 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -92,6 +92,15 @@ class Settings(BaseSettings): domain: str = "" acme_email: str = "" + # ===== Agnes LLM(智能增强)===== + # 留空 = 不启用 LLM 增强(翻译后只走默认排版,提示词也不读) + agnes_api_key: str = "" + agnes_base_url: str = "https://apihub.agnes-ai.com/v1" + agnes_chat_model: str = "agnes-2.0-flash" + agnes_image_model: str = "agnes-image-2.1-flash" + # 全局 LLM 调用间隔(秒),避免被限流 + llm_interval_sec: float = 2.0 + # ===== 内部路径(部署后可调) ===== project_root: Path = Path(__file__).resolve().parents[2] diff --git a/backend/app/main.py b/backend/app/main.py index 46c9d38..42ddd23 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -16,7 +16,7 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from starlette.exceptions import HTTPException as StarletteHTTPException -from app.api import admin, articles, auth, bookmarks, me, sources, subscriptions +from app.api import admin, admin_llm, articles, auth, bookmarks, me, sources, subscriptions from app.config import settings from app.database import engine from app.redis_client import close_redis, get_redis @@ -100,6 +100,7 @@ app.include_router(sources.router, prefix=API_PREFIX) app.include_router(bookmarks.router, prefix=API_PREFIX) app.include_router(subscriptions.router, prefix=API_PREFIX) app.include_router(admin.router, prefix=API_PREFIX) +app.include_router(admin_llm.router, prefix=API_PREFIX) # === 健康检查 === diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 85a2040..d16d410 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -5,6 +5,7 @@ from app.models.api_token import ApiToken # noqa: F401 from app.models.article import Article # noqa: F401 from app.models.bookmark import Bookmark # noqa: F401 +from app.models.llm_setting import LlmSetting # noqa: F401 from app.models.source import Source, SourceKind # noqa: F401 from app.models.subscription import Subscription # noqa: F401 from app.models.user import User, UserRole # noqa: F401 @@ -13,6 +14,7 @@ __all__ = [ "ApiToken", "Article", "Bookmark", + "LlmSetting", "Source", "SourceKind", "Subscription", diff --git a/backend/app/schemas/article.py b/backend/app/schemas/article.py index 7bf8bee..66c048e 100644 --- a/backend/app/schemas/article.py +++ b/backend/app/schemas/article.py @@ -46,14 +46,21 @@ class ArticleDetail(BaseModel): 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 + commentary_status: str | None = None commentary: str | None = None entities: dict | None = None sentiment: float | None = None diff --git a/backend/app/workers/__main__.py b/backend/app/workers/__main__.py index e3b1c2b..bd6c853 100644 --- a/backend/app/workers/__main__.py +++ b/backend/app/workers/__main__.py @@ -17,6 +17,7 @@ from sqlalchemy import select from app.config import settings from app.database import AsyncSessionLocal from app.models.source import Source +from app.services.llm.enrichment import enrichment_loop from app.workers.pipeline import fetch_one_source, run_once, translation_loop logger = logging.getLogger("news.worker") @@ -93,6 +94,10 @@ async def main() -> None: translation_task = asyncio.create_task(translation_loop(), name="translation_loop") logger.info("translation_loop task scheduled (1 article/sec)") + # 独立的 LLM 增强后台循环(翻译完成后,跑 4 项 LLM 任务) + enrichment_task = asyncio.create_task(enrichment_loop(), name="enrichment_loop") + logger.info("enrichment_loop task scheduled (scans translated articles)") + stop = asyncio.Event() def _signal_handler(): @@ -108,12 +113,13 @@ async def main() -> None: pass await stop.wait() - logger.info("stopping scheduler and translation loop") - translation_task.cancel() - try: - await translation_task - except asyncio.CancelledError: - pass + logger.info("stopping scheduler and background loops") + for t in (translation_task, enrichment_task): + t.cancel() + try: + await t + except asyncio.CancelledError: + pass scheduler.shutdown(wait=False)