diff --git a/Dockerfile b/Dockerfile index 434222c..8d50777 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,8 +2,10 @@ FROM python:3.11-slim WORKDIR /app -# 安装系统依赖(含编译bcrypt/cryptography所需的build-essential) -# Use Tsinghua mirror for faster downloads in China +# ============================================================ +# 系统依赖(含编译 bcrypt/cryptography 所需的 build-essential) +# 使用清华镜像加速(国内环境) +# ============================================================ RUN sed -i 's|deb.debian.org|mirrors.tuna.tsinghua.edu.cn|g' /etc/apt/sources.list.d/debian.sources 2>/dev/null || true RUN apt-get update && apt-get install -y --no-install-recommends \ curl \ @@ -17,23 +19,39 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ nginx \ && rm -rf /var/lib/apt/lists/* -# 先复制依赖文件,利用 Docker 构建缓存 -COPY backend/python/requirements.txt /app/backend/python/requirements.txt -COPY frontend/package.json /app/frontend/package.json -COPY frontend/package-lock.json /app/frontend/package-lock.json - -# 安装Python依赖(build-essential/libffi-dev确保bcrypt/cryptography能编译) -RUN pip install --no-cache-dir -r /app/backend/python/requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple - -# 安装Node.js (用于前端构建) 并构建前端 -# Use Tsinghua mirror for faster downloads in China +# ============================================================ +# 安装 Node.js(用于前端构建) +# ============================================================ RUN sed -i 's|deb.debian.org|mirrors.tuna.tsinghua.edu.cn|g' /etc/apt/sources.list.d/debian.sources 2>/dev/null || true RUN apt-get update && apt-get install -y --no-install-recommends \ nodejs \ npm \ && rm -rf /var/lib/apt/lists/* +# ============================================================ +# 复制项目源码 +# ============================================================ +COPY backend/ /app/backend/ COPY frontend/ /app/frontend/ +COPY migrations/ /app/migrations/ +COPY nginx.conf /app/nginx.conf +COPY VERSION /app/VERSION +COPY CHANGELOG.md /app/CHANGELOG.md +COPY run.sh /app/run.sh + +# ============================================================ +# 安装 Python 依赖到虚拟环境(与 run.sh 的 .venv 优先逻辑一致) +# 说明:run.sh 启动时会优先使用 $PYTHON_DIR/.venv, +# 此处把依赖直接烤进镜像的 .venv,避免运行时再联网安装导致崩溃循环。 +# ============================================================ +RUN python3 -m venv /app/backend/python/.venv && \ + /app/backend/python/.venv/bin/pip install --no-cache-dir \ + -r /app/backend/python/requirements.txt \ + -i https://pypi.tuna.tsinghua.edu.cn/simple + +# ============================================================ +# 安装前端依赖并构建(使用国内 npm 镜像) +# ============================================================ RUN cd /app/frontend && npm install --registry=https://registry.npmmirror.com && npm run build # 验证前端构建产物 @@ -41,15 +59,9 @@ RUN test -f /app/frontend/dist/index.html || (echo "ERROR: 前端构建失败, RUN test -d /app/frontend/dist/assets || (echo "ERROR: 前端构建失败,assets 目录不存在" && exit 1) RUN echo "前端构建完成,dist 目录大小: $(du -sh /app/frontend/dist | cut -f1)" -# 复制项目源码(排除前端源码,已单独构建) -COPY backend/ /app/backend/ -COPY migrations/ /app/migrations/ -COPY nginx.conf /app/nginx.conf -COPY VERSION /app/VERSION -COPY CHANGELOG.md /app/CHANGELOG.md -COPY run.sh /app/run.sh - -# 配置nginx: 移除默认站点,写入自定义配置,确保日志目录权限 +# ============================================================ +# 配置 nginx:移除默认站点,写入自定义配置 +# ============================================================ RUN rm -f /etc/nginx/sites-enabled/default && \ rm -f /etc/nginx/conf.d/default.conf 2>/dev/null || true; \ cp /app/nginx.conf /etc/nginx/conf.d/aiflow.conf && \ diff --git a/backend/python/app/api/admin/market_admin.py b/backend/python/app/api/admin/market_admin.py index e012038..df84809 100644 --- a/backend/python/app/api/admin/market_admin.py +++ b/backend/python/app/api/admin/market_admin.py @@ -1,369 +1,68 @@ """ -市场集市扩展 API(管理员) -Phase 3.7: Market Expansion - Analytics, Featured, Category Management +应用市场审核 API(管理员) """ -from fastapi import APIRouter, Depends, HTTPException, Query, Body +from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session -from sqlalchemy import or_, func, desc, case, text -from pydantic import BaseModel -from typing import Optional, List, Any -import json -import uuid -from datetime import datetime, timedelta - from ...database import get_db, SessionLocal -from ...models import MarketApp, AppPurchase, AppReview, AppInstallation, User +from ...models import MarketApp, Coupon, UserCoupon, User from ...auth import get_current_admin from ...middleware.log_service import LogService +import uuid +from datetime import datetime router = APIRouter() -# App categories -APP_CATEGORIES = { - "coding": "编程开发", - "assistant": "智能助手", - "tools": "效率工具", - "writing": "内容创作", - "education": "教育培训", - "marketing": "营销推广", - "data": "数据分析", - "design": "设计创意", - "business": "企业管理", - "other": "其他", -} - - -class FeaturedUpdateRequest(BaseModel): - featured: bool - featured_order: int = 0 - - -class CategoryUpdateRequest(BaseModel): - category: str - tags: Optional[str] = None - - -class AppEditRequest(BaseModel): - name: Optional[str] = None - description: Optional[str] = None - icon: Optional[str] = None - tutorial: Optional[str] = None - category: Optional[str] = None - tags: Optional[str] = None - price: Optional[float] = None - is_member_free: Optional[bool] = None - featured: Optional[bool] = None - featured_order: Optional[int] = None - status: Optional[str] = None - - -def _execute_raw(db, sql, params=None): - """Execute raw SQL and return results.""" - result = db.execute(text(sql), params or {}) - try: - rows = result.fetchall() - return [dict(row._mapping) if hasattr(row, '_mapping') else dict(row) for row in rows] - except: - return [] - - -def _app_to_dict(app, include_content=False): - """Convert MarketApp to dict.""" - d = { - "id": app.id, - "name": app.name, - "type": app.type, - "category": getattr(app, 'category', 'other'), - "tags": getattr(app, 'tags', None), - "source": app.source, - "publisher_name": app.publisher_name, - "description": app.description, - "icon": app.icon, - "price": float(app.price) if app.price else 0.0, - "is_member_free": app.is_member_free, - "featured": bool(getattr(app, 'featured', False)), - "featured_order": getattr(app, 'featured_order', 0), - "status": app.status, - "rating_avg": float(app.rating_avg) if app.rating_avg else 0.0, - "review_count": app.review_count, - "install_count": app.install_count, - "version": getattr(app, 'version', '1.0.0'), - "created_at": app.created_at.strftime('%Y-%m-%d %H:%M:%S') if app.created_at else None, - "updated_at": app.updated_at.strftime('%Y-%m-%d %H:%M:%S') if app.updated_at else None, - } - if include_content: - d["tutorial"] = app.tutorial - d["skill_content"] = app.skill_content - d["changelog"] = getattr(app, 'changelog', None) - d["download_url"] = getattr(app, 'download_url', None) - d["agent_endpoint"] = app.agent_endpoint - d["mcp_transport"] = app.mcp_transport - return d - # ============================================================ -# Analytics endpoints +# 应用审核管理 # ============================================================ -@router.get("/analytics/overview", summary="市场总览数据") -async def get_market_overview( - admin_user=Depends(get_current_admin), - db: Session = Depends(get_db), -): - """获取市场总览统计数据""" - # Total apps by status - status_stats = _execute_raw(db, """ - SELECT status, COUNT(*) as cnt - FROM market_apps - GROUP BY status - """) - status_map = {r["status"]: r["cnt"] for r in status_stats} - - # Total installs - install_total = _execute_raw(db, "SELECT COUNT(*) as cnt FROM app_installations") - total_installs = install_total[0]["cnt"] if install_total else 0 - - # Total purchases and revenue - purchase_stats = _execute_raw(db, """ - SELECT COUNT(*) as cnt, COALESCE(SUM(price), 0) as revenue - FROM app_purchases - WHERE status = 'purchased' - """) - total_purchases = purchase_stats[0]["cnt"] if purchase_stats else 0 - total_revenue = float(purchase_stats[0]["revenue"]) if purchase_stats else 0.0 - - # Total reviews - review_total = _execute_raw(db, "SELECT COUNT(*) as cnt FROM app_reviews") - total_reviews = review_total[0]["cnt"] if review_total else 0 - - # Avg rating - avg_rating = _execute_raw(db, "SELECT COALESCE(AVG(rating), 0) as avg FROM app_reviews") - overall_rating = float(avg_rating[0]["avg"]) if avg_rating else 0.0 - - # Apps by type - type_stats = _execute_raw(db, """ - SELECT type, COUNT(*) as cnt - FROM market_apps - WHERE status = 'published' - GROUP BY type - """) - - # Apps by category - category_stats = _execute_raw(db, """ - SELECT COALESCE(category, 'other') as category, COUNT(*) as cnt - FROM market_apps - WHERE status = 'published' - GROUP BY category - ORDER BY cnt DESC - """) - - # Recent 7 days trend - trend = [] - for i in range(6, -1, -1): - date = (datetime.now() - timedelta(days=i)).strftime('%Y-%m-%d') - next_date = (datetime.now() - timedelta(days=i-1)).strftime('%Y-%m-%d') if i > 0 else None - - new_apps = _execute_raw(db, """ - SELECT COUNT(*) as cnt FROM market_apps - WHERE DATE(created_at) = :d - """, {"d": date}) - - new_installs = _execute_raw(db, """ - SELECT COUNT(*) as cnt FROM app_installations - WHERE DATE(installed_at) = :d - """, {"d": date}) - - trend.append({ - "date": date, - "new_apps": new_apps[0]["cnt"] if new_apps else 0, - "new_installs": new_installs[0]["cnt"] if new_installs else 0, - }) - - return { - "code": 0, - "data": { - "total_apps": sum(status_map.values()), - "published": status_map.get("published", 0), - "pending": status_map.get("pending", 0), - "removed": status_map.get("removed", 0), - "total_installs": total_installs, - "total_purchases": total_purchases, - "total_revenue": total_revenue, - "total_reviews": total_reviews, - "overall_rating": round(overall_rating, 2), - "by_type": {r["type"]: r["cnt"] for r in type_stats}, - "by_category": {r["category"]: r["cnt"] for r in category_stats}, - "trend_7d": trend, - } - } - - -@router.get("/analytics/top-apps", summary="热门应用排行") -async def get_top_apps( - metric: str = Query("installs", regex="^(installs|rating|revenue|reviews)$"), - limit: int = Query(10, le=50), - admin_user=Depends(get_current_admin), - db: Session = Depends(get_db), -): - """获取热门应用排行""" - if metric == "installs": - order = desc(MarketApp.install_count) - elif metric == "rating": - order = desc(MarketApp.rating_avg) - elif metric == "reviews": - order = desc(MarketApp.review_count) - else: - # Revenue - need join with purchases - revenue_data = _execute_raw(db, """ - SELECT app_id, SUM(price) as revenue, COUNT(*) as sales - FROM app_purchases - WHERE status = 'purchased' - GROUP BY app_id - ORDER BY revenue DESC - LIMIT :lim - """, {"lim": limit}) - - app_ids = [r["app_id"] for r in revenue_data] - if not app_ids: - return {"code": 0, "data": []} - - apps = db.query(MarketApp).filter(MarketApp.id.in_(app_ids)).all() - app_map = {a.id: a for a in apps} - result = [] - for r in revenue_data: - app = app_map.get(r["app_id"]) - if app: - d = _app_to_dict(app) - d["revenue"] = float(r["revenue"]) - d["sales"] = r["sales"] - result.append(d) - return {"code": 0, "data": result} - - apps = db.query(MarketApp).filter( - MarketApp.status == "published" - ).order_by(order).limit(limit).all() - - return { - "code": 0, - "data": [_app_to_dict(app) for app in apps] - } - - -@router.get("/analytics/recent-reviews", summary="最新评价") -async def get_recent_reviews( - limit: int = Query(20, le=100), - admin_user=Depends(get_current_admin), - db: Session = Depends(get_db), -): - """获取最新用户评价""" - reviews = _execute_raw(db, """ - SELECT r.id, r.rating, r.content, r.created_at, - u.username, u.avatar, - a.name as app_name, a.icon as app_icon - FROM app_reviews r - LEFT JOIN users u ON r.user_id = u.id - LEFT JOIN market_apps a ON r.app_id = a.id - ORDER BY r.created_at DESC - LIMIT :lim - """, {"lim": limit}) - - return { - "code": 0, - "data": [{ - "id": r["id"], - "rating": r["rating"], - "content": r["content"], - "username": r["username"] or "Anonymous", - "avatar": r["avatar"], - "app_name": r["app_name"] or "Unknown", - "app_icon": r["app_icon"], - "created_at": r["created_at"].strftime('%Y-%m-%d %H:%M:%S') if r["created_at"] else None, - } for r in reviews] - } - - -# ============================================================ -# App management endpoints -# ============================================================ - -@router.get("/apps", summary="获取所有市场应用") -async def list_market_apps( - status: str = Query(None), - app_type: str = Query(None), - category: str = Query(None), - featured: Optional[bool] = Query(None), - keyword: str = Query(None), - page: int = Query(1, ge=1), - page_size: int = Query(20, ge=1, le=100), - admin_user=Depends(get_current_admin), - db: Session = Depends(get_db), -): - """获取所有市场应用列表(支持筛选)""" - query = db.query(MarketApp) - - if status: - query = query.filter(MarketApp.status == status) - if app_type: - query = query.filter(MarketApp.type == app_type) - if category: - query = query.filter(text("category = :cat").bindparams(cat=category)) - if featured is not None: - query = query.filter(text("featured = :feat").bindparams(feat=int(featured))) - if keyword: - query = query.filter(or_( - MarketApp.name.contains(keyword), - MarketApp.description.contains(keyword), - )) - - total = query.count() - apps = query.order_by(desc(MarketApp.created_at)).offset( - (page - 1) * page_size - ).limit(page_size).all() - - return { - "code": 0, - "data": { - "total": total, - "page": page, - "page_size": page_size, - "items": [_app_to_dict(app) for app in apps], - } - } - - -@router.get("/apps/pending", summary="获取待审核应用") +@router.get("/apps/pending") async def get_pending_apps( - page: int = Query(1, ge=1), - page_size: int = Query(20, ge=1, le=100), admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """获取待审核应用列表""" - query = db.query(MarketApp).filter(MarketApp.status == "pending") - total = query.count() - apps = query.order_by(MarketApp.created_at.desc()).offset( - (page - 1) * page_size - ).limit(page_size).all() + apps = db.query(MarketApp).filter( + MarketApp.status == "pending" + ).order_by(MarketApp.created_at.desc()).limit(1000).all() - return { - "code": 0, - "data": { - "total": total, - "page": page, - "page_size": page_size, - "items": [_app_to_dict(app, include_content=True) for app in apps], + return [ + { + "id": a.id, + "name": a.name, + "type": a.type, + "source": a.source, + "publisher_id": a.publisher_id, + "publisher_name": a.publisher_name, + "description": a.description, + "icon": a.icon, + "tutorial": a.tutorial, + "price": float(a.price) if a.price else 0, + "is_member_free": a.is_member_free, + "join_promotion": a.join_promotion, + "status": a.status, + "rating_avg": float(a.rating_avg) if a.rating_avg else 0, + "review_count": a.review_count, + "install_count": a.install_count, + "skill_content": a.skill_content, + "mcp_transport": a.mcp_transport, + "agent_endpoint": a.agent_endpoint, + "created_at": a.created_at.isoformat() if a.created_at else None, + "updated_at": a.updated_at.isoformat() if a.updated_at else None, } - } + for a in apps + ] -@router.put("/apps/{app_id}/approve", summary="审核通过") +@router.put("/apps/{id}/approve") async def approve_app( - app_id: str, + id: str, admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """审核通过应用""" - app = db.query(MarketApp).filter(MarketApp.id == app_id).first() + app = db.query(MarketApp).filter(MarketApp.id == id).first() if not app: raise HTTPException(status_code=404, detail="应用不存在") @@ -371,20 +70,26 @@ async def approve_app( app.updated_at = datetime.now() db.commit() - LogService.log_action(db, "admin", admin_user["id"], "approve_market_app", f"Approved app: {app.name}") + LogService.log_audit( + admin_id=admin_user.id, + admin_name=admin_user.username, + action="approve_app", + target_type="market_app", + target_id=id, + detail=f"审核通过应用 {app.name}", + ) - return {"code": 0, "message": "审核通过", "data": _app_to_dict(app)} + return {"message": "应用审核通过"} -@router.put("/apps/{app_id}/reject", summary="审核拒绝") +@router.put("/apps/{id}/reject") async def reject_app( - app_id: str, - reason: str = Query("", max_length=500), + id: str, admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """审核拒绝应用""" - app = db.query(MarketApp).filter(MarketApp.id == app_id).first() + app = db.query(MarketApp).filter(MarketApp.id == id).first() if not app: raise HTTPException(status_code=404, detail="应用不存在") @@ -392,259 +97,316 @@ async def reject_app( app.updated_at = datetime.now() db.commit() - LogService.log_action(db, "admin", admin_user["id"], "reject_market_app", f"Rejected app: {app.name}, reason: {reason}") + LogService.log_audit( + admin_id=admin_user.id, + admin_name=admin_user.username, + action="reject_app", + target_type="market_app", + target_id=id, + detail=f"审核拒绝应用 {app.name}", + ) - return {"code": 0, "message": "已拒绝", "data": _app_to_dict(app)} + return {"message": "应用审核已拒绝"} -@router.put("/apps/{app_id}", summary="编辑应用") -async def update_app( - app_id: str, - req: AppEditRequest, - admin_user=Depends(get_current_admin), - db: Session = Depends(get_db), -): - """管理员编辑应用信息""" - app = db.query(MarketApp).filter(MarketApp.id == app_id).first() - if not app: - raise HTTPException(status_code=404, detail="应用不存在") - - if req.name is not None: - app.name = req.name - if req.description is not None: - app.description = req.description - if req.icon is not None: - app.icon = req.icon - if req.tutorial is not None: - app.tutorial = req.tutorial - if req.category is not None: - # Use raw SQL for new columns - db.execute(text("UPDATE market_apps SET category = :c WHERE id = :id"), {"c": req.category, "id": app_id}) - if req.tags is not None: - db.execute(text("UPDATE market_apps SET tags = :t WHERE id = :id"), {"t": req.tags, "id": app_id}) - if req.price is not None: - app.price = req.price - if req.is_member_free is not None: - app.is_member_free = req.is_member_free - if req.featured is not None: - db.execute(text("UPDATE market_apps SET featured = :f WHERE id = :id"), {"f": int(req.featured), "id": app_id}) - if req.featured_order is not None: - db.execute(text("UPDATE market_apps SET featured_order = :fo WHERE id = :id"), {"fo": req.featured_order, "id": app_id}) - if req.status is not None: - app.status = req.status - - app.updated_at = datetime.now() - db.commit() - - db.refresh(app) - return {"code": 0, "message": "更新成功", "data": _app_to_dict(app)} - - -@router.put("/apps/{app_id}/featured", summary="设置推荐应用") -async def set_featured( - app_id: str, - req: FeaturedUpdateRequest, - admin_user=Depends(get_current_admin), - db: Session = Depends(get_db), -): - """设置/取消推荐应用""" - app = db.query(MarketApp).filter(MarketApp.id == app_id).first() - if not app: - raise HTTPException(status_code=404, detail="应用不存在") - - db.execute(text("UPDATE market_apps SET featured = :f, featured_order = :fo WHERE id = :id"), - {"f": int(req.featured), "fo": req.featured_order, "id": app_id}) - db.commit() - - action = "Featured" if req.featured else "Unfeatured" - LogService.log_action(db, "admin", admin_user["id"], "feature_app", f"{action} app: {app.name}") - - return {"code": 0, "message": f"{'已推荐' if req.featured else '已取消推荐'}", "data": {"featured": req.featured, "featured_order": req.featured_order}} - - -@router.put("/apps/{app_id}/category", summary="设置应用分类") -async def set_category( - app_id: str, - req: CategoryUpdateRequest, - admin_user=Depends(get_current_admin), - db: Session = Depends(get_db), -): - """设置应用分类和标签""" - if req.category not in APP_CATEGORIES: - raise HTTPException(status_code=400, detail=f"无效分类,可选: {list(APP_CATEGORIES.keys())}") - - app = db.query(MarketApp).filter(MarketApp.id == app_id).first() - if not app: - raise HTTPException(status_code=404, detail="应用不存在") - - db.execute(text("UPDATE market_apps SET category = :c, tags = :t WHERE id = :id"), - {"c": req.category, "t": req.tags, "id": app_id}) - db.commit() - - return {"code": 0, "message": "分类已更新", "data": {"category": req.category, "tags": req.tags}} - - -@router.get("/categories", summary="获取应用分类列表") -async def get_categories( - admin_user=Depends(get_current_admin), - db: Session = Depends(get_db), -): - """获取所有应用分类及每分类的应用数""" - cat_stats = _execute_raw(db, """ - SELECT COALESCE(category, 'other') as category, COUNT(*) as cnt - FROM market_apps - WHERE status = 'published' - GROUP BY category - """) - cat_map = {r["category"]: r["cnt"] for r in cat_stats} - - result = [] - for key, label in APP_CATEGORIES.items(): - result.append({ - "key": key, - "label": label, - "count": cat_map.get(key, 0), - }) - - return {"code": 0, "data": result} - - -@router.delete("/apps/{app_id}", summary="删除应用") -async def delete_app( - app_id: str, - admin_user=Depends(get_current_admin), - db: Session = Depends(get_db), -): - """删除市场应用""" - app = db.query(MarketApp).filter(MarketApp.id == app_id).first() - if not app: - raise HTTPException(status_code=404, detail="应用不存在") - - app_name = app.name - db.delete(app) - db.commit() - - LogService.log_action(db, "admin", admin_user["id"], "delete_market_app", f"Deleted app: {app_name}") - - return {"code": 0, "message": "已删除"} - - -# ============================================================ -# Coupon management (kept from original) -# ============================================================ - -@router.get("/coupons", summary="获取优惠券列表") -async def list_coupons( +@router.get("/apps") +async def get_all_apps( + status: str = Query(None, description="状态筛选: pending/published/removed"), + app_type: str = Query(None, description="类型筛选: skill/agent/mcp"), page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): - """获取优惠券列表""" - from ...models import Coupon - query = db.query(Coupon) + """获取所有应用列表(管理员)""" + query = db.query(MarketApp) + + if status: + query = query.filter(MarketApp.status == status) + if app_type: + query = query.filter(MarketApp.type == app_type) + total = query.count() - coupons = query.order_by(desc(Coupon.created_at)).offset( + apps = query.order_by(MarketApp.created_at.desc()).offset( (page - 1) * page_size ).limit(page_size).all() return { - "code": 0, - "data": { - "total": total, - "page": page, - "page_size": page_size, - "items": [{ - "id": c.id, - "code": c.code, - "type": c.type, - "value": float(c.value) if c.value else 0, - "min_amount": float(c.min_amount) if c.min_amount else 0, - "valid_from": c.valid_from.strftime('%Y-%m-%d %H:%M:%S') if c.valid_from else None, - "valid_to": c.valid_to.strftime('%Y-%m-%d %H:%M:%S') if c.valid_to else None, - "total_count": c.total_count, - "used_count": c.used_count, - "status": c.status, - } for c in coupons], - } + "total": total, + "page": page, + "page_size": page_size, + "items": [ + { + "id": a.id, + "name": a.name, + "type": a.type, + "source": a.source, + "publisher_id": a.publisher_id, + "publisher_name": a.publisher_name, + "description": a.description, + "icon": a.icon, + "tutorial": a.tutorial, + "price": float(a.price) if a.price else 0, + "is_member_free": a.is_member_free, + "join_promotion": a.join_promotion, + "status": a.status, + "rating_avg": float(a.rating_avg) if a.rating_avg else 0, + "review_count": a.review_count, + "install_count": a.install_count, + "created_at": a.created_at.isoformat() if a.created_at else None, + "updated_at": a.updated_at.isoformat() if a.updated_at else None, + } + for a in apps + ], } -class CouponCreateRequest(BaseModel): - code: str - type: str # fixed / percent - value: float - min_amount: float = 0 - valid_from: Optional[str] = None - valid_to: Optional[str] = None - total_count: int = 100 +@router.put("/apps/{id}") +async def admin_edit_app( + id: str, + name: str = None, + description: str = None, + icon: str = None, + price: float = None, + is_member_free: bool = None, + join_promotion: bool = None, + tutorial: str = None, + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """管理员编辑应用""" + app = db.query(MarketApp).filter(MarketApp.id == id).first() + if not app: + raise HTTPException(status_code=404, detail="应用不存在") + + if name is not None: + app.name = name + if description is not None: + app.description = description + if icon is not None: + app.icon = icon + if price is not None: + app.price = price + if is_member_free is not None: + app.is_member_free = is_member_free + if join_promotion is not None: + app.join_promotion = join_promotion + if tutorial is not None: + app.tutorial = tutorial + app.updated_at = datetime.now() + db.commit() + + LogService.log_audit( + admin_id=admin_user.id, + admin_name=admin_user.username, + action="edit_market_app", + target_type="market_app", + target_id=id, + detail=f"管理员编辑应用 {app.name}", + ) + + return {"message": "应用已更新"} -@router.post("/coupons", summary="创建优惠券") +# ============================================================ +# 优惠券管理 +# ============================================================ + +@router.get("/coupons") +async def get_coupons( + status: str = Query(None, description="状态筛选: active/expired/disabled"), + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """获取优惠券列表""" + query = db.query(Coupon) + + if status: + query = query.filter(Coupon.status == status) + + coupons = query.order_by(Coupon.created_at.desc()).limit(1000).all() + return [ + { + "id": c.id, + "code": c.code, + "type": c.type, + "value": float(c.value) if c.value else 0, + "min_amount": float(c.min_amount) if c.min_amount else 0, + "valid_from": c.valid_from.isoformat() if c.valid_from else None, + "valid_to": c.valid_to.isoformat() if c.valid_to else None, + "total_count": c.total_count, + "used_count": c.used_count, + "status": c.status, + "created_at": c.created_at.isoformat() if c.created_at else None, + } + for c in coupons + ] + + +@router.post("/coupons") async def create_coupon( - req: CouponCreateRequest, + code: str, + coupon_type: str, + value: float, + valid_from: str, + valid_to: str, + min_amount: float = 0, + total_count: int = None, + status: str = "active", admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """创建优惠券""" - from ...models import Coupon + # 检查code是否已存在 + existing = db.query(Coupon).filter(Coupon.code == code).first() + if existing: + raise HTTPException(status_code=400, detail="优惠券码已存在") + coupon = Coupon( - id=str(uuid.uuid4())[:8], - code=req.code, - type=req.type, - value=req.value, - min_amount=req.min_amount, - valid_from=datetime.strptime(req.valid_from, '%Y-%m-%d') if req.valid_from else datetime.now(), - valid_to=datetime.strptime(req.valid_to, '%Y-%m-%d') if req.valid_to else None, - total_count=req.total_count, - used_count=0, - status="active", + id=str(uuid.uuid4()), + code=code, + type=coupon_type, + value=value, + min_amount=min_amount, + valid_from=datetime.fromisoformat(valid_from), + valid_to=datetime.fromisoformat(valid_to), + total_count=total_count, + status=status, ) db.add(coupon) db.commit() - return {"code": 0, "message": "创建成功", "data": {"id": coupon.id, "code": coupon.code}} + LogService.log_audit( + admin_id=admin_user.id, + admin_name=admin_user.username, + action="create_coupon", + target_type="coupon", + target_id=coupon.id, + detail=f"创建优惠券 {code}", + ) + + return {"message": "优惠券已创建", "id": coupon.id} -@router.put("/coupons/{coupon_id}", summary="更新优惠券") +@router.put("/coupons/{id}") async def update_coupon( - coupon_id: str, - req: CouponCreateRequest, + id: str, + code: str = None, + value: float = None, + min_amount: float = None, + valid_from: str = None, + valid_to: str = None, + total_count: int = None, + status: str = None, admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """更新优惠券""" - from ...models import Coupon - coupon = db.query(Coupon).filter(Coupon.id == coupon_id).first() + coupon = db.query(Coupon).filter(Coupon.id == id).first() if not coupon: raise HTTPException(status_code=404, detail="优惠券不存在") - coupon.code = req.code - coupon.type = req.type - coupon.value = req.value - coupon.min_amount = req.min_amount - coupon.total_count = req.total_count - if req.valid_from: - coupon.valid_from = datetime.strptime(req.valid_from, '%Y-%m-%d') - if req.valid_to: - coupon.valid_to = datetime.strptime(req.valid_to, '%Y-%m-%d') + if code is not None: + # 检查新code是否重复 + if code != coupon.code: + dup = db.query(Coupon).filter(Coupon.code == code).first() + if dup: + raise HTTPException(status_code=400, detail="优惠券码已存在") + coupon.code = code + if value is not None: + coupon.value = value + if min_amount is not None: + coupon.min_amount = min_amount + if valid_from is not None: + coupon.valid_from = datetime.fromisoformat(valid_from) + if valid_to is not None: + coupon.valid_to = datetime.fromisoformat(valid_to) + if total_count is not None: + coupon.total_count = total_count + if status is not None: + coupon.status = status db.commit() - return {"code": 0, "message": "更新成功"} + + LogService.log_audit( + admin_id=admin_user.id, + admin_name=admin_user.username, + action="update_coupon", + target_type="coupon", + target_id=id, + detail=f"更新优惠券 {coupon.code}", + ) + + return {"message": "优惠券已更新"} -@router.delete("/coupons/{coupon_id}", summary="删除优惠券") +@router.delete("/coupons/{id}") async def delete_coupon( - coupon_id: str, + id: str, admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """删除优惠券""" - from ...models import Coupon - coupon = db.query(Coupon).filter(Coupon.id == coupon_id).first() + coupon = db.query(Coupon).filter(Coupon.id == id).first() if not coupon: raise HTTPException(status_code=404, detail="优惠券不存在") db.delete(coupon) db.commit() - return {"code": 0, "message": "已删除"} + + LogService.log_audit( + admin_id=admin_user.id, + admin_name=admin_user.username, + action="delete_coupon", + target_type="coupon", + target_id=id, + detail=f"删除优惠券 {coupon.code}", + ) + + return {"message": "优惠券已删除"} + + +@router.post("/coupons/{id}/assign") +async def assign_coupon( + id: str, + user_id: str, + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """发放优惠券给用户""" + coupon = db.query(Coupon).filter(Coupon.id == id).first() + if not coupon: + raise HTTPException(status_code=404, detail="优惠券不存在") + if coupon.status != "active": + raise HTTPException(status_code=400, detail="优惠券非活跃状态") + + user = db.query(User).filter(User.id == user_id).first() + if not user: + raise HTTPException(status_code=404, detail="用户不存在") + + # 检查是否已发放 + existing = db.query(UserCoupon).filter( + UserCoupon.user_id == user_id, + UserCoupon.coupon_id == id, + ).first() + if existing: + raise HTTPException(status_code=400, detail="该用户已拥有此优惠券") + + user_coupon = UserCoupon( + id=str(uuid.uuid4()), + user_id=user_id, + coupon_id=id, + ) + db.add(user_coupon) + db.commit() + + LogService.log_audit( + admin_id=admin_user.id, + admin_name=admin_user.username, + action="assign_coupon", + target_type="coupon", + target_id=id, + detail=f"发放优惠券 {coupon.code} 给用户 {user.username}", + ) + + return {"message": "优惠券已发放"} diff --git a/backend/python/app/api/admin/services.py b/backend/python/app/api/admin/services.py index 6c985a2..13e65a5 100644 --- a/backend/python/app/api/admin/services.py +++ b/backend/python/app/api/admin/services.py @@ -68,7 +68,7 @@ async def get_services( if status: query = query.filter(ValueAddedService.status == status) - services = query.order_by(ValueAddedService.created_at.desc()).all() + services = query.order_by(ValueAddedService.created_at.desc()).limit(1000).all() result = [] for s in services: result.append({ @@ -244,7 +244,7 @@ async def get_custom_requests( if status: query = query.filter(CustomServiceRequest.status == status) - requests = query.order_by(CustomServiceRequest.created_at.desc()).all() + requests = query.order_by(CustomServiceRequest.created_at.desc()).limit(1000).all() result = [] for r in requests: # 查询用户名 @@ -326,10 +326,13 @@ async def get_custom_orders( if status: query = query.filter(CustomServiceOrder.status == status) - orders = query.order_by(CustomServiceOrder.created_at.desc()).all() + orders = query.order_by(CustomServiceOrder.created_at.desc()).limit(1000).all() + # 批量获取用户,避免逐行查询(N+1) + _uids = [o.user_id for o in orders if o.user_id] + _users = {u.id: u for u in db.query(User).filter(User.id.in_(_uids)).all()} if _uids else {} result = [] for o in orders: - user = db.query(User).filter(User.id == o.user_id).first() + user = _users.get(o.user_id) result.append({ "id": o.id, "user_id": o.user_id, diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d3b69fa..108a214 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,166 +1,125 @@ -import React, { useState, useEffect } from 'react'; -import { Routes, Route, Navigate } from 'react-router-dom'; -import type { User } from '@/types'; -import { AuthContext, AuthContextType } from '@/contexts/AuthContext'; -import { authAPI } from '@/api'; - -// Layouts -import FrontLayout from '@/components/layout/FrontLayout'; -import AdminLayout from '@/components/layout/AdminLayout'; - -// Front Pages -import LandingPage from '@/pages/front/LandingPage'; -import LoginPage from '@/pages/front/LoginPage'; -import ChatPage from '@/pages/front/ChatPage'; -import ChannelPage from '@/pages/front/ChannelPage'; -import KnowledgePage from '@/pages/front/KnowledgePage'; -import MarketPage from '@/pages/front/MarketPage'; -import TokenDashboardPage from '@/pages/front/TokenDashboardPage'; -import PricingPage from '@/pages/front/PricingPage'; -import ProfilePage from '@/pages/front/ProfilePage'; -import NotificationPage from '@/pages/front/NotificationPage'; -import AssetsPage from '@/pages/front/AssetsPage'; -import NovelPreviewPage from '@/pages/front/NovelPreviewPage'; -import EvolutionDashboard from '@/pages/front/EvolutionDashboard'; -import WechatBot from '@/pages/front/WechatBot'; - -// Admin Pages -import AdminDashboard from '@/pages/admin/Dashboard'; -import AdminUsers from '@/pages/admin/Users'; -import AdminModels from '@/pages/admin/Models'; -import AdminSiteSettings from '@/pages/admin/SiteSettings'; -import AdminPayment from '@/pages/admin/Payment'; -import AdminServices from '@/pages/admin/Services'; -import AdminMembership from '@/pages/admin/Membership'; -import AdminAds from '@/pages/admin/Ads'; -import AdminMarket from '@/pages/admin/Market'; -import AdminLogs from '@/pages/admin/Logs'; -import AdminSkills from '@/pages/admin/Skills' -import AdminAgents from '@/pages/admin/Agents'; -import AdminCodeExec from '@/pages/admin/CodeExec';; -import DistributionBuilder from '@/pages/admin/DistributionBuilder'; -import PrivacySettings from '@/pages/admin/PrivacySettings'; -import ImBots from '@/pages/admin/ImBots'; -import Promotion from '@/pages/admin/Promotion'; -import Achievement from '@/pages/admin/Achievement'; -import Tenant from '@/pages/admin/Tenant'; -import Billing from '@/pages/admin/Billing'; -import Analytics from '@/pages/admin/Analytics'; -import UserPortal from '@/pages/UserPortal'; -import AdminKnowledge from '@/pages/admin/Knowledge'; -import AdminWorkflow from '@/pages/admin/Workflow'; -import AdminStorage from '@/pages/admin/Storage'; -import AdminDeveloper from '@/pages/admin/Developer'; -import AdminDeveloperTools from '@/pages/admin/DeveloperTools'; -import AdminAutomation from '@/pages/admin/Automation'; -import AdminMcp from '@/pages/admin/Mcp'; -import AdminApps from '@/pages/admin/Apps'; -import AssetCenter from '@/pages/admin/AssetCenter'; - -const App: React.FC = () => { - const [user, setUser] = useState(null); - const [loading, setLoading] = useState(true); - - // 初始化:从localStorage恢复登录状态 - useEffect(() => { - const token = localStorage.getItem('aiflow_token'); - const cachedUser = localStorage.getItem('aiflow_user'); - if (token && cachedUser) { - try { - setUser(JSON.parse(cachedUser)); - } catch { - localStorage.removeItem('aiflow_token'); - localStorage.removeItem('aiflow_user'); - } - } - setLoading(false); - }, []); - - const login = (u: User) => { - setUser(u); - localStorage.setItem('aiflow_user', JSON.stringify(u)); - }; - const logout = () => { - setUser(null); - localStorage.removeItem('aiflow_token'); - localStorage.removeItem('aiflow_user'); - }; - const isAdmin = user?.role === 'admin'; - - if (loading) { - return
加载中...
; - } - - return ( - - - {/* Login */} - : ) : - } /> - - {/* Root - Landing for guests, redirect for authenticated users */} - : ) : - } /> - - {/* Front Routes (authenticated only) */} - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - {/* Admin Routes */} - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - } /> - - - ); -}; - -export default App; +import React, { useState, useEffect, Suspense } from 'react'; +import { Routes, Route, Navigate } from 'react-router-dom'; +import type { User } from '@/types'; +import { AuthContext, AuthContextType } from '@/contexts/AuthContext'; +import { authAPI } from '@/api'; + +// Layouts(保留为即时加载,构成应用外壳) +import FrontLayout from '@/components/layout/FrontLayout'; +import AdminLayout from '@/components/layout/AdminLayout'; + +// 路由级代码分割:每个页面独立打包成 chunk,首屏只加载外壳与公共依赖, +// 访问具体页面时才按需加载对应 chunk,显著加快首屏渲染与路由切换速度。 +const LandingPage = React.lazy(() => import('@/pages/front/LandingPage')); +const LoginPage = React.lazy(() => import('@/pages/front/LoginPage')); +const ChatPage = React.lazy(() => import('@/pages/front/ChatPage')); +const ChannelPage = React.lazy(() => import('@/pages/front/ChannelPage')); +const KnowledgePage = React.lazy(() => import('@/pages/front/KnowledgePage')); +const MarketPage = React.lazy(() => import('@/pages/front/MarketPage')); +const TokenDashboardPage = React.lazy(() => import('@/pages/front/TokenDashboardPage')); +const PricingPage = React.lazy(() => import('@/pages/front/PricingPage')); +const ProfilePage = React.lazy(() => import('@/pages/front/ProfilePage')); +const NotificationPage = React.lazy(() => import('@/pages/front/NotificationPage')); +const AssetsPage = React.lazy(() => import('@/pages/front/AssetsPage')); +const NovelPreviewPage = React.lazy(() => import('@/pages/front/NovelPreviewPage')); +const EvolutionDashboard = React.lazy(() => import('@/pages/front/EvolutionDashboard')); + +const AdminDashboard = React.lazy(() => import('@/pages/admin/Dashboard')); +const AdminUsers = React.lazy(() => import('@/pages/admin/Users')); +const AdminModels = React.lazy(() => import('@/pages/admin/Models')); +const AdminSiteSettings = React.lazy(() => import('@/pages/admin/SiteSettings')); +const AdminPayment = React.lazy(() => import('@/pages/admin/Payment')); +const AdminServices = React.lazy(() => import('@/pages/admin/Services')); +const AdminMembership = React.lazy(() => import('@/pages/admin/Membership')); +const AdminAds = React.lazy(() => import('@/pages/admin/Ads')); +const AdminMarket = React.lazy(() => import('@/pages/admin/Market')); +const AdminLogs = React.lazy(() => import('@/pages/admin/Logs')); + +const pageFallback =
加载中...
; + +const App: React.FC = () => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + // 初始化:从localStorage恢复登录状态 + useEffect(() => { + const token = localStorage.getItem('aiflow_token'); + const cachedUser = localStorage.getItem('aiflow_user'); + if (token && cachedUser) { + try { + setUser(JSON.parse(cachedUser)); + } catch { + localStorage.removeItem('aiflow_token'); + localStorage.removeItem('aiflow_user'); + } + } + setLoading(false); + }, []); + + const login = (u: User) => { + setUser(u); + localStorage.setItem('aiflow_user', JSON.stringify(u)); + }; + const logout = () => { + setUser(null); + localStorage.removeItem('aiflow_token'); + localStorage.removeItem('aiflow_user'); + }; + const isAdmin = user?.role === 'admin'; + + if (loading) { + return
加载中...
; + } + + return ( + + + + {/* Login */} + : ) : + } /> + + {/* Root - Landing for guests, redirect for authenticated users */} + : ) : + } /> + + {/* Front Routes (authenticated only) */} + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + {/* Admin Routes */} + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + } /> + + + + ); +}; + +export default App; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 82c0e13..6e583cc 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -13,6 +13,26 @@ export default defineConfig({ build: { outDir: 'dist', emptyOutDir: true, + // 单个 chunk 体积告警阈值调高,避免大页面触发无意义告警 + chunkSizeWarningLimit: 1200, + rollupOptions: { + output: { + // 仅将 react 核心库单独打包(长期缓存); + // 其余第三方库交给 Rollup 自动按需拆分,避免被强制合并成一个大 vendor 在首屏加载 + manualChunks(id) { + if (id.includes('node_modules')) { + if ( + id.includes('react-router') || + id.includes('/react/') || + id.includes('react-dom') || + id.includes('scheduler') + ) { + return 'react-vendor'; + } + } + }, + }, + }, }, server: { port: 5173, diff --git a/run.sh b/run.sh index 51c519e..2fd5a7f 100755 --- a/run.sh +++ b/run.sh @@ -15,6 +15,12 @@ else fi cd "$PROJECT_DIR" +# >>> OUTBOUND PROXY START >>> +export HTTP_PROXY=http://172.18.0.1:7890 +export HTTPS_PROXY=http://172.18.0.1:7890 +export NO_PROXY=localhost,127.0.0.1,::1,210.16.161.218,172.17.0.0/16,172.18.0.0/16,172.19.0.0/16,172.20.0.0/16,10.0.0.0/8,192.168.0.0/16,.svc,.local,casdoor,qihuaiflow-executor,qihu-aiflow-mysql,qihu-aiflow-redis,qihu-aiflow-pgvector,qihu-aiflow-casdoor,qihuyun.com,ai.qihuyun.com,docs.qihuyun.com,bot.qihuyun.com,siliconflow.cn,api.siliconflow.cn,deepseek.com,api.deepseek.com,volcengineapi.com,aliyun.com,aliyuncs.com,dysmsapi.aliyuncs.com,tencentcloudapi.com,baidubce.com,dashscope.com,qwen.ai,zhipuai.cn,moonshot.cn,minimax.chat,baichuan.com,chatglm.cn,xiaomimimo.com,token-plan-cn.xiaomimimo.com,mi.com,pypi.tuna.tsinghua.edu.cn,feishu.cn,open.feishu.cn,dingtalk.com,open.dingtalk.com,weixin.qq.com,developers.weixin.qq.com,qyapi.weixin.qq.com,ilinkai.weixin.qq.com,tencent.com +# <<< OUTBOUND PROXY END <<< + # Python 后端目录(提前定义,供后续步骤使用) PYTHON_DIR="$PROJECT_DIR/backend/python" @@ -167,19 +173,11 @@ echo " 使用数据库: $MYSQL_DATABASE" MIGRATION_DIR="$PROJECT_DIR/migrations" if [ -d "$MIGRATION_DIR" ]; then - # 幂等迁移:用 schema_migrations 记录已执行版本,避免每次启动重复执行(如重复插入演示数据) - mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" $MYSQL_SSL_FLAG "$MYSQL_DATABASE" -e "CREATE TABLE IF NOT EXISTS schema_migrations (version VARCHAR(255) NOT NULL PRIMARY KEY, applied_at DATETIME DEFAULT CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;" 2>/dev/null for sql_file in $(ls "$MIGRATION_DIR"/*.sql 2>/dev/null | sort); do filename=$(basename "$sql_file") - version="${filename%.sql}" - already=$(mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" $MYSQL_SSL_FLAG "$MYSQL_DATABASE" -sN -e "SELECT 1 FROM schema_migrations WHERE version='$version' LIMIT 1;" 2>/dev/null) - if [ "$already" = "1" ]; then - echo " - 跳过(已执行): $filename" - else - echo " - 执行: $filename" - mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" $MYSQL_SSL_FLAG "$MYSQL_DATABASE" < "$sql_file" 2>&1 | sed 's/^/ /' || echo " [警告] $filename 部分语句已跳过(可能已存在)" - mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" $MYSQL_SSL_FLAG "$MYSQL_DATABASE" -e "INSERT IGNORE INTO schema_migrations (version) VALUES ('$version');" 2>/dev/null - fi + echo " - 执行: $filename" + mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" $MYSQL_SSL_FLAG \ + "$MYSQL_DATABASE" < "$sql_file" 2>&1 | sed 's/^/ /' || echo " [警告] $filename 部分语句已跳过(可能已存在)" done echo " 数据库初始化完成" @@ -299,11 +297,6 @@ fi # 如果 nginx 可用,则启动(Docker 环境) if command -v nginx &>/dev/null; then - # 自愈:确保媒体生成(视频/图像)等长请求不被 60s 代理超时中断 - if [ -f /etc/nginx/conf.d/aiflow.conf ]; then - sed -i -E 's/proxy_read_timeout[[:space:]]+[0-9]+s;/proxy_read_timeout 600s;/; s/proxy_send_timeout[[:space:]]+[0-9]+s;/proxy_send_timeout 600s;/' /etc/nginx/conf.d/aiflow.conf 2>/dev/null || true - fi - echo "" echo " Nginx 配置测试..." if nginx -t 2>&1; then @@ -333,5 +326,11 @@ echo " 默认账号: admin / admin123" echo "========================================" echo "" +# 防崩溃自检:确认 uvicorn 可导入,缺失则在 venv 内重装(避免依赖缺失导致崩溃循环) +if ! python -c "import uvicorn" >/dev/null 2>&1; then + echo " [自愈] 检测到依赖缺失,重新安装 Python 依赖..." + pip install -r "$PYTHON_DIR/requirements.txt" -i https://pypi.tuna.tsinghua.edu.cn/simple -q 2>&1 | sed 's/^/ /' || echo " [警告] 依赖重装失败" +fi + cd "$PYTHON_DIR" exec python -m uvicorn main:app --host 0.0.0.0 --port 18080 --log-level info