commit 9c1512889cd5eea25718acd91a26bbc2edc3600b Author: yyh Date: Sat Mar 22 20:52:03 2025 +0800 init-commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a993663 --- /dev/null +++ b/.env.example @@ -0,0 +1,18 @@ +# 数据库配置 +DATABASE_URL=sqlite:///data/sms_database.db +# 也可以使用其他数据库,例如: +# DATABASE_URL=postgresql://user:password@localhost:5432/sms_db +# DATABASE_URL=mysql+pymysql://user:password@localhost:3306/sms_db + +# API密钥配置 +API_KEY=sk-nmhjklnm + +# 服务器配置 +HOST=0.0.0.0 +PORT=8322 +LOG_LEVEL=debug # 控制日志级别: debug, info, warning, error, critical + + +# 其他高级选项 +# SSL_CERT_PATH=/path/to/cert.pem +# SSL_KEY_PATH=/path/to/key.pem \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..46e55a8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,65 @@ +# 系统文件 +.DS_Store +Thumbs.db +desktop.ini + +# Python相关 +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# 虚拟环境 +venv/ +ENV/ +env/ +.env +.venv/ + +# 数据库 +data/sms_database.db +*.db +*.sqlite3 + +# 日志 +*.log +logs/ + +# IDE相关 +.idea/ +.vscode/ +*.swp +*.swo +.project +.pydevproject +.settings/ +.pytest_cache/ +.coverage +htmlcov/ + +# 本地配置 +.env +.env.local +.env.development +.env.test +.env.production + +# 部署相关 +*.pem +*.key diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1f9429b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM python:3.9-slim + +WORKDIR /app + +# 安装依赖 +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# 创建数据目录 +RUN mkdir -p /app/data + +# 复制应用代码 +COPY . . + +# 确保数据目录有正确的权限 +RUN chmod -R 755 /app/data + +# 暴露应用端口 +EXPOSE 8322 + +# 运行应用 +CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8322"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..2ddf656 --- /dev/null +++ b/README.md @@ -0,0 +1,195 @@ +# SMS接收服务器 + +这是一个短信接收平台,提供强大的SMS接收服务和验证码提取功能。 + +## 核心功能 + +- **短信接收**:支持接收来自多种服务的SMS消息 +- **验证码自动提取**:智能识别并提取短信中的验证码 +- **历史记录查询**:方便查看已接收的所有短信 +- **实时更新**:无需刷新页面即可获取最新短信 +- **多平台支持**:兼容各类应用和网站的验证流程 + +## 项目结构 + +``` +sms_server +├── backend +│ ├── main.py # FastAPI应用程序入口 +│ ├── __init__.py # 后端包初始化 +│ ├── routes +│ │ ├── __init__.py # 路由包初始化 +│ │ └── v1 +│ │ ├── __init__.py # v1版本API路由模块初始化 +│ │ └── api.py # 定义SMS和验证码提取相关的API路由 +│ ├── services +│ │ ├── __init__.py # 服务包初始化 +│ │ └── sms_service.py # SMS处理服务和业务逻辑 +│ ├── models +│ │ ├── __init__.py # 模型包初始化 +│ │ ├── schema.py # Pydantic模型定义 +│ │ └── sms.py # 数据库模型定义 +│ ├── middlewares +│ │ └── logging_middleware.py # 日志中间件 +│ ├── utils +│ │ ├── __init__.py # 工具包初始化 +│ │ └── logger.py # 日志工具 +│ └── config +│ ├── __init__.py # 配置模块初始化 +│ ├── settings.py # 加载和管理应用配置 +│ └── .env # 存储环境变量 +├── frontend +│ ├── templates +│ │ ├── base.html # 基础HTML模板 +│ │ └── index.html # 首页模板 +│ └── static +│ ├── css +│ │ └── styles.css # 应用样式 +│ └── js +│ └── app.js # 前端JavaScript逻辑 +├── data # 数据存储目录 +│ └── sms_database.db # SQLite数据库文件 +├── Dockerfile # Docker镜像构建文件 +├── .env # 环境变量配置 +├── .env.example # 环境变量示例 +├── requirements.txt # 项目依赖列表 +├── sms_service.sh # systemd服务安装脚本 +└── README.md # 项目文档和说明 +``` + +## 安装指南 + +### 方法1: 直接安装 + +1. 克隆仓库: + ``` + git clone <仓库地址> + cd sms_server + ``` + +2. 安装所需依赖: + ``` + pip install -r requirements.txt + ``` + +3. 配置环境变量: + ``` + cp .env.example .env + # 编辑.env文件设置必要的环境变量 + ``` + +### 方法2: Docker安装 + +1. 使用 Docker 构建镜像: + ``` + docker build -t sms-server . + ``` + +2. 启动容器: + ``` + docker run -d --name sms-server -p 8322:8322 -v ./data:/app/data sms-server + ``` + +### 方法3: 使用systemd服务 + +1. 执行服务安装脚本: + ``` + sudo bash sms_service.sh + ``` + +2. 检查服务状态: + ``` + sudo systemctl status sms-webhook.service + ``` + +## 服务配置 + +您可以通过编辑 `.env` 文件来配置服务: + +``` +# 数据库配置 +DATABASE_URL=sqlite:///data/sms_database.db + +# API密钥配置 +API_KEY=your_api_key_here + +# 服务器配置 +HOST=0.0.0.0 +PORT=8322 +LOG_LEVEL=info # 可选: debug, info, warning, error, critical +``` + +## 运行应用 + +### 方法1: 使用Python运行 + +在项目根目录下执行以下命令启动FastAPI应用: +``` +uvicorn backend.main:app --host 0.0.0.0 --port 8322 --reload +``` + +### 方法2: 脚本运行 + +在项目根目录下执行: +``` +python -m backend.main +``` + +应用将在配置的端口上运行(默认为 `http://0.0.0.0:8322`)。 + +## API使用示例 + +### 接收短信 + +```bash +curl -X POST "http://localhost:8322/v1/sms/receive" \ + -H "Content-Type: application/json" \ + -H "X-API-Key: your_api_key_here" \ + -d '{ + "from": "+1234567890", + "contact_name": "验证码服务", + "sms": "您的验证码是: 123456,请在5分钟内使用。", + "sim_slot": "13800138000", + "receive_time": "2023-04-01T12:00:00Z" + }' +``` + +### 查询验证码 + +```bash +curl "http://localhost:8322/v1/sms/code?phone_number=13800138000" +``` + +### 获取历史记录 + +```bash +curl "http://localhost:8322/v1/sms/history?limit=10&offset=0" +``` + +## 应用场景 + +- 注册新账号需要接收验证码 +- 测试短信发送功能 +- 需要临时手机号接收短信 +- 不想泄露个人手机号 +- API集成到自动化测试流程 + +## API文档 + +API文档可在`http://localhost:8322/docs`访问。 + +## 前端界面 + +前端界面可通过访问`http://localhost:8322/`使用。主要功能包括: + +- 查询特定手机号的验证码 +- 查看最近接收的短信记录 +- 查看短信详细内容 + +## 贡献指南 + +欢迎贡献!请通过issue或提交pull request来提供改进或修复bug。 + +## 许可证 + +本项目采用MIT许可证。详情请查看LICENSE文件。 \ No newline at end of file diff --git a/backend/__init__.py b/backend/__init__.py new file mode 100644 index 0000000..32e59bc --- /dev/null +++ b/backend/__init__.py @@ -0,0 +1 @@ +# 初始化 backend 包 diff --git a/backend/config/__init__.py b/backend/config/__init__.py new file mode 100644 index 0000000..08d2a33 --- /dev/null +++ b/backend/config/__init__.py @@ -0,0 +1,4 @@ +# 初始化配置包 +from .settings import Settings + +__all__ = ["Settings"] \ No newline at end of file diff --git a/backend/config/settings.py b/backend/config/settings.py new file mode 100644 index 0000000..8730233 --- /dev/null +++ b/backend/config/settings.py @@ -0,0 +1,25 @@ +import os +from typing import Optional, Dict, Any +from pydantic_settings import BaseSettings + +class Settings(BaseSettings): + # 基本配置 + database_url: str = os.getenv("DATABASE_URL", "sqlite:///sms_database.db") + api_key: str = os.getenv("API_KEY", "sk-nmhjklnm") + host: str = os.getenv("HOST", "0.0.0.0") + port: int = int(os.getenv("PORT", "8322")) + log_level: str = os.getenv("LOG_LEVEL", "info").lower() + + # 数据库配置 + db_connect_args: Dict[str, Any] = {"check_same_thread": False} + + # 应用配置 + app_title: str = "SMS 验证码接收服务" + app_version: str = "1.0" + + class Config: + env_file = ".env" + case_sensitive = False + +# 创建一个全局可用的设置实例 +settings = Settings() diff --git a/backend/main.py b/backend/main.py new file mode 100755 index 0000000..a5423ac --- /dev/null +++ b/backend/main.py @@ -0,0 +1,56 @@ +from fastapi import FastAPI, Request +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates +import os +from contextlib import asynccontextmanager + +from backend.routes.v1.api import router +from backend.models.sms import create_db_and_tables, ensure_data_directory +from backend.config.settings import settings +from backend.middlewares.logging_middleware import logging_middleware +from backend.utils.logger import setup_logging + +# 设置日志配置 +logger = setup_logging() + +# 创建 lifespan 上下文管理器 +@asynccontextmanager +async def lifespan(app: FastAPI): + # 在应用启动时执行的代码 + # 确保数据目录存在 + ensure_data_directory() + create_db_and_tables() + logger.info("数据库表已初始化") + yield + # 在应用关闭时执行的代码 + logger.info("应用已关闭") + +# 创建应用 +app = FastAPI( + title=settings.app_title, + version=settings.app_version, + debug=(settings.log_level == "debug"), # 根据log_level设置debug + lifespan=lifespan +) + +# 注册路由和中间件 +app.include_router(router) +app.middleware("http")(logging_middleware) + +# 配置静态文件和模板 +templates_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "frontend", "templates") +static_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "frontend", "static") + +templates = Jinja2Templates(directory=templates_dir) +app.mount("/static", StaticFiles(directory=static_dir), name="static") + +# 主页路由 +@app.get("/", tags=["UI"]) +async def index(request: Request): + logger.debug("访问主页路由") + return templates.TemplateResponse("index.html", {"request": request}) + +# 使用该方法启动应用: uvicorn backend.main:app --host 0.0.0.0 --port 8322 +if __name__ == "__main__": + import uvicorn + uvicorn.run("backend.main:app", host=settings.host, port=settings.port, reload=True, log_level=settings.log_level) \ No newline at end of file diff --git a/backend/middlewares/logging_middleware.py b/backend/middlewares/logging_middleware.py new file mode 100644 index 0000000..10d82a7 --- /dev/null +++ b/backend/middlewares/logging_middleware.py @@ -0,0 +1,39 @@ +from fastapi import Request, Response +import traceback +from backend.utils.logger import get_logger + +# 获取日志记录器 +logger = get_logger("uvicorn.error") + +async def logging_middleware(request: Request, call_next): + # 记录请求信息 + logger.info(f"请求 {request.method} {request.url.path}") + + # 记录详细的请求信息(如果日志级别为DEBUG) + if logger.level <= logging.DEBUG: + headers_str = "\n".join([f"{k}: {v}" for k, v in request.headers.items()]) + logger.debug(f"请求 Headers: {headers_str}") + + body = await request.body() + if body: + logger.debug(f"请求 Body: {body.decode('utf-8', errors='replace')}") + + # 恢复请求体流,供下游消费 + async def receive(): + return {"type": "http.request", "body": body} + request._receive = receive + + try: + # 处理请求 + response = await call_next(request) + logger.info(f"响应状态码: {response.status_code}") + return response + except Exception as e: + # 记录异常信息 + error_details = traceback.format_exc() + logger.error(f"处理请求时发生异常: {str(e)}\n{error_details}") + # 重新抛出异常,让FastAPI处理 + raise + +# 添加缺失的导入 +import logging \ No newline at end of file diff --git a/backend/models/__init__.py b/backend/models/__init__.py new file mode 100644 index 0000000..d0324ed --- /dev/null +++ b/backend/models/__init__.py @@ -0,0 +1 @@ +# 初始化 models 包 diff --git a/backend/models/schema.py b/backend/models/schema.py new file mode 100644 index 0000000..540d58f --- /dev/null +++ b/backend/models/schema.py @@ -0,0 +1,33 @@ +from pydantic import BaseModel, Field +from typing import Optional, Dict, Any, List +from datetime import datetime + +# 用于接收短信的请求模型 +class SMSRecordCreate(BaseModel): + from_: str = Field(..., alias='from') + contact_name: Optional[str] = None + phone_area: Optional[str] = None + sms: str + sim_slot: Optional[str] = None + sim_sub_id: Optional[str] = None + device_name: Optional[str] = None + receive_time: datetime + + class Config: + populate_by_name = True + json_schema_extra = { + "example": { + "from": "10086", + "contact_name": "中国移动", + "sms": "您的验证码是123456,请在5分钟内完成验证。", + "sim_slot": "SIM1(15012345678)", + "receive_time": "2023-03-15T14:30:00Z" + } + } + +# 通用响应包装器 +class ResponseWrapper(BaseModel): + result: str + code: str + message: str + data: Optional[Dict[str, Any]] = None diff --git a/backend/models/sms.py b/backend/models/sms.py new file mode 100644 index 0000000..7ee8a15 --- /dev/null +++ b/backend/models/sms.py @@ -0,0 +1,48 @@ +from sqlmodel import SQLModel, Field, create_engine, Session +from typing import Optional +from datetime import datetime +import os +from backend.config.settings import settings + +# 确保数据目录存在 +def ensure_data_directory(): + # 从数据库URL中提取文件路径 + if settings.database_url.startswith('sqlite:///'): + db_path = settings.database_url.replace('sqlite:///', '') + # 获取目录路径 + dir_path = os.path.dirname(db_path) + # 如果目录不为空且不存在,则创建目录 + if dir_path and not os.path.exists(dir_path): + os.makedirs(dir_path, exist_ok=True) + print(f"已创建数据目录: {dir_path}") + +# 确保数据目录存在 +ensure_data_directory() + +# 数据库引擎 +engine = create_engine(settings.database_url, echo=False, connect_args=settings.db_connect_args) + +# 数据模型 +class SMSRecord(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + from_: str = Field(..., alias='from') + contact_name: Optional[str] + phone_area: Optional[str] + sms: str + sim_slot: Optional[str] + sim_sub_id: Optional[str] + device_name: Optional[str] + receive_time: datetime + extracted_code: Optional[str] = None + phone_number: Optional[str] = None # 从 sim_slot 中提取的手机号 + +# 创建表结构 +def create_db_and_tables(): + # 确保数据目录存在 + ensure_data_directory() + SQLModel.metadata.create_all(engine) + +# 创建会话工厂 +def get_session(): + with Session(engine) as session: + yield session diff --git a/backend/routes/__init__.py b/backend/routes/__init__.py new file mode 100644 index 0000000..f2c53b4 --- /dev/null +++ b/backend/routes/__init__.py @@ -0,0 +1,4 @@ +# 初始化 routes 包 +from . import v1 + +__all__ = ["v1"] diff --git a/backend/routes/v1/__init__.py b/backend/routes/v1/__init__.py new file mode 100644 index 0000000..34964f5 --- /dev/null +++ b/backend/routes/v1/__init__.py @@ -0,0 +1,4 @@ +# 初始化 v1 API 包 +from . import api + +__all__ = ["api"] diff --git a/backend/routes/v1/api.py b/backend/routes/v1/api.py new file mode 100644 index 0000000..47b6791 --- /dev/null +++ b/backend/routes/v1/api.py @@ -0,0 +1,106 @@ +import asyncio +from fastapi import APIRouter, HTTPException, Header, Query, Depends +from sqlmodel import Session, select +from typing import Optional, List +from datetime import datetime, timedelta + +from backend.models.sms import SMSRecord, get_session +from backend.models.schema import SMSRecordCreate, ResponseWrapper +from backend.services.sms_service import ( + extract_code_with_context, + extract_phone_from_sim_slot, + query_latest_code +) +from backend.config.settings import settings + +# 创建API路由器 +router = APIRouter(prefix="/v1/sms", tags=["sms"]) + +# API KEY 校验 +def check_api_key(x_api_key: Optional[str] = Header(None)): + if (x_api_key != settings.api_key): + raise HTTPException(status_code=401, detail="Unauthorized") + +# 接收短信接口 +@router.post("/receive") +async def receive_sms( + payload: SMSRecordCreate, + session: Session = Depends(get_session), + x_api_key: Optional[str] = Header(None) +): + check_api_key(x_api_key) + + code = extract_code_with_context(payload.sms) + extracted_phone = extract_phone_from_sim_slot(payload.sim_slot) + + record_data = payload.dict() + record = SMSRecord(**record_data, extracted_code=code, phone_number=extracted_phone) + + session.add(record) + session.commit() + + print(f"[RECEIVED] From: {payload.from_}, Code: {code}, Time: {payload.receive_time.isoformat()}") + return ResponseWrapper(result="ok", code="SUCCESS", message="短信接收成功").dict() + +# 获取验证码接口 +@router.get("/code") +async def get_sms_code( + phone_number: str = Query(...), + platform_keyword: Optional[str] = Query(None), + wait_timeout: int = Query(5) +): + end_time = datetime.utcnow() + timedelta(seconds=wait_timeout) + while datetime.utcnow() < end_time: + code, record = query_latest_code(phone_number, platform_keyword) + if code: + return ResponseWrapper( + result="ok", + code="SUCCESS", + message="验证码获取成功", + data={ + "code": code, + "matched_by": "keyword" if platform_keyword else "fallback", + "sms_excerpt": record.sms[:50], + "received_time": record.receive_time.isoformat() + } + ).dict() + await asyncio.sleep(1) + return ResponseWrapper(result="not_found", code="TIMEOUT", message="验证码超时未找到").dict() + +# 历史记录接口 +@router.get("/history", response_model=List[SMSRecord]) +async def list_sms( + limit: int = Query(20), + offset: int = Query(0), + session: Session = Depends(get_session) +): + records = session.exec( + select(SMSRecord).order_by(SMSRecord.receive_time.desc()).offset(offset).limit(limit) + ).all() + return records + +# 获取单条短信详情 +@router.get("/{sms_id}", response_model=SMSRecord) +async def get_sms_detail( + sms_id: int, + session: Session = Depends(get_session) +): + record = session.get(SMSRecord, sms_id) + if not record: + raise HTTPException(status_code=404, detail="短信记录不存在") + return record + +# 删除短信记录 +@router.delete("/{sms_id}") +async def delete_sms( + sms_id: int, + session: Session = Depends(get_session), + x_api_key: Optional[str] = Header(None) +): + check_api_key(x_api_key) + record = session.get(SMSRecord, sms_id) + if not record: + raise HTTPException(status_code=404, detail="短信记录不存在") + session.delete(record) + session.commit() + return ResponseWrapper(result="ok", code="SUCCESS", message="短信记录已删除").dict() diff --git a/backend/services/__init__.py b/backend/services/__init__.py new file mode 100644 index 0000000..c2ba688 --- /dev/null +++ b/backend/services/__init__.py @@ -0,0 +1 @@ +# 初始化 services 包 diff --git a/backend/services/functions.py b/backend/services/functions.py new file mode 100644 index 0000000..cc49490 --- /dev/null +++ b/backend/services/functions.py @@ -0,0 +1,18 @@ +from typing import Optional +import re + +def extract_code_with_context(sms_content: str) -> Optional[str]: + pattern = r'(?:验证码|auth|code)[^0-9]{0,20}(\d{4,8})' + match = re.search(pattern, sms_content, re.IGNORECASE) + if match: + return match.group(1) + fallback = re.findall(r'(\d{4,8})', sms_content) + return fallback[0] if fallback else None + +def extract_phone_from_sim_slot(sim_slot: Optional[str]) -> Optional[str]: + if not sim_slot: + return None + match = re.search(r'1\d{10}', sim_slot) + if match: + return match.group(0) + return None \ No newline at end of file diff --git a/backend/services/sms_service.py b/backend/services/sms_service.py new file mode 100644 index 0000000..dbec524 --- /dev/null +++ b/backend/services/sms_service.py @@ -0,0 +1,54 @@ +import re +from typing import Optional, Tuple +from datetime import datetime, timedelta +from sqlmodel import select, Session + +from backend.models.sms import SMSRecord, engine + +def extract_code_with_context(sms_content: str) -> Optional[str]: + """从短信内容中提取验证码""" + pattern = r'(?:验证码|auth|code)[^0-9]{0,20}(\d{4,8})' + match = re.search(pattern, sms_content, re.IGNORECASE) + if match: + return match.group(1) + fallback = re.findall(r'(\d{4,8})', sms_content) + return fallback[0] if fallback else None + +def extract_phone_from_sim_slot(sim_slot: Optional[str]) -> Optional[str]: + """从SIM卡信息中提取手机号""" + if not sim_slot: + return None + # 尝试提取11位手机号 + match = re.search(r'1\d{10}', sim_slot) + if match: + return match.group(0) + return None + +def query_latest_code(phone_number: str, platform_keyword: Optional[str]) -> Tuple[Optional[str], Optional[SMSRecord]]: + """查询最新的验证码""" + ten_minutes_ago = datetime.utcnow() - timedelta(minutes=10) + + with Session(engine) as session: + query = select(SMSRecord).where( + SMSRecord.receive_time >= ten_minutes_ago + ) + + + primary_query = query.where(SMSRecord.phone_number == phone_number) + if platform_keyword: + primary_query = primary_query.where(SMSRecord.sms.contains(platform_keyword)) + + primary_records = session.exec(primary_query.order_by(SMSRecord.receive_time.desc()).limit(5)).all() + + if not primary_records: + fallback_query = query.where(SMSRecord.sim_slot.contains(phone_number)) + if platform_keyword: + fallback_query = fallback_query.where(SMSRecord.sms.contains(platform_keyword)) + + primary_records = session.exec(fallback_query.order_by(SMSRecord.receive_time.desc()).limit(5)).all() + + for record in primary_records: + if record.extracted_code: + return record.extracted_code, record + + return None, None diff --git a/backend/utils/__init__.py b/backend/utils/__init__.py new file mode 100644 index 0000000..88f38e5 --- /dev/null +++ b/backend/utils/__init__.py @@ -0,0 +1,4 @@ +# 初始化工具包 +from . import logger + +__all__ = ["logger"] diff --git a/backend/utils/logger.py b/backend/utils/logger.py new file mode 100644 index 0000000..925c83b --- /dev/null +++ b/backend/utils/logger.py @@ -0,0 +1,61 @@ +import logging +import os +import sys +from logging.handlers import RotatingFileHandler +from datetime import datetime + +from backend.config.settings import settings + +def setup_logging(): + """设置日志配置""" + # 创建日志目录 + log_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "logs") + os.makedirs(log_dir, exist_ok=True) + + # 设置日志文件路径 + log_file = os.path.join(log_dir, f"sms_server_{datetime.now().strftime('%Y%m%d')}.log") + + # 设置日志级别 + log_level_name = settings.log_level.upper() + log_level = getattr(logging, log_level_name, logging.INFO) + + # 配置根日志记录器 + logger = logging.getLogger() + logger.setLevel(log_level) + + # 清除现有处理器 + for handler in logger.handlers[:]: + logger.removeHandler(handler) + + # 创建格式化器 + formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # 添加控制台处理器 + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setFormatter(formatter) + console_handler.setLevel(log_level) + logger.addHandler(console_handler) + + # 添加文件处理器 + file_handler = RotatingFileHandler( + log_file, maxBytes=10*1024*1024, backupCount=5, encoding='utf-8' + ) + file_handler.setFormatter(formatter) + file_handler.setLevel(log_level) + logger.addHandler(file_handler) + + # 为第三方库设置更高的日志级别以减少噪音 + for module in ["uvicorn", "uvicorn.error", "fastapi"]: + if log_level > logging.DEBUG: + logging.getLogger(module).setLevel(logging.WARNING) + + logger.info(f"日志级别设置为: {log_level_name}") + logger.info(f"日志保存在: {log_file}") + + return logger + +def get_logger(name): + """获取指定名称的日志记录器""" + return logging.getLogger(name) diff --git a/frontend/static/css/styles.css b/frontend/static/css/styles.css new file mode 100644 index 0000000..0182e4f --- /dev/null +++ b/frontend/static/css/styles.css @@ -0,0 +1,301 @@ +/* 全局样式 */ +:root { + --primary-color: #4e73df; + --secondary-color: #6c757d; + --success-color: #1cc88a; + --info-color: #36b9cc; + --warning-color: #f6c23e; + --danger-color: #e74a3b; + --light-color: #f8f9fc; + --dark-color: #5a5c69; + --transition-speed: 0.3s; +} + +body { + font-family: 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + background-color: #f8f9fc; + color: #5a5c69; + line-height: 1.6; +} + +/* 导航栏样式 */ +.navbar { + box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.15); +} + +.navbar-dark { + background: linear-gradient(135deg, #4e73df 0%, #224abe 100%); +} + +.navbar-brand { + font-weight: 700; + font-size: 1.25rem; +} + +/* 卡片样式 */ +.card { + border: none; + border-radius: 0.5rem; + box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.1); + transition: all var(--transition-speed) ease; + margin-bottom: 1.5rem; + overflow: hidden; +} + +.card:hover { + box-shadow: 0 0.5rem 2rem 0 rgba(58, 59, 69, 0.15); + transform: translateY(-2px); +} + +.card-header { + padding: 1rem 1.25rem; + border-bottom: 1px solid #e3e6f0; + background-color: #fff; +} + +.bg-gradient-primary { + background: linear-gradient(135deg, #4e73df 0%, #224abe 100%); + color: white; +} + +.bg-gradient-secondary { + background: linear-gradient(135deg, #858796 0%, #60616f 100%); + color: white; +} + +.bg-gradient-info { + background: linear-gradient(135deg, #36b9cc 0%, #258391 100%); + color: white; +} + +/* 欢迎横幅 */ +.welcome-banner { + background: linear-gradient(135deg, #4e73df 0%, #224abe 100%); + color: white; + padding: 2rem; + border-radius: 0.5rem; + margin-bottom: 1.5rem; + box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.15); +} + +/* 特性卡片 */ +.feature-card { + display: flex; + align-items: flex-start; + padding: 1rem; + border-radius: 0.5rem; + transition: all var(--transition-speed) ease; + background-color: #f8f9fc; +} + +.feature-card:hover { + background-color: #eaecf4; + transform: translateY(-2px); +} + +.feature-icon { + font-size: 1.5rem; + margin-right: 1rem; + color: var(--primary-color); + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + background-color: rgba(78, 115, 223, 0.1); + border-radius: 50%; +} + +.feature-content h5 { + margin-bottom: 0.5rem; + font-weight: 600; +} + +.feature-content p { + margin-bottom: 0; + font-size: 0.875rem; + color: #858796; +} + +/* 表单样式 */ +.form-label { + font-weight: 600; + margin-bottom: 0.5rem; +} + +.input-group-text { + background-color: #f8f9fc; + border-right: none; +} + +.form-control { + border-left: none; + padding-left: 0; +} + +.form-control:focus { + box-shadow: none; + border-color: #d1d3e2; +} + +/* 按钮样式 */ +.btn { + font-weight: 600; + padding: 0.5rem 1rem; + border-radius: 0.25rem; + transition: all var(--transition-speed) ease; +} + +.btn-primary { + background-color: var(--primary-color); + border-color: var(--primary-color); +} + +.btn-primary:hover { + background-color: #2653d4; + border-color: #244ec9; +} + +/* 查询结果样式 */ +.result-container { + border-radius: 0.5rem; + overflow: hidden; + border: 1px solid #e3e6f0; + transition: all var(--transition-speed) ease; +} + +.result-header { + padding: 0.75rem 1rem; + background-color: #f8f9fc; + font-weight: 600; + border-bottom: 1px solid #e3e6f0; + color: var(--primary-color); +} + +.result-body { + padding: 1rem; + background-color: white; +} + +/* 表格样式 */ +.table { + margin-bottom: 0; +} + +.table th { + font-weight: 600; + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.05em; + border-top: none; + padding: 1rem; +} + +.table td { + padding: 1rem; + vertical-align: middle; +} + +.sms-content { + max-width: 300px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* 分页样式 */ +.pagination { + margin-bottom: 0; +} + +.page-item.active .page-link { + background-color: var(--primary-color); + border-color: var(--primary-color); +} + +.page-link { + color: var(--primary-color); +} + +.page-link:hover { + color: #224abe; +} + +/* 加载动画 */ +.spinner-border { + width: 1.5rem; + height: 1.5rem; +} + +/* 页脚样式 */ +.footer { + background-color: white; + box-shadow: 0 -0.15rem 1.75rem 0 rgba(58, 59, 69, 0.1); + color: #858796; +} + +/* 动画效果 */ +.fade-in { + animation: fadeIn 0.5s ease-in-out; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +/* 响应式调整 */ +@media (max-width: 768px) { + .table th, .table td { + padding: 0.5rem; + } + + .feature-card { + padding: 0.75rem; + } + + .welcome-banner { + padding: 1.5rem; + } +} + +/* 模态框样式 */ +.modal-content { + border: none; + border-radius: 0.5rem; + box-shadow: 0 0.5rem 2rem 0 rgba(58, 59, 69, 0.2); +} + +.modal-header { + background: linear-gradient(135deg, #4e73df 0%, #224abe 100%); + color: white; + border-bottom: none; + border-top-left-radius: 0.5rem; + border-top-right-radius: 0.5rem; +} + +.modal-footer { + border-top: 1px solid #e3e6f0; +} + +/* 状态徽章 */ +.badge { + font-weight: 600; + padding: 0.35em 0.65em; + border-radius: 0.25rem; +} + +.badge-success { + background-color: var(--success-color); + color: white; +} + +.badge-warning { + background-color: var(--warning-color); + color: #5a5c69; +} + +/* 工具提示 */ +.tooltip { + font-size: 0.75rem; +} \ No newline at end of file diff --git a/frontend/static/js/app.js b/frontend/static/js/app.js new file mode 100644 index 0000000..ec05316 --- /dev/null +++ b/frontend/static/js/app.js @@ -0,0 +1,1072 @@ +/** + * SMS 验证码服务前端 JavaScript + * 主要功能:短信记录查询和验证码获取 + */ +document.addEventListener("DOMContentLoaded", function() { + // 初始化常量和变量 + const PAGE_SIZE = 10; + let currentPage = 1; + let totalRecords = 0; + + // DOM 元素 + const codeQueryForm = document.getElementById('codeQueryForm'); + const queryResult = document.getElementById('queryResult'); + const resultContent = document.getElementById('resultContent'); + const smsRecordsTable = document.getElementById('smsRecordsTable'); + const loadingIndicator = document.getElementById('loadingIndicator'); + const noRecords = document.getElementById('noRecords'); + const pagination = document.getElementById('pagination'); + const refreshButton = document.querySelector('.refresh-records'); + + // 初始化页面 + initializePage(); + + /** + * 初始化页面 + */ + function initializePage() { + // 加载短信记录 + loadSMSRecords(currentPage); + + // 绑定事件监听器 + bindEventListeners(); + + // 初始化工具提示 + initializeTooltips(); + } + + /** + * 绑定事件监听器 + */ + function bindEventListeners() { + // 验证码查询表单提交 + if (codeQueryForm) { + codeQueryForm.addEventListener('submit', function(e) { + e.preventDefault(); + queryVerificationCode(); + }); + } + + // 刷新按钮点击 + if (refreshButton) { + refreshButton.addEventListener('click', function() { + loadSMSRecords(currentPage); + }); + } + + // 监听窗口大小变化,调整表格显示 + window.addEventListener('resize', function() { + adjustTableColumns(); + }); + } + + /** + * 初始化工具提示 + */ + function initializeTooltips() { + // 如果Bootstrap tooltip可用 + if (typeof bootstrap !== 'undefined' && bootstrap.Tooltip) { + const tooltips = document.querySelectorAll('[data-bs-toggle="tooltip"]'); + tooltips.forEach(tooltip => { + new bootstrap.Tooltip(tooltip); + }); + } + } + + /** + * 加载短信记录 + * @param {number} page - 页码 + */ + function loadSMSRecords(page) { + const offset = (page - 1) * PAGE_SIZE; + + // 显示加载指示器 + if (loadingIndicator) { + loadingIndicator.classList.remove('d-none'); + } + + // 隐藏无记录提示 + if (noRecords) { + noRecords.classList.add('d-none'); + } + + // 清空表格内容 + if (smsRecordsTable) { + const tableBody = smsRecordsTable.querySelector('tbody'); + if (tableBody) { + tableBody.innerHTML = ''; + } + } + + // 获取短信记录 + fetch(`/v1/sms/history?limit=${PAGE_SIZE}&offset=${offset}`) + .then(response => { + if (!response.ok) { + throw new Error('网络响应异常'); + } + return response.json(); + }) + .then(data => { + if (Array.isArray(data)) { + // 更新记录总数 + totalRecords = data.length === PAGE_SIZE ? -1 : offset + data.length; + + // 渲染记录 + renderSMSRecords(data); + + // 更新分页 + updatePagination(data.length === PAGE_SIZE); + + // 调整表格列 + adjustTableColumns(); + + // 显示无记录提示 + if (data.length === 0 && noRecords) { + noRecords.classList.remove('d-none'); + } + } + }) + .catch(error => { + console.error('获取短信记录失败:', error); + showToast('error', '获取短信记录失败', error.message); + }) + .finally(() => { + // 隐藏加载指示器 + if (loadingIndicator) { + loadingIndicator.classList.add('d-none'); + } + }); + } + + /** + * 渲染短信记录到表格 + * @param {Array} records - 短信记录数组 + */ + function renderSMSRecords(records) { + if (!smsRecordsTable) return; + + const tableBody = smsRecordsTable.querySelector('tbody'); + if (!tableBody) return; + + tableBody.innerHTML = ''; + + if (records.length === 0) { + return; + } + + records.forEach(record => { + const row = document.createElement('tr'); + row.classList.add('fade-in'); + + // 为验证码创建适当的徽章 + const codeElement = record.extracted_code + ? `${record.extracted_code}` + : `未提取`; + + row.innerHTML = ` + ${record.id} + ${escapeHtml(record.from_ || record.from)} + ${escapeHtml(record.sms)} + ${codeElement} + ${formatDateTime(record.receive_time)} + + + + `; + + tableBody.appendChild(row); + }); + + // 添加查看按钮事件监听 + tableBody.querySelectorAll('.view-sms').forEach(button => { + button.addEventListener('click', function() { + const smsId = this.getAttribute('data-id'); + viewSMSDetail(smsId); + }); + }); + + // 重新初始化工具提示 + initializeTooltips(); + } + + /** + * 更新分页控件 + * @param {boolean} hasMore - 是否有更多记录 + */ + function updatePagination(hasMore) { + if (!pagination) return; + + pagination.innerHTML = ''; + + // 上一页按钮 + pagination.appendChild(createPageItem('上一页', currentPage - 1, currentPage === 1, 'fas fa-chevron-left')); + + // 数字页码 + const totalPages = totalRecords > 0 ? Math.ceil(totalRecords / PAGE_SIZE) : currentPage + (hasMore ? 1 : 0); + let startPage = Math.max(1, currentPage - 2); + const endPage = Math.min(totalPages, startPage + 4); + + // 首页按钮 + if (startPage > 1) { + pagination.appendChild(createPageItem('1', 1, false)); + if (startPage > 2) { + const ellipsis = document.createElement('li'); + ellipsis.className = 'page-item disabled'; + ellipsis.innerHTML = '...'; + pagination.appendChild(ellipsis); + } + } + + // 页码按钮 + for (let i = startPage; i <= endPage; i++) { + pagination.appendChild(createPageItem(i.toString(), i, i === currentPage)); + } + + // 最后一页按钮 + if (endPage < totalPages) { + if (endPage < totalPages - 1) { + const ellipsis = document.createElement('li'); + ellipsis.className = 'page-item disabled'; + ellipsis.innerHTML = '...'; + pagination.appendChild(ellipsis); + } + pagination.appendChild(createPageItem(totalPages.toString(), totalPages, false)); + } + + // 下一页按钮 + pagination.appendChild(createPageItem('下一页', currentPage + 1, !hasMore && totalRecords > 0 && currentPage >= totalPages, 'fas fa-chevron-right')); + } + + /** + * 创建分页项 + * @param {string} text - 显示文本 + * @param {number} page - 页码 + * @param {boolean} isActive - 是否是当前页 + * @param {string} icon - 图标类名(可选) + * @returns {HTMLElement} 分页项元素 + */ + function createPageItem(text, page, isDisabled, icon) { + const pageItem = document.createElement('li'); + pageItem.className = `page-item ${isDisabled ? 'disabled' : ''} ${page === currentPage ? 'active' : ''}`; + + const link = document.createElement('a'); + link.className = 'page-link'; + link.href = '#'; + link.setAttribute('data-page', page); + + if (icon) { + link.innerHTML = ``; + link.setAttribute('aria-label', text); + } else { + link.textContent = text; + } + + pageItem.appendChild(link); + + if (!isDisabled) { + link.addEventListener('click', function(e) { + e.preventDefault(); + if (page !== currentPage) { + currentPage = page; + loadSMSRecords(currentPage); + } + }); + } + + return pageItem; + } + + /** + * 查询验证码 + */ + function queryVerificationCode() { + const phoneNumber = document.getElementById('phoneNumber').value; + const platformKeyword = document.getElementById('platformKeyword').value; + const timeout = document.getElementById('timeout').value; + + if (!phoneNumber) { + showToast('warning', '请输入手机号码', '请输入有效的手机号码以查询验证码'); + return; + } + + // 显示查询结果区域 + if (queryResult) { + queryResult.classList.remove('d-none'); + } + + // 显示加载状态 + if (resultContent) { + resultContent.innerHTML = ` +
+
+

