diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..f56b842 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(git checkout *)", + "Bash(git add *)", + "Bash(git commit -m ' *)" + ] + } +} diff --git a/.gitignore b/.gitignore index 686c8a1..b7f0952 100644 --- a/.gitignore +++ b/.gitignore @@ -191,4 +191,6 @@ cython_debug/ **/docker/* backend/app/static/* -test*.* \ No newline at end of file +test*.* + +docs/** \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9dc9aa7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,108 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## 项目简介 + +InsightRadar(聚势智见)是一个热点资讯聚合平台。核心流程:定时爬取微博、知乎、百度等平台热搜 → 用本地 Embedding 模型(Qwen3-Embedding-4B)做余弦相似度语义聚类 → 合并为 `UnifiedEvent`(大事件)→ 调用 DeepSeek 等大模型生成 AI 摘要与标签 → 按用户订阅关键词定时推送邮件简报。 + +## 开发命令 + +### 后端(Python / FastAPI) + +```bash +cd backend +uv sync # 安装依赖 +uv run python main.py # 启动开发服务器(默认 :8000) +# 或 +uv run uvicorn app.main:app --reload --port 8000 +``` + +### 前端(Vue 3 / Vite) + +```bash +cd frontend +npm install +npm run dev # 开发服务器(Vite,默认 :5173,代理到后端) +npm run build # 构建产物到 dist/ +npm run type-check # TypeScript 类型检查 +npm run lint # oxlint + eslint 双重 lint(自动修复) +npm run format # Prettier 格式化 src/ +``` + +### 生产部署(将前端打包集成到后端) + +```bash +cd frontend && npm run build +cp -r dist/* ../backend/app/static/ +``` + +## 架构概览 + +### 后端分层 + +``` +backend/app/ +├── main.py # FastAPI 入口,APScheduler 调度(抓取/摘要/推送三个定时任务) +├── database.py # SQLAlchemy engine(SQLite WAL 模式,支持 SQLALCHEMY_DATABASE_URL 切换) +├── initialize.py # 启动时幂等写入默认信息源(今日头条、微博等11个平台) +├── models/models.py # 全部 ORM 表定义(单文件) +├── api/ +│ ├── router.py # 统一挂载所有子路由,前缀 /api/v1 +│ └── endpoints/ # auth / events / preferences / delivery / revisions / sources / stats +├── services/ +│ ├── fetcher_service.py # 爬取热搜 + Embedding 生成 + 语义聚类入库(核心) +│ ├── summary_service.py # 调用大模型生成 AI 摘要与标签 +│ ├── matching_service.py # 精确 + 语义双模式匹配用户兴趣 +│ └── delivery_service.py # 检查推送时间窗口并发送邮件简报 +├── core/ +│ ├── security.py # JWT 签发与校验 +│ └── verification/ # 验证码逻辑(Redis 或 DB 双模式存储) +├── crud/ # 数据库 CRUD 操作 +├── schemas/ # Pydantic 请求/响应 Schema +├── prompts/ # LLM Prompt 模板 +└── static/ # 前端构建产物(生产环境) +``` + +### 前端分层 + +``` +frontend/src/ +├── api/ # 封装 fetch 请求(基于 config/apiBase.ts,前缀 /api/v1) +├── stores/ # Pinia 状态(auth / theme) +├── router/ # Vue Router(requiresAuth / guestOnly meta 守卫) +├── views/ # 页面:Dashboard / Search / Topics / Delivery / Revisions / Login / Register +├── layouts/ # DashboardLayout(统一侧边栏) +└── components/ # 通用组件(UnifiedEventCard 等) +``` + +### 关键数据模型 + +- `UnifiedEvent`:语义聚类后的"大事件",含 AI 摘要、`center_embedding`(聚类中心向量)、`hot_score` +- `TrendingEvent`:各平台原始热搜,通过 `external_id`(MD5 指纹)去重,`unified_event_id` 关联大事件 +- `ExtractedTopic` / `DiscussionComment`:多态设计,`target_type` 区分挂载在 EVENT / TREND / ARTICLE 下 +- `DeliveryHistory`:防重推记录,唯一约束 `(user_id, target_type, target_id)` + +### Embedding 模型 + +`fetcher_service.py` 在模块级加载 `SentenceTransformer` 全局单例(`embedder_model`)。`matching_service.py` 直接 import 复用该单例,避免重复加载。模型路径由 `EMBEDDING_MODEL_PATH` 配置,需提前将模型文件放入 `backend/data/` 目录。 + +## 配置 + +`.env` 文件放在项目根目录(或 `backend/data/`,两处均可),关键变量: + +| 变量 | 说明 | +|------|------| +| `SQLALCHEMY_DATABASE_URL` | 默认 `sqlite:///./data/demo.db`,可切换 PostgreSQL | +| `EMBEDDING_MODEL_PATH` | 本地 Embedding 模型路径 | +| `AI_API_KEY` | 大模型 API Key(DeepSeek 等 OpenAI 兼容接口) | +| `SIMILARITY_THRESHOLD` | 热搜语义聚类阈值(默认 0.72) | +| `AUTH_CODE_STORE` | 验证码存储模式:`db`(无 Redis 时)或 `redis` | +| `REDIS_URL` | Redis 连接,为空时验证码自动回退到数据库 | + +## 注意事项 + +- **后端工作目录**:必须在 `backend/` 下运行,静态文件路径 `app/static` 是相对路径 +- **Embedding 模型冷启动慢**:首次加载 Qwen3-Embedding-4B 约需数十秒,是正常现象 +- **前端 API 路径**:所有请求统一经 `src/config/apiBase.ts` 的 `fetchApi()` 发出,前缀 `/api/v1`,无需手动拼接 +- **数据库迁移**:当前使用 `Base.metadata.create_all()` 自动建表,不使用 Alembic;修改 Model 字段后需手动处理已有数据库 diff --git a/backend/app/api/endpoints/auth.py b/backend/app/api/endpoints/auth.py index f01a519..090cb84 100644 --- a/backend/app/api/endpoints/auth.py +++ b/backend/app/api/endpoints/auth.py @@ -1,6 +1,3 @@ -""" -认证模块:用户注册、登录、邮箱验证码(支持 Redis / 数据库双存储与自动降级) -""" import json import math import os diff --git a/backend/app/api/endpoints/delivery.py b/backend/app/api/endpoints/delivery.py index d029ad3..95d6ac6 100644 --- a/backend/app/api/endpoints/delivery.py +++ b/backend/app/api/endpoints/delivery.py @@ -1,5 +1,3 @@ -# 推送设置 API:管理用户的推送时间表和推送渠道 -# 关键约束:同一用户两条推送时间间隔至少 30 分钟 from datetime import time as dt_time from typing import List diff --git a/backend/app/api/endpoints/events.py b/backend/app/api/endpoints/events.py index cff78b6..7a0be99 100644 --- a/backend/app/api/endpoints/events.py +++ b/backend/app/api/endpoints/events.py @@ -1,7 +1,3 @@ -# app/api/endpoints/events.py -""" -事件模块:统一事件列表、详情、搜索时间线(支持精确/语义/混合匹配) -""" import json import os import time @@ -41,10 +37,8 @@ SEARCH_MAX_HOURS = int(os.getenv("SEARCH_MAX_HOURS", "168")) router = APIRouter() -# 排名轨迹最多返回的点数,避免时间跨度过大时响应体过重。 MAX_RANKING_POINTS = 30 -# 统一事件列表接口的短期缓存。 _UNIFIED_EVENTS_CACHE: Dict[str, Tuple[float, PaginatedUnifiedEventResponse]] = {} CACHE_TTL_SECONDS = 60 diff --git a/backend/app/api/endpoints/preferences.py b/backend/app/api/endpoints/preferences.py index ca7df4a..2b6db81 100644 --- a/backend/app/api/endpoints/preferences.py +++ b/backend/app/api/endpoints/preferences.py @@ -1,6 +1,3 @@ -""" -用户偏好模块:兴趣关键词的增删查、基于关键词的个性化事件推荐 -""" import time from typing import Any, Dict, List, Tuple @@ -20,7 +17,6 @@ from app.services.matching_service import recommend_events_for_user router = APIRouter() -# --- 轻量级接口缓存配置 --- _RECOMMEND_CACHE: Dict[str, Tuple[float, Any]] = {} CACHE_TTL_SECONDS = 60 @@ -29,7 +25,6 @@ def _invalidate_user_cache(user_id: int): keys_to_delete = [k for k in _RECOMMEND_CACHE.keys() if k.startswith(f"{user_id}:")] for k in keys_to_delete: _RECOMMEND_CACHE.pop(k, None) -# --------------------------- def _ensure_self_access(path_user_id: int, current_user: AppUser) -> None: """校验路径 user_id 是否为当前登录用户本人。""" @@ -93,7 +88,7 @@ def create_user_preference( ) db.refresh(db_obj) - _invalidate_user_cache(user_id) # 失效推荐缓存 + _invalidate_user_cache(user_id) return db_obj @@ -122,7 +117,7 @@ def delete_user_preference( db.delete(preference) db.commit() - _invalidate_user_cache(user_id) # 失效推荐缓存 + _invalidate_user_cache(user_id) return None @@ -143,7 +138,6 @@ def recommend_events( """基于用户兴趣词推荐事件(精确匹配 + 语义匹配)。""" _ensure_self_access(user_id, current_user) - # 推荐结果缓存,避免频繁调用匹配服务 cache_key = f"{user_id}:{min_hot}:{hours}:{limit}:{semantic_threshold}:{sort_by}" current_time = time.time() @@ -151,7 +145,6 @@ def recommend_events( expire_time, cached_data = _RECOMMEND_CACHE[cache_key] if current_time < expire_time: return cached_data - # ----------------------- matched = recommend_events_for_user( db, @@ -189,10 +182,8 @@ def recommend_events( # 写入缓存,超过 2000 条时清空防止内存膨胀 if len(_RECOMMEND_CACHE) > 2000: - # 防止内存无限增长 _RECOMMEND_CACHE.clear() _RECOMMEND_CACHE[cache_key] = (current_time + CACHE_TTL_SECONDS, response) - # ------------------ return response diff --git a/backend/app/api/endpoints/revisions.py b/backend/app/api/endpoints/revisions.py index 38f5bd5..d8d794a 100644 --- a/backend/app/api/endpoints/revisions.py +++ b/backend/app/api/endpoints/revisions.py @@ -1,4 +1,3 @@ -# 公关修改追踪 API:查询热搜标题被偷偷修改的历史记录,用于舆情监测 from datetime import timedelta from typing import List, Optional @@ -39,7 +38,6 @@ def list_headline_revisions( """ time_limit = utcnow() - timedelta(hours=hours) - # 关联 TrendingEvent、InfoSource 获取平台名和链接 rows = ( db.query(HeadlineRevision, InfoSource.source_name, TrendingEvent.event_url) .join(TrendingEvent, HeadlineRevision.event_id == TrendingEvent.id) diff --git a/backend/app/api/endpoints/sources.py b/backend/app/api/endpoints/sources.py index 08d4c2a..0687946 100644 --- a/backend/app/api/endpoints/sources.py +++ b/backend/app/api/endpoints/sources.py @@ -1,7 +1,3 @@ -# app/api/endpoints/sources.py -""" -信息源模块:信息源的增删改查,供爬虫与后台管理使用 -""" from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session from typing import List @@ -43,6 +39,4 @@ async def update_info_source(source_id: int, source_in: InfoSourceUpdate, db: Se source = crud_source.get(db=db, source_id=source_id) if not source: raise HTTPException(status_code=404, detail="该信息源不存在") - - # 直接把查出来的数据库对象和前端传来的 Pydantic 对象丢给 CRUD 处理 return crud_source.update(db=db, db_obj=source, obj_in=source_in) diff --git a/backend/app/api/endpoints/stats.py b/backend/app/api/endpoints/stats.py index 84bbf46..a7b1e50 100644 --- a/backend/app/api/endpoints/stats.py +++ b/backend/app/api/endpoints/stats.py @@ -1,4 +1,3 @@ -# 系统状态监控 API:返回爬虫集群运行概况(信息源数、今日抓取量、最近同步时间等) from datetime import datetime, timedelta from typing import Optional @@ -28,7 +27,6 @@ def get_system_stats(db: Session = Depends(get_db)): """获取爬虫集群的当日运行状态。""" today_start = utcnow().replace(hour=0, minute=0, second=0, microsecond=0) - # 信息源统计:总数与启用数 total_sources = db.query(func.count(InfoSource.id)).scalar() or 0 active_sources = ( db.query(func.count(InfoSource.id)) @@ -36,7 +34,6 @@ def get_system_stats(db: Session = Depends(get_db)): .scalar() or 0 ) - # 今日任务统计:抓取条数、成功/失败任务数 today_tasks = ( db.query(DataSyncTask) .filter(DataSyncTask.created_at >= today_start) @@ -47,7 +44,6 @@ def get_system_stats(db: Session = Depends(get_db)): success_count = sum(1 for t in today_tasks if t.task_status == TaskStatus.SUCCESS) error_count = sum(1 for t in today_tasks if t.task_status == TaskStatus.ERROR) - # 最后一次同步时间 last_task = ( db.query(DataSyncTask) .filter(DataSyncTask.task_status == TaskStatus.SUCCESS) diff --git a/backend/app/api/router.py b/backend/app/api/router.py index 31405c2..47fe601 100644 --- a/backend/app/api/router.py +++ b/backend/app/api/router.py @@ -1,4 +1,3 @@ -# app/api/router.py from fastapi import APIRouter from app.api.endpoints import auth, delivery, events, preferences, revisions, sources, stats diff --git a/backend/app/core/verification/email/RespositoryImpl/MemoryRepository.py b/backend/app/core/verification/email/RespositoryImpl/MemoryRepository.py index 926579a..4be6b31 100644 --- a/backend/app/core/verification/email/RespositoryImpl/MemoryRepository.py +++ b/backend/app/core/verification/email/RespositoryImpl/MemoryRepository.py @@ -1,5 +1,3 @@ -# app/verification/backends/memory.py - from functools import lru_cache import time import json diff --git a/backend/app/crud/crud_source.py b/backend/app/crud/crud_source.py index 9c7aa0a..73a0076 100644 --- a/backend/app/crud/crud_source.py +++ b/backend/app/crud/crud_source.py @@ -1,7 +1,3 @@ -# app/crud/crud_source.py -""" -信息源 CRUD:对 InfoSource 的增删改查,供 API 与爬虫使用 -""" from sqlite3 import IntegrityError from sqlalchemy.orm import Session diff --git a/backend/app/database.py b/backend/app/database.py index 812790f..028582d 100644 --- a/backend/app/database.py +++ b/backend/app/database.py @@ -1,4 +1,4 @@ -# database.py +# AI辅助生成:deepseek-v3-2,2026年3月20日 import os from dotenv import load_dotenv diff --git a/backend/app/initialize.py b/backend/app/initialize.py index 2e728bb..2406bc5 100644 --- a/backend/app/initialize.py +++ b/backend/app/initialize.py @@ -1,14 +1,12 @@ -import json - from app.database import SessionLocal from app.crud.crud_source import create from app.models.models import SourceType from app.schemas.source_schema import InfoSourceCreate +# AI辅助生成:deepseek-v3-2,2026年3月20日 def init(): - # 解析后的数据源列表 sources_data = [ {"name": "今日头条", "url": "toutiao"}, {"name": "百度热搜", "url": "baidu"}, @@ -23,11 +21,8 @@ def init(): {"name": "知乎", "url": "zhihu"} ] - # 遍历数据并发送 POST 请求 for item in sources_data: - try: - with SessionLocal() as db: create(db, InfoSourceCreate( diff --git a/backend/app/main.py b/backend/app/main.py index 566558c..4030fac 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,4 +1,4 @@ -# app/main.py +# AI辅助生成:deepseek-v3-2,2026年3月20日 import logging import os from pathlib import Path @@ -35,9 +35,6 @@ SUMMARY_INTERVAL = int(os.getenv("SUMMARY_INTERVAL_MINUTES", 30)) scheduler = AsyncIOScheduler() -# ========================================== -# 1. 生命周期管理:App 启动时自动建表 & 启动调度器 -# ========================================== @asynccontextmanager async def lifespan(app: FastAPI): # 1. 数据库建表 @@ -49,7 +46,7 @@ async def lifespan(app: FastAPI): init() logging.info("订阅源初始化完毕") - # 2. 配置并启动定时任务 + # 爬取订阅源 scheduler.add_job( fetch_and_save_trending_data, 'interval', @@ -66,7 +63,7 @@ async def lifespan(app: FastAPI): id='ai_summary_job', replace_existing=True ) - # 推送调度:每分钟检查是否有用户需要接收邮件推送 + # 推送调度 scheduler.add_job( check_and_deliver, 'interval', @@ -80,24 +77,14 @@ async def lifespan(app: FastAPI): logging.info(f"AI 摘要生成任务已启动,每 {SUMMARY_INTERVAL} 分钟执行一次") logging.info("邮件推送调度已启动,每分钟检查一次") - # 为了测试方便,启动时立即执行一次 - # await fetch_and_save_trending_data() + yield - # await generate_unified_summaries() - - yield # 此时 FastAPI 开始接受请求 - - # 优雅关闭 scheduler.shutdown() logging.info("定时任务已安全关闭") -# 初始化 FastAPI app = FastAPI(title="AI 新闻聚合引擎 API", lifespan=lifespan) -# ========================================== -# 2. CORS 中间件:允许前端开发服务器跨域请求 -# ========================================== app.add_middleware( CORSMiddleware, # allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"], @@ -107,32 +94,26 @@ app.add_middleware( allow_headers=["*"], ) -# ========================================== -# 3. 挂载路由总线 -# ========================================== -# 版本控制 app.include_router(api_router, prefix="/api/v1") -# 只需要保留API的优先匹配,catch_all可以简化成这样 +# AI辅助生成结束 + @app.get("/api/{full_path:path}") async def api_not_found(full_path: str): return {"detail": "API Not Found"} staticPath = staticfiles.StaticFiles(directory="app/static", html=True) -# 把目录改成static对应我们放dist内容的路径就可以 app.mount("/", staticPath, name="static") INDEX_HTML = Path("app/static/index.html").read_text(encoding="utf-8") @app.exception_handler(404) async def not_found_handler(request: Request, exc: HTTPException): - # 如果是API路径才返回404,前端路径走catch-all不会进这里 if request.url.path.startswith("/api/"): return JSONResponse({"detail": "Not Found"}, status_code=404) return HTMLResponse(INDEX_HTML) -# 健康检查 @app.get("/", tags=["健康检查"]) async def root(): return {"message": "Welcome to AI News Aggregator API", "status": "ok"} diff --git a/backend/app/models/models.py b/backend/app/models/models.py index f6bb7fb..0454c43 100644 --- a/backend/app/models/models.py +++ b/backend/app/models/models.py @@ -1,4 +1,3 @@ -# models.py from datetime import datetime, timezone, time from typing import Optional, Any import enum @@ -9,11 +8,6 @@ from sqlalchemy import ( ) from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship - -# ========================================== -# 0. 全局基类、枚举定义与动态类型 -# ========================================== - class Base(DeclarativeBase): """ SQLAlchemy 2.0 声明式基类 @@ -21,9 +15,6 @@ class Base(DeclarativeBase): """ pass - -# 让代码在 SQLite 环境下自动降级为 Integer 以保证自增正常工作, -# 而在生产环境部署到 PostgreSQL 或 MySQL 时,依然会使用容量更大的 BigInteger。 BigIntType = BigInteger().with_variant(Integer, "sqlite") @@ -70,10 +61,6 @@ def utcnow(): """ return datetime.now(timezone.utc) - -# ========================================== -# 模块一:信息源管理 -# ========================================== class InfoSource(Base): """ 抓取源配置表 @@ -98,10 +85,6 @@ class InfoSource(Base): UniqueConstraint("source_name", name="uix_source_name"), ) - -# ========================================== -# 模块二:AI 语义聚类中枢 (大事件池) -# ========================================== class UnifiedEvent(Base): """ AI 统一事件表 (核心大脑) @@ -124,10 +107,6 @@ class UnifiedEvent(Base): created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, onupdate=utcnow) - -# ========================================== -# 模块三:内容存储库 (热搜 & 新闻子节点) -# ========================================== class TrendingEvent(Base): """ 各平台热搜数据明细表 @@ -199,10 +178,6 @@ class NewsArticle(Base): created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, onupdate=utcnow) - -# ========================================== -# 模块四:热度与轨迹追踪 -# ========================================== class HeadlineRevision(Base): """ 标题修订历史表 @@ -241,10 +216,6 @@ class RankingLog(Base): created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) - -# ========================================== -# 模块五:多态话题与多态评论 -# ========================================== class ExtractedTopic(Base): """ AI 提取的核心话题标签表 @@ -291,10 +262,6 @@ class DiscussionComment(Base): created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) - -# ========================================== -# 模块六:用户画像与多渠道高可用推送系统 -# ========================================== class AppUser(Base): """ 系统核心用户表 @@ -305,16 +272,10 @@ class AppUser(Base): id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) email: Mapped[str] = mapped_column(String(150), unique=True, index=True, comment="主账号邮箱") password_hash: Mapped[Optional[str]] = mapped_column(String(255), comment="密码哈希(第三方登录可为空)") - nickname: Mapped[Optional[str]] = mapped_column(String(100), comment="用户展示昵称") avatar_url: Mapped[Optional[str]] = mapped_column(String(500), comment="用户头像地址") gender: Mapped[GenderType] = mapped_column(Enum(GenderType), default=GenderType.UNKNOWN, comment="用户性别(用于AI调整行文语气)") - - # 极其强大:一个万能收纳箱!前端未来想加任何诸如“夜间模式”、“字体变大”的开关, - # 全部丢进这个 JSON 字段即可,从此免去手动修改后端表结构的麻烦。 metadata_: Mapped[Optional[Any]] = mapped_column("metadata", JSON, comment="JSON扩展字段: 存放灵活多变的前端用户偏好设置") - - # 时区对于定时推送系统极其重要!保证纽约的用户和北京的用户都能在早晨8点收到新闻。 timezone: Mapped[str] = mapped_column(String(50), default="Asia/Shanghai", comment="用户所在地时区") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, onupdate=utcnow) @@ -333,14 +294,10 @@ class UserPushEndpoint(Base): id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) user_id: Mapped[int] = mapped_column(ForeignKey("app_users.id"), comment="所属用户ID") - # 填入大写的纯字符串,如 EMAIL, WECHAT_BOT, TELEGRAM channel_type: Mapped[str] = mapped_column(String(50), comment="推送渠道类型标识") - # 具体的发送目标地址 channel_account: Mapped[str] = mapped_column(String(255), comment="具体的接收账号(邮箱号/微信号/Webhook)") is_active: Mapped[bool] = mapped_column(Boolean, default=True, comment="用户是否临时关闭了该渠道") - # 高可用容灾:比如 1 代表必须先发微信,如果报错了,再去找 priority=2 的邮箱补发 priority_level: Mapped[int] = mapped_column(Integer, default=1, comment="推送优先级(1最高,用于错误降级重试)") - created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, onupdate=utcnow) @@ -352,7 +309,6 @@ class UserTopicPreference(Base): """ __tablename__ = "user_topic_preferences" __table_args__ = ( - # 联合防抖限制:防止用户在界面卡顿时连点两次,订阅了两个同样的词 UniqueConstraint("user_id", "interested_keyword", name="idx_unique_preference"), ) @@ -389,7 +345,6 @@ class DeliveryHistory(Base): """ __tablename__ = "delivery_history" __table_args__ = ( - # 终极去重约束:一个用户,针对同一篇新闻,永远只允许存在一条记录 UniqueConstraint("user_id", "target_type", "target_id", name="idx_prevent_duplicate_push"), ) @@ -397,15 +352,10 @@ class DeliveryHistory(Base): user_id: Mapped[int] = mapped_column(ForeignKey("app_users.id"), comment="接收推送的用户") target_type: Mapped[TargetType] = mapped_column(Enum(TargetType), comment="推送出去的具体内容类型") target_id: Mapped[int] = mapped_column(BigIntType, comment="推送内容的主键ID") - # 记录这次推送是彻底成功了,还是由于渠道网络问题失败了 status: Mapped[TaskStatus] = mapped_column(Enum(TaskStatus), comment="最终推送结果状态") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, comment="记录或实际推送的准确时间") - -# ========================================== -# 模块七:系统任务监控 -# ========================================== class DataSyncTask(Base): """ 数据同步健康度监控表 (运维巡检专用) @@ -418,7 +368,6 @@ class DataSyncTask(Base): source_id: Mapped[int] = mapped_column(ForeignKey("info_sources.id"), comment="本次运行爬取的哪个源") items_fetched: Mapped[int] = mapped_column(Integer, default=0, comment="本次爬虫成功插入或更新的新闻条数") task_status: Mapped[TaskStatus] = mapped_column(Enum(TaskStatus), comment="该平台的宏观抓取状态") - # 如果代码意外崩溃、或是遭遇403/502,把 Python的 traceback 堆栈原封不动存进这里 error_trace: Mapped[Optional[str]] = mapped_column(Text, comment="若失败则保存完整报错堆栈") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, comment="任务执行的发生时间") diff --git a/backend/app/prompts/digest_email_template.py b/backend/app/prompts/digest_email_template.py index 29db03d..00db2c2 100644 --- a/backend/app/prompts/digest_email_template.py +++ b/backend/app/prompts/digest_email_template.py @@ -1,7 +1,3 @@ -# 推送邮件 HTML 模板 -# 用于生成定时推送给用户的热点摘要邮件 - -# 邮件客户端不支持 Font Awesome,改用 Emoji 代替平台图标 PLATFORM_EMOJI: dict[str, str] = { "微博热搜": "🔴", "微博": "🔴", diff --git a/backend/app/schemas/delivery_schema.py b/backend/app/schemas/delivery_schema.py index 21d7427..b8685b4 100644 --- a/backend/app/schemas/delivery_schema.py +++ b/backend/app/schemas/delivery_schema.py @@ -1,9 +1,9 @@ -# 推送设置相关的请求/响应模型 from datetime import datetime from typing import List, Optional from pydantic import BaseModel, ConfigDict, Field +# AI辅助生成:deepseek-v3-2,2026年3月20日 # ========================================== # 推送时间表 (UserDeliverySchedule) diff --git a/backend/app/schemas/event_schema.py b/backend/app/schemas/event_schema.py index 5ea4268..6a33626 100644 --- a/backend/app/schemas/event_schema.py +++ b/backend/app/schemas/event_schema.py @@ -1,9 +1,7 @@ -# app/schemas/event_schema.py from pydantic import BaseModel, Field from typing import List, Optional from datetime import datetime - class PlatformTrendResponse(BaseModel): source_id: int platform_name: str diff --git a/backend/app/schemas/preference_schema.py b/backend/app/schemas/preference_schema.py index 849182c..55eb033 100644 --- a/backend/app/schemas/preference_schema.py +++ b/backend/app/schemas/preference_schema.py @@ -3,7 +3,6 @@ from typing import List, Optional from pydantic import BaseModel, ConfigDict, Field - class UserTopicPreferenceCreate(BaseModel): """新增用户兴趣词请求体。""" interested_keyword: str = Field(..., min_length=1, max_length=100, description="用户感兴趣的关键词") diff --git a/backend/app/schemas/source_schema.py b/backend/app/schemas/source_schema.py index 621c7cf..032fb4d 100644 --- a/backend/app/schemas/source_schema.py +++ b/backend/app/schemas/source_schema.py @@ -1,4 +1,3 @@ -# app/schemas/source_schema.py from pydantic import BaseModel, ConfigDict, Field from typing import Optional from datetime import datetime @@ -6,6 +5,7 @@ from datetime import datetime # 枚举 from app.models.models import SourceType +# AI辅助生成:deepseek-v3-2,2026年3月20日 # ========================================== # InfoSource (信息源) 相关的 Schemas diff --git a/backend/app/services/delivery_service.py b/backend/app/services/delivery_service.py index cec2b38..b7b737d 100644 --- a/backend/app/services/delivery_service.py +++ b/backend/app/services/delivery_service.py @@ -1,7 +1,3 @@ -# 定时推送调度服务 -# 由 APScheduler 每分钟调用,检查当前时刻是否有用户需要接收推送, -# 如匹配则生成摘要邮件并发送,同时写入 DeliveryHistory 防重复。 -# 推送优先级:有关键词且匹配 → 个性化简报;无关键词或无匹配 → 默认热点快报 import logging import os from logging.handlers import TimedRotatingFileHandler @@ -34,7 +30,7 @@ from app.utils.email_utils import send_html_email logger = logging.getLogger("delivery_service") -# delivery_service 日志单独写文件 + _delivery_log_dir = Path(__file__).resolve().parents[2] / "logs" _delivery_log_dir.mkdir(parents=True, exist_ok=True) _delivery_log_file = _delivery_log_dir / "delivery_check.log" @@ -51,6 +47,8 @@ if not logger.handlers: logger.setLevel(logging.INFO) logger.propagate = False +# AI辅助生成:deepseek-v3-2,2026年3月20日 + # 推送时间窗口:实际执行时刻与设定时间的最大容差(分钟) DELIVERY_WINDOW_MINUTES = int(os.getenv("DELIVERY_WINDOW_MINUTES", 2)) # 同一用户两次推送之间的最小间隔(分钟) @@ -64,13 +62,10 @@ DEFAULT_MODE_HOURS = int(os.getenv("DEFAULT_MODE_HOURS", 24)) # 用户时区无效时的兜底时区 DEFAULT_FALLBACK_TIMEZONE = os.getenv("DEFAULT_FALLBACK_TIMEZONE", "Asia/Shanghai") - -# ========================================== -# 默认热点事件容器(无关键词时使用) -# ========================================== @dataclass class _DefaultEventItem: """ + 默认热点事件容器 无关键词订阅或关键词无匹配时的默认热点包装器, 接口与 MatchedEventResult 保持一致,方便统一传给模板。 """ @@ -81,10 +76,6 @@ class _DefaultEventItem: tags: list[str] = field(default_factory=list) is_default: bool = True - -# ========================================== -# 时区工具 -# ========================================== def _time_to_minutes(t: dt_time) -> int: return t.hour * 60 + t.minute @@ -125,10 +116,10 @@ def _ensure_aware(dt: datetime) -> datetime: return dt.replace(tzinfo=timezone.utc) return dt +# AI辅助生成结束 + -# ========================================== # 数据库查询辅助 -# ========================================== def _should_skip_by_interval(db: Session, user_id: int) -> bool: """检查用户是否仍在冷却期内,避免短时间内重复推送""" row = ( @@ -297,9 +288,9 @@ def _record_delivery( db.commit() -# ========================================== +# AI辅助生成:deepseek-v3-2,2026年3月20日 + # 推送准备 -# ========================================== @dataclass class _PendingPush: """暂存需要发送邮件的信息,便于在 async 上下文中发送。""" @@ -309,6 +300,7 @@ class _PendingPush: html_body: str event_ids: list[int] +# AI生成结束 def _prepare_user_push(db: Session, user: AppUser, schedule: UserDeliverySchedule) -> _PendingPush | None: """ @@ -331,7 +323,6 @@ def _prepare_user_push(db: Session, user: AppUser, schedule: UserDeliverySchedul pushed_ids = _get_already_pushed_event_ids(db, user_id) - # 决策:有关键词且有匹配 → 匹配模式;否则 → 默认热点模式 items: list = [] is_default = False @@ -361,7 +352,6 @@ def _prepare_user_push(db: Session, user: AppUser, schedule: UserDeliverySchedul logger.info(f"用户 {user_id} 默认热点无可推送内容,跳过") return None - # 批量加载平台数据(来源名、标题、URL、排名) event_ids = [item.event.id for item in items] platforms_map = _load_event_platforms(db, event_ids) @@ -383,9 +373,6 @@ def _prepare_user_push(db: Session, user: AppUser, schedule: UserDeliverySchedul ) -# ========================================== -# 调度主入口 -# ========================================== async def check_and_deliver() -> None: """ 定时推送主入口,由 APScheduler 每分钟调用。 @@ -412,7 +399,6 @@ async def check_and_deliver() -> None: if not user: continue - # 将 UTC 转为用户本地时间,判断是否落在推送窗口内 user_current = _user_local_time(now, user.timezone) if not _is_within_window(schedule.delivery_time, user_current): continue @@ -422,7 +408,6 @@ async def check_and_deliver() -> None: if pending is None: continue - # 异步按优先级尝试各邮件渠道 sent = False for target_email in pending.email_targets: try: diff --git a/backend/app/services/fetcher_service.py b/backend/app/services/fetcher_service.py index 4aaba3c..80c63ea 100644 --- a/backend/app/services/fetcher_service.py +++ b/backend/app/services/fetcher_service.py @@ -1,8 +1,3 @@ -# app/services/fetcher_service.py -""" -抓取服务:从外部 API 拉取热搜/RSS 数据,做查重、向量聚类、入库 -热搜分支:语义聚类到 UnifiedEvent;RSS 分支:写入 NewsArticle -""" import os import hashlib from datetime import timedelta @@ -19,6 +14,8 @@ from app.models.models import ( HeadlineRevision, RankingLog, SourceType, utcnow, UnifiedEvent ) +# AI辅助生成:deepseek-v3-2,2026年3月20日 + # 加载环境变量 load_dotenv() hf_token = os.getenv("HF_TOKEN") @@ -31,6 +28,8 @@ print("正在加载模型...") embedder_model = SentenceTransformer(EMBEDDING_MODEL_PATH, local_files_only=True) print("模型加载完成。") +# AI生成结束 + def generate_md5(text: str) -> str: """生成 32 位 MD5 作为 external_id,用于跨平台去重""" @@ -88,10 +87,10 @@ class UnifiedEventClusterer: new_unified = UnifiedEvent( unified_title=title, center_embedding=embedding_json, - hot_score=1 # 初始热度 + hot_score=1 ) self.db.add(new_unified) - self.db.flush() # 获取自增的主键 ID + self.db.flush() # 更新缓存 self.event_vectors.append(new_vec) @@ -109,11 +108,8 @@ def process_hot_trend_item(db, source, item, index: int, external_id: str, exist event_to_log = None - # 查重:已存在则可能只需更新标题/排名;不存在则需聚类并新建 if existing_event: - # 场景 A1:老熟人 if existing_event.current_headline != title: - # 标题被暗改,此时需要重新算一次 Embedding new_embedding_json, _ = embeddings_dict[title] revision = HeadlineRevision( @@ -123,30 +119,25 @@ def process_hot_trend_item(db, source, item, index: int, external_id: str, exist ) db.add(revision) existing_event.current_headline = title - existing_event.title_embedding = new_embedding_json # 更新为新标题的语义向量 - # 注:这里不改变它所属的 unified_event_id,因为大体还是同一件事 + existing_event.title_embedding = new_embedding_json existing_event.current_ranking = index existing_event.event_url = item_url event_to_log = existing_event else: - # 场景 A2:这是一条彻底的全新热搜 - # 1. 计算向量 - new_embedding_json, new_vec = embeddings_dict[title] - # 2. 扔进聚类中枢找归宿 + new_embedding_json, new_vec = embeddings_dict[title] matched_event_id = clusterer.match_or_create(title, new_embedding_json, new_vec) - # 3. 落库 new_event = TrendingEvent( source_id=source.id, external_id=external_id, current_headline=title, event_url=item_url, current_ranking=index, - title_embedding=new_embedding_json, # 存入向量 - unified_event_id=matched_event_id # 挂载到大事件下 + title_embedding=new_embedding_json, + unified_event_id=matched_event_id ) db.add(new_event) db.flush() @@ -192,7 +183,6 @@ def process_source_data(db, source, items: list) -> int: saved_count = 0 platform_id = source.home_url - # 1. 批量计算外部 ID 并聚合要计算的文本 valid_items = [] external_ids = [] for item in items: @@ -209,7 +199,6 @@ def process_source_data(db, source, items: list) -> int: if not valid_items: return 0 - # 批量查重:按 external_id 判断是更新还是新增 existing_events_dict = {} existing_articles_dict = {} @@ -226,7 +215,6 @@ def process_source_data(db, source, items: list) -> int: ).all() existing_articles_dict = {art.external_id: art for art in existing_articles} - # 仅对需要算向量的标题做批量 embedding,避免重复计算 texts_to_embed = [] if source.source_type in (SourceType.HOT_TREND, SourceType.API): for item, external_id in valid_items: @@ -238,15 +226,12 @@ def process_source_data(db, source, items: list) -> int: else: texts_to_embed.append(title) - # 4. 批量执行大模型推理 embeddings_dict = generate_embeddings_batch(texts_to_embed) - # 初始化聚类器(只在热搜模式下需要,且只初始化一次) clusterer = None if source.source_type in (SourceType.HOT_TREND, SourceType.API): clusterer = UnifiedEventClusterer(db) - # 按来源类型分流:热搜/API → TrendingEvent + 聚类;RSS → NewsArticle for index, (item, external_id) in enumerate(valid_items, 1): if source.source_type in (SourceType.HOT_TREND, SourceType.API): existing_event = existing_events_dict.get(external_id) @@ -269,14 +254,12 @@ async def fetch_and_save_trending_data(): """ print(f"[{utcnow()}] 开始执行定时抓取任务...") - # 获取启用的信息源 - 这个只读操作用一个短连接 with SessionLocal() as db: sources = db.query(InfoSource).filter(InfoSource.is_enabled == True).all() if not sources: print("没有找到启用的信息源,任务结束。") return - # 我们把 source 的信息提前提取出来,避免在异步中长期持有 session source_configs = [ { "id": s.id, @@ -287,7 +270,6 @@ async def fetch_and_save_trending_data(): for s in sources ] - # 伪装请求头,规避反爬 custom_headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36", "Accept": "application/json, text/plain, */*", @@ -304,13 +286,11 @@ async def fetch_and_save_trending_data(): url = f"{API_BASE_URL}?id={platform_id}&latest" try: - # 1. 网络请求(可能耗时较长,不要包在 db session 里) response = await client.get(url) response.raise_for_status() data_json = response.json() items = data_json.get("items", []) - # 2. 数据库事务操作(尽量短,单独使用 session) with SessionLocal() as db: # 重新从短 session 中获取 source 实例,以免 detached source = db.query(InfoSource).get(s_config["id"]) @@ -319,10 +299,8 @@ async def fetch_and_save_trending_data(): task_log = DataSyncTask(source_id=source.id, items_fetched=0) try: - # 调用数据处理层 saved_count = process_source_data(db, source, items) - # 业务事务成功提交 task_log.items_fetched = saved_count task_log.task_status = TaskStatus.SUCCESS db.add(task_log) @@ -330,10 +308,9 @@ async def fetch_and_save_trending_data(): print(f"[{source.source_name}] ({source.source_type}) 成功抓取并更新了 {saved_count} 条数据") except Exception as e: db.rollback() - raise e # 抛出给外层捕获记录日志 + raise e except Exception as e: - # 异常拦截与错误隔离,另起一个超短事务记录日志 with SessionLocal() as log_db: try: new_task_log = DataSyncTask(source_id=s_config["id"], items_fetched=0) diff --git a/backend/app/services/matching_service.py b/backend/app/services/matching_service.py index 09a814a..548096f 100644 --- a/backend/app/services/matching_service.py +++ b/backend/app/services/matching_service.py @@ -1,7 +1,3 @@ -""" -匹配服务:根据用户兴趣关键词(精确 + 语义)推荐事件 -打分融合:标签/标题匹配分 + 标签相关度 + 热度 + 新鲜度加成 -""" import os from dataclasses import dataclass from datetime import datetime, timedelta, timezone @@ -13,6 +9,7 @@ from sqlalchemy.orm import Session from app.models.models import ExtractedTopic, TargetType, UnifiedEvent, UserTopicPreference, utcnow from app.services.fetcher_service import embedder_model +# AI辅助生成:deepseek-v3-2,2026年3月20日 # 语义匹配阈值:用户关键词和事件标签/标题向量相似度达到该值才计入语义命中 DEFAULT_PREFERENCE_SEMANTIC_THRESHOLD = 0.78 @@ -35,6 +32,7 @@ class MatchedEventResult: semantic_hits: list[dict[str, Any]] tags: list[str] +# AI生成结束 def _normalize_text(text: str) -> str: """统一小写与首尾空白,便于做稳定匹配。""" @@ -80,7 +78,6 @@ def _build_keyword_embedding_map(keywords: list[str]) -> dict[str, np.ndarray]: uncached_keywords = [] - # 1. 尝试从缓存获取 for keyword in keywords: if not keyword: continue @@ -89,9 +86,7 @@ def _build_keyword_embedding_map(keywords: list[str]) -> dict[str, np.ndarray]: else: uncached_keywords.append(keyword) - # 2. 对未命中的词进行统一的批量推理 if uncached_keywords: - # 去重,避免同一个未缓存的词被计算多次 unique_uncached = list(dict.fromkeys(uncached_keywords)) vectors = embedder_model.encode(unique_uncached, normalize_embeddings=True, show_progress_bar=False) @@ -102,7 +97,6 @@ def _build_keyword_embedding_map(keywords: list[str]) -> dict[str, np.ndarray]: for k in keys_to_delete: del _EMBEDDING_CACHE[k] - # 3. 将新计算的向量存入缓存并回填结果 for keyword, vec in zip(unique_uncached, vectors): vec_array = np.asarray(vec, dtype=np.float32) _EMBEDDING_CACHE[keyword] = vec_array @@ -172,7 +166,6 @@ def recommend_events_for_user( else PREFERENCE_SEMANTIC_THRESHOLD ) - # 1. 读取用户兴趣词 preferences = ( db.query(UserTopicPreference) .filter(UserTopicPreference.user_id == user_id) @@ -185,7 +178,6 @@ def recommend_events_for_user( if not preference_keywords: return [] - # 2. 读取候选事件(时间 + 热度过滤,避免全表扫描) time_limit = utcnow() - timedelta(hours=hours) events = ( db.query(UnifiedEvent) @@ -213,20 +205,17 @@ def recommend_events_for_user( .all() ) - # 组织事件标签映射:event_id -> [(tag, relevance_score), ...] event_topics: dict[int, list[tuple[str, float | None]]] = {} for event_id, topic_keyword, relevance_score in topic_rows: if not topic_keyword: continue event_topics.setdefault(event_id, []).append((topic_keyword, relevance_score)) - # 3. 批量编码用户词与标签词,减少模型调用次数 unique_preference_keywords = list(dict.fromkeys(preference_keywords)) unique_topic_keywords = list(dict.fromkeys([row[1] for row in topic_rows if row[1]])) pref_vec_map = _build_keyword_embedding_map(unique_preference_keywords) topic_vec_map = _build_keyword_embedding_map(unique_topic_keywords) - # 预先建立“标准化后用户词集合”,用于精确匹配 normalized_preference_pairs = [ (word, _normalize_text(word)) for word in unique_preference_keywords @@ -246,20 +235,15 @@ def recommend_events_for_user( exact_hits: list[str] = [] semantic_hits: list[dict[str, Any]] = [] score = 0.0 - - # 对每个事件标签做精确匹配或语义匹配 for topic_keyword, topic_relevance in topic_list: topic_relevance_score = float(topic_relevance) if topic_relevance is not None else 50.0 - - # 1) 精确命中(包括完全相等与包含关系) + matched_pref = _find_exact_preference_match(topic_keyword, normalized_preference_pairs) if matched_pref is not None: exact_hits.append(topic_keyword) - # 精确命中给较高基础分,标签自身相关度作为增益 score += 45.0 + topic_relevance_score * 0.2 continue - # 2) 语义命中(未精确命中时再算) best_pref, best_sim = _find_best_semantic_match(topic_keyword, topic_vec_map, pref_vec_map) if best_pref is not None and best_sim >= similarity_threshold: @@ -270,10 +254,8 @@ def recommend_events_for_user( "similarity": round(best_sim, 4), } ) - # 语义命中分略低于精确命中,并由相似度放大 score += best_sim * 35.0 + topic_relevance_score * 0.12 - # 标题也参与匹配,但权重低于结构化标签,避免长标题过度主导排序。 event_title = (event.unified_title or "").strip() if event_title: title_exact_pref = _find_exact_preference_match(event_title, normalized_preference_pairs) @@ -292,15 +274,12 @@ def recommend_events_for_user( ) score += best_sim * 24.0 - # 如果精确和语义都没命中,直接跳过 if not exact_hits and not semantic_hits: continue - # 融合事件热度和新鲜度,避免只看语义分 score += min(event.hot_score, 100) * 0.3 score += _calc_freshness_bonus(event) - # 返回标签时做去重,保证接口稳定 tags = list(dict.fromkeys([item[0] for item in topic_list])) scored_results.append( MatchedEventResult( diff --git a/backend/app/services/summary_service.py b/backend/app/services/summary_service.py index e3b3cc1..5ffeb03 100644 --- a/backend/app/services/summary_service.py +++ b/backend/app/services/summary_service.py @@ -1,8 +1,3 @@ -# app/services/summary_service.py -""" -摘要服务:调用 LLM 生成统一标题、综合摘要、话题标签 -定时任务:对热度达标且未摘要的事件批量处理 -""" import json import os from datetime import timedelta @@ -26,12 +21,16 @@ from app.prompts.summary_prompts import ( ) from app.services.fetcher_service import embedder_model +# AI辅助生成:deepseek-v3-2,2026年3月20日 + HOT_SCORE_THRESHOLD = int(os.getenv("HOT_SCORE_THRESHOLD", 3)) TOPIC_TAG_MIN_HOT_SCORE = int(os.getenv("TOPIC_TAG_MIN_HOT_SCORE", HOT_SCORE_THRESHOLD)) TOPIC_SIMILARITY_THRESHOLD = float(os.getenv("TOPIC_SIMILARITY_THRESHOLD", 0.82)) TOPIC_TAG_MAX_COUNT = int(os.getenv("TOPIC_TAG_MAX_COUNT", 8)) AI_API_KEY = os.getenv("AI_API_KEY", "") +# AI生成结束 + deepseek_client = AsyncOpenAI( api_key=AI_API_KEY, @@ -184,7 +183,6 @@ async def generate_unified_summaries(): """定时任务:对热度达标且未摘要的事件刷新标题、摘要、标签""" print(f"[{utcnow()}] Start unified summary generation task...") - # 先提取需要处理的事件 ID,尽早释放 session,不长期占用 db session with SessionLocal() as db: recent_threshold = utcnow() - timedelta(days=3) events = db.query(UnifiedEvent).filter( @@ -197,11 +195,9 @@ async def generate_unified_summaries(): print("No events require summary update in this round.") return - # 复制出需要的信息,脱离 session event_ids = [e.id for e in events] event_hot_scores = {e.id: e.hot_score for e in events} - # 外层循环:针对每个 event_id 开启一个极短生命周期的 session 获取依赖数据 for event_id in event_ids: platform_dict: dict[str, set[str]] = {} with SessionLocal() as db: diff --git a/backend/app/utils/email_utils.py b/backend/app/utils/email_utils.py index 5e0a9b7..2463872 100644 --- a/backend/app/utils/email_utils.py +++ b/backend/app/utils/email_utils.py @@ -1,4 +1,4 @@ -# app/utils/email_utils.py +# AI辅助生成:deepseek-v3-2,2026年3月20日 import os from email.message import EmailMessage import aiosmtplib diff --git a/backend/main.py b/backend/main.py index 9b3a42f..e39c2d0 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,4 +1,4 @@ -# run.py +# AI辅助生成:deepseek-v3-2,2026年3月20日 import uvicorn import os from dotenv import load_dotenv @@ -8,11 +8,9 @@ if __name__ == "__main__": load_dotenv() PORT = int(os.getenv("PORT", 8000)) - # 启动服务 uvicorn.run( app="app.main:app", host="0.0.0.0", port=PORT, - # reload=True, workers=1 )