release: Elysia ToDo v1.0.0

鍏ㄦ爤涓汉淇℃伅绠$悊搴旂敤锛岄泦鎴愬緟鍔炰换鍔°€佷範鎯墦鍗°€佺邯蹇垫棩鎻愰啋銆佽祫浜ф€昏鍔熻兘銆

Made-with: Cursor
This commit is contained in:
祀梦
2026-03-14 22:21:26 +08:00
commit 2979197b1c
104 changed files with 21737 additions and 0 deletions

229
main.py Normal file
View File

@@ -0,0 +1,229 @@
"""
爱莉希雅待办事项 - 项目启动入口
功能:
1. 编译前端项目
2. 启动 FastAPI 后端服务
"""
import os
import sys
import shutil
import subprocess
import time
# 路径配置
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
WEBUI_DIR = os.path.join(PROJECT_ROOT, "WebUI")
DIST_DIR = os.path.join(WEBUI_DIR, "dist")
WEBUI_TARGET = os.path.join(PROJECT_ROOT, "api", "webui")
API_MAIN = os.path.join(PROJECT_ROOT, "api", "app", "main.py")
def log_info(msg):
"""打印信息"""
print(f"[启动] {msg}")
def log_error(msg):
"""打印错误"""
print(f"[错误] {msg}", file=sys.stderr)
def needs_rebuild() -> bool:
"""判断是否需要重新编译前端(基于文件修改时间)"""
if not os.path.exists(DIST_DIR):
return True
src_dir = os.path.join(WEBUI_DIR, "src")
if not os.path.exists(src_dir):
return True
# 获取 dist 目录最新修改时间
dist_mtime = max(
os.path.getmtime(os.path.join(dp, f))
for dp, dn, filenames in os.walk(DIST_DIR)
for f in filenames
)
# 检查 src 目录中是否有比 dist 更新的文件
for dp, dn, filenames in os.walk(src_dir):
for f in filenames:
if f.endswith(('.vue', '.ts', '.js', '.scss', '.css')):
filepath = os.path.join(dp, f)
if os.path.getmtime(filepath) > dist_mtime:
return True
return False
def build_frontend():
"""编译前端项目"""
# 智能判断是否需要重新编译
if not needs_rebuild():
log_info("前端产物已是最新,跳过编译")
return True
log_info("开始编译前端项目...")
# 检查 WebUI 目录是否存在
if not os.path.exists(WEBUI_DIR):
log_error(f"WebUI 目录不存在: {WEBUI_DIR}")
return False
# 检查 node_modules 是否存在
node_modules = os.path.join(WEBUI_DIR, "node_modules")
if not os.path.exists(node_modules):
log_info("安装前端依赖...")
# Windows 上使用 npm.cmd
npm_cmd = "npm.cmd" if os.name == "nt" else "npm"
result = subprocess.run(
[npm_cmd, "install"],
cwd=WEBUI_DIR,
encoding="utf-8",
errors="ignore"
)
if result.returncode != 0:
log_error(f"安装依赖失败")
return False
# 执行编译
log_info("执行 npm run build...")
npm_cmd = "npm.cmd" if os.name == "nt" else "npm"
result = subprocess.run(
[npm_cmd, "run", "build"],
cwd=WEBUI_DIR,
encoding="utf-8",
errors="ignore"
)
if result.returncode != 0:
log_error(f"编译失败")
return False
log_info("前端编译完成!")
return True
def copy_dist_to_webui():
"""复制编译产物到 webui 目录"""
log_info("复制编译产物到 webui 目录...")
# 检查 dist 目录是否存在
if not os.path.exists(DIST_DIR):
log_error(f"编译产物目录不存在: {DIST_DIR}")
return False
# 删除旧的 webui 目录(如果存在)
if os.path.exists(WEBUI_TARGET):
shutil.rmtree(WEBUI_TARGET)
# 复制 dist 到 webui
shutil.copytree(DIST_DIR, WEBUI_TARGET)
log_info(f"编译产物已复制到: {WEBUI_TARGET}")
return True
def find_pid_on_port(port: int) -> int | None:
"""查找占用指定端口的进程 PID"""
if os.name == "nt":
result = subprocess.run(
["netstat", "-ano", "-p", "TCP"],
capture_output=True, text=True, encoding="gbk", errors="ignore"
)
for line in result.stdout.splitlines():
if f":{port}" in line and "LISTENING" in line:
parts = line.split()
return int(parts[-1])
else:
result = subprocess.run(
["lsof", "-t", "-i", f":{port}", "-sTCP:LISTEN"],
capture_output=True, text=True, errors="ignore"
)
output = result.stdout.strip()
if output:
return int(output.splitlines()[0])
return None
def kill_process(pid: int) -> bool:
"""终止指定 PID 的进程"""
log_info(f"正在终止占用端口的进程 (PID: {pid})...")
try:
if os.name == "nt":
subprocess.run(["taskkill", "/PID", str(pid), "/F"],
capture_output=True, timeout=10)
else:
os.kill(pid, 9)
time.sleep(1)
log_info(f"进程 {pid} 已终止")
return True
except Exception as e:
log_error(f"终止进程 {pid} 失败: {e}")
return False
def check_and_free_port(port: int) -> bool:
"""检测端口是否被占用,如果被占用则尝试释放"""
pid = find_pid_on_port(port)
if pid is None:
return True
log_info(f"端口 {port} 已被进程 {pid} 占用")
if kill_process(pid):
# 验证端口是否已释放
if find_pid_on_port(port) is None:
log_info(f"端口 {port} 已释放")
return True
log_error(f"端口 {port} 仍被占用,尝试再次终止...")
time.sleep(2)
pid2 = find_pid_on_port(port)
if pid2 is not None:
kill_process(pid2)
if find_pid_on_port(port) is None:
return True
log_error(f"无法释放端口 {port},请手动处理")
return False
def start_backend():
"""启动后端服务"""
log_info("启动后端服务...")
# 添加 api 目录到 Python 路径(不使用 os.chdir避免全局副作用
api_dir = os.path.join(PROJECT_ROOT, "api")
if api_dir not in sys.path:
sys.path.insert(0, api_dir)
from app.config import HOST, PORT
# 检查端口是否被占用,自动释放
if not check_and_free_port(PORT):
log_error(f"端口 {PORT} 无法释放,启动失败")
sys.exit(1)
import uvicorn
log_info(f"服务启动成功: http://{HOST}:{PORT}")
log_info(f"API 文档: http://{HOST}:{PORT}/docs")
log_info(f"前端页面: http://{HOST}:{PORT}/")
uvicorn.run("app.main:app", host=HOST, port=PORT, reload=False)
def main():
"""主函数"""
print("=" * 50)
print(" 爱莉希雅待办事项 - 项目启动")
print("=" * 50)
# 1. 编译前端
if not build_frontend():
sys.exit(1)
# 2. 复制编译产物
if not copy_dist_to_webui():
sys.exit(1)
# 3. 启动后端
start_backend()
if __name__ == "__main__":
main()