正在查询验证码,请稍候...

+
+ `; + } + + // 构建请求URL + let url = `/v1/sms/code?phone_number=${encodeURIComponent(phoneNumber)}&wait_timeout=${timeout}`; + if (platformKeyword) { + url += `&platform_keyword=${encodeURIComponent(platformKeyword)}`; + } + + // 发送请求 + fetch(url) + .then(response => { + if (!response.ok) { + throw new Error('网络响应异常'); + } + return response.json(); + }) + .then(data => { + // 显示查询结果 + if (resultContent) { + if (data.result === 'ok') { + resultContent.innerHTML = ` +
+
+ +
验证码获取成功!
+
+
+
+
+ 验证码: +
${data.data.code}
+
+
+ 接收时间: +
${formatDateTime(data.data.received_time)}
+
+
+
+ 短信内容: +
${escapeHtml(data.data.sms_excerpt)}...
+
+
+ +
+
+ `; + + // 添加复制验证码功能 + const copyButton = resultContent.querySelector('.copy-code'); + if (copyButton) { + copyButton.addEventListener('click', function() { + const code = this.getAttribute('data-code'); + copyToClipboard(code); + showToast('success', '已复制验证码', `验证码 ${code} 已复制到剪贴板`); + }); + } + } else { + resultContent.innerHTML = ` +
+
+ +
未找到验证码
+
+
+

