diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e82f928 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,50 @@ +# Git +.git +.gitignore + +# Python +__pycache__ +*.py[cod] +*$py.class +*.so +.Python +.venv +venv +ENV + +# IDE +.idea +.vscode +*.swp +*.swo + +# Node +webui/node_modules +webui/.vite + +# Build artifacts (前端构建产物在 Docker 中重新生成) +static/admin + +# 配置和敏感文件 +.env +.env.* +config.json + +# 日志和临时文件 +*.log +logs/ +tmp/ +temp/ + +# 测试 +tests/ +*.test.py + +# 文档和截图 +*.md +截图/ +docs/ + +# Claude Code +.claude/ +CLAUDE*.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8186c1c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,128 @@ +name: Release to Aliyun CR + +on: + workflow_dispatch: + inputs: + version_type: + description: '版本类型' + required: true + default: 'patch' + type: choice + options: + - patch + - minor + - major + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v5 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Get current version + id: get_version + run: | + LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") + TAG_VERSION=${LATEST_TAG#v} + + if [ -f VERSION ]; then + FILE_VERSION=$(cat VERSION | tr -d '[:space:]') + else + FILE_VERSION="0.0.0" + fi + + function version_gt() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"; } + + if version_gt "$FILE_VERSION" "$TAG_VERSION"; then + VERSION="$FILE_VERSION" + else + VERSION="$TAG_VERSION" + fi + + echo "Current version: $VERSION" + echo "current_version=$VERSION" >> $GITHUB_OUTPUT + + - name: Calculate next version + id: next_version + env: + VERSION_TYPE: ${{ github.event.inputs.version_type }} + run: | + VERSION="${{ steps.get_version.outputs.current_version }}" + BASE_VERSION=$(echo "$VERSION" | sed 's/-.*$//') + + IFS='.' read -r -a version_parts <<< "$BASE_VERSION" + MAJOR="${version_parts[0]:-0}" + MINOR="${version_parts[1]:-0}" + PATCH="${version_parts[2]:-0}" + + case "$VERSION_TYPE" in + major) + NEW_VERSION="$((MAJOR + 1)).0.0" + ;; + minor) + NEW_VERSION="${MAJOR}.$((MINOR + 1)).0" + ;; + *) + NEW_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))" + ;; + esac + + echo "New version: $NEW_VERSION" + echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT + echo "new_tag=v$NEW_VERSION" >> $GITHUB_OUTPUT + + - name: Update VERSION file + run: | + echo "${{ steps.next_version.outputs.new_version }}" > VERSION + + - name: Commit VERSION and create tag + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git add VERSION + if ! git diff --cached --quiet; then + git commit -m "chore: bump version to ${{ steps.next_version.outputs.new_tag }} [skip ci]" + fi + + NEW_TAG="${{ steps.next_version.outputs.new_tag }}" + git tag -a "$NEW_TAG" -m "Release $NEW_TAG" + git push origin HEAD:main "$NEW_TAG" + + # Docker 构建并推送到阿里云 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Aliyun Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ secrets.ALIYUN_REGISTRY }} + username: ${{ secrets.ALIYUN_REGISTRY_USER }} + password: ${{ secrets.ALIYUN_REGISTRY_PASSWORD }} + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: | + ${{ secrets.ALIYUN_REGISTRY }}/${{ secrets.ALIYUN_REGISTRY_NAMESPACE }}/ds2api:${{ steps.next_version.outputs.new_tag }} + ${{ secrets.ALIYUN_REGISTRY }}/${{ secrets.ALIYUN_REGISTRY_NAMESPACE }}/ds2api:${{ steps.next_version.outputs.new_version }} + ${{ secrets.ALIYUN_REGISTRY }}/${{ secrets.ALIYUN_REGISTRY_NAMESPACE }}/ds2api:latest + labels: | + org.opencontainers.image.version=${{ steps.next_version.outputs.new_version }} + org.opencontainers.image.revision=${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1.0 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..54cec15 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +services: + ds2api: + image: crpi-cnazxqmg4avmg4fq.cn-beijing.personal.cr.aliyuncs.com/ronghuaxueleng/ds2api:latest + container_name: ds2api + restart: always + ports: + - "6011:5001" + volumes: + - ./config.json:/app/config.json # 配置文件 + - ./.env:/app/.env # 环境变量 + environment: + - TZ=Asia/Shanghai + - LOG_LEVEL=INFO + - DS2API_ADMIN_KEY=${DS2API_ADMIN_KEY:-ds2api} diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..66d71e0 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,70 @@ +# ========== 阶段1: 构建前端 ========== +FROM node:20-slim AS frontend-builder + +WORKDIR /app/webui + +# 复制前端依赖文件 +COPY webui/package.json webui/package-lock.json ./ + +# 安装依赖 +RUN npm ci + +# 复制前端源码 +COPY webui/ ./ + +# 构建前端 +RUN npm run build + +# ========== 阶段2: 构建后端 ========== +FROM python:3.11-slim + +WORKDIR /app + +# 设置环境变量 +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +ENV TZ=Asia/Shanghai + +# 安装系统依赖 +# curl_cffi 需要 libcurl 和编译工具 +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + g++ \ + libffi-dev \ + libcurl4-openssl-dev \ + libssl-dev \ + curl \ + && ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone \ + && rm -rf /var/lib/apt/lists/* + +# 复制并安装 Python 依赖 +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple + +# 复制后端代码 +COPY app.py . +COPY core/ ./core/ +COPY routes/ ./routes/ + +# 创建 templates 目录(预留扩展用) +RUN mkdir -p ./templates + +# 复制 WASM 文件和 Tokenizer 相关文件 +COPY sha3_wasm_bg.7b9ca65ddd.wasm ./ +COPY tokenizer.json tokenizer_config.json ./ + +# 从前端构建阶段复制构建产物到 static/admin +COPY --from=frontend-builder /app/webui/dist ./static/admin + +# 创建配置文件目录(运行时挂载) +RUN mkdir -p /app/data + +# 暴露端口 +EXPOSE 5001 + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:5001/ || exit 1 + +# 启动命令 +CMD ["python", "-m", "uvicorn", "app:app", "--host", "0.0.0.0", "--port", "5001"] diff --git a/start.mjs b/start.mjs new file mode 100644 index 0000000..d4711af --- /dev/null +++ b/start.mjs @@ -0,0 +1,613 @@ +#!/usr/bin/env node +/** + * DS2API 启动脚本 - 交互式菜单 + * + * 使用方法: + * node start.mjs # 显示交互式菜单 + * node start.mjs dev # 开发模式(后端+前端) + * node start.mjs prod # 生产模式 + */ + +import { spawn, execSync } from 'child_process'; +import { createInterface } from 'readline'; +import { existsSync } from 'fs'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// 判断是否为 Windows +const isWindows = process.platform === 'win32'; + +// 配置 +const CONFIG = { + backendPort: process.env.PORT || 5001, + frontendPort: 5173, + host: process.env.HOST || '0.0.0.0', + logLevel: process.env.LOG_LEVEL || 'info', + adminKey: process.env.DS2API_ADMIN_KEY || 'ds2api', + webuiDir: join(__dirname, 'webui'), + venvDir: join(__dirname, '.venv'), +}; + +// venv 中的可执行文件路径 +const VENV = { + python: isWindows + ? join(CONFIG.venvDir, 'Scripts', 'python.exe') + : join(CONFIG.venvDir, 'bin', 'python'), + pip: isWindows + ? join(CONFIG.venvDir, 'Scripts', 'pip.exe') + : join(CONFIG.venvDir, 'bin', 'pip'), +}; + +// 存储子进程 +const processes = []; + +// 颜色输出 +const colors = { + reset: '\x1b[0m', + bright: '\x1b[1m', + dim: '\x1b[2m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + magenta: '\x1b[35m', + cyan: '\x1b[36m', +}; + +const log = { + info: (msg) => console.log(`${colors.cyan}[INFO]${colors.reset} ${msg}`), + success: (msg) => console.log(`${colors.green}[OK]${colors.reset} ${msg}`), + warn: (msg) => console.log(`${colors.yellow}[WARN]${colors.reset} ${msg}`), + error: (msg) => console.log(`${colors.red}[ERROR]${colors.reset} ${msg}`), + title: (msg) => console.log(`\n${colors.bright}${colors.magenta}${msg}${colors.reset}`), +}; + +// 清理并退出 +function cleanup() { + console.log('\n'); + log.info('正在关闭所有服务...'); + processes.forEach(proc => { + if (proc && !proc.killed) { + proc.kill('SIGTERM'); + } + }); + log.success('已退出'); + process.exit(0); +} + +// 注册退出处理 +process.on('SIGINT', cleanup); +process.on('SIGTERM', cleanup); + +// 检查命令是否存在 +function commandExists(cmd) { + try { + execSync(`${isWindows ? 'where' : 'which'} ${cmd}`, { stdio: 'ignore' }); + return true; + } catch { + return false; + } +} + +// 获取系统 Python 命令 +function getSystemPython() { + const candidates = isWindows + ? ['python', 'python3', 'py'] + : ['python3', 'python']; + + for (const cmd of candidates) { + if (commandExists(cmd)) { + return cmd; + } + } + return null; +} + +// 系统 Python 命令 +const SYSTEM_PYTHON = getSystemPython(); + +// 检查 venv 是否存在 +function venvExists() { + return existsSync(VENV.python); +} + +// 检查 Python 依赖是否已安装 +function checkPythonDeps() { + if (!venvExists()) return false; + try { + execSync(`"${VENV.python}" -c "import fastapi, uvicorn"`, { + stdio: 'ignore', + shell: true, + }); + return true; + } catch { + return false; + } +} + +// 检查前端依赖是否已安装 +function checkFrontendDeps() { + if (!existsSync(CONFIG.webuiDir)) return null; + return existsSync(join(CONFIG.webuiDir, 'node_modules')); +} + +// 获取依赖状态 +function getDepsStatus() { + return { + venv: venvExists(), + python: checkPythonDeps(), + frontend: checkFrontendDeps(), + }; +} + +// 查找占用端口的进程 PID +function findPidByPort(port) { + try { + if (isWindows) { + const output = execSync(`netstat -ano | findstr :${port} | findstr LISTENING`, { + encoding: 'utf-8', + shell: true, + stdio: ['pipe', 'pipe', 'ignore'], + }); + const pids = new Set(); + for (const line of output.trim().split('\n')) { + const parts = line.trim().split(/\s+/); + const pid = parts[parts.length - 1]; + if (pid && pid !== '0') pids.add(pid); + } + return [...pids]; + } else { + const output = execSync(`lsof -ti :${port}`, { + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'ignore'], + }); + return output.trim().split('\n').filter(Boolean); + } + } catch { + return []; + } +} + +// 获取运行中的服务状态 +function getRunningStatus() { + const backendPids = findPidByPort(CONFIG.backendPort); + const frontendPids = findPidByPort(CONFIG.frontendPort); + return { + backend: backendPids, + frontend: frontendPids, + isRunning: backendPids.length > 0 || frontendPids.length > 0, + }; +} + +// 停止服务 +async function stopServices() { + const running = getRunningStatus(); + + if (!running.isRunning) { + log.warn('没有检测到正在运行的服务'); + return; + } + + log.title('========== 停止服务 =========='); + + const killProcess = async (pid) => { + try { + if (isWindows) { + try { + execSync(`taskkill /PID ${pid}`, { stdio: 'ignore', shell: true }); + } catch { + execSync(`taskkill /F /T /PID ${pid}`, { stdio: 'ignore', shell: true }); + } + } else { + execSync(`kill -15 ${pid}`, { stdio: 'ignore' }); + await new Promise(r => setTimeout(r, 500)); + try { + execSync(`kill -0 ${pid}`, { stdio: 'ignore' }); + execSync(`kill -9 ${pid}`, { stdio: 'ignore' }); + } catch { /* 进程已退出 */ } + } + } catch { /* 进程可能已退出 */ } + }; + + if (running.backend.length > 0) { + log.info(`停止后端服务 (端口 ${CONFIG.backendPort}, PID: ${running.backend.join(', ')})...`); + for (const pid of running.backend) { + await killProcess(pid); + } + log.success('后端服务已停止'); + } + + if (running.frontend.length > 0) { + log.info(`停止前端服务 (端口 ${CONFIG.frontendPort}, PID: ${running.frontend.join(', ')})...`); + for (const pid of running.frontend) { + await killProcess(pid); + } + log.success('前端服务已停止'); + } +} + +// 创建 venv +async function createVenv() { + if (venvExists()) { + log.info('虚拟环境已存在'); + return true; + } + + if (!SYSTEM_PYTHON) { + throw new Error('未找到 Python,请先安装 Python'); + } + + log.info('创建 Python 虚拟环境...'); + return new Promise((resolve, reject) => { + const proc = spawn(SYSTEM_PYTHON, ['-m', 'venv', CONFIG.venvDir], { + cwd: __dirname, + stdio: 'inherit', + shell: true, + }); + proc.on('close', code => { + if (code === 0) { + log.success('虚拟环境创建成功'); + resolve(true); + } else { + reject(new Error('虚拟环境创建失败')); + } + }); + }); +} + +// 确保 venv 存在 +async function ensureVenv() { + if (!venvExists()) { + await createVenv(); + } +} + +// 确保 Python 依赖已安装 +async function ensurePythonDeps() { + await ensureVenv(); + if (!checkPythonDeps()) { + log.warn('检测到 Python 依赖未安装,正在安装...'); + await installPythonDeps(); + } +} + +// 确保前端依赖已安装 +async function ensureFrontendDeps() { + if (checkFrontendDeps() === false) { + log.warn('检测到前端依赖未安装,正在安装...'); + await installFrontendDeps(); + } +} + +// 安装 Python 依赖 +async function installPythonDeps() { + await ensureVenv(); + log.info('安装 Python 依赖...'); + return new Promise((resolve, reject) => { + const proc = spawn(VENV.pip, ['install', '-r', 'requirements.txt'], { + cwd: __dirname, + stdio: 'inherit', + shell: true, + }); + proc.on('close', code => code === 0 ? resolve() : reject(new Error('Python 依赖安装失败'))); + }); +} + +// 安装前端依赖 +async function installFrontendDeps() { + if (!existsSync(CONFIG.webuiDir)) { + log.warn('webui 目录不存在,跳过前端依赖安装'); + return; + } + log.info('安装前端依赖...'); + return new Promise((resolve, reject) => { + const proc = spawn('npm', ['install'], { + cwd: CONFIG.webuiDir, + stdio: 'inherit', + shell: true, + }); + proc.on('close', code => code === 0 ? resolve() : reject(new Error('前端依赖安装失败'))); + }); +} + +// 安装所有依赖 +async function installAll() { + log.title('========== 安装依赖 =========='); + try { + await installPythonDeps(); + log.success('Python 依赖安装完成'); + await installFrontendDeps(); + log.success('前端依赖安装完成'); + log.success('所有依赖安装完成!'); + } catch (e) { + log.error(e.message); + } +} + +// 启动后端 +async function startBackend(devMode = true) { + await ensurePythonDeps(); + + log.info(`启动后端服务... http://localhost:${CONFIG.backendPort}`); + + const args = [ + '-m', 'uvicorn', + 'app:app', + '--host', CONFIG.host, + '--port', String(CONFIG.backendPort), + '--log-level', CONFIG.logLevel, + ]; + + if (devMode) { + args.push('--reload', '--reload-dir', __dirname); + } + + const proc = spawn(VENV.python, args, { + cwd: __dirname, + stdio: 'inherit', + shell: true, + env: { + ...process.env, + DS2API_ADMIN_KEY: CONFIG.adminKey, + }, + }); + + processes.push(proc); + return proc; +} + +// 启动前端 +async function startFrontend() { + if (!existsSync(CONFIG.webuiDir)) { + log.warn('webui 目录不存在,跳过前端启动'); + return null; + } + + await ensureFrontendDeps(); + + log.info(`启动前端服务... http://localhost:${CONFIG.frontendPort}`); + + const proc = spawn('npm', ['run', 'dev'], { + cwd: CONFIG.webuiDir, + stdio: 'inherit', + shell: true, + }); + + processes.push(proc); + return proc; +} + +// 构建前端 +async function buildFrontend() { + if (!existsSync(CONFIG.webuiDir)) { + log.warn('webui 目录不存在'); + return; + } + + log.info('构建前端...'); + return new Promise((resolve, reject) => { + const proc = spawn('npm', ['run', 'build'], { + cwd: CONFIG.webuiDir, + stdio: 'inherit', + shell: true, + }); + proc.on('close', code => code === 0 ? resolve() : reject(new Error('前端构建失败'))); + }); +} + +// 显示状态信息 +function showStatus() { + console.log('\n' + '─'.repeat(50)); + log.success(`后端 API: http://localhost:${CONFIG.backendPort}`); + if (existsSync(CONFIG.webuiDir)) { + log.success(`管理界面: http://localhost:${CONFIG.frontendPort}`); + } + console.log('─'.repeat(50)); + log.info('按 Ctrl+C 停止所有服务\n'); +} + +// 等待进程 +function waitForProcesses() { + return new Promise(resolve => { + const checkInterval = setInterval(() => { + const alive = processes.filter(p => !p.killed); + if (alive.length === 0) { + clearInterval(checkInterval); + resolve(); + } + }, 1000); + }); +} + +// 交互式菜单 +async function showMenu() { + const rl = createInterface({ + input: process.stdin, + output: process.stdout, + }); + + const question = (prompt) => new Promise(resolve => rl.question(prompt, resolve)); + + console.clear(); + log.title('╔══════════════════════════════════════════╗'); + log.title('║ DS2API 启动脚本 ║'); + log.title('╚══════════════════════════════════════════╝'); + + // 获取依赖状态 + const deps = getDepsStatus(); + const running = getRunningStatus(); + + const statusText = (ok) => ok ? `${colors.green}已安装${colors.reset}` : `${colors.yellow}未安装${colors.reset}`; + + console.log(`\n${colors.bright}环境状态:${colors.reset}`); + console.log(` Python: ${SYSTEM_PYTHON || `${colors.red}未找到${colors.reset}`}`); + console.log(` 虚拟环境: ${deps.venv ? `${colors.green}已创建${colors.reset}` : `${colors.yellow}未创建${colors.reset}`} (${CONFIG.venvDir})`); + console.log(` 后端依赖: ${statusText(deps.python)}`); + if (deps.frontend !== null) { + console.log(` 前端依赖: ${statusText(deps.frontend)}`); + } + + console.log(`\n${colors.bright}服务状态:${colors.reset}`); + console.log(` 后端 (${CONFIG.backendPort}): ${running.backend.length > 0 ? `${colors.green}运行中${colors.reset} (PID: ${running.backend.join(', ')})` : `${colors.dim}未运行${colors.reset}`}`); + console.log(` 前端 (${CONFIG.frontendPort}): ${running.frontend.length > 0 ? `${colors.green}运行中${colors.reset} (PID: ${running.frontend.join(', ')})` : `${colors.dim}未运行${colors.reset}`}`); + + console.log(`\n${colors.bright}管理员密钥:${colors.reset} ${colors.cyan}${CONFIG.adminKey}${colors.reset}`); + console.log(`${colors.dim} 自定义: DS2API_ADMIN_KEY=你的密钥 node start.mjs${colors.reset}`); + + console.log(` +${colors.bright}请选择操作:${colors.reset} + + ${colors.cyan}1.${colors.reset} 开发模式 (后端 + 前端热重载) + ${colors.cyan}2.${colors.reset} 仅启动后端 (开发模式) + ${colors.cyan}3.${colors.reset} 仅启动前端 + ${colors.cyan}4.${colors.reset} 生产模式 (仅后端,无热重载) + ${colors.cyan}5.${colors.reset} 构建前端 + ${colors.cyan}6.${colors.reset} 安装依赖 (创建venv + 安装包) + ${colors.red}7.${colors.reset} 停止所有服务 + ${colors.cyan}0.${colors.reset} 退出 +`); + + const choice = await question(`${colors.yellow}请输入选项 [1]: ${colors.reset}`); + rl.close(); + + switch (choice.trim() || '1') { + case '1': + log.title('========== 开发模式 =========='); + await startBackend(true); + await new Promise(r => setTimeout(r, 1500)); + await startFrontend(); + showStatus(); + await waitForProcesses(); + break; + + case '2': + log.title('========== 仅后端 (开发模式) =========='); + await startBackend(true); + showStatus(); + await waitForProcesses(); + break; + + case '3': + log.title('========== 仅前端 =========='); + await startFrontend(); + showStatus(); + await waitForProcesses(); + break; + + case '4': + log.title('========== 生产模式 =========='); + await startBackend(false); + showStatus(); + await waitForProcesses(); + break; + + case '5': + await buildFrontend(); + log.success('前端构建完成!'); + break; + + case '6': + await installAll(); + break; + + case '7': + await stopServices(); + break; + + case '0': + log.info('再见!'); + process.exit(0); + break; + + default: + log.warn('无效选项'); + await showMenu(); + } +} + +// 命令行参数处理 +async function main() { + const args = process.argv.slice(2); + const cmd = args[0]; + + // 检查必要工具 + if (!SYSTEM_PYTHON) { + log.error('未找到 Python,请先安装 Python (尝试了 python, python3, py)'); + process.exit(1); + } + + switch (cmd) { + case 'dev': + log.title('========== 开发模式 =========='); + await startBackend(true); + await new Promise(r => setTimeout(r, 1500)); + await startFrontend(); + showStatus(); + await waitForProcesses(); + break; + + case 'prod': + log.title('========== 生产模式 =========='); + await startBackend(false); + showStatus(); + await waitForProcesses(); + break; + + case 'build': + await buildFrontend(); + log.success('前端构建完成!'); + break; + + case 'install': + await installAll(); + break; + + case 'stop': + await stopServices(); + break; + + case 'status': + const status = getRunningStatus(); + console.log(`\n${colors.bright}服务状态:${colors.reset}`); + console.log(` 后端 (${CONFIG.backendPort}): ${status.backend.length > 0 ? `${colors.green}运行中${colors.reset} (PID: ${status.backend.join(', ')})` : `${colors.dim}未运行${colors.reset}`}`); + console.log(` 前端 (${CONFIG.frontendPort}): ${status.frontend.length > 0 ? `${colors.green}运行中${colors.reset} (PID: ${status.frontend.join(', ')})` : `${colors.dim}未运行${colors.reset}`}\n`); + break; + + case 'help': + case '-h': + case '--help': + console.log(` +${colors.bright}DS2API 启动脚本${colors.reset} + +${colors.cyan}使用方法:${colors.reset} + node start.mjs 显示交互式菜单 + node start.mjs dev 开发模式 (后端 + 前端) + node start.mjs prod 生产模式 (无热重载) + node start.mjs build 构建前端 + node start.mjs install 安装所有依赖 (自动创建venv) + node start.mjs stop 停止所有服务 + node start.mjs status 查看服务状态 + +${colors.cyan}环境变量:${colors.reset} + PORT 后端端口 (默认: 5001) + HOST 监听地址 (默认: 0.0.0.0) + LOG_LEVEL 日志级别 (默认: info) + +${colors.cyan}虚拟环境:${colors.reset} + 默认路径: .venv/ + 首次运行 install 时自动创建 +`); + break; + + default: + await showMenu(); + } +} + +main().catch(e => { + log.error(e.message); + process.exit(1); +});