feat: 添加 Docker 和 GitHub Actions 支持

- 添加 docker/Dockerfile 多阶段构建(前端+后端)
- 添加 docker-compose.yml 支持阿里云镜像部署
- 添加 .github/workflows/release.yml 自动发布到阿里云
- 添加 .dockerignore 优化构建
- 添加 VERSION 版本管理文件
- 添加 start.mjs 本地开发启动脚本
This commit is contained in:
root
2026-02-02 20:23:33 +08:00
parent eef4ec4a65
commit 22a2a97a76
6 changed files with 876 additions and 0 deletions

50
.dockerignore Normal file
View File

@@ -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

128
.github/workflows/release.yml vendored Normal file
View File

@@ -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

1
VERSION Normal file
View File

@@ -0,0 +1 @@
0.1.0

14
docker-compose.yml Normal file
View File

@@ -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}

70
docker/Dockerfile Normal file
View File

@@ -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"]

613
start.mjs Normal file
View File

@@ -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);
});