${data.message}

+

请检查手机号码是否正确,或稍后再试。

+
+ `; + } + } + + // 刷新短信记录,以便查看最新记录 + loadSMSRecords(1); + }) + .catch(error => { + console.error('查询验证码失败:', error); + if (resultContent) { + resultContent.innerHTML = ` +
+
+ +
查询失败
+
+
+

发生错误: ${escapeHtml(error.message)}

+

请检查网络连接或稍后再试。

+
+ `; + } + }); + } + + /** + * 查看短信详情 + * @param {string} smsId - 短信ID + */ + function viewSMSDetail(smsId) { + // 显示加载中模态框 + showLoadingModal('正在加载短信详情...'); + + fetch(`/v1/sms/${smsId}`) + .then(response => { + if (!response.ok) { + throw new Error('网络响应异常'); + } + return response.json(); + }) + .then(data => { + // 关闭加载中模态框 + closeModal('loadingModal'); + + // 创建详情模态框 + const modalHtml = ` + + `; + + // 添加到DOM并显示 + const modalContainer = document.createElement('div'); + modalContainer.innerHTML = modalHtml; + document.body.appendChild(modalContainer); + + const modal = new bootstrap.Modal(document.getElementById('smsDetailModal')); + modal.show(); + + // 添加复制验证码功能 + const copyButton = document.querySelector('.copy-code'); + if (copyButton) { + copyButton.addEventListener('click', function() { + const code = this.getAttribute('data-code'); + copyToClipboard(code); + showToast('success', '已复制验证码', `验证码 ${code} 已复制到剪贴板`); + }); + } + + // 模态框关闭后从DOM中移除 + document.getElementById('smsDetailModal').addEventListener('hidden.bs.modal', function () { + this.remove(); + }); + }) + .catch(error => { + // 关闭加载中模态框 + closeModal('loadingModal'); + + console.error('获取短信详情失败:', error); + showToast('error', '获取短信详情失败', error.message); + }); + } + + /** + * 显示加载中模态框 + * @param {string} message - 加载提示信息 + */ + function showLoadingModal(message) { + const modalHtml = ` + + `; + + const modalContainer = document.createElement('div'); + modalContainer.innerHTML = modalHtml; + document.body.appendChild(modalContainer); + + const modal = new bootstrap.Modal(document.getElementById('loadingModal')); + modal.show(); + } + + /** + * 关闭模态框 + * @param {string} modalId - 模态框ID + */ + function closeModal(modalId) { + const modalElement = document.getElementById(modalId); + if (modalElement) { + const modal = bootstrap.Modal.getInstance(modalElement); + if (modal) { + modal.hide(); + modalElement.addEventListener('hidden.bs.modal', function() { + this.remove(); + }); + } else { + modalElement.remove(); + } + } + } + + /** + * 复制文本到剪贴板 + * @param {string} text - 要复制的文本 + */ + function copyToClipboard(text) { + // 创建临时文本区域 + const textArea = document.createElement('textarea'); + textArea.value = text; + textArea.style.position = 'fixed'; + textArea.style.left = '-999999px'; + textArea.style.top = '-999999px'; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + // 执行复制命令 + try { + document.execCommand('copy'); + } catch (err) { + console.error('复制失败:', err); + } + + // 移除临时文本区域 + document.body.removeChild(textArea); + } + + /** + * 显示提示消息 + * @param {string} type - 提示类型(success, warning, error, info) + * @param {string} title - 提示标题 + * @param {string} message - 提示内容 + */ + function showToast(type, title, message) { + // 映射类型到Bootstrap颜色 + const typeMap = { + 'success': 'bg-success', + 'warning': 'bg-warning', + 'error': 'bg-danger', + 'info': 'bg-info' + }; + + // 映射类型到图标 + const iconMap = { + 'success': 'fas fa-check-circle', + 'warning': 'fas fa-exclamation-triangle', + 'error': 'fas fa-times-circle', + 'info': 'fas fa-info-circle' + }; + + // 创建Toast元素 + const toastId = `toast-${Date.now()}`; + const toastHtml = ` + + `; + + // 创建或获取Toast容器 + let toastContainer = document.querySelector('.toast-container'); + if (!toastContainer) { + toastContainer = document.createElement('div'); + toastContainer.className = 'toast-container position-fixed bottom-0 end-0 p-3'; + document.body.appendChild(toastContainer); + } + + // 添加Toast到容器 + const toastElement = document.createElement('div'); + toastElement.innerHTML = toastHtml; + toastContainer.appendChild(toastElement.firstChild); + + // 显示Toast + const toast = new bootstrap.Toast(document.getElementById(toastId)); + toast.show(); + + // Toast关闭后移除DOM元素 + document.getElementById(toastId).addEventListener('hidden.bs.toast', function() { + this.remove(); + if (toastContainer.children.length === 0) { + toastContainer.remove(); + } + }); + } + + /** + * 格式化日期时间 + * @param {string} dateTimeStr - ISO格式的日期时间字符串 + * @returns {string} 格式化后的日期时间字符串 + */ + function formatDateTime(dateTimeStr) { + if (!dateTimeStr) return '未知时间'; + + const date = new Date(dateTimeStr); + if (isNaN(date.getTime())) return dateTimeStr; + + return date.toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); + } + + /** + * 转义HTML特殊字符 + * @param {string} text - 要转义的文本 + * @returns {string} 转义后的文本 + */ + function escapeHtml(text) { + if (!text) return ''; + + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + + /** + * 根据窗口宽度调整表格列显示 + */ + function adjustTableColumns() { + if (!smsRecordsTable) return; + + const tableHeaders = smsRecordsTable.querySelectorAll('th'); + const tableRows = smsRecordsTable.querySelectorAll('tbody tr'); + + // 如果窗口宽度小于768px,隐藏某些列 + if (window.innerWidth < 768) { + // 例如,隐藏ID列和操作列之外的第3列 + tableHeaders.forEach((th, index) => { + if (index === 2) { // 短信内容列 + th.style.width = '30%'; + } + }); + } else { + // 重置列宽 + tableHeaders.forEach((th, index) => { + if (index === 2) { // 短信内容列 + th.style.width = 'auto'; + } + }); + } + } +}); + +// 在文档加载完成后添加样式类 +document.addEventListener('DOMContentLoaded', function() { + // 为导航栏添加阴影效果 + const navbar = document.querySelector('.navbar'); + if (navbar) { + window.addEventListener('scroll', function() { + if (window.scrollY > 0) { + navbar.classList.add('navbar-shadow'); + } else { + navbar.classList.remove('navbar-shadow'); + } + }); + } +}); + +// 处理敏感信息的函数 +function maskPhoneNumber(phone) { + if (!phone) return ''; + + // 查找符合手机号格式的内容 + const phonePattern = /1[3-9]\d{9}/g; + return phone.replace(phonePattern, match => { + // 保留前3位和后4位,中间用星号替换 + return match.substring(0, 3) + '****' + match.substring(match.length - 4); + }); +} + +function maskSensitiveInfo(text) { + if (!text) return ''; + + // 处理可能包含的手机号 + let maskedText = maskPhoneNumber(text); + + // 处理身份证号 (18位或15位) + const idCardPattern = /(\d{6})\d{8,11}(\d{2}[0-9Xx]?)/g; + maskedText = maskedText.replace(idCardPattern, '$1********$2'); + + return maskedText; +} + +// 页面加载完成后执行 +document.addEventListener('DOMContentLoaded', function() { + // 查询验证码表单处理 + const codeQueryForm = document.getElementById('codeQueryForm'); + const queryResult = document.getElementById('queryResult'); + const resultContent = document.getElementById('resultContent'); + + if (codeQueryForm) { + codeQueryForm.addEventListener('submit', async function(e) { + e.preventDefault(); + + const phoneNumber = document.getElementById('phoneNumber').value; + const platformKeyword = document.getElementById('platformKeyword').value; + const timeout = document.getElementById('timeout').value; + + // 显示加载状态 + resultContent.innerHTML = ` +
+
+ Loading... +
+

