mirror of
https://github.com/countbot-ai/CountBot.git
synced 2026-09-14 20:46:47 +08:00
功能(启动): 添加应用启动脚本
- start_app.py: 生产模式启动(自动打开浏览器) - start_dev.py: 开发模式启动(热重载) - start_desktop.py: 桌面客户端启动(pywebview) - scripts/init_database.py: 数据库初始化脚本
This commit is contained in:
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
"""数据库初始化脚本"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到 Python 路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
|
||||
async def init_database():
|
||||
"""初始化数据库表"""
|
||||
print("正在初始化数据库...")
|
||||
|
||||
try:
|
||||
from backend.database import init_db
|
||||
|
||||
# 调用异步初始化函数
|
||||
await init_db()
|
||||
|
||||
print("✓ 数据库初始化成功")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 数据库初始化失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
success = asyncio.run(init_database())
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
CountBot 应用启动脚本
|
||||
生产模式启动,自动打开浏览器
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import webbrowser
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到 Python 路径
|
||||
project_root = Path(__file__).parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
# 跨平台 SSL 兼容性处理(macOS 需要额外配置证书)
|
||||
from backend.utils.ssl_compat import ensure_ssl_certificates
|
||||
ensure_ssl_certificates()
|
||||
|
||||
|
||||
def open_browser_delayed(url: str, delay: float = 2.0) -> None:
|
||||
"""
|
||||
延迟打开浏览器
|
||||
|
||||
Args:
|
||||
url: 要打开的 URL
|
||||
delay: 延迟时间(秒)
|
||||
"""
|
||||
def _open():
|
||||
import time
|
||||
time.sleep(delay)
|
||||
try:
|
||||
webbrowser.open(url)
|
||||
except Exception:
|
||||
pass # 静默失败,不影响服务器启动
|
||||
|
||||
threading.Thread(target=_open, daemon=True).start()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""启动应用"""
|
||||
import uvicorn
|
||||
from backend.utils.logger import setup_logger
|
||||
from backend.utils.process_manager import setup_graceful_shutdown
|
||||
from loguru import logger
|
||||
|
||||
# 初始化日志系统
|
||||
setup_logger()
|
||||
|
||||
# 设置优雅关闭机制(包括清理孤儿进程、写入 PID、注册信号处理器)
|
||||
process_manager = setup_graceful_shutdown(logger=logger)
|
||||
|
||||
# 配置
|
||||
host = os.getenv("HOST", "127.0.0.1")
|
||||
port = int(os.getenv("PORT", "8000"))
|
||||
|
||||
# 确保环境变量与实际绑定地址一致(供 app.py 读取)
|
||||
os.environ["HOST"] = host
|
||||
|
||||
# 打印启动信息
|
||||
logger.info("=" * 60)
|
||||
logger.info("CountBot 启动中...")
|
||||
if host == "127.0.0.1":
|
||||
logger.info("远程访问已开启 — 监听所有网络接口")
|
||||
logger.info(f"本地访问: http://localhost:{port}")
|
||||
logger.info(f"远程访问: http://<your-ip>:{port}")
|
||||
else:
|
||||
logger.info(f"访问地址: http://localhost:{port}")
|
||||
logger.info("如需远程访问,请设置 HOST=0.0.0.0")
|
||||
logger.info("=" * 60)
|
||||
|
||||
|
||||
try:
|
||||
# 启动服务器
|
||||
uvicorn.run(
|
||||
"backend.app:app",
|
||||
host=host,
|
||||
port=port,
|
||||
reload=False, # 生产模式不启用热重载
|
||||
log_level="info"
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Received keyboard interrupt")
|
||||
except Exception as e:
|
||||
logger.error(f"Server error: {e}")
|
||||
raise
|
||||
finally:
|
||||
# 确保清理 PID 文件
|
||||
process_manager.remove_pid_file()
|
||||
logger.info("Application shutdown complete")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env python3
|
||||
"""CountBot Desktop — pywebview 桌面启动入口"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import platform
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
# 项目根目录(兼容 PyInstaller 打包)
|
||||
if getattr(sys, "frozen", False):
|
||||
PROJECT_ROOT = Path(sys._MEIPASS)
|
||||
else:
|
||||
PROJECT_ROOT = Path(__file__).parent
|
||||
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from backend.utils.ssl_compat import ensure_ssl_certificates
|
||||
ensure_ssl_certificates()
|
||||
|
||||
_server = None
|
||||
RESOURCES_DIR = PROJECT_ROOT / "resources"
|
||||
|
||||
|
||||
# ── 图标 ──────────────────────────────────────────────
|
||||
|
||||
def get_icon_path() -> str | None:
|
||||
"""按平台返回图标路径: .ico(Win) / .icns(Mac) / .png(Linux)"""
|
||||
name_map = {"Windows": "countbot.ico", "Darwin": "countbot.icns"}
|
||||
icon = RESOURCES_DIR / name_map.get(platform.system(), "countbot.png")
|
||||
return str(icon) if icon.exists() else None
|
||||
|
||||
|
||||
def _set_macos_dock_icon(path: str) -> None:
|
||||
"""通过 PyObjC 设置 macOS Dock 图标"""
|
||||
try:
|
||||
from AppKit import NSApplication, NSImage
|
||||
img = NSImage.alloc().initWithContentsOfFile_(path)
|
||||
if img:
|
||||
NSApplication.sharedApplication().setApplicationIconImage_(img)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _set_windows_app_id() -> None:
|
||||
"""设置 Windows AppUserModelID,使任务栏显示自定义图标"""
|
||||
try:
|
||||
import ctypes
|
||||
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(
|
||||
"countbot.desktop.app"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# ── 后端服务 ──────────────────────────────────────────
|
||||
|
||||
def _start_backend(host: str, port: int) -> None:
|
||||
"""后台线程启动 FastAPI/Uvicorn"""
|
||||
global _server
|
||||
import uvicorn
|
||||
from loguru import logger
|
||||
|
||||
try:
|
||||
cfg = uvicorn.Config("backend.app:app", host=host, port=port,
|
||||
reload=False, log_level="info")
|
||||
_server = uvicorn.Server(cfg)
|
||||
_server.run()
|
||||
except Exception as e:
|
||||
logger.error(f"后端启动失败: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _shutdown() -> None:
|
||||
global _server
|
||||
if _server:
|
||||
_server.should_exit = True
|
||||
|
||||
|
||||
def _wait_for_server(host: str, port: int, timeout: float = 15.0) -> bool:
|
||||
"""轮询 /api/health 直到后端就绪"""
|
||||
import time, urllib.request
|
||||
url = f"http://{host}:{port}/api/health"
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
if urllib.request.urlopen(url, timeout=2).status == 200:
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.3)
|
||||
return False
|
||||
|
||||
|
||||
def _check_frontend() -> bool:
|
||||
index = PROJECT_ROOT / "frontend" / "dist" / "index.html"
|
||||
return index.exists()
|
||||
|
||||
|
||||
# ── 主入口 ────────────────────────────────────────────
|
||||
|
||||
def main():
|
||||
import webview
|
||||
from loguru import logger
|
||||
|
||||
host = os.getenv("HOST", "127.0.0.1")
|
||||
port = int(os.getenv("PORT", "8000"))
|
||||
os.environ["HOST"] = host
|
||||
|
||||
logger.info(f"CountBot Desktop 启动中… http://{host}:{port}")
|
||||
|
||||
if not _check_frontend():
|
||||
logger.error("frontend/dist/index.html 不存在,无法启动")
|
||||
sys.exit(1)
|
||||
|
||||
# 启动后端
|
||||
threading.Thread(target=_start_backend, args=(host, port), daemon=True).start()
|
||||
if not _wait_for_server(host, port):
|
||||
logger.error("后端启动超时")
|
||||
sys.exit(1)
|
||||
|
||||
# 设置平台图标
|
||||
icon_path = get_icon_path()
|
||||
if icon_path:
|
||||
logger.info(f"图标: {icon_path}")
|
||||
if platform.system() == "Darwin":
|
||||
_set_macos_dock_icon(icon_path)
|
||||
elif platform.system() == "Windows":
|
||||
_set_windows_app_id()
|
||||
|
||||
# 创建窗口
|
||||
window = webview.create_window(
|
||||
title="CountBot Desktop",
|
||||
url=f"http://{host}:{port}",
|
||||
width=960, height=680,
|
||||
min_size=(720, 480),
|
||||
resizable=True, text_select=True,
|
||||
)
|
||||
window.events.closing += lambda: _shutdown()
|
||||
|
||||
start_kwargs = {"debug": os.getenv("DEBUG", "").lower() in ("1", "true")}
|
||||
if icon_path:
|
||||
start_kwargs["icon"] = icon_path
|
||||
webview.start(**start_kwargs)
|
||||
|
||||
logger.info("CountBot Desktop 已退出")
|
||||
os._exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
CountBot 应用启动脚本
|
||||
开发模式启动,支持热重载
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到 Python 路径
|
||||
project_root = Path(__file__).parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
|
||||
def main():
|
||||
"""启动应用(开发模式)"""
|
||||
import uvicorn
|
||||
from loguru import logger
|
||||
|
||||
# 配置
|
||||
host = os.getenv("HOST", "127.0.0.1")
|
||||
port = int(os.getenv("PORT", "8000"))
|
||||
|
||||
logger.info("=" * 60)
|
||||
logger.info("CountBot 开发模式启动中...")
|
||||
logger.info(f"访问地址: http://localhost:{port}")
|
||||
logger.info("热重载已启用")
|
||||
logger.info("=" * 60)
|
||||
|
||||
# 不自动打开浏览器,用户可以手动访问
|
||||
|
||||
# 启动服务器(开发模式)
|
||||
uvicorn.run(
|
||||
"backend.app:app",
|
||||
host=host,
|
||||
port=port,
|
||||
reload=True, # 开发模式启用热重载
|
||||
reload_dirs=["backend"], # 监控 backend 目录
|
||||
log_level="debug"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user