Spaces:
Running
Running
| #!/usr/bin/env python3 | |
| """ | |
| AI 学术写作助手 - 统一入口 | |
| 将前后端整合为一个可执行文件 | |
| """ | |
| import os | |
| import sys | |
| import webbrowser | |
| import threading | |
| import time | |
| import signal | |
| from typing import Optional | |
| # 获取应用运行目录 | |
| if getattr(sys, 'frozen', False): | |
| # PyInstaller 打包后的 exe 运行 | |
| APP_DIR = os.path.dirname(sys.executable) | |
| # 静态文件在 exe 内部的 _internal 目录或与 exe 同级目录 | |
| STATIC_DIR = os.path.join(sys._MEIPASS, 'static') | |
| else: | |
| # 正常 Python 运行 | |
| APP_DIR = os.path.dirname(os.path.abspath(__file__)) | |
| STATIC_DIR = os.path.join(APP_DIR, 'static') | |
| # 设置工作目录为应用目录(确保数据库和配置文件在正确位置) | |
| os.chdir(APP_DIR) | |
| # 设置环境变量指向 exe 同目录的 .env 文件 | |
| ENV_FILE = os.path.join(APP_DIR, '.env') | |
| DB_FILE = os.path.join(APP_DIR, 'ai_polish.db') | |
| # 加载环境变量 | |
| if os.path.exists(ENV_FILE): | |
| from dotenv import load_dotenv | |
| load_dotenv(ENV_FILE) | |
| # 设置默认数据库路径到 exe 同目录 | |
| if 'DATABASE_URL' not in os.environ: | |
| os.environ['DATABASE_URL'] = f"sqlite:///{DB_FILE}" | |
| # 添加 backend 到 Python 路径 | |
| backend_path = os.path.join(APP_DIR, 'backend') if not getattr(sys, 'frozen', False) else APP_DIR | |
| if backend_path not in sys.path: | |
| sys.path.insert(0, backend_path) | |
| from fastapi import FastAPI, Request, HTTPException, Response | |
| from fastapi.staticfiles import StaticFiles | |
| from fastapi.responses import FileResponse, HTMLResponse, JSONResponse | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.middleware.gzip import GZipMiddleware | |
| import uvicorn | |
| # 导入后端应用组件 | |
| from app.config import settings | |
| from app.database import init_db | |
| from app.routes import admin, prompts, optimization | |
| from app.word_formatter import router as word_formatter_router | |
| from app.word_formatter.services import get_job_manager | |
| from app.models.models import CustomPrompt | |
| from app.database import SessionLocal | |
| from app.services.ai_service import get_default_polish_prompt, get_default_enhance_prompt | |
| # 检查默认密钥(仅警告,不退出) | |
| if settings.SECRET_KEY == "your-secret-key-change-this-in-production": | |
| print("\n" + "="*60) | |
| print("⚠️ 安全警告: 检测到默认 SECRET_KEY!") | |
| print("="*60) | |
| print("生产环境必须修改 SECRET_KEY,否则 JWT token 可被伪造!") | |
| print(f"请在 {ENV_FILE} 文件中设置强密钥:") | |
| print(" 使用命令生成: python -c \"import secrets; print(secrets.token_urlsafe(32))\"") | |
| print("="*60 + "\n") | |
| if settings.ADMIN_PASSWORD == "admin123": | |
| print("\n" + "="*60) | |
| print("⚠️ 安全警告: 检测到默认管理员密码!") | |
| print("="*60) | |
| print("生产环境必须修改 ADMIN_PASSWORD!") | |
| print(f"请在 {ENV_FILE} 文件中设置强密码 (建议12位以上)") | |
| print("="*60 + "\n") | |
| # 创建 FastAPI 应用 | |
| app = FastAPI( | |
| title="AI 论文润色增强系统", | |
| description="高质量论文润色与原创性学术表达增强", | |
| version="1.0.0" | |
| ) | |
| # 添加 Gzip 压缩中间件以减少响应体积 | |
| app.add_middleware(GZipMiddleware, minimum_size=1000) | |
| # CORS 配置 | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # 添加中间件:为所有 API 响应添加禁止缓存的头部 | |
| async def add_no_cache_headers(request: Request, call_next): | |
| """为 API 请求添加禁止缓存的响应头""" | |
| response = await call_next(request) | |
| # 只对 API 路径添加禁止缓存头,静态资源可以缓存 | |
| if request.url.path.startswith('/api/'): | |
| response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate, max-age=0" | |
| response.headers["Pragma"] = "no-cache" | |
| response.headers["Expires"] = "0" | |
| return response | |
| # 注册 API 路由(添加 /api 前缀,与 backend/app/main.py 保持一致) | |
| app.include_router(admin.router, prefix="/api") | |
| app.include_router(prompts.router, prefix="/api") | |
| app.include_router(optimization.router, prefix="/api") | |
| app.include_router(word_formatter_router, prefix="/api") | |
| async def startup_event(): | |
| """启动时初始化""" | |
| print(f"\n📁 应用目录: {APP_DIR}") | |
| print(f"📁 配置文件: {ENV_FILE}") | |
| print(f"📁 数据库文件: {DB_FILE}") | |
| print(f"📁 静态文件目录: {STATIC_DIR}") | |
| # 初始化数据库 | |
| init_db() | |
| # 创建系统默认提示词 | |
| db = SessionLocal() | |
| try: | |
| # 检查是否已存在系统提示词 | |
| polish_prompt = db.query(CustomPrompt).filter( | |
| CustomPrompt.is_system.is_(True), | |
| CustomPrompt.stage == "polish" | |
| ).first() | |
| if not polish_prompt: | |
| polish_prompt = CustomPrompt( | |
| name="默认润色提示词", | |
| stage="polish", | |
| content=get_default_polish_prompt(), | |
| is_default=True, | |
| is_system=True | |
| ) | |
| db.add(polish_prompt) | |
| enhance_prompt = db.query(CustomPrompt).filter( | |
| CustomPrompt.is_system.is_(True), | |
| CustomPrompt.stage == "enhance" | |
| ).first() | |
| if not enhance_prompt: | |
| enhance_prompt = CustomPrompt( | |
| name="默认增强提示词", | |
| stage="enhance", | |
| content=get_default_enhance_prompt(), | |
| is_default=True, | |
| is_system=True | |
| ) | |
| db.add(enhance_prompt) | |
| db.commit() | |
| finally: | |
| db.close() | |
| async def shutdown_event(): | |
| """关闭时清理资源""" | |
| job_manager = get_job_manager() | |
| await job_manager.shutdown() | |
| async def health_check(): | |
| """健康检查""" | |
| return JSONResponse( | |
| content={"status": "healthy"}, | |
| headers={ | |
| "Cache-Control": "no-cache, no-store, must-revalidate", | |
| "Pragma": "no-cache", | |
| "Expires": "0" | |
| } | |
| ) | |
| def _check_url_format(base_url: Optional[str]) -> tuple: | |
| """检查 URL 格式是否正确 | |
| Returns: | |
| tuple: (is_valid, error_message) | |
| """ | |
| import re | |
| if not base_url or not base_url.strip(): | |
| return False, "Base URL 未配置" | |
| # 验证 base_url 是否符合 OpenAI API 格式 | |
| # 使用更严格的 URL 验证模式 | |
| url_pattern = re.compile(r'^https?://[^\s/$.?#].[^\s]*$', re.IGNORECASE) | |
| if not url_pattern.match(base_url): | |
| return False, "Base URL 格式不正确,应为有效的 HTTP/HTTPS URL" | |
| return True, None | |
| # 缓存已检查的 URL 结果,避免重复检查 | |
| _url_check_cache: dict = {} | |
| async def _check_model_health(model_name: str, model: str, api_key: Optional[str], base_url: Optional[str]) -> dict: | |
| """检查单个模型的健康状态 - 只验证URL格式,不测试实际连接""" | |
| try: | |
| # 检查必需的配置项 | |
| if not model or not model.strip(): | |
| return { | |
| "status": "unavailable", | |
| "model": model, | |
| "base_url": base_url, | |
| "error": "模型名称未配置" | |
| } | |
| # 先检查 URL 格式是否有效 | |
| is_valid, error_msg = _check_url_format(base_url) | |
| if not is_valid: | |
| return { | |
| "status": "unavailable", | |
| "model": model, | |
| "base_url": base_url, | |
| "error": error_msg | |
| } | |
| # URL 有效时才检查缓存(此时 base_url 不为 None) | |
| if base_url in _url_check_cache: | |
| cached_result = _url_check_cache[base_url] | |
| result = { | |
| "status": cached_result["status"], | |
| "model": model, | |
| "base_url": base_url | |
| } | |
| if cached_result["status"] == "unavailable": | |
| result["error"] = cached_result.get("error") | |
| return result | |
| # URL 格式正确,认为配置有效 | |
| result = { | |
| "status": "available", | |
| "model": model, | |
| "base_url": base_url | |
| } | |
| # 缓存检查结果 | |
| _url_check_cache[base_url] = {"status": "available"} | |
| return result | |
| except Exception as e: | |
| error_msg = str(e) if str(e) else "未知错误" | |
| return { | |
| "status": "unavailable", | |
| "model": model, | |
| "base_url": base_url, | |
| "error": error_msg | |
| } | |
| async def check_models_health(): | |
| """检查 AI 模型可用性 - 只验证URL格式,如果URL相同则只检查一次""" | |
| global _url_check_cache | |
| # 清空缓存以确保每次请求都重新检查 | |
| _url_check_cache = {} | |
| results = { | |
| "overall_status": "healthy", | |
| "models": {} | |
| } | |
| # 检查润色模型 | |
| results["models"]["polish"] = await _check_model_health( | |
| "polish", | |
| settings.POLISH_MODEL, | |
| settings.POLISH_API_KEY, | |
| settings.POLISH_BASE_URL | |
| ) | |
| if results["models"]["polish"]["status"] == "unavailable": | |
| results["overall_status"] = "degraded" | |
| # 检查增强模型 | |
| results["models"]["enhance"] = await _check_model_health( | |
| "enhance", | |
| settings.ENHANCE_MODEL, | |
| settings.ENHANCE_API_KEY, | |
| settings.ENHANCE_BASE_URL | |
| ) | |
| if results["models"]["enhance"]["status"] == "unavailable": | |
| results["overall_status"] = "degraded" | |
| # 检查感情润色模型(如果配置了) | |
| if settings.EMOTION_MODEL: | |
| results["models"]["emotion"] = await _check_model_health( | |
| "emotion", | |
| settings.EMOTION_MODEL, | |
| settings.EMOTION_API_KEY, | |
| settings.EMOTION_BASE_URL | |
| ) | |
| if results["models"]["emotion"]["status"] == "unavailable": | |
| results["overall_status"] = "degraded" | |
| # 返回带缓存控制头的响应,确保数据始终是最新的 | |
| return JSONResponse( | |
| content=results, | |
| headers={ | |
| "Cache-Control": "no-cache, no-store, must-revalidate", | |
| "Pragma": "no-cache", | |
| "Expires": "0" | |
| } | |
| ) | |
| # 挂载静态文件(前端构建产物) | |
| if os.path.exists(STATIC_DIR): | |
| # 挂载 assets 目录(JS, CSS 等) | |
| assets_dir = os.path.join(STATIC_DIR, 'assets') | |
| if os.path.exists(assets_dir): | |
| app.mount("/assets", StaticFiles(directory=assets_dir), name="assets") | |
| # 处理根路径和其他前端路由 | |
| async def serve_root(): | |
| """服务根路径""" | |
| index_file = os.path.join(STATIC_DIR, 'index.html') | |
| if os.path.exists(index_file): | |
| return FileResponse(index_file) | |
| return {"message": "AI 论文润色增强系统 API", "version": "1.0.0", "docs": "/docs"} | |
| async def serve_admin(path: str = ""): | |
| """服务管理后台页面""" | |
| index_file = os.path.join(STATIC_DIR, 'index.html') | |
| if os.path.exists(index_file): | |
| return FileResponse(index_file) | |
| return {"error": "Admin page not found"} | |
| async def serve_workspace(path: str = ""): | |
| """服务工作区页面""" | |
| index_file = os.path.join(STATIC_DIR, 'index.html') | |
| if os.path.exists(index_file): | |
| return FileResponse(index_file) | |
| return {"error": "Workspace page not found"} | |
| async def serve_word_formatter(path: str = ""): | |
| """服务 Word 格式化页面""" | |
| index_file = os.path.join(STATIC_DIR, 'index.html') | |
| if os.path.exists(index_file): | |
| return FileResponse(index_file) | |
| return {"error": "Word formatter page not found"} | |
| async def serve_session(session_id: str): | |
| """服务会话详情页面""" | |
| index_file = os.path.join(STATIC_DIR, 'index.html') | |
| if os.path.exists(index_file): | |
| return FileResponse(index_file) | |
| return {"error": "Session page not found"} | |
| async def serve_access(card_key: str): | |
| """服务访问页面""" | |
| index_file = os.path.join(STATIC_DIR, 'index.html') | |
| if os.path.exists(index_file): | |
| return FileResponse(index_file) | |
| return {"error": "Access page not found"} | |
| # 处理其他静态文件 | |
| async def serve_static(file_path: str): | |
| """服务其他静态文件""" | |
| # 如果是 API 路径,抛出 404 让 FastAPI 正确处理 | |
| if file_path.startswith('api/') or file_path.startswith('docs') or file_path.startswith('openapi'): | |
| raise HTTPException(status_code=404, detail="Not found") | |
| full_path = os.path.join(STATIC_DIR, file_path) | |
| if os.path.exists(full_path) and os.path.isfile(full_path): | |
| return FileResponse(full_path) | |
| # 对于 SPA 路由,返回 index.html | |
| index_file = os.path.join(STATIC_DIR, 'index.html') | |
| if os.path.exists(index_file): | |
| return FileResponse(index_file) | |
| raise HTTPException(status_code=404, detail="File not found") | |
| else: | |
| async def root(): | |
| """根路径""" | |
| return { | |
| "message": "AI 论文润色增强系统 API", | |
| "version": "1.0.0", | |
| "docs": "/docs", | |
| "note": "静态文件目录不存在,仅 API 可用" | |
| } | |
| def open_browser(port: int): | |
| """延迟打开浏览器""" | |
| time.sleep(2) # 等待服务器启动 | |
| url = f"http://localhost:{port}" | |
| print(f"\n🌐 正在打开浏览器: {url}") | |
| webbrowser.open(url) | |
| def create_sample_env(): | |
| """创建示例 .env 文件(如果不存在)""" | |
| if not os.path.exists(ENV_FILE): | |
| sample_content = """# AI 学术写作助手配置文件 | |
| # 请根据实际情况修改以下配置 | |
| # 数据库配置 (SQLite 默认在 exe 同目录) | |
| # DATABASE_URL=sqlite:///./ai_polish.db | |
| # Redis 配置 (用于并发控制和队列) | |
| REDIS_URL=redis://localhost:6379/0 | |
| # OpenAI API 配置 | |
| OPENAI_API_KEY=your-api-key-here | |
| OPENAI_BASE_URL=https://api.openai.com/v1 | |
| # 第一阶段模型配置 (论文润色) - 推荐使用 gemini-2.5-pro | |
| POLISH_MODEL=gemini-2.5-pro | |
| POLISH_API_KEY=your-api-key-here | |
| POLISH_BASE_URL=https://api.openai.com/v1 | |
| # 第二阶段模型配置 (原创性增强) - 推荐使用 gemini-2.5-pro | |
| ENHANCE_MODEL=gemini-2.5-pro | |
| ENHANCE_API_KEY=your-api-key-here | |
| ENHANCE_BASE_URL=https://api.openai.com/v1 | |
| # 感情文章润色模型配置 - 推荐使用 gemini-2.5-pro | |
| EMOTION_MODEL=gemini-2.5-pro | |
| EMOTION_API_KEY=your-api-key-here | |
| EMOTION_BASE_URL=https://api.openai.com/v1 | |
| # 并发配置 | |
| MAX_CONCURRENT_USERS=7 | |
| # 会话压缩配置 | |
| HISTORY_COMPRESSION_THRESHOLD=2000 | |
| COMPRESSION_MODEL=gemini-2.5-pro | |
| COMPRESSION_API_KEY=your-api-key-here | |
| COMPRESSION_BASE_URL=https://api.openai.com/v1 | |
| # JWT 密钥 (请修改为随机字符串) | |
| SECRET_KEY=please-change-this-to-a-random-string-32-chars | |
| ALGORITHM=HS256 | |
| ACCESS_TOKEN_EXPIRE_MINUTES=60 | |
| # 管理员账户 (请修改默认密码) | |
| ADMIN_USERNAME=admin | |
| ADMIN_PASSWORD=please-change-this-password | |
| DEFAULT_USAGE_LIMIT=1 | |
| SEGMENT_SKIP_THRESHOLD=15 | |
| """ | |
| with open(ENV_FILE, 'w', encoding='utf-8') as f: | |
| f.write(sample_content) | |
| print(f"✅ 已创建示例配置文件: {ENV_FILE}") | |
| print(" 请编辑此文件,填入您的 API Key 和其他配置") | |
| def main(): | |
| """主入口函数""" | |
| port = 8000 | |
| host = "127.0.0.1" | |
| print("\n" + "="*60) | |
| print("🚀 AI 学术写作助手 - 启动中...") | |
| print("="*60) | |
| # 创建示例配置文件 | |
| create_sample_env() | |
| print(f"\n📍 服务地址: http://{host}:{port}") | |
| print(f"📍 管理后台: http://{host}:{port}/admin") | |
| print(f"📍 API 文档: http://{host}:{port}/docs") | |
| print("\n按 Ctrl+C 停止服务") | |
| print("="*60 + "\n") | |
| # 在后台线程中打开浏览器 | |
| browser_thread = threading.Thread(target=open_browser, args=(port,)) | |
| browser_thread.daemon = True | |
| browser_thread.start() | |
| # 启动 uvicorn 服务器 | |
| try: | |
| uvicorn.run( | |
| app, | |
| host=host, | |
| port=port, | |
| log_level="info", | |
| access_log=True | |
| ) | |
| except KeyboardInterrupt: | |
| print("\n\n👋 服务已停止") | |
| sys.exit(0) | |
| if __name__ == "__main__": | |
| main() | |