luojunhui 13 часов назад
Родитель
Сommit
65dd5492ef

+ 11 - 0
.dockerignore

@@ -0,0 +1,11 @@
+.git
+.idea
+.vscode
+.env
+__pycache__
+*.pyc
+venv
+.venv
+.pytest_cache
+htmlcov
+.coverage

+ 13 - 0
.gitignore

@@ -1,3 +1,16 @@
+# Environment
+.env
+
+# IDE
+.idea/
+.vscode/
+*.swp
+*.swo
+
+# Virtual environments
+venv/
+.venv/
+
 # ---> Python
 # ---> Python
 # Byte-compiled / optimized / DLL files
 # Byte-compiled / optimized / DLL files
 __pycache__/
 __pycache__/

+ 16 - 0
Dockerfile

@@ -0,0 +1,16 @@
+FROM python:3.10-slim AS base
+
+WORKDIR /app
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+    gcc default-libmysqlclient-dev \
+    && rm -rf /var/lib/apt/lists/*
+
+COPY requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt
+
+COPY . .
+
+EXPOSE 8000
+
+CMD ["hypercorn", "app:app", "--bind", "0.0.0.0:8000"]

+ 1 - 1
LICENSE

@@ -1,5 +1,5 @@
 MIT License
 MIT License
-Copyright (c) <year> <copyright holders>
+Copyright (c) 2026 LongArticleSearchAgent Contributors
 
 
 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 
 

+ 32 - 0
Makefile

@@ -0,0 +1,32 @@
+.PHONY: run dev test lint clean install
+
+install:
+	pip install -r requirements.txt
+
+run:
+	hypercorn app:app --bind 0.0.0.0:8000
+
+dev:
+	hypercorn app:app --bind 0.0.0.0:8000 --reload
+
+test:
+	pytest tests/ -v
+
+lint:
+	ruff check src/ tests/ app.py
+	ruff format --check src/ tests/ app.py
+
+format:
+	ruff check --fix src/ tests/ app.py
+	ruff format src/ tests/ app.py
+
+clean:
+	find . -type d -name __pycache__ -exec rm -rf {} +
+	find . -type f -name "*.pyc" -delete
+	rm -rf .pytest_cache htmlcov .coverage
+
+docker-build:
+	docker build -t long-article-search-agent .
+
+docker-run:
+	docker run --rm --env-file .env -p 8000:8000 long-article-search-agent

+ 94 - 1
README.md

@@ -1,3 +1,96 @@
 # LongArticleSearchAgent
 # LongArticleSearchAgent
 
 
-长文供给寻找 agent
+长文供给寻找 Agent
+
+## 1. 项目结构
+
+```
+LongArticleSearchAgent/
+├── app.py                                  # 应用入口
+├── pyproject.toml                          # 项目元信息 & 工具配置
+├── requirements.txt                        # Python 依赖
+├── Makefile                                # 常用命令快捷方式
+├── Dockerfile                              # 容器构建
+├── .env.example                            # 环境变量模板
+│
+├── src/
+│   ├── agent/                              # Agent 业务逻辑(待开发)
+│   │
+│   ├── config/                             # 配置层(Pydantic Settings)
+│   │   ├── agent_config.py                 # 聚合配置入口
+│   │   ├── api/
+│   │   │   └── deepseek.py                 # DeepSeek LLM 配置
+│   │   ├── aliyun/
+│   │   │   └── log.py                      # 阿里云日志配置
+│   │   └── database/
+│   │       └── mysql_config.py             # MySQL 数据库配置
+│   │
+│   ├── core/                               # 核心层
+│   │   ├── api/v1/                         # API 路由 & 端点
+│   │   │   ├── endpoints/
+│   │   │   │   └── health.py               # 健康检查端点
+│   │   │   ├── middleware/
+│   │   │   │   ├── error_handler.py        # 全局异常处理
+│   │   │   │   └── response.py             # 统一响应构建器 R
+│   │   │   ├── routes/
+│   │   │   │   └── route.py                # 路由统一注册
+│   │   │   └── utils/
+│   │   │       └── deps.py                 # API 依赖容器
+│   │   ├── bootstrap/
+│   │   │   ├── logging_config.py           # 统一日志配置
+│   │   │   └── resource_manager.py         # 应用生命周期管理
+│   │   └── dependency/
+│   │       └── dependencies.py             # DI 容器(dependency_injector)
+│   │
+│   └── infra/                              # 基础设施层
+│       ├── database/
+│       │   └── mysql/
+│       │       └── async_mysql_pool.py     # 异步 MySQL 连接池
+│       └── trace/
+│           └── logging/
+│               └── log_service.py          # 阿里云日志服务
+│
+└── tests/                                  # 测试
+    ├── conftest.py                         # 公共 fixtures
+    ├── unit/
+    │   └── test_config.py
+    └── integration/
+        └── test_health.py
+```
+
+## 2. 快速开始
+
+```bash
+# 安装依赖
+make install
+
+# 复制环境变量模板并填写真实配置
+cp .env.example .env
+
+# 开发模式运行(热重载)
+make dev
+
+# 生产模式运行
+make run
+```
+
+## 3. 常用命令
+
+| 命令 | 说明 |
+|------|------|
+| `make install` | 安装 Python 依赖 |
+| `make dev` | 开发模式启动(热重载) |
+| `make run` | 生产模式启动 |
+| `make test` | 运行测试 |
+| `make lint` | 代码检查 |
+| `make format` | 代码格式化 |
+| `make docker-build` | 构建 Docker 镜像 |
+| `make docker-run` | 以 Docker 容器运行 |
+
+## 4. 环境变量
+
+参考 [.env.example](.env.example) 查看所有需要配置的环境变量。
+
+## 5. License
+
+[MIT](LICENSE)

+ 12 - 8
app.py

@@ -1,16 +1,21 @@
 import logging
 import logging
-from quart_cors import cors
+
 from quart import Quart
 from quart import Quart
+from quart_cors import cors
 
 
-from src.core.bootstrap import AppContext
+from src.core.bootstrap import AppContext, setup_logging
 from src.core.dependency import ServerContainer
 from src.core.dependency import ServerContainer
 from src.core.api.v1.routes import server_routes
 from src.core.api.v1.routes import server_routes
+from src.core.api.v1.middleware import register_error_handlers
 
 
-logging.basicConfig(level=logging.INFO)
+setup_logging()
+logger = logging.getLogger(__name__)
 
 
 app = Quart(__name__)
 app = Quart(__name__)
 app = cors(app, allow_origin="*")
 app = cors(app, allow_origin="*")
 
 
+register_error_handlers(app)
+
 server_container = ServerContainer()
 server_container = ServerContainer()
 ctx = AppContext(server_container)
 ctx = AppContext(server_container)
 
 
@@ -18,20 +23,19 @@ config = server_container.config()
 log_service = server_container.log_service()
 log_service = server_container.log_service()
 mysql_pool = server_container.mysql_pool()
 mysql_pool = server_container.mysql_pool()
 
 
-
 routes = server_routes(mysql_pool, log_service, config)
 routes = server_routes(mysql_pool, log_service, config)
 app.register_blueprint(routes)
 app.register_blueprint(routes)
 
 
 
 
 @app.before_serving
 @app.before_serving
 async def startup():
 async def startup():
-    logging.info("Starting application...")
+    logger.info("Starting application...")
     await ctx.start_up()
     await ctx.start_up()
-    logging.info("Application started successfully")
+    logger.info("Application started successfully")
 
 
 
 
 @app.after_serving
 @app.after_serving
 async def shutdown():
 async def shutdown():
-    logging.info("Shutting down application...")
+    logger.info("Shutting down application...")
     await ctx.shutdown()
     await ctx.shutdown()
-    logging.info("Application shutdown successfully")
+    logger.info("Application shutdown successfully")

+ 21 - 0
pyproject.toml

@@ -0,0 +1,21 @@
+[project]
+name = "long-article-search-agent"
+version = "0.1.0"
+description = "长文供给寻找 Agent"
+requires-python = ">=3.11"
+
+[tool.pytest.ini_options]
+asyncio_mode = "auto"
+testpaths = ["tests"]
+pythonpath = ["."]
+
+[tool.ruff]
+target-version = "py311"
+line-length = 120
+
+[tool.ruff.lint]
+select = ["E", "F", "I", "W"]
+ignore = ["E501"]
+
+[tool.ruff.lint.isort]
+known-first-party = ["src"]

+ 9 - 9
requirements.txt

@@ -1,13 +1,16 @@
 # ASGI / Web
 # ASGI / Web
 hypercorn>=0.16.0
 hypercorn>=0.16.0
 quart>=0.19.0
 quart>=0.19.0
+quart-cors>=0.7.0
 
 
 # Config & validation
 # Config & validation
 pydantic>=2.0.0
 pydantic>=2.0.0
 pydantic-settings>=2.0.0
 pydantic-settings>=2.0.0
 
 
+# DI
+dependency-injector>=4.41.0
+
 # Database
 # Database
-sqlalchemy[asyncio]>=2.0.0
 aiomysql>=0.2.0
 aiomysql>=0.2.0
 
 
 # Cache & search
 # Cache & search
@@ -18,21 +21,18 @@ pymilvus>=2.3.0
 # Auth
 # Auth
 PyJWT>=2.8.0
 PyJWT>=2.8.0
 
 
-# LLM (interaction)
-langchain>=0.3.0
-langchain-core>=0.3.0
-langchain-openai>=0.2.0
-
 # LLM
 # LLM
 openai>=1.0.0
 openai>=1.0.0
-
-# LangChain
 langchain>=0.3.0
 langchain>=0.3.0
 langchain-core>=0.3.0
 langchain-core>=0.3.0
 langchain-openai>=0.2.0
 langchain-openai>=0.2.0
 langchain-community>=0.3.0
 langchain-community>=0.3.0
 
 
-# aliyun log
+# Aliyun
 aliyun-log-python-sdk
 aliyun-log-python-sdk
 aliyun-python-sdk-core
 aliyun-python-sdk-core
 aliyun-python-sdk-kms
 aliyun-python-sdk-kms
+
+# Dev & test
+pytest>=8.0.0
+pytest-asyncio>=0.23.0

+ 0 - 0
src/__init__.py


+ 0 - 0
src/agent/__init__.py


+ 3 - 3
src/config/agent_config.py

@@ -1,9 +1,9 @@
 from pydantic import Field
 from pydantic import Field
 from pydantic_settings import BaseSettings, SettingsConfigDict
 from pydantic_settings import BaseSettings, SettingsConfigDict
 
 
-from src.config.api import *
-from src.config.aliyun import *
-from src.config.database import *
+from src.config.api import DeepSeekConfig
+from src.config.aliyun import AliyunLogConfig
+from src.config.database import SearchAgentMySQLConfig
 
 
 
 
 class LongArticlesSearchAgentConfig(BaseSettings):
 class LongArticlesSearchAgentConfig(BaseSettings):

+ 5 - 5
src/config/aliyun/log.py

@@ -4,11 +4,11 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
 class AliyunLogConfig(BaseSettings):
 class AliyunLogConfig(BaseSettings):
     """阿里云日志配置"""
     """阿里云日志配置"""
 
 
-    endpoint: str = "cn-hangzhou.log.aliyuncs.com"
-    access_key_id: str = "LTAIP6x1l3DXfSxm"
-    access_key_secret: str = "KbTaM9ars4OX3PMS6Xm7rtxGr1FLon"
-    project: str = "changwen-alg"
-    logstore: str = "long_articles_job"
+    endpoint: str = ""
+    access_key_id: str = ""
+    access_key_secret: str = ""
+    project: str = ""
+    logstore: str = ""
 
 
     model_config = SettingsConfigDict(
     model_config = SettingsConfigDict(
         env_prefix="ALIYUN_LOG_", env_file=".env", case_sensitive=False, extra="ignore"
         env_prefix="ALIYUN_LOG_", env_file=".env", case_sensitive=False, extra="ignore"

+ 4 - 3
src/config/database/mysql_config.py

@@ -33,9 +33,10 @@ class MySQLConfig(BaseSettings):
 
 
 class SearchAgentMySQLConfig(MySQLConfig):
 class SearchAgentMySQLConfig(MySQLConfig):
     host: str = "localhost"
     host: str = "localhost"
-    user: str = "root"
-    db: str = "better_me"
-    password: str = "ljh000118"
+    port: int = 3306
+    user: str = ""
+    db: str = ""
+    password: str = ""
 
 
     model_config = SettingsConfigDict(
     model_config = SettingsConfigDict(
         env_prefix="SEARCH_AGENT_DB_",
         env_prefix="SEARCH_AGENT_DB_",

+ 4 - 4
src/core/api/v1/endpoints/health.py

@@ -1,6 +1,8 @@
 from __future__ import annotations
 from __future__ import annotations
 
 
-from quart import Blueprint, jsonify
+from quart import Blueprint
+
+from src.core.api.v1.middleware import R
 
 
 
 
 def create_health_bp() -> Blueprint:
 def create_health_bp() -> Blueprint:
@@ -8,8 +10,6 @@ def create_health_bp() -> Blueprint:
 
 
     @bp.route("/health", methods=["GET"])
     @bp.route("/health", methods=["GET"])
     async def health():
     async def health():
-        return jsonify(
-            {"code": 0, "message": "success", "data": {"message": "hello world"}}
-        )
+        return R.ok(data={"status": "healthy"})
 
 
     return bp
     return bp

+ 2 - 0
src/core/api/v1/middleware/__init__.py

@@ -0,0 +1,2 @@
+from .error_handler import register_error_handlers
+from .response import R

+ 36 - 0
src/core/api/v1/middleware/error_handler.py

@@ -0,0 +1,36 @@
+from __future__ import annotations
+
+import logging
+from quart import Quart, jsonify
+
+logger = logging.getLogger(__name__)
+
+
+def register_error_handlers(app: Quart):
+    """注册全局异常处理,返回统一 JSON 格式。"""
+
+    @app.errorhandler(400)
+    async def bad_request(e):
+        return jsonify({"code": 400, "message": str(e), "data": None}), 400
+
+    @app.errorhandler(404)
+    async def not_found(e):
+        return jsonify({"code": 404, "message": "Not Found", "data": None}), 404
+
+    @app.errorhandler(405)
+    async def method_not_allowed(e):
+        return jsonify({"code": 405, "message": "Method Not Allowed", "data": None}), 405
+
+    @app.errorhandler(422)
+    async def unprocessable(e):
+        return jsonify({"code": 422, "message": str(e), "data": None}), 422
+
+    @app.errorhandler(500)
+    async def internal_error(e):
+        logger.exception("Unhandled server error")
+        return jsonify({"code": 500, "message": "Internal Server Error", "data": None}), 500
+
+    @app.errorhandler(Exception)
+    async def handle_unexpected(e):
+        logger.exception("Unexpected exception: %s", e)
+        return jsonify({"code": 500, "message": "Internal Server Error", "data": None}), 500

+ 21 - 0
src/core/api/v1/middleware/response.py

@@ -0,0 +1,21 @@
+from __future__ import annotations
+
+from typing import Any, Optional
+
+from quart import jsonify
+
+
+class R:
+    """统一 JSON 响应构建器。"""
+
+    @staticmethod
+    def ok(data: Any = None, message: str = "success"):
+        return jsonify({"code": 0, "message": message, "data": data})
+
+    @staticmethod
+    def fail(code: int = -1, message: str = "error", data: Any = None, http_status: int = 200):
+        return jsonify({"code": code, "message": message, "data": data}), http_status
+
+    @staticmethod
+    def error(message: str = "Internal Server Error", http_status: int = 500):
+        return jsonify({"code": http_status, "message": message, "data": None}), http_status

+ 1 - 0
src/core/bootstrap/__init__.py

@@ -1 +1,2 @@
 from .resource_manager import AppContext
 from .resource_manager import AppContext
+from .logging_config import setup_logging

+ 20 - 0
src/core/bootstrap/logging_config.py

@@ -0,0 +1,20 @@
+import logging
+import sys
+
+
+def setup_logging(level: int = logging.INFO):
+    """统一日志配置,项目启动时调用一次即可。"""
+    root = logging.getLogger()
+    if root.handlers:
+        return
+
+    root.setLevel(level)
+
+    handler = logging.StreamHandler(sys.stdout)
+    handler.setLevel(level)
+    formatter = logging.Formatter(
+        fmt="%(asctime)s | %(levelname)-7s | %(name)s | %(message)s",
+        datefmt="%Y-%m-%d %H:%M:%S",
+    )
+    handler.setFormatter(formatter)
+    root.addHandler(handler)

+ 8 - 11
src/core/bootstrap/resource_manager.py

@@ -9,25 +9,22 @@ class AppContext:
         self.container = container
         self.container = container
 
 
     async def start_up(self):
     async def start_up(self):
-        logger.info("初始化数据库连接池")
-        mysql = self.container.mysql_pool()
-        await mysql.init_pools()
-        logger.info("Mysql pools init successfully")
-
-        logger.info("初始化日志服务")
         log_service = self.container.log_service()
         log_service = self.container.log_service()
         await log_service.start()
         await log_service.start()
-        logger.info("aliyun log service init successfully")
+        logger.info("Log service started")
+
+        mysql = self.container.mysql_pool()
+        await mysql.init_pools()
+        logger.info("MySQL pools initialized")
 
 
     async def shutdown(self):
     async def shutdown(self):
-        logger.info("关闭数据库连接池")
         mysql = self.container.mysql_pool()
         mysql = self.container.mysql_pool()
         await mysql.close_pools()
         await mysql.close_pools()
-        logger.info("应用资源已释放")
-        logger.info("关闭日志服务")
+        logger.info("MySQL pools closed")
+
         log_service = self.container.log_service()
         log_service = self.container.log_service()
         await log_service.stop()
         await log_service.stop()
-        logger.info("aliyun log service stopped")
+        logger.info("Log service stopped")
 
 
 
 
 __all__ = [
 __all__ = [

+ 5 - 4
src/core/dependency/dependencies.py

@@ -6,14 +6,15 @@ from src.infra.trace import LogService
 
 
 
 
 class ServerContainer(containers.DeclarativeContainer):
 class ServerContainer(containers.DeclarativeContainer):
-    # config
     config = providers.Singleton(LongArticlesSearchAgentConfig)
     config = providers.Singleton(LongArticlesSearchAgentConfig)
 
 
-    # 阿里云日志
     log_service = providers.Singleton(LogService, log_config=config.provided.aliyun_log)
     log_service = providers.Singleton(LogService, log_config=config.provided.aliyun_log)
 
 
-    # MySQL
-    mysql_pool = providers.Singleton(AsyncMySQLPool, config=config)
+    mysql_pool = providers.Singleton(
+        AsyncMySQLPool,
+        config=config,
+        log_service=log_service,
+    )
 
 
 
 
 __all__ = [
 __all__ = [

+ 34 - 27
src/infra/database/mysql/async_mysql_pool.py

@@ -1,4 +1,5 @@
 import logging
 import logging
+from typing import Optional
 
 
 from aiomysql import create_pool
 from aiomysql import create_pool
 from aiomysql.cursors import DictCursor
 from aiomysql.cursors import DictCursor
@@ -6,20 +7,22 @@ from aiomysql.cursors import DictCursor
 from src.config import LongArticlesSearchAgentConfig
 from src.config import LongArticlesSearchAgentConfig
 from src.infra.trace import LogService
 from src.infra.trace import LogService
 
 
+logger = logging.getLogger(__name__)
 
 
-logging.basicConfig(level=logging.INFO)
 
 
-
-class AsyncMySQLPool(LogService):
-    def __init__(self, config: LongArticlesSearchAgentConfig):
-        super().__init__(config.aliyun_log)
+class AsyncMySQLPool:
+    def __init__(self, config: LongArticlesSearchAgentConfig, log_service: Optional[LogService] = None):
+        self.log_service = log_service
         self.database_mapper = {
         self.database_mapper = {
             "search_agent": config.search_agent_db,
             "search_agent": config.search_agent_db,
         }
         }
-        self.pools = {}
+        self.pools: dict = {}
+
+    async def _log_error(self, contents: dict):
+        if self.log_service:
+            await self.log_service.log(contents)
 
 
     async def init_pools(self):
     async def init_pools(self):
-        # 从配置获取数据库配置,也可以直接在这里配置
         for db_name, config in self.database_mapper.items():
         for db_name, config in self.database_mapper.items():
             try:
             try:
                 pool = await create_pool(
                 pool = await create_pool(
@@ -34,11 +37,10 @@ class AsyncMySQLPool(LogService):
                     autocommit=True,
                     autocommit=True,
                 )
                 )
                 self.pools[db_name] = pool
                 self.pools[db_name] = pool
-                logging.info(f"{db_name} MYSQL连接池 created successfully")
-
+                logger.info(f"{db_name} MySQL pool created successfully")
             except Exception as e:
             except Exception as e:
-                await self.log(
-                    contents={
+                await self._log_error(
+                    {
                         "db_name": db_name,
                         "db_name": db_name,
                         "error": str(e),
                         "error": str(e),
                         "message": f"Failed to create pool for {db_name}",
                         "message": f"Failed to create pool for {db_name}",
@@ -51,41 +53,46 @@ class AsyncMySQLPool(LogService):
             if pool:
             if pool:
                 pool.close()
                 pool.close()
                 await pool.wait_closed()
                 await pool.wait_closed()
-                logging.info(f"{name} MYSQL连接池 closed successfully")
+                logger.info(f"{name} MySQL pool closed successfully")
 
 
     async def async_fetch(
     async def async_fetch(
-        self, query, db_name="long_articles", params=None, cursor_type=DictCursor
+        self, query, db_name="search_agent", params=None, cursor_type=DictCursor
     ):
     ):
-        pool = self.pools[db_name]
+        pool = self.pools.get(db_name)
         if not pool:
         if not pool:
             await self.init_pools()
             await self.init_pools()
-        # fetch from db
+            pool = self.pools.get(db_name)
+        if not pool:
+            logger.error(f"No available pool for {db_name}")
+            return None
+
         try:
         try:
             async with pool.acquire() as conn:
             async with pool.acquire() as conn:
                 async with conn.cursor(cursor_type) as cursor:
                 async with conn.cursor(cursor_type) as cursor:
                     await cursor.execute(query, params)
                     await cursor.execute(query, params)
-                    fetch_response = await cursor.fetchall()
-
-            return fetch_response
+                    return await cursor.fetchall()
         except Exception as e:
         except Exception as e:
-            await self.log(
-                contents={
+            await self._log_error(
+                {
                     "task": "async_fetch",
                     "task": "async_fetch",
                     "db_name": db_name,
                     "db_name": db_name,
                     "error": str(e),
                     "error": str(e),
                     "message": f"Failed to fetch data from {db_name}",
                     "message": f"Failed to fetch data from {db_name}",
                     "query": query,
                     "query": query,
-                    "params": params,
+                    "params": str(params),
                 }
                 }
             )
             )
             return None
             return None
 
 
     async def async_save(
     async def async_save(
-        self, query, params, db_name="long_articles", batch: bool = False
+        self, query, params, db_name="search_agent", batch: bool = False
     ):
     ):
-        pool = self.pools[db_name]
+        pool = self.pools.get(db_name)
         if not pool:
         if not pool:
             await self.init_pools()
             await self.init_pools()
+            pool = self.pools.get(db_name)
+        if not pool:
+            raise ConnectionError(f"No available pool for {db_name}")
 
 
         async with pool.acquire() as connection:
         async with pool.acquire() as connection:
             async with connection.cursor() as cursor:
             async with connection.cursor() as cursor:
@@ -99,17 +106,17 @@ class AsyncMySQLPool(LogService):
                     return affected_rows
                     return affected_rows
                 except Exception as e:
                 except Exception as e:
                     await connection.rollback()
                     await connection.rollback()
-                    await self.log(
-                        contents={
+                    await self._log_error(
+                        {
                             "task": "async_save",
                             "task": "async_save",
                             "db_name": db_name,
                             "db_name": db_name,
                             "error": str(e),
                             "error": str(e),
                             "message": f"Failed to save data to {db_name}",
                             "message": f"Failed to save data to {db_name}",
                             "query": query,
                             "query": query,
-                            "params": params,
+                            "params": str(params),
                         }
                         }
                     )
                     )
-                    raise e
+                    raise
 
 
     def get_pool(self, db_name):
     def get_pool(self, db_name):
         return self.pools.get(db_name)
         return self.pools.get(db_name)

+ 0 - 0
tests/__init__.py


+ 16 - 0
tests/conftest.py

@@ -0,0 +1,16 @@
+import pytest
+
+from app import app as quart_app
+
+
+@pytest.fixture
+def app():
+    """提供 Quart 测试 app 实例。"""
+    quart_app.config["TESTING"] = True
+    return quart_app
+
+
+@pytest.fixture
+def client(app):
+    """提供 Quart 异步测试客户端。"""
+    return app.test_client()

+ 0 - 0
tests/integration/__init__.py


+ 11 - 0
tests/integration/test_health.py

@@ -0,0 +1,11 @@
+import pytest
+
+
+@pytest.mark.asyncio
+async def test_health_returns_ok(client):
+    response = await client.get("/api/health")
+    assert response.status_code == 200
+
+    data = await response.get_json()
+    assert data["code"] == 0
+    assert data["data"]["status"] == "healthy"

+ 0 - 0
tests/unit/__init__.py


+ 8 - 0
tests/unit/test_config.py

@@ -0,0 +1,8 @@
+from src.config import LongArticlesSearchAgentConfig
+
+
+def test_default_config_loads():
+    config = LongArticlesSearchAgentConfig()
+    assert config.app_name == "LongArticleSearchAgent"
+    assert config.environment == "development"
+    assert config.debug is False