正在查询验证码,请稍候...

+
+ `; + queryResult.classList.remove('d-none'); + + try { + // 调用API查询验证码 + const response = await fetch(`/v1/sms/code?phone_number=${encodeURIComponent(phoneNumber)}&platform_keyword=${encodeURIComponent(platformKeyword || '')}&wait_timeout=${timeout}`); + const data = await response.json(); + + // 显示结果 + if (data.result === 'ok') { + resultContent.innerHTML = ` +
+
验证码获取成功
+
+

验证码: ${data.data.code}

+

接收时间: ${new Date(data.data.received_time).toLocaleString()}

+

短信内容: ${maskSensitiveInfo(data.data.sms_excerpt)}...

+
+ `; + } else { + resultContent.innerHTML = ` +
+
未找到验证码
+
+

${data.message}

+
+ `; + } + } catch (error) { + resultContent.innerHTML = ` +
+
查询出错
+
+

请求失败: ${error.message}

+
+ `; + } + }); + } + + // 加载短信历史记录 + loadSMSRecords(); + + // 监听刷新按钮 + const refreshButton = document.querySelector('.refresh-records'); + if (refreshButton) { + refreshButton.addEventListener('click', () => loadSMSRecords()); + } +}); + +// 加载短信历史记录 +async function loadSMSRecords(page = 1, limit = 10) { + const tableBody = document.querySelector('#smsRecordsTable tbody'); + const loadingIndicator = document.getElementById('loadingIndicator'); + const noRecords = document.getElementById('noRecords'); + const pagination = document.getElementById('pagination'); + + if (!tableBody) return; + + // 显示加载指示器 + tableBody.innerHTML = ''; + loadingIndicator.classList.remove('d-none'); + noRecords.classList.add('d-none'); + + try { + // 计算偏移量 + const offset = (page - 1) * limit; + + // 获取短信历史记录 + const response = await fetch(`/v1/sms/history?limit=${limit}&offset=${offset}`); + const records = await response.json(); + + // 隐藏加载指示器 + loadingIndicator.classList.add('d-none'); + + if (records.length === 0) { + // 显示无记录提示 + noRecords.classList.remove('d-none'); + pagination.innerHTML = ''; + } else { + // 渲染记录 + records.forEach(record => { + const row = document.createElement('tr'); + + // 格式化日期时间 + const date = new Date(record.receive_time); + const formattedDate = date.toLocaleString(); + + // 应用敏感信息掩码 + const maskedSMS = maskSensitiveInfo(record.sms); + const maskedFrom = maskSensitiveInfo(record.from_); + const maskedSimSlot = record.sim_slot ? maskSensitiveInfo(record.sim_slot) : ''; + + row.innerHTML = ` + ${record.id} + ${maskedFrom} + +
${maskedSMS}
+ + + + ${record.extracted_code || '无'} + + + ${formattedDate} + + + + `; + + tableBody.appendChild(row); + }); + + // 添加事件监听器到查看详情按钮 + document.querySelectorAll('.view-details').forEach(button => { + button.addEventListener('click', async () => { + const id = button.getAttribute('data-id'); + await showRecordDetails(id); + }); + }); + + // 创建简单分页 + generatePagination(page, limit, records.length === limit); + } + } catch (error) { + console.error('加载短信记录失败:', error); + loadingIndicator.classList.add('d-none'); + tableBody.innerHTML = ` + + + 加载失败: ${error.message} + + + `; + } +} + +// 生成分页控件 +function generatePagination(currentPage, limit, hasMore) { + const pagination = document.getElementById('pagination'); + if (!pagination) return; + + pagination.innerHTML = ''; + + // 上一页按钮 + const prevItem = document.createElement('li'); + prevItem.className = `page-item ${currentPage === 1 ? 'disabled' : ''}`; + prevItem.innerHTML = ``; + if (currentPage > 1) { + prevItem.querySelector('a').addEventListener('click', (e) => { + e.preventDefault(); + loadSMSRecords(currentPage - 1, limit); + }); + } + pagination.appendChild(prevItem); + + // 当前页 + const currentItem = document.createElement('li'); + currentItem.className = 'page-item active'; + currentItem.innerHTML = `${currentPage}`; + pagination.appendChild(currentItem); + + // 下一页按钮 + const nextItem = document.createElement('li'); + nextItem.className = `page-item ${!hasMore ? 'disabled' : ''}`; + nextItem.innerHTML = ``; + if (hasMore) { + nextItem.querySelector('a').addEventListener('click', (e) => { + e.preventDefault(); + loadSMSRecords(currentPage + 1, limit); + }); + } + pagination.appendChild(nextItem); +} + +// 显示记录详情 +async function showRecordDetails(id) { + try { + const response = await fetch(`/v1/sms/${id}`); + const record = await response.json(); + + // 应用敏感信息掩码 + const maskedSMS = maskSensitiveInfo(record.sms); + const maskedFrom = maskSensitiveInfo(record.from_); + const maskedSimSlot = record.sim_slot ? maskSensitiveInfo(record.sim_slot) : '未知'; + const maskedPhoneNumber = record.phone_number ? maskSensitiveInfo(record.phone_number) : '未提取'; + + // 创建模态框 + const modalId = 'recordDetailModal'; + let modal = document.getElementById(modalId); + + // 如果模态框不存在,创建一个 + if (!modal) { + modal = document.createElement('div'); + modal.id = modalId; + modal.className = 'modal fade'; + modal.tabIndex = -1; + modal.setAttribute('aria-hidden', 'true'); + document.body.appendChild(modal); + } + + // 格式化日期时间 + const formattedDate = new Date(record.receive_time).toLocaleString(); + + // 设置模态框内容 + modal.innerHTML = ` + + `; + + // 显示模态框 + const modalInstance = new bootstrap.Modal(modal); + modalInstance.show(); + } catch (error) { + console.error('获取记录详情失败:', error); + alert('获取详情失败: ' + error.message); + } +} \ No newline at end of file diff --git a/frontend/templates/base.html b/frontend/templates/base.html new file mode 100644 index 0000000..ac56a07 --- /dev/null +++ b/frontend/templates/base.html @@ -0,0 +1,65 @@ + + + + + + {% block title %}SMS 验证码服务{% endblock %} + + + + {% block styles %}{% endblock %} + + +
+ +
+ +
+
+ {% block content %}{% endblock %} +
+
+ + + + + + {% block scripts %}{% endblock %} + + \ No newline at end of file diff --git a/frontend/templates/index.html b/frontend/templates/index.html new file mode 100644 index 0000000..d19cd42 --- /dev/null +++ b/frontend/templates/index.html @@ -0,0 +1,205 @@ +{% extends "base.html" %} + +{% block title %}SMS 验证码服务 - 首页{% endblock %} + +{% block content %} +
+
+
+
+

欢迎使用 SMS 验证码服务

+

高效便捷的短信验证码管理平台,帮助您轻松接收和查询短信验证码。

+
+
+
+
+ +
+ +
+
+
+
+ +
验证码查询
+
+
+
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+ +
+ +
+
+
+ 查询结果 +
+
+
+
+
+
+
+ + +
+
+
+
+ +
服务信息
+
+
+
+
+
+
+
+ +
+
+
短信接收
+

自动接收短信并提取验证码,支持多种平台和格式

+
+
+
+
+
+
+ +
+
+
验证码提取
+

智能识别短信中的验证码,精准提取数字密码

+
+
+
+
+
+
+ +
+
+
历史记录
+

保存并展示历史短信记录,方便随时查询

+
+
+
+
+
+
+ +
+
+
API 接口
+

提供丰富的API接口,便于集成到您的应用

+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ +
短信历史记录
+
+
+ +
+
+
+
+
+ + + + + + + + + + + + + + +
ID发送方短信内容验证码接收时间操作
+
+ +
+
+ 加载中... +
+

正在加载短信记录...

+
+ +
+ +

暂无短信记录

+
+ +
+ +
+
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..dadcede --- /dev/null +++ b/poetry.lock @@ -0,0 +1,714 @@ +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "aliyun" + +[[package]] +name = "anyio" +version = "4.9.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.9" +files = [ + {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, + {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}, +] + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} + +[package.extras] +doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] +test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] +trio = ["trio (>=0.26.1)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "aliyun" + +[[package]] +name = "click" +version = "8.1.8" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "aliyun" + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "aliyun" + +[[package]] +name = "fastapi" +version = "0.115.11" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fastapi-0.115.11-py3-none-any.whl", hash = "sha256:32e1541b7b74602e4ef4a0260ecaf3aadf9d4f19590bba3e1bf2ac4666aa2c64"}, + {file = "fastapi-0.115.11.tar.gz", hash = "sha256:cc81f03f688678b92600a65a5e618b93592c65005db37157147204d8924bf94f"}, +] + +[package.dependencies] +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.40.0,<0.47.0" +typing-extensions = ">=4.8.0" + +[package.extras] +all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "aliyun" + +[[package]] +name = "greenlet" +version = "3.1.1" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.7" +files = [ + {file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617"}, + {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7"}, + {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6"}, + {file = "greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80"}, + {file = "greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a"}, + {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511"}, + {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395"}, + {file = "greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39"}, + {file = "greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9"}, + {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0"}, + {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942"}, + {file = "greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01"}, + {file = "greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e"}, + {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1"}, + {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c"}, + {file = "greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822"}, + {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01"}, + {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47da355d8687fd65240c364c90a31569a133b7b60de111c255ef5b606f2ae291"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98884ecf2ffb7d7fe6bd517e8eb99d31ff7855a840fa6d0d63cd07c037f6a981"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1d4aeb8891338e60d1ab6127af1fe45def5259def8094b9c7e34690c8858803"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db32b5348615a04b82240cc67983cb315309e88d444a288934ee6ceaebcad6cc"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcc62f31eae24de7f8dce72134c8651c58000d3b1868e01392baea7c32c247de"}, + {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1d3755bcb2e02de341c55b4fca7a745a24a9e7212ac953f6b3a48d117d7257aa"}, + {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b8da394b34370874b4572676f36acabac172602abf054cbc4ac910219f3340af"}, + {file = "greenlet-3.1.1-cp37-cp37m-win32.whl", hash = "sha256:a0dfc6c143b519113354e780a50381508139b07d2177cb6ad6a08278ec655798"}, + {file = "greenlet-3.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:54558ea205654b50c438029505def3834e80f0869a70fb15b871c29b4575ddef"}, + {file = "greenlet-3.1.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1"}, + {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd"}, + {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:03a088b9de532cbfe2ba2034b2b85e82df37874681e8c470d6fb2f8c04d7e4b7"}, + {file = "greenlet-3.1.1-cp38-cp38-win32.whl", hash = "sha256:8b8b36671f10ba80e159378df9c4f15c14098c4fd73a36b9ad715f057272fbef"}, + {file = "greenlet-3.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:7017b2be767b9d43cc31416aba48aab0d2309ee31b4dbf10a1d38fb7972bdf9d"}, + {file = "greenlet-3.1.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c"}, + {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e"}, + {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e"}, + {file = "greenlet-3.1.1-cp39-cp39-win32.whl", hash = "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c"}, + {file = "greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22"}, + {file = "greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467"}, +] + +[package.extras] +docs = ["Sphinx", "furo"] +test = ["objgraph", "psutil"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "aliyun" + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "aliyun" + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "aliyun" + +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "aliyun" + +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "aliyun" + +[[package]] +name = "pydantic" +version = "2.10.6" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"}, + {file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.27.2" +typing-extensions = ">=4.12.2" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "aliyun" + +[[package]] +name = "pydantic-core" +version = "2.27.2" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, + {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"}, + {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"}, + {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"}, + {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"}, + {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, + {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, + {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"}, + {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"}, + {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"}, + {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"}, + {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"}, + {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"}, + {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"}, + {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"}, + {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"}, + {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"}, + {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "aliyun" + +[[package]] +name = "pydantic-settings" +version = "2.8.1" +description = "Settings management using Pydantic" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c"}, + {file = "pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585"}, +] + +[package.dependencies] +pydantic = ">=2.7.0" +python-dotenv = ">=0.21.0" + +[package.extras] +azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] +toml = ["tomli (>=2.0.1)"] +yaml = ["pyyaml (>=6.0.1)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "aliyun" + +[[package]] +name = "python-dotenv" +version = "1.0.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "aliyun" + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "aliyun" + +[[package]] +name = "sqlalchemy" +version = "2.0.39" +description = "Database Abstraction Library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "SQLAlchemy-2.0.39-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:66a40003bc244e4ad86b72abb9965d304726d05a939e8c09ce844d27af9e6d37"}, + {file = "SQLAlchemy-2.0.39-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67de057fbcb04a066171bd9ee6bcb58738d89378ee3cabff0bffbf343ae1c787"}, + {file = "SQLAlchemy-2.0.39-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:533e0f66c32093a987a30df3ad6ed21170db9d581d0b38e71396c49718fbb1ca"}, + {file = "SQLAlchemy-2.0.39-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7399d45b62d755e9ebba94eb89437f80512c08edde8c63716552a3aade61eb42"}, + {file = "SQLAlchemy-2.0.39-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:788b6ff6728072b313802be13e88113c33696a9a1f2f6d634a97c20f7ef5ccce"}, + {file = "SQLAlchemy-2.0.39-cp37-cp37m-win32.whl", hash = "sha256:01da15490c9df352fbc29859d3c7ba9cd1377791faeeb47c100832004c99472c"}, + {file = "SQLAlchemy-2.0.39-cp37-cp37m-win_amd64.whl", hash = "sha256:f2bcb085faffcacf9319b1b1445a7e1cfdc6fb46c03f2dce7bc2d9a4b3c1cdc5"}, + {file = "SQLAlchemy-2.0.39-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b761a6847f96fdc2d002e29e9e9ac2439c13b919adfd64e8ef49e75f6355c548"}, + {file = "SQLAlchemy-2.0.39-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0d7e3866eb52d914aea50c9be74184a0feb86f9af8aaaa4daefe52b69378db0b"}, + {file = "SQLAlchemy-2.0.39-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:995c2bacdddcb640c2ca558e6760383dcdd68830160af92b5c6e6928ffd259b4"}, + {file = "SQLAlchemy-2.0.39-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:344cd1ec2b3c6bdd5dfde7ba7e3b879e0f8dd44181f16b895940be9b842fd2b6"}, + {file = "SQLAlchemy-2.0.39-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:5dfbc543578058c340360f851ddcecd7a1e26b0d9b5b69259b526da9edfa8875"}, + {file = "SQLAlchemy-2.0.39-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3395e7ed89c6d264d38bea3bfb22ffe868f906a7985d03546ec7dc30221ea980"}, + {file = "SQLAlchemy-2.0.39-cp38-cp38-win32.whl", hash = "sha256:bf555f3e25ac3a70c67807b2949bfe15f377a40df84b71ab2c58d8593a1e036e"}, + {file = "SQLAlchemy-2.0.39-cp38-cp38-win_amd64.whl", hash = "sha256:463ecfb907b256e94bfe7bcb31a6d8c7bc96eca7cbe39803e448a58bb9fcad02"}, + {file = "sqlalchemy-2.0.39-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6827f8c1b2f13f1420545bd6d5b3f9e0b85fe750388425be53d23c760dcf176b"}, + {file = "sqlalchemy-2.0.39-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9f119e7736967c0ea03aff91ac7d04555ee038caf89bb855d93bbd04ae85b41"}, + {file = "sqlalchemy-2.0.39-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4600c7a659d381146e1160235918826c50c80994e07c5b26946a3e7ec6c99249"}, + {file = "sqlalchemy-2.0.39-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a06e6c8e31c98ddc770734c63903e39f1947c9e3e5e4bef515c5491b7737dde"}, + {file = "sqlalchemy-2.0.39-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4c433f78c2908ae352848f56589c02b982d0e741b7905228fad628999799de4"}, + {file = "sqlalchemy-2.0.39-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7bd5c5ee1448b6408734eaa29c0d820d061ae18cb17232ce37848376dcfa3e92"}, + {file = "sqlalchemy-2.0.39-cp310-cp310-win32.whl", hash = "sha256:87a1ce1f5e5dc4b6f4e0aac34e7bb535cb23bd4f5d9c799ed1633b65c2bcad8c"}, + {file = "sqlalchemy-2.0.39-cp310-cp310-win_amd64.whl", hash = "sha256:871f55e478b5a648c08dd24af44345406d0e636ffe021d64c9b57a4a11518304"}, + {file = "sqlalchemy-2.0.39-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a28f9c238f1e143ff42ab3ba27990dfb964e5d413c0eb001b88794c5c4a528a9"}, + {file = "sqlalchemy-2.0.39-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:08cf721bbd4391a0e765fe0fe8816e81d9f43cece54fdb5ac465c56efafecb3d"}, + {file = "sqlalchemy-2.0.39-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a8517b6d4005facdbd7eb4e8cf54797dbca100a7df459fdaff4c5123265c1cd"}, + {file = "sqlalchemy-2.0.39-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b2de1523d46e7016afc7e42db239bd41f2163316935de7c84d0e19af7e69538"}, + {file = "sqlalchemy-2.0.39-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:412c6c126369ddae171c13987b38df5122cb92015cba6f9ee1193b867f3f1530"}, + {file = "sqlalchemy-2.0.39-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b35e07f1d57b79b86a7de8ecdcefb78485dab9851b9638c2c793c50203b2ae8"}, + {file = "sqlalchemy-2.0.39-cp311-cp311-win32.whl", hash = "sha256:3eb14ba1a9d07c88669b7faf8f589be67871d6409305e73e036321d89f1d904e"}, + {file = "sqlalchemy-2.0.39-cp311-cp311-win_amd64.whl", hash = "sha256:78f1b79132a69fe8bd6b5d91ef433c8eb40688ba782b26f8c9f3d2d9ca23626f"}, + {file = "sqlalchemy-2.0.39-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c457a38351fb6234781d054260c60e531047e4d07beca1889b558ff73dc2014b"}, + {file = "sqlalchemy-2.0.39-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:018ee97c558b499b58935c5a152aeabf6d36b3d55d91656abeb6d93d663c0c4c"}, + {file = "sqlalchemy-2.0.39-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5493a8120d6fc185f60e7254fc056a6742f1db68c0f849cfc9ab46163c21df47"}, + {file = "sqlalchemy-2.0.39-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2cf5b5ddb69142511d5559c427ff00ec8c0919a1e6c09486e9c32636ea2b9dd"}, + {file = "sqlalchemy-2.0.39-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f03143f8f851dd8de6b0c10784363712058f38209e926723c80654c1b40327a"}, + {file = "sqlalchemy-2.0.39-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06205eb98cb3dd52133ca6818bf5542397f1dd1b69f7ea28aa84413897380b06"}, + {file = "sqlalchemy-2.0.39-cp312-cp312-win32.whl", hash = "sha256:7f5243357e6da9a90c56282f64b50d29cba2ee1f745381174caacc50d501b109"}, + {file = "sqlalchemy-2.0.39-cp312-cp312-win_amd64.whl", hash = "sha256:2ed107331d188a286611cea9022de0afc437dd2d3c168e368169f27aa0f61338"}, + {file = "sqlalchemy-2.0.39-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fe193d3ae297c423e0e567e240b4324d6b6c280a048e64c77a3ea6886cc2aa87"}, + {file = "sqlalchemy-2.0.39-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:79f4f502125a41b1b3b34449e747a6abfd52a709d539ea7769101696bdca6716"}, + {file = "sqlalchemy-2.0.39-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a10ca7f8a1ea0fd5630f02feb055b0f5cdfcd07bb3715fc1b6f8cb72bf114e4"}, + {file = "sqlalchemy-2.0.39-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6b0a1c7ed54a5361aaebb910c1fa864bae34273662bb4ff788a527eafd6e14d"}, + {file = "sqlalchemy-2.0.39-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52607d0ebea43cf214e2ee84a6a76bc774176f97c5a774ce33277514875a718e"}, + {file = "sqlalchemy-2.0.39-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c08a972cbac2a14810463aec3a47ff218bb00c1a607e6689b531a7c589c50723"}, + {file = "sqlalchemy-2.0.39-cp313-cp313-win32.whl", hash = "sha256:23c5aa33c01bd898f879db158537d7e7568b503b15aad60ea0c8da8109adf3e7"}, + {file = "sqlalchemy-2.0.39-cp313-cp313-win_amd64.whl", hash = "sha256:4dabd775fd66cf17f31f8625fc0e4cfc5765f7982f94dc09b9e5868182cb71c0"}, + {file = "sqlalchemy-2.0.39-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2600a50d590c22d99c424c394236899ba72f849a02b10e65b4c70149606408b5"}, + {file = "sqlalchemy-2.0.39-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4eff9c270afd23e2746e921e80182872058a7a592017b2713f33f96cc5f82e32"}, + {file = "sqlalchemy-2.0.39-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7332868ce891eda48896131991f7f2be572d65b41a4050957242f8e935d5d7"}, + {file = "sqlalchemy-2.0.39-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:125a7763b263218a80759ad9ae2f3610aaf2c2fbbd78fff088d584edf81f3782"}, + {file = "sqlalchemy-2.0.39-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:04545042969833cb92e13b0a3019549d284fd2423f318b6ba10e7aa687690a3c"}, + {file = "sqlalchemy-2.0.39-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:805cb481474e111ee3687c9047c5f3286e62496f09c0e82e8853338aaaa348f8"}, + {file = "sqlalchemy-2.0.39-cp39-cp39-win32.whl", hash = "sha256:34d5c49f18778a3665d707e6286545a30339ad545950773d43977e504815fa70"}, + {file = "sqlalchemy-2.0.39-cp39-cp39-win_amd64.whl", hash = "sha256:35e72518615aa5384ef4fae828e3af1b43102458b74a8c481f69af8abf7e802a"}, + {file = "sqlalchemy-2.0.39-py3-none-any.whl", hash = "sha256:a1c6b0a5e3e326a466d809b651c63f278b1256146a377a528b6938a279da334f"}, + {file = "sqlalchemy-2.0.39.tar.gz", hash = "sha256:5d2d1fe548def3267b4c70a8568f108d1fed7cbbeccb9cc166e05af2abc25c22"}, +] + +[package.dependencies] +greenlet = {version = "!=0.4.17", markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} +typing-extensions = ">=4.6.0" + +[package.extras] +aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] +aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] +aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] +asyncio = ["greenlet (!=0.4.17)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"] +mssql = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] +mypy = ["mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0)"] +mysql-connector = ["mysql-connector-python"] +oracle = ["cx_oracle (>=8)"] +oracle-oracledb = ["oracledb (>=1.0.1)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] +postgresql-pg8000 = ["pg8000 (>=1.29.1)"] +postgresql-psycopg = ["psycopg (>=3.0.7)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] +postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] +pymysql = ["pymysql"] +sqlcipher = ["sqlcipher3_binary"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "aliyun" + +[[package]] +name = "sqlmodel" +version = "0.0.24" +description = "SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness." +optional = false +python-versions = ">=3.7" +files = [ + {file = "sqlmodel-0.0.24-py3-none-any.whl", hash = "sha256:6778852f09370908985b667d6a3ab92910d0d5ec88adcaf23dbc242715ff7193"}, + {file = "sqlmodel-0.0.24.tar.gz", hash = "sha256:cc5c7613c1a5533c9c7867e1aab2fd489a76c9e8a061984da11b4e613c182423"}, +] + +[package.dependencies] +pydantic = ">=1.10.13,<3.0.0" +SQLAlchemy = ">=2.0.14,<2.1.0" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "aliyun" + +[[package]] +name = "starlette" +version = "0.46.1" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.9" +files = [ + {file = "starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227"}, + {file = "starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230"}, +] + +[package.dependencies] +anyio = ">=3.6.2,<5" + +[package.extras] +full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "aliyun" + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "aliyun" + +[[package]] +name = "uvicorn" +version = "0.34.0" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.9" +files = [ + {file = "uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4"}, + {file = "uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9"}, +] + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" + +[package.extras] +standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "aliyun" + +[metadata] +lock-version = "2.0" +python-versions = "^3.12" +content-hash = "da2ad4d00f401976ecd02c29c4d24223b16094734f2c557add12d737f9441b87" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f687f66 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,24 @@ +[tool.poetry] +name = "sms-server" +version = "0.1.0" +description = "" +authors = ["yyh"] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.12" +fastapi = "^0.115.11" +sqlmodel = "^0.0.24" +uvicorn = "^0.34.0" +pydantic-settings = "^2.8.1" +jinja2 = "^3.1.6" + + +[[tool.poetry.source]] +name = "aliyun" +url = "https://mirrors.aliyun.com/pypi/simple/" +priority = "primary" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9a271d8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +fastapi>=0.115.11 +uvicorn>=0.34.0 +pydantic>=2.0.0 +pydantic-settings>=2.8.1 +sqlmodel>=0.0.24 +python-multipart>=0.0.5 +aiosqlite>=0.17.0 +jinja2>=3.1.6 +python-dotenv>=0.19.0 diff --git a/sms_service.sh b/sms_service.sh new file mode 100644 index 0000000..a1050c5 --- /dev/null +++ b/sms_service.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +# 确保脚本以root权限运行 +if [ "$(id -u)" -ne 0; then + echo "请使用sudo运行此脚本" + exit 1 +fi + +# 创建systemd服务文件 +cat > /etc/systemd/system/sms-webhook.service << 'EOF' +[Unit] +Description=SMS Webhook Service +After=network.target + +[Service] +User=root +WorkingDirectory=/root/project/sms_server +EnvironmentFile=-/root/project/sms_server/.env +ExecStart=/bin/bash -c "cd /root/project/sms_server && /usr/bin/poetry run python backend/main.py" +Restart=always +RestartSec=5 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target +EOF + +# 重新加载systemd配置 +systemctl daemon-reload + +# 启用并启动服务 +systemctl enable sms-webhook.service +systemctl start sms-webhook.service + +echo "SMS Webhook服务已安装并启动" +echo "查看状态: systemctl status sms-webhook.service" +echo "查看日志: journalctl -u sms-webhook.service" +echo "访问服务: http://your-server-ip:$(grep -oP 'PORT=\K[0-9]+' /root/project/sms_server/.env 2>/dev/null || echo 8322)" \ No newline at end of file