Bläddra i källkod

Merge branch 'feature/luojunhui/20260310-long_article_mcp' of Server/LongArticleTaskServer into master

luojunhui 2 dagar sedan
förälder
incheckning
8737e5d352

+ 2 - 0
app/api/v1/endpoints/__init__.py

@@ -3,6 +3,7 @@ from .health import create_health_bp
 from .tasks import create_tasks_bp
 from .tokens import create_tokens_bp
 from .monitor import create_monitor_bp
+from .mcp import create_mcp_bp
 
 __all__ = [
     "create_abtest_bp",
@@ -10,4 +11,5 @@ __all__ = [
     "create_tasks_bp",
     "create_tokens_bp",
     "create_monitor_bp",
+    "create_mcp_bp",
 ]

+ 52 - 0
app/api/v1/endpoints/mcp.py

@@ -0,0 +1,52 @@
+from __future__ import annotations
+
+from pydantic import ValidationError
+from quart import Blueprint, jsonify
+
+from app.api.v1.utils import ApiDependencies, LongArticlesMcpRequest
+from app.api.v1.utils import parse_json, validation_error_response
+from app.domains.mcp import deal, UnknownMcpTaskName
+
+
+def create_mcp_bp(deps: ApiDependencies) -> Blueprint:
+    bp = Blueprint("mcp", __name__)
+
+    @bp.route("/long_articles_mcp/<string:task_name>", methods=["POST"])
+    async def long_articles_mcp(task_name: str):
+        """MCP 交互接口
+
+        - POST /api/long_articles_mcp/<task_name>
+        - 通过 task_name 分发到 MCP 域内对应的方法
+        - 请求体为 JSON,可选字段如 page、page_size、sort_by、sort_order 等
+        """
+        try:
+            req, _ = await parse_json(LongArticlesMcpRequest)
+        except ValidationError as e:
+            payload, status = validation_error_response(e)
+            return jsonify(payload), status
+
+        # 使用校验后的模型转 dict,保证类型正确,且含 extra 透传字段
+        params = req.model_dump(exclude_none=True)
+
+        try:
+            result = await deal(
+                task_name=task_name,
+                pool=deps.db,
+                log_service=deps.log,
+                params=params,
+            )
+        except UnknownMcpTaskName:
+            return (
+                jsonify(
+                    {
+                        "code": 404,
+                        "message": f"unknown task_name: {task_name}",
+                        "data": None,
+                    }
+                ),
+                404,
+            )
+
+        return jsonify({"code": 0, "message": "success", "data": result})
+
+    return bp

+ 2 - 0
app/api/v1/routes/routes.py

@@ -9,6 +9,7 @@ from app.api.v1.endpoints import (
     create_tasks_bp,
     create_tokens_bp,
     create_monitor_bp,
+    create_mcp_bp
 )
 from app.core.config import GlobalConfigSettings
 from app.core.database import DatabaseManager
@@ -32,6 +33,7 @@ def register_v1_blueprints(deps: ApiDependencies) -> Blueprint:
     api.register_blueprint(create_tokens_bp(deps))
     api.register_blueprint(create_abtest_bp(deps))
     api.register_blueprint(create_monitor_bp(deps))
+    api.register_blueprint(create_mcp_bp(deps))
 
     return api
 

+ 8 - 1
app/api/v1/utils/__init__.py

@@ -1,6 +1,12 @@
 from ._utils import parse_json, validation_error_response
 from .deps import ApiDependencies
-from .schemas import RunTaskRequest, TaskListRequest, SaveTokenRequest, GetCoverRequest
+from .schemas import (
+    RunTaskRequest,
+    TaskListRequest,
+    SaveTokenRequest,
+    GetCoverRequest,
+    LongArticlesMcpRequest,
+)
 
 
 __all__ = [
@@ -10,5 +16,6 @@ __all__ = [
     "TaskListRequest",
     "SaveTokenRequest",
     "GetCoverRequest",
+    "LongArticlesMcpRequest",
     "ApiDependencies",
 ]

+ 10 - 1
app/api/v1/utils/schemas.py

@@ -1,6 +1,6 @@
 from __future__ import annotations
 
-from typing import Any, Dict, Optional
+from typing import Any, Dict, Literal, Optional
 
 from pydantic import BaseModel, ConfigDict, Field
 
@@ -39,3 +39,12 @@ class SaveTokenRequest(BaseRequest):
     """GzhCookieManager 的请求体字段不固定,先保持兼容。"""
 
     token: Optional[str] = None
+
+
+class LongArticlesMcpRequest(BaseRequest):
+    """MCP 交互接口的请求体,按 task_name 可传不同参数。"""
+
+    page: int = Field(default=1, ge=1, description="页码,从 1 开始")
+    page_size: int = Field(default=20, ge=1, le=100, description="每页条数")
+    sort_by: Optional[str] = Field(default=None, description="排序字段")
+    sort_order: Optional[Literal["asc", "desc"]] = Field(default=None, description="排序方向: asc / desc")

+ 3 - 0
app/domains/mcp/__init__.py

@@ -0,0 +1,3 @@
+from .entrance import deal, UnknownMcpTaskName
+
+__all__ = ["deal", "UnknownMcpTaskName"]

+ 27 - 0
app/domains/mcp/_const.py

@@ -0,0 +1,27 @@
+import math
+
+
+class LongArticlesMcpConst:
+    """MCP 配置层:排序字段、SQL 片段、分页默认值等。"""
+    # 分页配置
+    DEFAULT_PAGE = 1
+    DEFAULT_PAGE_SIZE = 20
+    MAX_PAGE_SIZE = 100
+
+    @staticmethod
+    def normalize_pagination(page: int | None, page_size: int | None) -> tuple[int, int]:
+        page = page or LongArticlesMcpConst.DEFAULT_PAGE
+        page = max(page, 1)
+
+        page_size = page_size or LongArticlesMcpConst.DEFAULT_PAGE_SIZE
+        page_size = max(1, min(page_size, LongArticlesMcpConst.MAX_PAGE_SIZE))
+        return page, page_size
+
+    @staticmethod
+    def calc_total_pages(total: int, page_size: int) -> int:
+        if total <= 0:
+            return 0
+        return math.ceil(total / page_size)
+
+
+__all__ = ["LongArticlesMcpConst"]

+ 24 - 0
app/domains/mcp/_handler_map.py

@@ -0,0 +1,24 @@
+from typing import Any, Awaitable, Callable, Dict
+
+from app.core.database import DatabaseManager
+from app.core.observability import LogService
+
+
+HandlerType = Callable[[DatabaseManager, LogService, Dict[str, Any] | None], Awaitable[Any]]
+
+
+async def _get_decode_response_wrapper(
+    pool: DatabaseManager,
+    log_service: LogService,
+    params: Dict[str, Any] | None,
+):
+    # 这里只是为 handler_map 统一签名做一层薄封装
+    return await get_decode_response(pool=pool, log_service=log_service, params=params)
+
+
+HANDLER_MAP: Dict[str, HandlerType] = {
+    "get_decode_response": _get_decode_response_wrapper,
+}
+
+
+__all__ = ["HANDLER_MAP"]

+ 21 - 0
app/domains/mcp/_mapper.py

@@ -0,0 +1,21 @@
+from typing import Any, Dict, List, Optional
+
+from app.core.database import DatabaseManager
+
+from ._const import LongArticlesMcpConst
+
+
+class LongArticlesMcpMapper(LongArticlesMcpConst):
+    """MCP 方法层:只负责拼 SQL + 访问 DB,不做业务编排。"""
+
+    def __init__(self, pool: DatabaseManager):
+        self.pool = pool
+
+    # 解构
+
+    # 抓取
+
+    # 统计
+
+
+__all__ = ["LongArticlesMcpMapper"]

+ 1 - 0
app/domains/monitor_tasks/gzh_article_monitor.py

@@ -286,6 +286,7 @@ class InnerGzhArticlesMonitor(MonitorConst):
         )
         try:
             response = await get_article_detail(url, is_cache=False)
+            print(response)
             response_code = response["code"]
             if response_code == self.ARTICLE_ILLEGAL_CODE:
                 error_detail = response.get("msg")

+ 0 - 0
app/domains/recommend/__init__.py


+ 0 - 0
app/domains/recommend/offline_recommend/__init__.py


+ 4 - 4
app/recommend/offline_recommend/core.py → app/domains/recommend/offline_recommend/core.py

@@ -8,10 +8,10 @@ from app.infra.mapper import LongArticleDatabaseMapper
 from app.infra.mapper import PiaoquanCrawlerDatabaseMapper
 from app.infra.external import OdpsService
 
-from app.recommend.offline_recommend.strategy import I2I
-from app.recommend.offline_recommend.strategy import GetTopArticleStrategy
-from app.recommend.offline_recommend.utils import RecommendApolloClient
-from app.recommend.offline_recommend.utils import ProduceBaseData
+from app.domains.recommend.offline_recommend.strategy import I2I
+from app.domains.recommend.offline_recommend.strategy import GetTopArticleStrategy
+from app.domains.recommend.offline_recommend.utils import RecommendApolloClient
+from app.domains.recommend.offline_recommend.utils import ProduceBaseData
 
 
 class BaseOffRecommendUtils:

+ 0 - 0
app/recommend/offline_recommend/strategy/__init__.py → app/domains/recommend/offline_recommend/strategy/__init__.py


+ 0 - 0
app/recommend/offline_recommend/strategy/base.py → app/domains/recommend/offline_recommend/strategy/base.py


+ 0 - 0
app/recommend/offline_recommend/strategy/get_top_article.py → app/domains/recommend/offline_recommend/strategy/get_top_article.py


+ 0 - 0
app/recommend/offline_recommend/strategy/i2i.py → app/domains/recommend/offline_recommend/strategy/i2i.py


+ 0 - 0
app/recommend/offline_recommend/utils/__init__.py → app/domains/recommend/offline_recommend/utils/__init__.py


+ 0 - 0
app/recommend/offline_recommend/utils/produce_data.py → app/domains/recommend/offline_recommend/utils/produce_data.py


+ 0 - 0
app/recommend/offline_recommend/utils/recommend_apollo.py → app/domains/recommend/offline_recommend/utils/recommend_apollo.py