From 22a2a97a76e8794610bcafca393d617036438217 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 2 Feb 2026 20:23:33 +0800 Subject: [PATCH 01/10] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20Docker=20?= =?UTF-8?q?=E5=92=8C=20GitHub=20Actions=20=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 docker/Dockerfile 多阶段构建(前端+后端) - 添加 docker-compose.yml 支持阿里云镜像部署 - 添加 .github/workflows/release.yml 自动发布到阿里云 - 添加 .dockerignore 优化构建 - 添加 VERSION 版本管理文件 - 添加 start.mjs 本地开发启动脚本 --- .dockerignore | 50 +++ .github/workflows/release.yml | 128 +++++++ VERSION | 1 + docker-compose.yml | 14 + docker/Dockerfile | 70 ++++ start.mjs | 613 ++++++++++++++++++++++++++++++++++ 6 files changed, 876 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/release.yml create mode 100644 VERSION create mode 100644 docker-compose.yml create mode 100644 docker/Dockerfile create mode 100644 start.mjs 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); +}); From bd4c2bacbcc34bea2cc7d6a34b98d1eac0705492 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 2 Feb 2026 20:31:42 +0800 Subject: [PATCH 02/10] =?UTF-8?q?merge:=20=E5=90=88=E5=B9=B6=20main=20?= =?UTF-8?q?=E5=88=86=E6=94=AF=E5=88=B0=20docker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 100 ++--- .github/PULL_REQUEST_TEMPLATE.md | 44 +-- .github/workflows/release.yml | 256 ++++++------ .gitignore | 163 ++++---- DEPLOY.md | 659 +++++++++++++++---------------- VERSION | 2 +- docker-compose.yml | 28 +- docker/Dockerfile | 140 +++---- routes/home.py | 604 ++++++++++++++-------------- static/admin/index.html | 84 ++-- 10 files changed, 1026 insertions(+), 1054 deletions(-) diff --git a/.dockerignore b/.dockerignore index e82f928..3fc3934 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,50 +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 +# 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/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index cdc141f..22c8204 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,24 +1,20 @@ -#### 💻 变更类型 | Change Type - - - -- [ ] ✨ feat -- [ ] 🐛 fix -- [ ] ♻️ refactor -- [ ] 💄 style -- [ ] 👷 build -- [ ] ⚡️ perf -- [ ] 📝 docs -- [ ] 🔨 chore - -#### 🔀 变更说明 | Description of Change - - - -#### 📝 补充信息 | Additional Information - - - ---- - -> 💡 **提示**:如果修改了 `webui/` 目录下的文件,PR 合并后 CI 会自动构建并提交产物,无需手动构建。 \ No newline at end of file +#### 💻 变更类型 | Change Type + + + +- [ ] ✨ feat +- [ ] 🐛 fix +- [ ] ♻️ refactor +- [ ] 💄 style +- [ ] 👷 build +- [ ] ⚡️ perf +- [ ] 📝 docs +- [ ] 🔨 chore + +#### 🔀 变更说明 | Description of Change + + + +#### 📝 补充信息 | Additional Information + + \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8186c1c..8b7cbc1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,128 +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 +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/.gitignore b/.gitignore index 1c77a62..7c56b63 100644 --- a/.gitignore +++ b/.gitignore @@ -1,82 +1,81 @@ -*.bak -config.json -.env - -# Python -__pycache__/ -*.py[cod] -*$py.class -*.so -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# Virtual environments -venv/ -ENV/ -env/ -.venv - -# IDE -.vscode/ -.idea/ -*.swp -*.swo -*~ -.DS_Store - -# Logs -*.log -logs/ -uvicorn.log - -# Vercel -.vercel - -# Node.js / Frontend -node_modules/ -webui/node_modules/ -webui/dist/ -.npm -.pnpm-store/ -# 保留 webui/package-lock.json 用于 CI 缓存 -# package-lock.json # 如果有根目录的可以忽略 -yarn.lock -pnpm-lock.yaml - -# Build artifacts -*.tsbuildinfo -.cache/ -.parcel-cache/ - -# Environment -.env.local -.env.*.local - -# Testing -.coverage -htmlcov/ -.pytest_cache/ -.tox/ - -# Misc -*.pyc -*.pyo -.git/ -Thumbs.db +*.bak +config.json +.env + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Virtual environments +venv/ +ENV/ +env/ +.venv + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Logs +*.log +logs/ +uvicorn.log + +# Vercel +.vercel + +# Node.js / Frontend +node_modules/ +webui/node_modules/ +webui/dist/ +.npm +.pnpm-store/ +package-lock.json +yarn.lock +pnpm-lock.yaml + +# Build artifacts +*.tsbuildinfo +.cache/ +.parcel-cache/ + +# Environment +.env.local +.env.*.local + +# Testing +.coverage +htmlcov/ +.pytest_cache/ +.tox/ + +# Misc +*.pyc +*.pyo +.git/ +Thumbs.db diff --git a/DEPLOY.md b/DEPLOY.md index ff13005..09a6462 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -1,340 +1,319 @@ -# DS2API 部署指南 - -本文档详细介绍 DS2API 的各种部署方式。 - ---- - -## 目录 - -- [Vercel 部署(推荐)](#vercel-部署推荐) -- [本地开发](#本地开发) -- [生产环境部署](#生产环境部署) -- [常见问题](#常见问题) - ---- - -## Vercel 部署(推荐) - -### 一键部署 - -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FCJackHwang%2Fds2api&env=DS2API_ADMIN_KEY&envDescription=管理面板访问密码(必填)&envLink=https%3A%2F%2Fgithub.com%2FCJackHwang%2Fds2api%23环境变量&project-name=ds2api&repository-name=ds2api) - -### 部署步骤 - -1. **点击部署按钮** - - 登录你的 GitHub 账号 - - 授权 Vercel 访问 - -2. **设置环境变量** - - `DS2API_ADMIN_KEY`: 管理面板密码(**必填**) - -3. **等待部署完成** - - Vercel 会自动构建并部署项目 - - 部署完成后获得访问 URL - -4. **配置账号** - - 访问 `https://your-project.vercel.app/admin` - - 输入管理密码登录 - - 添加 DeepSeek 账号 - - 设置自定义 API Key - -5. **同步配置** - - 点击「同步到 Vercel」按钮 - - 首次需要输入 Vercel Token 和 Project ID - - 同步成功后配置会持久化 - -### 获取 Vercel 凭证 - -**Vercel Token**: -1. 访问 https://vercel.com/account/tokens -2. 点击 "Create Token" -3. 设置名称和有效期 -4. 复制生成的 Token - -**Project ID**: -1. 进入 Vercel 项目页面 -2. 点击 Settings -> General -3. 复制 "Project ID" - ---- - -## 本地开发 - -### 环境要求 - -- Python 3.9+ -- Node.js 18+ (WebUI 开发) -- pip - -### 快速开始 - -```bash -# 1. 克隆项目 -git clone https://github.com/CJackHwang/ds2api.git -cd ds2api - -# 2. 安装 Python 依赖 -pip install -r requirements.txt - -# 3. 配置账号 -cp config.example.json config.json -# 编辑 config.json,填入 DeepSeek 账号信息 - -# 4. 启动服务 -python dev.py -``` - -### 配置文件示例 - -```json -{ - "keys": ["my-api-key-1", "my-api-key-2"], - "accounts": [ - { - "email": "your-email@example.com", - "password": "your-password", - "token": "" - }, - { - "mobile": "12345678901", - "password": "your-password", - "token": "" - } - ] -} -``` - -**说明**: -- `keys`: 自定义 API Key,用于调用本服务的接口 -- `accounts`: DeepSeek 网页版账号 - - 支持 `email` 或 `mobile` 登录 - - `token` 留空,系统会自动获取 - -### WebUI 开发 - -```bash -# 进入 WebUI 目录 -cd webui - -# 安装依赖 -npm install - -# 启动开发服务器 -npm run dev -``` - -WebUI 开发服务器会启动在 `http://localhost:5173`,并自动代理 API 请求到后端 `http://localhost:5001`。 - -### WebUI 构建 - -WebUI 构建产物位于 `static/admin/` 目录。 - -**自动构建(推荐)**: -- 当 `webui/` 目录下的文件变更并推送到 `main` 分支时,GitHub Actions 会自动构建并提交产物 -- PR 合并时会自动触发构建 - -**手动构建**: -```bash -# 方式1:使用脚本 -./scripts/build-webui.sh - -# 方式2:直接执行 -cd webui -npm install -npm run build -``` - -> **贡献者注意**:修改 WebUI 后无需手动构建,CI 会自动处理。 - ---- - -## 生产环境部署 - -### 使用 systemd (Linux) - -1. **创建服务文件** - -```bash -sudo nano /etc/systemd/system/ds2api.service -``` - -```ini -[Unit] -Description=DS2API Service -After=network.target - -[Service] -Type=simple -User=www-data -WorkingDirectory=/opt/ds2api -ExecStart=/usr/bin/python3 app.py -Restart=always -RestartSec=10 -Environment=PORT=5001 -Environment=DS2API_ADMIN_KEY=your-admin-key - -[Install] -WantedBy=multi-user.target -``` - -2. **启动服务** - -```bash -sudo systemctl daemon-reload -sudo systemctl enable ds2api -sudo systemctl start ds2api -``` - -3. **查看状态** - -```bash -sudo systemctl status ds2api -sudo journalctl -u ds2api -f -``` - -### Nginx 反向代理 - -```nginx -server { - listen 80; - server_name api.yourdomain.com; - - # SSL 配置(推荐) - # listen 443 ssl http2; - # ssl_certificate /path/to/cert.pem; - # ssl_certificate_key /path/to/key.pem; - - location / { - proxy_pass http://127.0.0.1:5001; - proxy_http_version 1.1; - - # 关闭缓冲,支持 SSE - proxy_buffering off; - proxy_cache off; - - # 连接设置 - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - # SSE 超时设置 - proxy_read_timeout 300s; - proxy_send_timeout 300s; - - # 分块传输 - chunked_transfer_encoding on; - tcp_nopush on; - tcp_nodelay on; - keepalive_timeout 120; - } -} -``` - -### Docker 部署(可选) - -```dockerfile -# Dockerfile -FROM python:3.11-slim - -WORKDIR /app -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt - -COPY . . - -EXPOSE 5001 -CMD ["python", "app.py"] -``` - -```bash -# 构建镜像 -docker build -t ds2api . - -# 运行容器 -docker run -d \ - --name ds2api \ - -p 5001:5001 \ - -e DS2API_ADMIN_KEY=your-admin-key \ - -e DS2API_CONFIG_JSON='{"keys":["api-key"],"accounts":[...]}' \ - ds2api -``` - -### Docker Compose - -```yaml -# docker-compose.yml -version: '3.8' - -services: - ds2api: - build: . - ports: - - "5001:5001" - environment: - - DS2API_ADMIN_KEY=${DS2API_ADMIN_KEY} - - DS2API_CONFIG_JSON=${DS2API_CONFIG_JSON} - restart: unless-stopped -``` - ---- - -## 常见问题 - -### Q: 账号验证失败怎么办? - -**A**: 检查以下几点: -1. 确认 DeepSeek 账号密码正确 -2. 检查账号是否被封禁或需要验证 -3. 尝试在浏览器中手动登录一次 -4. 查看日志获取详细错误信息 - -### Q: 流式响应断开怎么办? - -**A**: -1. 检查 Nginx/反向代理配置,确保关闭了 `proxy_buffering` -2. 增加 `proxy_read_timeout` 超时时间 -3. 检查网络连接稳定性 - -### Q: Vercel 部署后配置丢失? - -**A**: -1. 确保点击了「同步到 Vercel」按钮 -2. 检查 Vercel Token 是否正确且未过期 -3. 确认 Project ID 正确 - -### Q: 如何更新到新版本? - -**本地部署**: -```bash -git pull origin main -pip install -r requirements.txt -# 重启服务 -``` - -**Vercel 部署**: -- 项目会自动从 GitHub 同步更新 -- 或在 Vercel 控制台手动触发重新部署 - -### Q: 如何查看日志? - -**本地开发**: -```bash -# 设置日志级别 -export LOG_LEVEL=DEBUG -python dev.py -``` - -**Vercel**: -- 访问 Vercel 控制台 -> 项目 -> Deployments -> Logs - -### Q: Token 计数不准确? - -**A**: DS2API 使用估算方式计算 token 数量(字符数 / 4),与 OpenAI 官方的 tokenizer 可能有差异,仅供参考。 - ---- - -## 获取帮助 - -- **GitHub Issues**: https://github.com/CJackHwang/ds2api/issues -- **文档**: https://github.com/CJackHwang/ds2api +# DS2API 部署指南 + +本文档详细介绍 DS2API 的各种部署方式。 + +--- + +## 目录 + +- [Vercel 部署(推荐)](#vercel-部署推荐) +- [本地开发](#本地开发) +- [生产环境部署](#生产环境部署) +- [常见问题](#常见问题) + +--- + +## Vercel 部署(推荐) + +### 一键部署 + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FCJackHwang%2Fds2api&env=DS2API_ADMIN_KEY&envDescription=管理面板访问密码(必填)&envLink=https%3A%2F%2Fgithub.com%2FCJackHwang%2Fds2api%23环境变量&project-name=ds2api&repository-name=ds2api) + +### 部署步骤 + +1. **点击部署按钮** + - 登录你的 GitHub 账号 + - 授权 Vercel 访问 + +2. **设置环境变量** + - `DS2API_ADMIN_KEY`: 管理面板密码(**必填**) + +3. **等待部署完成** + - Vercel 会自动构建并部署项目 + - 部署完成后获得访问 URL + +4. **配置账号** + - 访问 `https://your-project.vercel.app/admin` + - 输入管理密码登录 + - 添加 DeepSeek 账号 + - 设置自定义 API Key + +5. **同步配置** + - 点击「同步到 Vercel」按钮 + - 首次需要输入 Vercel Token 和 Project ID + - 同步成功后配置会持久化 + +### 获取 Vercel 凭证 + +**Vercel Token**: +1. 访问 https://vercel.com/account/tokens +2. 点击 "Create Token" +3. 设置名称和有效期 +4. 复制生成的 Token + +**Project ID**: +1. 进入 Vercel 项目页面 +2. 点击 Settings -> General +3. 复制 "Project ID" + +--- + +## 本地开发 + +### 环境要求 + +- Python 3.9+ +- Node.js 18+ (WebUI 开发) +- pip + +### 快速开始 + +```bash +# 1. 克隆项目 +git clone https://github.com/CJackHwang/ds2api.git +cd ds2api + +# 2. 安装 Python 依赖 +pip install -r requirements.txt + +# 3. 配置账号 +cp config.example.json config.json +# 编辑 config.json,填入 DeepSeek 账号信息 + +# 4. 启动服务 +python dev.py +``` + +### 配置文件示例 + +```json +{ + "keys": ["my-api-key-1", "my-api-key-2"], + "accounts": [ + { + "email": "your-email@example.com", + "password": "your-password", + "token": "" + }, + { + "mobile": "12345678901", + "password": "your-password", + "token": "" + } + ] +} +``` + +**说明**: +- `keys`: 自定义 API Key,用于调用本服务的接口 +- `accounts`: DeepSeek 网页版账号 + - 支持 `email` 或 `mobile` 登录 + - `token` 留空,系统会自动获取 + +### WebUI 开发 + +```bash +# 进入 WebUI 目录 +cd webui + +# 安装依赖 +npm install + +# 启动开发服务器 +npm run dev +``` + +WebUI 开发服务器会启动在 `http://localhost:5173`,并自动代理 API 请求到后端 `http://localhost:5001`。 + +--- + +## 生产环境部署 + +### 使用 systemd (Linux) + +1. **创建服务文件** + +```bash +sudo nano /etc/systemd/system/ds2api.service +``` + +```ini +[Unit] +Description=DS2API Service +After=network.target + +[Service] +Type=simple +User=www-data +WorkingDirectory=/opt/ds2api +ExecStart=/usr/bin/python3 app.py +Restart=always +RestartSec=10 +Environment=PORT=5001 +Environment=DS2API_ADMIN_KEY=your-admin-key + +[Install] +WantedBy=multi-user.target +``` + +2. **启动服务** + +```bash +sudo systemctl daemon-reload +sudo systemctl enable ds2api +sudo systemctl start ds2api +``` + +3. **查看状态** + +```bash +sudo systemctl status ds2api +sudo journalctl -u ds2api -f +``` + +### Nginx 反向代理 + +```nginx +server { + listen 80; + server_name api.yourdomain.com; + + # SSL 配置(推荐) + # listen 443 ssl http2; + # ssl_certificate /path/to/cert.pem; + # ssl_certificate_key /path/to/key.pem; + + location / { + proxy_pass http://127.0.0.1:5001; + proxy_http_version 1.1; + + # 关闭缓冲,支持 SSE + proxy_buffering off; + proxy_cache off; + + # 连接设置 + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # SSE 超时设置 + proxy_read_timeout 300s; + proxy_send_timeout 300s; + + # 分块传输 + chunked_transfer_encoding on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 120; + } +} +``` + +### Docker 部署(可选) + +```dockerfile +# Dockerfile +FROM python:3.11-slim + +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 5001 +CMD ["python", "app.py"] +``` + +```bash +# 构建镜像 +docker build -t ds2api . + +# 运行容器 +docker run -d \ + --name ds2api \ + -p 5001:5001 \ + -e DS2API_ADMIN_KEY=your-admin-key \ + -e DS2API_CONFIG_JSON='{"keys":["api-key"],"accounts":[...]}' \ + ds2api +``` + +### Docker Compose + +```yaml +# docker-compose.yml +version: '3.8' + +services: + ds2api: + build: . + ports: + - "5001:5001" + environment: + - DS2API_ADMIN_KEY=${DS2API_ADMIN_KEY} + - DS2API_CONFIG_JSON=${DS2API_CONFIG_JSON} + restart: unless-stopped +``` + +--- + +## 常见问题 + +### Q: 账号验证失败怎么办? + +**A**: 检查以下几点: +1. 确认 DeepSeek 账号密码正确 +2. 检查账号是否被封禁或需要验证 +3. 尝试在浏览器中手动登录一次 +4. 查看日志获取详细错误信息 + +### Q: 流式响应断开怎么办? + +**A**: +1. 检查 Nginx/反向代理配置,确保关闭了 `proxy_buffering` +2. 增加 `proxy_read_timeout` 超时时间 +3. 检查网络连接稳定性 + +### Q: Vercel 部署后配置丢失? + +**A**: +1. 确保点击了「同步到 Vercel」按钮 +2. 检查 Vercel Token 是否正确且未过期 +3. 确认 Project ID 正确 + +### Q: 如何更新到新版本? + +**本地部署**: +```bash +git pull origin main +pip install -r requirements.txt +# 重启服务 +``` + +**Vercel 部署**: +- 项目会自动从 GitHub 同步更新 +- 或在 Vercel 控制台手动触发重新部署 + +### Q: 如何查看日志? + +**本地开发**: +```bash +# 设置日志级别 +export LOG_LEVEL=DEBUG +python dev.py +``` + +**Vercel**: +- 访问 Vercel 控制台 -> 项目 -> Deployments -> Logs + +### Q: Token 计数不准确? + +**A**: DS2API 使用估算方式计算 token 数量(字符数 / 4),与 OpenAI 官方的 tokenizer 可能有差异,仅供参考。 + +--- + +## 获取帮助 + +- **GitHub Issues**: https://github.com/CJackHwang/ds2api/issues +- **文档**: https://github.com/CJackHwang/ds2api diff --git a/VERSION b/VERSION index 6e8bf73..7ecf123 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.0 +0.1.0 diff --git a/docker-compose.yml b/docker-compose.yml index 54cec15..3e6b605 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +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} +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 index 66d71e0..ccac75f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,70 +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"] +# ========== 阶段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/routes/home.py b/routes/home.py index dad0fb8..4105302 100644 --- a/routes/home.py +++ b/routes/home.py @@ -1,303 +1,301 @@ -# -*- coding: utf-8 -*- -"""首页和 WebUI 路由""" -import os -from fastapi import APIRouter, Request -from fastapi.responses import HTMLResponse, FileResponse - -from core.config import STATIC_ADMIN_DIR - -router = APIRouter() - -# 首页 HTML(内嵌避免依赖模板目录) -WELCOME_HTML = """ - - - - - DS2API - DeepSeek to OpenAI API - - - - - - - - -
-
-
- -
-
- -

DeepSeek to OpenAI & Claude Compatible API Interface

-
- - - -
-
- 🚀 -

全面兼容

-

完美适配 OpenAI 与 Claude API 格式,无缝集成现有工具。

-
-
- ⚖️ -

负载均衡

-

内置智能轮询机制,支持多账号并发,稳定高效。

-
-
- 🧠 -

深度思考

-

完整支持 推理过程输出,让思考可见。

-
-
- 🔍 -

联网搜索

-

集成 DeepSeek 原生搜索能力,获取最新实时资讯。

-
-
- -
-

© 2026 DS2API Project. Designed for flexibility & performance.

-
-
- -""" - - -@router.get("/") -def index(request: Request): - return HTMLResponse(content=WELCOME_HTML) - - -@router.get("/admin") -@router.get("/admin/{path:path}") -async def webui(request: Request, path: str = ""): - """提供 WebUI 静态文件""" - # 检查 static/admin 目录是否存在 - if not os.path.isdir(STATIC_ADMIN_DIR): - return HTMLResponse( - content="

WebUI not built

Run cd webui && npm run build first.

", - status_code=404 - ) - - # 如果请求的是具体文件(如 js, css) - if path and "." in path: - file_path = os.path.join(STATIC_ADMIN_DIR, path) - if os.path.isfile(file_path): - return FileResponse(file_path) - return HTMLResponse(content="Not Found", status_code=404) - - # 否则返回 index.html(SPA 路由) - index_path = os.path.join(STATIC_ADMIN_DIR, "index.html") - if os.path.isfile(index_path): - return FileResponse(index_path) - - return HTMLResponse(content="index.html not found", status_code=404) - - +# -*- coding: utf-8 -*- +"""首页和 WebUI 路由""" +import os +from fastapi import APIRouter, Request +from fastapi.responses import HTMLResponse, FileResponse + +from core.config import STATIC_ADMIN_DIR + +router = APIRouter() + +# 首页 HTML(内嵌避免依赖模板目录) +WELCOME_HTML = """ + + + + + DS2API - DeepSeek to OpenAI API + + + + + + + + +
+
+
+ +
+
+ +

DeepSeek to OpenAI & Claude Compatible API Interface

+
+ + + +
+
+ 🚀 +

全面兼容

+

完美适配 OpenAI 与 Claude API 格式,无缝集成现有工具。

+
+
+ ⚖️ +

负载均衡

+

内置智能轮询机制,支持多账号并发,稳定高效。

+
+
+ 🧠 +

深度思考

+

完整支持 推理过程输出,让思考可见。

+
+
+ 🔍 +

联网搜索

+

集成 DeepSeek 原生搜索能力,获取最新实时资讯。

+
+
+ +
+

© 2026 DS2API Project. Designed for flexibility & performance.

+
+
+ +""" + + +@router.get("/") +def index(request: Request): + return HTMLResponse(content=WELCOME_HTML) + + +@router.get("/admin") +@router.get("/admin/{path:path}") +async def webui(request: Request, path: str = ""): + """提供 WebUI 静态文件""" + # 检查 static/admin 目录是否存在 + if not os.path.isdir(STATIC_ADMIN_DIR): + return HTMLResponse( + content="

WebUI not built

Run cd webui && npm run build first.

", + status_code=404 + ) + + # 如果请求的是具体文件(如 js, css) + if path and "." in path: + file_path = os.path.join(STATIC_ADMIN_DIR, path) + if os.path.isfile(file_path): + return FileResponse(file_path) + return HTMLResponse(content="Not Found", status_code=404) + + # 否则返回 index.html(SPA 路由) + index_path = os.path.join(STATIC_ADMIN_DIR, "index.html") + if os.path.isfile(index_path): + return FileResponse(index_path) + + return HTMLResponse(content="index.html not found", status_code=404) diff --git a/static/admin/index.html b/static/admin/index.html index 76e4e9d..a08181e 100644 --- a/static/admin/index.html +++ b/static/admin/index.html @@ -1,43 +1,43 @@ - - - - - - - - - DS2API - DeepSeek API 管理面板 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - + + + + + + + + + DS2API - DeepSeek API 管理面板 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + \ No newline at end of file From 6b8f7f8821b7403387b33632dd1b990287a3a402 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 7 Feb 2026 10:55:34 +0800 Subject: [PATCH 03/10] =?UTF-8?q?feat:=20=E5=90=AF=E5=8A=A8=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E6=98=BE=E7=A4=BA=E6=89=80=E6=9C=89=E7=8E=AF=E5=A2=83?= =?UTF-8?q?=E5=8F=98=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- start.mjs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/start.mjs b/start.mjs index d4711af..c706154 100644 --- a/start.mjs +++ b/start.mjs @@ -454,7 +454,11 @@ async function showMenu() { 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(`\n${colors.bright}环境变量:${colors.reset}`); + console.log(` DS2API_ADMIN_KEY: ${colors.cyan}${CONFIG.adminKey}${colors.reset}`); + console.log(` PORT: ${colors.cyan}${CONFIG.backendPort}${colors.reset}`); + console.log(` HOST: ${colors.cyan}${CONFIG.host}${colors.reset}`); + console.log(` LOG_LEVEL: ${colors.cyan}${CONFIG.logLevel}${colors.reset}`); console.log(`${colors.dim} 自定义: DS2API_ADMIN_KEY=你的密钥 node start.mjs${colors.reset}`); console.log(` From 3f3198c9598d9ca005ac147522f333d53f84bdfc Mon Sep 17 00:00:00 2001 From: root Date: Sat, 7 Feb 2026 13:40:14 +0800 Subject: [PATCH 04/10] =?UTF-8?q?feat:=20=E8=B4=A6=E5=8F=B7=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E7=95=8C=E9=9D=A2=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 账号列表支持分页(每页10条,倒序显示) - API 密钥列表支持展开/关闭 --- routes/admin/config.py | 43 ++++++ webui/src/components/AccountManager.jsx | 178 +++++++++++++++++------- webui/src/locales/en.json | 3 +- webui/src/locales/zh.json | 3 +- 4 files changed, 173 insertions(+), 54 deletions(-) diff --git a/routes/admin/config.py b/routes/admin/config.py index d0739a5..5a8471a 100644 --- a/routes/admin/config.py +++ b/routes/admin/config.py @@ -122,6 +122,49 @@ async def delete_key(key: str, _: bool = Depends(verify_admin)): # ---------------------------------------------------------------------- # 账号管理 # ---------------------------------------------------------------------- +@router.get("/accounts") +async def list_accounts( + page: int = 1, + page_size: int = 10, + _: bool = Depends(verify_admin) +): + """获取账号列表(分页,倒序,密码脱敏)""" + accounts = CONFIG.get("accounts", []) + total = len(accounts) + + # 倒序排列 + accounts = list(reversed(accounts)) + + # 计算分页 + page = max(1, page) + page_size = max(1, min(100, page_size)) # 限制每页最多 100 条 + total_pages = (total + page_size - 1) // page_size if total > 0 else 1 + + start = (page - 1) * page_size + end = start + page_size + page_accounts = accounts[start:end] + + # 脱敏处理 + safe_accounts = [] + for acc in page_accounts: + safe_acc = { + "email": acc.get("email", ""), + "mobile": acc.get("mobile", ""), + "has_password": bool(acc.get("password")), + "has_token": bool(acc.get("token")), + "token_preview": acc.get("token", "")[:20] + "..." if acc.get("token") else "", + } + safe_accounts.append(safe_acc) + + return JSONResponse(content={ + "items": safe_accounts, + "total": total, + "page": page, + "page_size": page_size, + "total_pages": total_pages, + }) + + @router.post("/accounts") async def add_account(request: Request, _: bool = Depends(verify_admin)): """添加账号""" diff --git a/webui/src/components/AccountManager.jsx b/webui/src/components/AccountManager.jsx index 25aa8c1..773b84e 100644 --- a/webui/src/components/AccountManager.jsx +++ b/webui/src/components/AccountManager.jsx @@ -8,7 +8,10 @@ import { Server, ShieldCheck, Copy, - Check + Check, + ChevronLeft, + ChevronRight, + ChevronDown } from 'lucide-react' import clsx from 'clsx' import { useI18n } from '../i18n' @@ -25,9 +28,36 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch const [testingAll, setTestingAll] = useState(false) const [batchProgress, setBatchProgress] = useState({ current: 0, total: 0, results: [] }) const [queueStatus, setQueueStatus] = useState(null) + const [keysExpanded, setKeysExpanded] = useState(false) + + // 分页状态 + const [accounts, setAccounts] = useState([]) + const [page, setPage] = useState(1) + const [pageSize] = useState(10) + const [totalPages, setTotalPages] = useState(1) + const [totalAccounts, setTotalAccounts] = useState(0) + const [loadingAccounts, setLoadingAccounts] = useState(false) const apiFetch = authFetch || fetch + const fetchAccounts = async (targetPage = page) => { + setLoadingAccounts(true) + try { + const res = await apiFetch(`/admin/accounts?page=${targetPage}&page_size=${pageSize}`) + if (res.ok) { + const data = await res.json() + setAccounts(data.items || []) + setTotalPages(data.total_pages || 1) + setTotalAccounts(data.total || 0) + setPage(data.page || 1) + } + } catch (e) { + console.error('Failed to fetch accounts:', e) + } finally { + setLoadingAccounts(false) + } + } + const fetchQueueStatus = async () => { try { const res = await apiFetch('/admin/queue/status') @@ -41,6 +71,7 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch } useEffect(() => { + fetchAccounts() fetchQueueStatus() const interval = setInterval(fetchQueueStatus, 5000) return () => clearInterval(interval) @@ -102,6 +133,7 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch onMessage('success', t('accountManager.addAccountSuccess')) setNewAccount({ email: '', mobile: '', password: '' }) setShowAddAccount(false) + fetchAccounts(1) // 添加后回到第一页 onRefresh() } else { const data = await res.json() @@ -120,6 +152,7 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch const res = await apiFetch(`/admin/accounts/${encodeURIComponent(id)}`, { method: 'DELETE' }) if (res.ok) { onMessage('success', t('messages.deleted')) + fetchAccounts() // 刷新当前页 onRefresh() } else { onMessage('error', t('messages.deleteFailed')) @@ -142,6 +175,7 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch ? t('apiTester.testSuccess', { account: identifier, time: data.response_time }) : `${identifier}: ${data.message}` onMessage(data.success ? 'success' : 'error', statusMessage) + fetchAccounts() // 刷新当前页 onRefresh() } catch (e) { onMessage('error', t('accountManager.testFailed', { error: e.message })) @@ -152,17 +186,17 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch const testAllAccounts = async () => { if (!confirm(t('accountManager.testAllConfirm'))) return - const accounts = config.accounts || [] - if (accounts.length === 0) return + const allAccounts = config.accounts || [] + if (allAccounts.length === 0) return setTestingAll(true) - setBatchProgress({ current: 0, total: accounts.length, results: [] }) + setBatchProgress({ current: 0, total: allAccounts.length, results: [] }) let successCount = 0 const results = [] - for (let i = 0; i < accounts.length; i++) { - const acc = accounts[i] + for (let i = 0; i < allAccounts.length; i++) { + const acc = allAccounts[i] const id = acc.email || acc.mobile try { @@ -178,10 +212,11 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch results.push({ id, success: false, message: e.message }) } - setBatchProgress({ current: i + 1, total: accounts.length, results: [...results] }) + setBatchProgress({ current: i + 1, total: allAccounts.length, results: [...results] }) } - onMessage('success', t('accountManager.testAllCompleted', { success: successCount, total: accounts.length })) + onMessage('success', t('accountManager.testAllCompleted', { success: successCount, total: allAccounts.length })) + fetchAccounts() // 刷新当前页 onRefresh() setTestingAll(false) } @@ -228,13 +263,22 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch {/* API Keys Section */}
-
-
-

{t('accountManager.apiKeysTitle')}

-

{t('accountManager.apiKeysDesc')}

+
setKeysExpanded(!keysExpanded)} + > +
+ +
+

{t('accountManager.apiKeysTitle')}

+

{t('accountManager.apiKeysDesc')} ({config.keys?.length || 0})

+
-
- {config.keys?.length > 0 ? ( - config.keys.map((key, i) => ( -
-
-
- {key.slice(0, 16)}**** + {keysExpanded && ( +
+ {config.keys?.length > 0 ? ( + config.keys.map((key, i) => ( +
+
+
+ {key.slice(0, 16)}**** +
+ {copiedKey === key && ( + {t('accountManager.copied')} + )} +
+
+ +
- {copiedKey === key && ( - {t('accountManager.copied')} - )}
-
- - -
-
- )) - ) : ( -
{t('accountManager.noApiKeys')}
- )} -
+ )) + ) : ( +
{t('accountManager.noApiKeys')}
+ )} +
+ )}
{/* Accounts Section */} @@ -292,7 +338,7 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
+ {page} / {totalPages} + +
+
+ )}
{/* Modals */} diff --git a/webui/src/locales/en.json b/webui/src/locales/en.json index 38d1795..2490dea 100644 --- a/webui/src/locales/en.json +++ b/webui/src/locales/en.json @@ -122,7 +122,8 @@ "passwordLabel": "Password", "passwordPlaceholder": "Account password", "addAccountLoading": "Adding...", - "addAccountAction": "Add account" + "addAccountAction": "Add account", + "pageInfo": "Page {current}/{total}, {count} accounts total" }, "apiTester": { "defaultMessage": "Hello, please introduce yourself in one sentence.", diff --git a/webui/src/locales/zh.json b/webui/src/locales/zh.json index a8c0f71..7e2c5a2 100644 --- a/webui/src/locales/zh.json +++ b/webui/src/locales/zh.json @@ -122,7 +122,8 @@ "passwordLabel": "密码", "passwordPlaceholder": "账号密码", "addAccountLoading": "添加中...", - "addAccountAction": "添加账号" + "addAccountAction": "添加账号", + "pageInfo": "第 {current}/{total} 页,共 {count} 个账号" }, "apiTester": { "defaultMessage": "你好,请用一句话介绍你自己。", From 36099a4ada3d78de0050bb98b4f704d3c3087645 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 18 Feb 2026 20:50:07 +0800 Subject: [PATCH 05/10] =?UTF-8?q?chore:=20=E5=88=A0=E9=99=A4=20Python=20?= =?UTF-8?q?=E6=AE=8B=E7=95=99=E6=96=87=E4=BB=B6=EF=BC=88=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E5=B7=B2=E8=BF=81=E7=A7=BB=E8=87=B3=20Go=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/admin/config.py | 226 ----------------------------------------- 1 file changed, 226 deletions(-) delete mode 100644 routes/admin/config.py diff --git a/routes/admin/config.py b/routes/admin/config.py deleted file mode 100644 index 5a8471a..0000000 --- a/routes/admin/config.py +++ /dev/null @@ -1,226 +0,0 @@ -# -*- coding: utf-8 -*- -"""Admin 配置管理模块 - 配置、API Keys、账号管理""" -import os - -from fastapi import APIRouter, HTTPException, Request, Depends -from fastapi.responses import JSONResponse - -from core.config import CONFIG, save_config, logger -from core.auth import init_account_queue, get_queue_status, get_account_identifier -from core.deepseek import login_deepseek_via_account - -from .auth import verify_admin - -router = APIRouter() - -# Vercel 预配置 -VERCEL_TOKEN = os.getenv("VERCEL_TOKEN", "") -VERCEL_PROJECT_ID = os.getenv("VERCEL_PROJECT_ID", "") -VERCEL_TEAM_ID = os.getenv("VERCEL_TEAM_ID", "") - - -# ---------------------------------------------------------------------- -# Vercel 预配置信息 -# ---------------------------------------------------------------------- -@router.get("/vercel/config") -async def get_vercel_config(_: bool = Depends(verify_admin)): - """获取预配置的 Vercel 信息(脱敏)""" - return JSONResponse(content={ - "has_token": bool(VERCEL_TOKEN), - "project_id": VERCEL_PROJECT_ID, - "team_id": VERCEL_TEAM_ID or None, - }) - - -# ---------------------------------------------------------------------- -# 配置管理 -# ---------------------------------------------------------------------- -@router.get("/config") -async def get_config(_: bool = Depends(verify_admin)): - """获取当前配置(密码脱敏)""" - safe_config = { - "keys": CONFIG.get("keys", []), - "accounts": [], - "claude_mapping": CONFIG.get("claude_mapping", {}), - } - - for acc in CONFIG.get("accounts", []): - safe_acc = { - "email": acc.get("email", ""), - "mobile": acc.get("mobile", ""), - "has_password": bool(acc.get("password")), - "has_token": bool(acc.get("token")), - "token_preview": acc.get("token", "")[:20] + "..." if acc.get("token") else "", - } - safe_config["accounts"].append(safe_acc) - - return JSONResponse(content=safe_config) - - -@router.post("/config") -async def update_config(request: Request, _: bool = Depends(verify_admin)): - """更新完整配置""" - data = await request.json() - - if "keys" in data: - CONFIG["keys"] = data["keys"] - - if "accounts" in data: - # 保留原有密码和 token - existing = {get_account_identifier(a): a for a in CONFIG.get("accounts", [])} - for acc in data["accounts"]: - acc_id = get_account_identifier(acc) - if acc_id in existing: - if not acc.get("password"): - acc["password"] = existing[acc_id].get("password", "") - if not acc.get("token"): - acc["token"] = existing[acc_id].get("token", "") - CONFIG["accounts"] = data["accounts"] - init_account_queue() - - if "claude_mapping" in data: - CONFIG["claude_mapping"] = data["claude_mapping"] - - save_config(CONFIG) - return JSONResponse(content={"success": True, "message": "配置已更新"}) - - -# ---------------------------------------------------------------------- -# API Keys 管理 -# ---------------------------------------------------------------------- -@router.post("/keys") -async def add_key(request: Request, _: bool = Depends(verify_admin)): - """添加 API Key""" - data = await request.json() - key = data.get("key", "").strip() - - if not key: - raise HTTPException(status_code=400, detail="Key 不能为空") - - if key in CONFIG.get("keys", []): - raise HTTPException(status_code=400, detail="Key 已存在") - - if "keys" not in CONFIG: - CONFIG["keys"] = [] - CONFIG["keys"].append(key) - save_config(CONFIG) - - return JSONResponse(content={"success": True, "total_keys": len(CONFIG["keys"])}) - - -@router.delete("/keys/{key}") -async def delete_key(key: str, _: bool = Depends(verify_admin)): - """删除 API Key""" - if key not in CONFIG.get("keys", []): - raise HTTPException(status_code=404, detail="Key 不存在") - - CONFIG["keys"].remove(key) - save_config(CONFIG) - return JSONResponse(content={"success": True, "total_keys": len(CONFIG["keys"])}) - - -# ---------------------------------------------------------------------- -# 账号管理 -# ---------------------------------------------------------------------- -@router.get("/accounts") -async def list_accounts( - page: int = 1, - page_size: int = 10, - _: bool = Depends(verify_admin) -): - """获取账号列表(分页,倒序,密码脱敏)""" - accounts = CONFIG.get("accounts", []) - total = len(accounts) - - # 倒序排列 - accounts = list(reversed(accounts)) - - # 计算分页 - page = max(1, page) - page_size = max(1, min(100, page_size)) # 限制每页最多 100 条 - total_pages = (total + page_size - 1) // page_size if total > 0 else 1 - - start = (page - 1) * page_size - end = start + page_size - page_accounts = accounts[start:end] - - # 脱敏处理 - safe_accounts = [] - for acc in page_accounts: - safe_acc = { - "email": acc.get("email", ""), - "mobile": acc.get("mobile", ""), - "has_password": bool(acc.get("password")), - "has_token": bool(acc.get("token")), - "token_preview": acc.get("token", "")[:20] + "..." if acc.get("token") else "", - } - safe_accounts.append(safe_acc) - - return JSONResponse(content={ - "items": safe_accounts, - "total": total, - "page": page, - "page_size": page_size, - "total_pages": total_pages, - }) - - -@router.post("/accounts") -async def add_account(request: Request, _: bool = Depends(verify_admin)): - """添加账号""" - data = await request.json() - email = data.get("email", "").strip() - mobile = data.get("mobile", "").strip() - password = data.get("password", "").strip() - token = data.get("token", "").strip() - - if not email and not mobile: - raise HTTPException(status_code=400, detail="需要 email 或 mobile") - - # 检查是否已存在 - for acc in CONFIG.get("accounts", []): - if email and acc.get("email") == email: - raise HTTPException(status_code=400, detail="邮箱已存在") - if mobile and acc.get("mobile") == mobile: - raise HTTPException(status_code=400, detail="手机号已存在") - - new_account = {} - if email: - new_account["email"] = email - if mobile: - new_account["mobile"] = mobile - if password: - new_account["password"] = password - if token: - new_account["token"] = token - - if "accounts" not in CONFIG: - CONFIG["accounts"] = [] - CONFIG["accounts"].append(new_account) - init_account_queue() - save_config(CONFIG) - - return JSONResponse(content={"success": True, "total_accounts": len(CONFIG["accounts"])}) - - -@router.delete("/accounts/{identifier}") -async def delete_account(identifier: str, _: bool = Depends(verify_admin)): - """删除账号(通过 email 或 mobile)""" - accounts = CONFIG.get("accounts", []) - for i, acc in enumerate(accounts): - if acc.get("email") == identifier or acc.get("mobile") == identifier: - accounts.pop(i) - init_account_queue() - save_config(CONFIG) - return JSONResponse(content={"success": True, "total_accounts": len(accounts)}) - - raise HTTPException(status_code=404, detail="账号不存在") - - -# ---------------------------------------------------------------------- -# 账号队列状态 -# ---------------------------------------------------------------------- -@router.get("/queue/status") -async def get_account_queue_status(_: bool = Depends(verify_admin)): - """获取账号轮询队列状态""" - return JSONResponse(content=get_queue_status()) From 2f853d73648ab6065faad86346e1d1368a3ffaa0 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 18 Feb 2026 20:53:10 +0800 Subject: [PATCH 06/10] =?UTF-8?q?feat:=20=E9=87=8D=E5=86=99=20start.mjs=20?= =?UTF-8?q?=E9=80=82=E9=85=8D=20Go=20=E8=BF=90=E8=A1=8C=E6=97=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- start.mjs | 423 +++++++++++++++++++++++------------------------------- 1 file changed, 179 insertions(+), 244 deletions(-) diff --git a/start.mjs b/start.mjs index c706154..896723f 100644 --- a/start.mjs +++ b/start.mjs @@ -4,8 +4,13 @@ * * 使用方法: * node start.mjs # 显示交互式菜单 - * node start.mjs dev # 开发模式(后端+前端) - * node start.mjs prod # 生产模式 + * node start.mjs dev # 开发模式(后端 + 前端热重载) + * node start.mjs prod # 生产模式(编译后运行) + * node start.mjs build # 编译后端二进制 + * node start.mjs webui # 构建前端静态文件 + * node start.mjs install # 安装前端依赖 + * node start.mjs stop # 停止所有服务 + * node start.mjs status # 查看服务状态 */ import { spawn, execSync } from 'child_process'; @@ -20,25 +25,17 @@ 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'), -}; +// 编译产物路径 +const BINARY = join(__dirname, isWindows ? 'ds2api.exe' : 'ds2api'); -// 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'), +// 配置(从环境变量读取,与 Go 主程序保持一致) +const CONFIG = { + port: process.env.PORT || '5001', + frontendPort: 5173, + logLevel: process.env.LOG_LEVEL || 'INFO', + adminKey: process.env.DS2API_ADMIN_KEY || 'admin', + webuiDir: join(__dirname, 'webui'), + staticAdminDir: process.env.DS2API_STATIC_ADMIN_DIR || join(__dirname, 'static', 'admin'), }; // 存储子进程 @@ -78,7 +75,6 @@ function cleanup() { process.exit(0); } -// 注册退出处理 process.on('SIGINT', cleanup); process.on('SIGTERM', cleanup); @@ -92,39 +88,17 @@ function commandExists(cmd) { } } -// 获取系统 Python 命令 -function getSystemPython() { - const candidates = isWindows - ? ['python', 'python3', 'py'] - : ['python3', 'python']; - - for (const cmd of candidates) { - if (commandExists(cmd)) { - return cmd; - } - } - return null; +// 检查 Go 是否安装 +function checkGo() { + return commandExists('go'); } -// 系统 Python 命令 -const SYSTEM_PYTHON = getSystemPython(); - -// 检查 venv 是否存在 -function venvExists() { - return existsSync(VENV.python); -} - -// 检查 Python 依赖是否已安装 -function checkPythonDeps() { - if (!venvExists()) return false; +// 获取 Go 版本 +function getGoVersion() { try { - execSync(`"${VENV.python}" -c "import fastapi, uvicorn"`, { - stdio: 'ignore', - shell: true, - }); - return true; + return execSync('go version', { encoding: 'utf-8' }).trim(); } catch { - return false; + return null; } } @@ -134,13 +108,14 @@ function checkFrontendDeps() { return existsSync(join(CONFIG.webuiDir, 'node_modules')); } -// 获取依赖状态 -function getDepsStatus() { - return { - venv: venvExists(), - python: checkPythonDeps(), - frontend: checkFrontendDeps(), - }; +// 检查前端是否已构建 +function checkWebuiBuilt() { + return existsSync(join(CONFIG.staticAdminDir, 'index.html')); +} + +// 检查后端二进制是否存在 +function binaryExists() { + return existsSync(BINARY); } // 查找占用端口的进程 PID @@ -173,7 +148,7 @@ function findPidByPort(port) { // 获取运行中的服务状态 function getRunningStatus() { - const backendPids = findPidByPort(CONFIG.backendPort); + const backendPids = findPidByPort(CONFIG.port); const frontendPids = findPidByPort(CONFIG.frontendPort); return { backend: backendPids, @@ -213,67 +188,35 @@ async function stopServices() { }; if (running.backend.length > 0) { - log.info(`停止后端服务 (端口 ${CONFIG.backendPort}, PID: ${running.backend.join(', ')})...`); - for (const pid of running.backend) { - await killProcess(pid); - } + log.info(`停止后端服务 (端口 ${CONFIG.port}, 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); - } + for (const pid of running.frontend) await killProcess(pid); log.success('前端服务已停止'); } } -// 创建 venv -async function createVenv() { - if (venvExists()) { - log.info('虚拟环境已存在'); - return true; +// 安装前端依赖 +async function installFrontendDeps() { + if (!existsSync(CONFIG.webuiDir)) { + log.warn('webui 目录不存在,跳过前端依赖安装'); + return; } - - if (!SYSTEM_PYTHON) { - throw new Error('未找到 Python,请先安装 Python'); - } - - log.info('创建 Python 虚拟环境...'); + log.info('安装前端依赖 (npm ci)...'); return new Promise((resolve, reject) => { - const proc = spawn(SYSTEM_PYTHON, ['-m', 'venv', CONFIG.venvDir], { - cwd: __dirname, + const proc = spawn('npm', ['ci'], { + cwd: CONFIG.webuiDir, stdio: 'inherit', shell: true, }); - proc.on('close', code => { - if (code === 0) { - log.success('虚拟环境创建成功'); - resolve(true); - } else { - reject(new Error('虚拟环境创建失败')); - } - }); + proc.on('close', code => code === 0 ? resolve() : 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) { @@ -282,140 +225,113 @@ async function ensureFrontendDeps() { } } -// 安装 Python 依赖 -async function installPythonDeps() { - await ensureVenv(); - log.info('安装 Python 依赖...'); +// 编译后端二进制 +async function buildBackend() { + if (!checkGo()) throw new Error('未找到 Go,请先安装 Go (https://go.dev/dl/)'); + log.info('编译后端二进制...'); return new Promise((resolve, reject) => { - const proc = spawn(VENV.pip, ['install', '-r', 'requirements.txt'], { + const proc = spawn('go', ['build', '-o', BINARY, './cmd/ds2api'], { cwd: __dirname, stdio: 'inherit', shell: true, }); - proc.on('close', code => code === 0 ? resolve() : reject(new Error('Python 依赖安装失败'))); + proc.on('close', code => code === 0 ? resolve() : reject(new Error('后端编译失败'))); }); } -// 安装前端依赖 -async function installFrontendDeps() { +// 构建前端静态文件 +async function buildWebui() { if (!existsSync(CONFIG.webuiDir)) { - log.warn('webui 目录不存在,跳过前端依赖安装'); + log.warn('webui 目录不存在'); return; } - log.info('安装前端依赖...'); + await ensureFrontendDeps(); + 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('前端依赖安装失败'))); + const proc = spawn( + 'npm', ['run', 'build', '--', '--outDir', CONFIG.staticAdminDir, '--emptyOutDir'], + { 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, { +// 启动后端(开发模式:go run,无需预编译) +async function startBackendDev() { + if (!checkGo()) throw new Error('未找到 Go,请先安装 Go (https://go.dev/dl/)'); + log.info(`启动后端(go run)... http://localhost:${CONFIG.port}`); + const proc = spawn('go', ['run', './cmd/ds2api'], { cwd: __dirname, stdio: 'inherit', shell: true, env: { ...process.env, + PORT: CONFIG.port, + LOG_LEVEL: CONFIG.logLevel, DS2API_ADMIN_KEY: CONFIG.adminKey, }, }); - processes.push(proc); return proc; } -// 启动前端 +// 启动后端(生产模式:运行编译好的二进制) +async function startBackendProd() { + if (!binaryExists()) { + log.warn('未找到编译产物,正在编译...'); + await buildBackend(); + } + log.info(`启动后端(二进制)... http://localhost:${CONFIG.port}`); + const proc = spawn(BINARY, [], { + cwd: __dirname, + stdio: 'inherit', + shell: false, + env: { + ...process.env, + PORT: CONFIG.port, + LOG_LEVEL: CONFIG.logLevel, + 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}`); - + 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}`); + log.success(`后端 API: http://localhost:${CONFIG.port}`); + log.success(`管理界面: http://localhost:${CONFIG.port}/admin`); if (existsSync(CONFIG.webuiDir)) { - log.success(`管理界面: http://localhost:${CONFIG.frontendPort}`); + log.success(`前端 Dev: 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); + const check = setInterval(() => { + if (processes.filter(p => !p.killed).length === 0) { + clearInterval(check); resolve(); } }, 1000); @@ -424,53 +340,50 @@ function waitForProcesses() { // 交互式菜单 async function showMenu() { - const rl = createInterface({ - input: process.stdin, - output: process.stdout, - }); - + 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('║ DS2API 启动脚本 (Go) ║'); log.title('╚══════════════════════════════════════════╝'); - // 获取依赖状态 - const deps = getDepsStatus(); + // 环境状态 + const goVersion = getGoVersion(); + const frontendDeps = checkFrontendDeps(); + const webuiBuilt = checkWebuiBuilt(); + const hasBinary = binaryExists(); const running = getRunningStatus(); - const statusText = (ok) => ok ? `${colors.green}已安装${colors.reset}` : `${colors.yellow}未安装${colors.reset}`; + const ok = (v) => v ? `${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(` Go: ${goVersion ? `${colors.green}${goVersion}${colors.reset}` : `${colors.red}未安装${colors.reset}`}`); + console.log(` 前端依赖: ${frontendDeps === null ? `${colors.dim}N/A${colors.reset}` : frontendDeps ? `${colors.green}已安装${colors.reset}` : `${colors.yellow}未安装${colors.reset}`}`); + console.log(` 前端构建: ${ok(webuiBuilt)} ${webuiBuilt ? `(${CONFIG.staticAdminDir})` : '未构建'}`); + console.log(` 后端二进制: ${ok(hasBinary)} ${hasBinary ? BINARY : '未编译'}`); 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(` 后端 (:${CONFIG.port}): ${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}`); - console.log(` DS2API_ADMIN_KEY: ${colors.cyan}${CONFIG.adminKey}${colors.reset}`); - console.log(` PORT: ${colors.cyan}${CONFIG.backendPort}${colors.reset}`); - console.log(` HOST: ${colors.cyan}${CONFIG.host}${colors.reset}`); - console.log(` LOG_LEVEL: ${colors.cyan}${CONFIG.logLevel}${colors.reset}`); - console.log(`${colors.dim} 自定义: DS2API_ADMIN_KEY=你的密钥 node start.mjs${colors.reset}`); + console.log(` PORT: ${colors.cyan}${CONFIG.port}${colors.reset}`); + console.log(` LOG_LEVEL: ${colors.cyan}${CONFIG.logLevel}${colors.reset}`); + console.log(` DS2API_ADMIN_KEY: ${colors.cyan}${CONFIG.adminKey}${colors.reset}`); + console.log(`${colors.dim} 自定义: DS2API_ADMIN_KEY=密钥 PORT=5001 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}1.${colors.reset} 开发模式 (go run + 前端热重载) + ${colors.cyan}2.${colors.reset} 仅后端 (go run,无需编译) + ${colors.cyan}3.${colors.reset} 仅前端 (npm dev) + ${colors.cyan}4.${colors.reset} 生产模式 (编译后运行,前端已嵌入) + ${colors.cyan}5.${colors.reset} 编译后端 (go build) + ${colors.cyan}6.${colors.reset} 构建前端 (npm build → static/admin) + ${colors.cyan}7.${colors.reset} 安装前端依赖 (npm ci) + ${colors.red}8.${colors.reset} 停止所有服务 ${colors.cyan}0.${colors.reset} 退出 `); @@ -480,7 +393,7 @@ ${colors.bright}请选择操作:${colors.reset} switch (choice.trim() || '1') { case '1': log.title('========== 开发模式 =========='); - await startBackend(true); + await startBackendDev(); await new Promise(r => setTimeout(r, 1500)); await startFrontend(); showStatus(); @@ -488,8 +401,8 @@ ${colors.bright}请选择操作:${colors.reset} break; case '2': - log.title('========== 仅后端 (开发模式) =========='); - await startBackend(true); + log.title('========== 仅后端 (go run) =========='); + await startBackendDev(); showStatus(); await waitForProcesses(); break; @@ -503,21 +416,30 @@ ${colors.bright}请选择操作:${colors.reset} case '4': log.title('========== 生产模式 =========='); - await startBackend(false); + await startBackendProd(); showStatus(); await waitForProcesses(); break; case '5': - await buildFrontend(); - log.success('前端构建完成!'); + log.title('========== 编译后端 =========='); + await buildBackend(); + log.success(`编译完成:${BINARY}`); break; case '6': - await installAll(); + log.title('========== 构建前端 =========='); + await buildWebui(); + log.success('前端构建完成!'); break; case '7': + log.title('========== 安装前端依赖 =========='); + await installFrontendDeps(); + log.success('前端依赖安装完成!'); + break; + + case '8': await stopServices(); break; @@ -534,19 +456,21 @@ ${colors.bright}请选择操作:${colors.reset} // 命令行参数处理 async function main() { - const args = process.argv.slice(2); - const cmd = args[0]; + const cmd = process.argv[2]; - // 检查必要工具 - if (!SYSTEM_PYTHON) { - log.error('未找到 Python,请先安装 Python (尝试了 python, python3, py)'); - process.exit(1); + if (!checkGo() && !['install', 'webui', 'stop', 'status', 'help', '-h', '--help'].includes(cmd)) { + log.error('未找到 Go,请先安装 Go: https://go.dev/dl/'); + if (!cmd) { + // 无 Go 时仍允许进入菜单(可以只操作前端) + } else { + process.exit(1); + } } switch (cmd) { case 'dev': log.title('========== 开发模式 =========='); - await startBackend(true); + await startBackendDev(); await new Promise(r => setTimeout(r, 1500)); await startFrontend(); showStatus(); @@ -555,54 +479,65 @@ async function main() { case 'prod': log.title('========== 生产模式 =========='); - await startBackend(false); + await startBackendProd(); showStatus(); await waitForProcesses(); break; case 'build': - await buildFrontend(); + await buildBackend(); + log.success(`编译完成:${BINARY}`); + break; + + case 'webui': + await buildWebui(); log.success('前端构建完成!'); break; case 'install': - await installAll(); + await installFrontendDeps(); + log.success('前端依赖安装完成!'); break; case 'stop': await stopServices(); break; - case 'status': + case 'status': { const status = getRunningStatus(); + const goVer = getGoVersion(); + console.log(`\n${colors.bright}环境:${colors.reset}`); + console.log(` Go: ${goVer || `${colors.red}未安装${colors.reset}`}`); 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`); + console.log(` 后端 (:${CONFIG.port}): ${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.bright}DS2API 启动脚本 (Go)${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 dev 开发模式 (go run + 前端热重载) + node start.mjs prod 生产模式 (编译产物,前端已嵌入) + node start.mjs build 编译后端二进制 (go build) + node start.mjs webui 构建前端静态文件 + node start.mjs install 安装前端依赖 (npm ci) 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} + PORT 后端端口 (默认: 5001) + LOG_LEVEL 日志级别: DEBUG|INFO|WARN|ERROR (默认: INFO) + DS2API_ADMIN_KEY 管理员密钥 (默认: admin) + DS2API_CONFIG_PATH 配置文件路径 (默认: config.json) -${colors.cyan}虚拟环境:${colors.reset} - 默认路径: .venv/ - 首次运行 install 时自动创建 +${colors.cyan}示例:${colors.reset} + DS2API_ADMIN_KEY=mykey PORT=8080 node start.mjs dev `); break; From e143d13ff642cffac33c195327ab202725c73e4c Mon Sep 17 00:00:00 2001 From: root Date: Wed, 18 Feb 2026 20:57:23 +0800 Subject: [PATCH 07/10] =?UTF-8?q?feat:=20=E7=BC=96=E8=AF=91=E5=92=8C?= =?UTF-8?q?=E5=AE=89=E8=A3=85=E4=BE=9D=E8=B5=96=E4=BD=BF=E7=94=A8=E5=9B=BD?= =?UTF-8?q?=E5=86=85=E9=95=9C=E5=83=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- start.mjs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/start.mjs b/start.mjs index 896723f..59c5101 100644 --- a/start.mjs +++ b/start.mjs @@ -38,6 +38,12 @@ const CONFIG = { staticAdminDir: process.env.DS2API_STATIC_ADMIN_DIR || join(__dirname, 'static', 'admin'), }; +// 国内镜像配置 +const MIRRORS = { + goproxy: process.env.GOPROXY || 'https://goproxy.cn,direct', + npm: process.env.NPM_REGISTRY || 'https://registry.npmmirror.com', +}; + // 存储子进程 const processes = []; @@ -206,9 +212,9 @@ async function installFrontendDeps() { log.warn('webui 目录不存在,跳过前端依赖安装'); return; } - log.info('安装前端依赖 (npm ci)...'); + log.info(`安装前端依赖 (npm ci, registry: ${MIRRORS.npm})...`); return new Promise((resolve, reject) => { - const proc = spawn('npm', ['ci'], { + const proc = spawn('npm', ['ci', '--registry', MIRRORS.npm], { cwd: CONFIG.webuiDir, stdio: 'inherit', shell: true, @@ -228,12 +234,13 @@ async function ensureFrontendDeps() { // 编译后端二进制 async function buildBackend() { if (!checkGo()) throw new Error('未找到 Go,请先安装 Go (https://go.dev/dl/)'); - log.info('编译后端二进制...'); + log.info(`编译后端二进制 (GOPROXY: ${MIRRORS.goproxy})...`); return new Promise((resolve, reject) => { const proc = spawn('go', ['build', '-o', BINARY, './cmd/ds2api'], { cwd: __dirname, stdio: 'inherit', shell: true, + env: { ...process.env, GOPROXY: MIRRORS.goproxy }, }); proc.on('close', code => code === 0 ? resolve() : reject(new Error('后端编译失败'))); }); @@ -269,6 +276,7 @@ async function startBackendDev() { PORT: CONFIG.port, LOG_LEVEL: CONFIG.logLevel, DS2API_ADMIN_KEY: CONFIG.adminKey, + GOPROXY: MIRRORS.goproxy, }, }); processes.push(proc); @@ -371,6 +379,8 @@ async function showMenu() { console.log(` PORT: ${colors.cyan}${CONFIG.port}${colors.reset}`); console.log(` LOG_LEVEL: ${colors.cyan}${CONFIG.logLevel}${colors.reset}`); console.log(` DS2API_ADMIN_KEY: ${colors.cyan}${CONFIG.adminKey}${colors.reset}`); + console.log(` GOPROXY: ${colors.cyan}${MIRRORS.goproxy}${colors.reset}`); + console.log(` NPM_REGISTRY: ${colors.cyan}${MIRRORS.npm}${colors.reset}`); console.log(`${colors.dim} 自定义: DS2API_ADMIN_KEY=密钥 PORT=5001 node start.mjs${colors.reset}`); console.log(` @@ -535,9 +545,12 @@ ${colors.cyan}常用环境变量:${colors.reset} LOG_LEVEL 日志级别: DEBUG|INFO|WARN|ERROR (默认: INFO) DS2API_ADMIN_KEY 管理员密钥 (默认: admin) DS2API_CONFIG_PATH 配置文件路径 (默认: config.json) + GOPROXY Go 模块代理 (默认: https://goproxy.cn,direct) + NPM_REGISTRY npm 镜像源 (默认: https://registry.npmmirror.com) ${colors.cyan}示例:${colors.reset} DS2API_ADMIN_KEY=mykey PORT=8080 node start.mjs dev + GOPROXY=off NPM_REGISTRY=https://registry.npmjs.org node start.mjs dev `); break; From 962700f525273c1aaf6f3c9e597a2b177910fadf Mon Sep 17 00:00:00 2001 From: root Date: Wed, 18 Feb 2026 21:06:02 +0800 Subject: [PATCH 08/10] =?UTF-8?q?chore:=20=E5=88=A0=E9=99=A4=E6=97=A0?= =?UTF-8?q?=E7=94=A8=E6=96=87=E4=BB=B6=EF=BC=8C=E6=B8=85=E7=90=86=20.gitig?= =?UTF-8?q?nore=20Python=20=E6=AE=8B=E7=95=99=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 42 ------------------------- docker/Dockerfile | 70 ----------------------------------------- static/admin/index.html | 43 ------------------------- 3 files changed, 155 deletions(-) delete mode 100644 docker/Dockerfile delete mode 100644 static/admin/index.html diff --git a/.gitignore b/.gitignore index 5f776e2..c1146eb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,37 +2,6 @@ config.json .env -# Python -__pycache__/ -*.py[cod] -*$py.class -*.so -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# Virtual environments -venv/ -ENV/ -env/ -.venv - # IDE .vscode/ .idea/ @@ -44,7 +13,6 @@ env/ # Logs *.log logs/ -uvicorn.log artifacts/ # Vercel @@ -56,8 +24,6 @@ webui/node_modules/ webui/dist/ .npm .pnpm-store/ -# 保留 webui/package-lock.json 用于 CI 缓存 -# package-lock.json # 如果有根目录的可以忽略 yarn.lock pnpm-lock.yaml @@ -76,14 +42,6 @@ ds2api-tests .env.local .env.*.local -# Testing -.coverage -htmlcov/ -.pytest_cache/ -.tox/ - # Misc -*.pyc -*.pyo .git/ Thumbs.db diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index ccac75f..0000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,70 +0,0 @@ -# ========== 阶段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/static/admin/index.html b/static/admin/index.html deleted file mode 100644 index a08181e..0000000 --- a/static/admin/index.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - DS2API - DeepSeek API 管理面板 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - \ No newline at end of file From 70c59eb71d4e8245f12708420b3c439d4dc33a1a Mon Sep 17 00:00:00 2001 From: root Date: Fri, 27 Feb 2026 20:19:00 +0800 Subject: [PATCH 09/10] =?UTF-8?q?chore:=20=E5=B0=86=20.claude/=20=E5=92=8C?= =?UTF-8?q?=20CLAUDE.local.md=20=E4=BB=8E=20git=20=E8=B7=9F=E8=B8=AA?= =?UTF-8?q?=E4=B8=AD=E6=8E=92=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/commands/throttled-task.md | 30 -- .claude/plans/compiled-discovering-sunset.md | 407 ------------------- .claude/remove-root-check.sh | 172 -------- .claude/settings.local.json | 24 -- .claude/show-status.mjs | 91 ----- .gitignore | 4 + CLAUDE.local.md | 261 ------------ 7 files changed, 4 insertions(+), 985 deletions(-) delete mode 100644 .claude/commands/throttled-task.md delete mode 100644 .claude/plans/compiled-discovering-sunset.md delete mode 100644 .claude/remove-root-check.sh delete mode 100644 .claude/settings.local.json delete mode 100644 .claude/show-status.mjs delete mode 100644 CLAUDE.local.md diff --git a/.claude/commands/throttled-task.md b/.claude/commands/throttled-task.md deleted file mode 100644 index 72ba1a5..0000000 --- a/.claude/commands/throttled-task.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -description: 执行带有请求节流的任务,可以控制工具调用的频率 ---- - -# 节流任务执行 - -你现在进入了节流模式。在这个模式下,你需要: - -## 节流规则 - -1. **工具调用间隔**:每次调用工具后,等待至少 2 秒再调用下一个工具 -2. **并行限制**:同时最多只能并行调用 2 个工具(原本可能更多) -3. **批处理优化**:优先将相关的操作合并到一个工具调用中 -4. **进度提示**:每次等待时向用户说明正在节流等待 - -## 执行方式 - -- 对于读取操作(Read, Glob, Grep),可以适当放宽并行限制 -- 对于写入操作(Write, Edit),严格遵守间隔要求 -- 对于 API 调用(Bash 中的 API 请求),必须串行执行 - -## 用户任务 - -请按照上述节流规则执行以下任务: - -{{PROMPT}} - ---- - -**注意**:完成任务后,你将自动退出节流模式,恢复正常的工具调用频率。 diff --git a/.claude/plans/compiled-discovering-sunset.md b/.claude/plans/compiled-discovering-sunset.md deleted file mode 100644 index 6769b63..0000000 --- a/.claude/plans/compiled-discovering-sunset.md +++ /dev/null @@ -1,407 +0,0 @@ -# 心跳配置功能重构计划 - -## 概述 - -将心跳配置从"选择curl命令"模式重构为"基于URL的可配置心跳"模式。 - -**核心变化**: -- ✅ 支持多个URL同时发送心跳 -- ✅ 每个URL独立配置间隔时间 -- ✅ curl命令作为导入模板,可解析为URL配置 -- ✅ 完全替换旧的基于curl选择的方式 - -## 架构设计 - -### 数据模型 - -**新表:heartbeat_url_configs** -```typescript -{ - id: number; - name: string; // URL配置名称 - url: string; // 目标URL - method: string; // HTTP方法(GET/POST/PUT/DELETE) - headers: Record; // 请求头(JSONB) - body: string | null; // 请求体 - intervalSeconds: number; // 独立的心跳间隔(10-3600秒) - isEnabled: boolean; // 是否启用此配置 - lastSuccessAt: Date | null; // 统计:上次成功时间 - lastErrorAt: Date | null; // 统计:上次失败时间 - lastErrorMessage: string | null; // 统计:上次错误信息 - successCount: number; // 统计:成功次数 - failureCount: number; // 统计:失败次数 - providerId: number | null; // 关联的供应商ID(可选) - model: string | null; // 模型名称(展示用) - endpoint: string | null; // 端点路径(展示用) - createdAt: Date; - updatedAt: Date; -} -``` - -**修改表:heartbeat_settings** -```typescript -{ - id: number; - enabled: boolean; // 全局开关(保留) - // 删除:intervalSeconds, savedCurls, selectedCurlIndex - createdAt: Date; - updatedAt: Date; -} -``` - -### 心跳执行逻辑 - -**ProviderHeartbeat类重构**: -```typescript -class ProviderHeartbeat { - // 多定时器管理:Map - private static timers: Map = new Map(); - - // 启动:为每个启用的URL配置创建独立定时器 - static async start() { - const configs = await findEnabledHeartbeatUrlConfigs(); - for (const config of configs) { - this.startConfigTimer(config); - } - } - - // 停止:清除所有定时器 - static stop() { - for (const timer of this.timers.values()) { - clearInterval(timer); - } - this.timers.clear(); - } - - // 单个配置的定时器 - private static startConfigTimer(config: HeartbeatUrlConfig) { - const interval = setInterval(() => { - this.sendHeartbeat(config); - }, config.intervalSeconds * 1000); - this.timers.set(config.id, interval); - } - - // 发送心跳并记录成功/失败 - private static async sendHeartbeat(config: HeartbeatUrlConfig) { - const response = await fetch(config.url, { - method: config.method, - headers: config.headers, - body: config.body, - signal: AbortSignal.timeout(10000), - }); - - if (response.ok) { - await recordHeartbeatSuccess(config.id); - } else { - await recordHeartbeatFailure(config.id, errorMessage); - } - } -} -``` - -### 前端UI设计 - -**页面布局**: -``` -/settings/heartbeat/page.tsx -├── GlobalSettingsCard(全局开关) -├── CurlHistorySection(curl历史记录 + 导入按钮) -└── UrlConfigsSection(URL配置列表 + 新建/编辑/删除) -``` - -**组件拆分**: -- `global-settings-card.tsx` - 全局开关卡片 -- `curl-history-section.tsx` - curl历史记录区域 -- `curl-history-card.tsx` - 单个curl历史卡片 -- `url-configs-section.tsx` - URL配置列表区域 -- `url-config-card.tsx` - 单个URL配置卡片 -- `url-config-dialog.tsx` - 新建/编辑对话框 -- `_lib/hooks.ts` - 自定义hooks(useHeartbeatPageData) - -**curl导入流程**: -1. 用户点击curl历史卡片上的"导入"按钮 -2. 使用`parseCurlCommand()`解析curl命令 -3. 自动打开新建对话框,表单预填充解析后的数据 -4. 用户修改后保存,创建URL配置 - -## 实施步骤 - -### 阶段1:数据库和Repository层 - -1. **修改schema.ts** - - 添加`heartbeatUrlConfigs`表定义 - - 修改`heartbeatSettings`表定义(删除3个字段) - -2. **生成和审查迁移** - ```bash - bun run db:generate - # 检查生成的 drizzle/0061_*.sql - # 确保数据迁移逻辑正确(将选中的curl转为第一个URL配置) - ``` - -3. **创建repository/heartbeat-url-configs.ts** - - 接口:`HeartbeatUrlConfig`、`CreateHeartbeatUrlConfigInput`、`UpdateHeartbeatUrlConfigInput` - - 函数: - - `findAllHeartbeatUrlConfigs()` - 获取所有配置 - - `findEnabledHeartbeatUrlConfigs()` - 获取启用的配置 - - `findHeartbeatUrlConfigById(id)` - 根据ID获取 - - `createHeartbeatUrlConfig(input)` - 创建配置 - - `updateHeartbeatUrlConfig(id, input)` - 更新配置 - - `deleteHeartbeatUrlConfig(id)` - 删除配置 - - `recordHeartbeatSuccess(id)` - 记录成功 - - `recordHeartbeatFailure(id, errorMessage)` - 记录失败 - -4. **修改repository/heartbeat-settings.ts** - - 简化为只管理全局开关 - - 删除`savedCurls`和`selectedCurlIndex`相关逻辑 - - 保持`getHeartbeatSettings()`和`updateHeartbeatSettings()`接口 - -5. **运行迁移** - ```bash - bun run db:migrate - ``` - -### 阶段2:Action层 - -6. **创建actions/heartbeat-url-configs.ts** - - `fetchHeartbeatUrlConfigs()` - 获取所有配置 - - `createHeartbeatUrlConfigAction(input)` - 创建配置 - - `updateHeartbeatUrlConfigAction(id, input)` - 更新配置 - - `deleteHeartbeatUrlConfigAction(id)` - 删除配置 - - 验证规则: - - 名称不能为空 - - URL不能为空 - - 间隔时间范围:10-3600秒 - - 权限检查:仅admin可操作 - - 副作用:修改配置后重启心跳任务 - -7. **修改actions/heartbeat-settings.ts** - - 简化为只管理全局开关 - - 保持`fetchHeartbeatSettings()`和`saveHeartbeatSettings()` - - 开关变化时重启心跳任务 - -### 阶段3:心跳执行逻辑 - -8. **重构lib/provider-heartbeat.ts** - - 添加`timers: Map` - - 修改`start()`:为每个启用的配置创建定时器 - - 修改`stop()`:清除所有定时器 - - 新增`startConfigTimer(config)`:创建单个配置的定时器 - - 新增`stopConfigTimer(configId)`:停止单个配置的定时器 - - 修改`sendHeartbeat(config)`:发送请求并记录结果 - - 删除curl解析逻辑(不再需要) - -9. **修改app/v1/_lib/proxy/forwarder.ts** - - 删除或注释掉`addSuccessfulCurl()`调用(第357-367行) - - curl历史功能迁移到独立模块(可选) - -### 阶段4:i18n文案 - -10. **更新翻译文件** - - `messages/zh-CN/settings/heartbeat.json` - - `messages/zh-TW/settings/heartbeat.json` - - `messages/en/settings/heartbeat.json` - - `messages/ja/settings/heartbeat.json` - - `messages/ru/settings/heartbeat.json` - - 新增key: - - `section.global.*` - 全局设置区域 - - `section.curlHistory.*` - curl历史区域 - - `section.urlConfigs.*` - URL配置区域 - - `form.name.*` - 配置名称字段 - - `form.url.*` - URL字段 - - `form.method.*` - HTTP方法字段 - - `form.headers.*` - 请求头字段 - - `form.body.*` - 请求体字段 - - `form.isEnabled.*` - 启用开关 - - `form.stats.*` - 统计信息 - - `form.createButton`、`importButton`等 - -### 阶段5:前端UI - -11. **创建组件** - - `app/[locale]/settings/heartbeat/_components/global-settings-card.tsx` - - Switch组件:全局开关 - - 说明文字 - - - `app/[locale]/settings/heartbeat/_components/curl-history-section.tsx` - - 区域标题和描述 - - curl历史卡片列表 - - 空状态提示 - - - `app/[locale]/settings/heartbeat/_components/curl-history-card.tsx` - - 显示:供应商名、端点、模型、时间 - - 导入按钮 - - - `app/[locale]/settings/heartbeat/_components/url-configs-section.tsx` - - 区域标题和描述 - - 新建按钮 - - URL配置卡片列表 - - 空状态提示 - - - `app/[locale]/settings/heartbeat/_components/url-config-card.tsx` - - 显示:名称、URL、方法、间隔、启用状态 - - 统计信息:成功次数、失败次数、最后成功/失败时间 - - 编辑按钮、删除按钮 - - Switch组件:快速启用/禁用 - - - `app/[locale]/settings/heartbeat/_components/url-config-dialog.tsx` - - Dialog表单:名称、URL、方法、headers、body、间隔 - - 支持新建和编辑模式 - - headers使用Textarea(JSON格式) - - body使用Textarea(可选) - - 验证和错误提示 - - - `app/[locale]/settings/heartbeat/_components/heartbeat-skeleton.tsx` - - 骨架屏加载状态 - -12. **创建hooks** - - `app/[locale]/settings/heartbeat/_lib/hooks.ts` - - `useHeartbeatPageData()`: - - 加载settings、configs、savedCurls - - 提供CRUD操作函数 - - 提供importFromCurl函数 - - 统一错误处理和toast提示 - -13. **重写page.tsx** - - 使用`useHeartbeatPageData()` - - 组合所有子组件 - - 加载状态和错误处理 - -### 阶段6:测试和验证 - -14. **类型检查和格式化** - ```bash - bun run typecheck - bun run lint:fix - ``` - -15. **手动测试流程** - - [ ] 访问 `/settings/heartbeat` 页面 - - [ ] 创建新的URL配置 - - [ ] 从curl历史导入配置 - - [ ] 编辑配置(修改URL、间隔等) - - [ ] 启用/禁用单个配置 - - [ ] 启用/禁用全局开关 - - [ ] 删除配置 - - [ ] 检查多个URL同时发送心跳 - - [ ] 检查失败记录和统计信息 - - [ ] 检查国际化(切换语言) - -16. **日志验证** - ```bash - # 检查心跳日志 - tail -f logs/app.log | grep "ProviderHeartbeat" - - # 应该看到: - # - "Timer started" - 定时器启动 - # - "Heartbeat sent successfully" - 成功日志 - # - "Heartbeat failed" - 失败日志 - ``` - -17. **数据库验证** - ```bash - bun run db:studio - # 检查 heartbeat_url_configs 表 - # 确认配置已保存 - # 确认成功/失败统计更新 - ``` - -## 关键文件清单 - -### 新建文件 -- `src/repository/heartbeat-url-configs.ts` - URL配置Repository -- `src/actions/heartbeat-url-configs.ts` - URL配置Actions -- `src/app/[locale]/settings/heartbeat/_components/global-settings-card.tsx` -- `src/app/[locale]/settings/heartbeat/_components/curl-history-section.tsx` -- `src/app/[locale]/settings/heartbeat/_components/curl-history-card.tsx` -- `src/app/[locale]/settings/heartbeat/_components/url-configs-section.tsx` -- `src/app/[locale]/settings/heartbeat/_components/url-config-card.tsx` -- `src/app/[locale]/settings/heartbeat/_components/url-config-dialog.tsx` -- `src/app/[locale]/settings/heartbeat/_lib/hooks.ts` -- `drizzle/0061_*.sql` - 数据库迁移文件(自动生成) - -### 修改文件 -- `src/drizzle/schema.ts` - 添加新表,修改旧表 -- `src/repository/heartbeat-settings.ts` - 简化逻辑 -- `src/actions/heartbeat-settings.ts` - 简化Action -- `src/lib/provider-heartbeat.ts` - 重构心跳执行逻辑 -- `src/app/v1/_lib/proxy/forwarder.ts` - 删除curl保存逻辑 -- `src/app/[locale]/settings/heartbeat/page.tsx` - 重写UI -- `messages/*/settings/heartbeat.json` - 更新翻译(5种语言) - -### 删除文件 -- `src/app/[locale]/settings/heartbeat/_components/heartbeat-form.tsx` - 旧表单组件 - -## 数据迁移策略 - -**迁移逻辑(在0061_*.sql中)**: -```sql --- 创建新表 -CREATE TABLE heartbeat_url_configs (...); - --- 迁移现有数据 -DO $$ -DECLARE - settings_row RECORD; - selected_curl JSONB; -BEGIN - SELECT * INTO settings_row FROM heartbeat_settings LIMIT 1; - - IF settings_row.selected_curl_index IS NOT NULL THEN - selected_curl := settings_row.saved_curls->settings_row.selected_curl_index; - - INSERT INTO heartbeat_url_configs ( - name, url, interval_seconds, is_enabled, ... - ) VALUES ( - selected_curl->>'providerName', - selected_curl->>'url', - settings_row.interval_seconds, - settings_row.enabled, - ... - ); - END IF; -END $$; - --- 删除旧字段 -ALTER TABLE heartbeat_settings - DROP COLUMN interval_seconds, - DROP COLUMN saved_curls, - DROP COLUMN selected_curl_index; -``` - -**回滚能力**:保留旧数据在迁移文件中,可以通过反向迁移恢复。 - -## 风险和缓解 - -| 风险 | 缓解措施 | -|------|----------| -| 数据迁移失败 | 1. 迁移前备份数据库
2. 在测试环境验证
3. 编写回滚脚本 | -| curl解析不完整 | 1. 复用现有`parseCurlCommand`
2. 添加解析错误提示
3. 允许手动编辑 | -| 多定时器性能问题 | 1. 限制最大配置数量(如20个)
2. 添加禁用功能
3. 监控日志 | -| 心跳发送失败 | 1. 记录失败日志
2. UI显示失败状态
3. 支持手动禁用 | - -## 验证清单 - -- [ ] 数据库迁移成功,旧数据已转移 -- [ ] 类型检查通过 (`bun run typecheck`) -- [ ] Lint检查通过 (`bun run lint`) -- [ ] 构建成功 (`bun run build`) -- [ ] 可以创建URL配置 -- [ ] 可以从curl导入配置 -- [ ] 可以编辑和删除配置 -- [ ] 全局开关控制所有心跳 -- [ ] 多个URL同时发送心跳(检查日志) -- [ ] 失败统计正确记录 -- [ ] 所有5种语言显示正常 -- [ ] 页面加载和交互流畅 - -## 预估工作量 - -- 数据库和Repository层:1-2小时 -- Action层:30分钟 -- 心跳执行逻辑:1小时 -- i18n文案:30分钟 -- 前端UI:2-3小时 -- 测试和验证:1小时 -- **总计:6-8小时** diff --git a/.claude/remove-root-check.sh b/.claude/remove-root-check.sh deleted file mode 100644 index 6df65a0..0000000 --- a/.claude/remove-root-check.sh +++ /dev/null @@ -1,172 +0,0 @@ -#!/bin/bash - -echo "==========================================" -echo "Claude Code Root Check 移除工具" -echo "==========================================" -echo "" - -# 通过 which 命令找到 claude 可执行文件 -echo "正在查找 claude 命令..." -CLAUDE_PATH=$(which claude) - -if [ -z "$CLAUDE_PATH" ]; then - echo "❌ 错误: 未找到 claude 命令" - exit 1 -fi - -echo "找到 claude 位置: $CLAUDE_PATH" - -# 如果是软链接,获取实际文件路径 -if [ -L "$CLAUDE_PATH" ]; then - REAL_PATH=$(readlink -f "$CLAUDE_PATH") - echo "这是一个软链接,实际路径: $REAL_PATH" -else - REAL_PATH="$CLAUDE_PATH" -fi - -# 获取 claude 所在的目录 -CLAUDE_DIR=$(dirname "$CLAUDE_PATH") -echo "claude 目录: $CLAUDE_DIR" -echo "" - -# 检查是否已经是包装脚本 -if grep -q "Claude Code Wrapper" "$CLAUDE_PATH" 2>/dev/null; then - echo "✓ 检测到已安装包装脚本" - echo "正在更新包装脚本..." -else - echo "正在创建包装脚本..." -fi - -# 创建 claude-wrapper.sh -WRAPPER_PATH="$CLAUDE_DIR/claude-wrapper.sh" - -cat > "$WRAPPER_PATH" << 'EOF' -#!/bin/bash - -# Claude Code Wrapper - 自动删除 root check 限制 -# 此脚本会在每次执行 claude 前绕过 root 用户限制 -# -# 新版本 (2.1.x+) 支持通过环境变量绕过检查: -# - IS_SANDBOX=1 -# - CLAUDE_CODE_BUBBLEWRAP=1 -# -# 旧版本需要修改 cli.js 文件删除检查代码 - -# 获取当前脚本的真实路径 -SCRIPT_PATH="$(readlink -f "$0")" -SCRIPT_DIR="$(dirname "$SCRIPT_PATH")" - -# 查找同目录下的 claude.bak(原始软链接) -CLAUDE_BAK="$SCRIPT_DIR/claude.bak" - -# 如果 claude.bak 不存在,尝试通过 which 和目录搜索找到真实路径 -if [ ! -L "$CLAUDE_BAK" ] && [ ! -f "$CLAUDE_BAK" ]; then - # 在当前目录查找指向 claude-code 的软链接或文件 - for file in "$SCRIPT_DIR"/*; do - if [ -L "$file" ] || [ -f "$file" ]; then - target=$(readlink -f "$file" 2>/dev/null) - if [[ "$target" == *"@anthropic-ai/claude-code/cli.js" ]]; then - CLAUDE_REAL_PATH="$target" - break - fi - fi - done - - # 如果还是没找到,尝试常见路径 - if [ -z "$CLAUDE_REAL_PATH" ]; then - for path in \ - "$SCRIPT_DIR/../lib/node_modules/@anthropic-ai/claude-code/cli.js" \ - "/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js" \ - "/usr/lib/node_modules/@anthropic-ai/claude-code/cli.js"; do - if [ -f "$path" ]; then - CLAUDE_REAL_PATH="$path" - break - fi - done - fi -else - # 通过 claude.bak 获取真实的 cli.js 路径 - CLAUDE_REAL_PATH="$(readlink -f "$CLAUDE_BAK")" -fi - -if [ -z "$CLAUDE_REAL_PATH" ] || [ ! -f "$CLAUDE_REAL_PATH" ]; then - echo "错误: 未找到真实的 claude cli.js 文件" >&2 - echo "请确保 claude 已正确安装" >&2 - exit 1 -fi - -# 获取 claude 版本号(用于提示信息) -CLAUDE_VERSION=$(node "$CLAUDE_REAL_PATH" --version 2>/dev/null | head -1 || echo "unknown") - -# 新版本 (2.1.x+) 直接使用环境变量绕过 root check -# 设置 IS_SANDBOX=1 或 CLAUDE_CODE_BUBBLEWRAP=1 即可 -export IS_SANDBOX=1 -export CLAUDE_CODE_BUBBLEWRAP=1 - -# 执行原始 claude 命令,传递所有参数 -exec node "$CLAUDE_REAL_PATH" "$@" -EOF - -# 给包装脚本添加执行权限 -chmod +x "$WRAPPER_PATH" -echo "✓ 已创建包装脚本: $WRAPPER_PATH" -echo "" - -# 备份原 claude 命令(如果尚未备份) -CLAUDE_BAK="$CLAUDE_DIR/claude.bak" -if [ ! -e "$CLAUDE_BAK" ]; then - if [ -L "$CLAUDE_PATH" ]; then - # 如果是软链接,复制软链接本身 - cp -P "$CLAUDE_PATH" "$CLAUDE_BAK" - echo "✓ 已备份原 claude 软链接为: $CLAUDE_BAK" - else - # 如果是普通文件,复制文件 - cp "$CLAUDE_PATH" "$CLAUDE_BAK" - echo "✓ 已备份原 claude 文件为: $CLAUDE_BAK" - fi -else - echo "✓ 检测到已存在备份: $CLAUDE_BAK" -fi - -# 替换 claude 命令为包装脚本 -echo "" -echo "正在替换 claude 命令..." - -# 删除原有的 claude(如果是软链接或文件) -rm -f "$CLAUDE_PATH" - -# 创建新的软链接指向包装脚本 -ln -s "$WRAPPER_PATH" "$CLAUDE_PATH" - -echo "✓ 已将 claude 命令替换为包装脚本" -echo "" - -# 验证安装 -echo "==========================================" -echo "验证安装..." -echo "" - -if [ -L "$CLAUDE_PATH" ]; then - TARGET_PATH=$(readlink "$CLAUDE_PATH") - echo "✓ claude 现在指向: $TARGET_PATH" -fi - -if [ -e "$CLAUDE_BAK" ]; then - echo "✓ 原始 claude 已备份为: $CLAUDE_BAK" -fi - -if [ -x "$WRAPPER_PATH" ]; then - echo "✓ 包装脚本具有执行权限" -fi - -echo "" -echo "==========================================" -echo "✓ 安装完成!" -echo "" -echo "现在你可以在 root 用户下使用:" -echo " claude --dangerously-skip-permissions" -echo "" -echo "如需恢复原始 claude 命令:" -echo " rm $CLAUDE_PATH" -echo " mv $CLAUDE_BAK $CLAUDE_PATH" -echo "==========================================" diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 24e6832..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "env": { - "ANTHROPIC_API_KEY": "sk-d87ce5b80978df466c81378d798ca39f", - "ANTHROPIC_BASE_URL": "https://cc.ronghuaxueleng.top", - "CLAUDE_CODE_ATTRIBUTION_HEADER": "0", - "DISABLE_AUTOUPDATER": 1, - "DISABLE_BUG_COMMAND": 1, - "DISABLE_ERROR_REPORTING": 1, - "DISABLE_TELEMETRY": 1, - "IS_SANDBOX": "1", - "USER_NAME": "腾讯云" - }, - "permissions": { - "allow": [ - "*" - ], - "defaultMode": "bypassPermissions" - }, - "statusLine": { - "command": "node \".claude/show-status.mjs\"", - "padding": 0, - "type": "command" - } -} \ No newline at end of file diff --git a/.claude/show-status.mjs b/.claude/show-status.mjs deleted file mode 100644 index 772ab85..0000000 --- a/.claude/show-status.mjs +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env node -/** - * Claude Code 积分状态栏脚本 - * 用途: 在状态栏显示配置信息 - */ - -import fs from 'fs'; -import path from 'path'; -import os from 'os'; - -// 禁用SSL证书验证警告 -process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; - -function getDisplayUrl() { - const baseUrl = process.env.ANTHROPIC_BASE_URL || ''; - if (baseUrl) { - const match = baseUrl.match(/https?:\/\/([^\/]+)/); - if (match) { - return match[1]; - } - } - return ''; -} - -function getCurrentModel() { - // 优先使用环境变量 - let model = process.env.ANTHROPIC_MODEL || ''; - - // 如果环境变量没有,检查settings.json - if (!model) { - try { - const settingsFile = path.join(os.homedir(), '.claude', 'settings.json'); - if (fs.existsSync(settingsFile)) { - const settings = JSON.parse(fs.readFileSync(settingsFile, 'utf8')); - model = settings.model || ''; - } - } catch (error) { - // 忽略错误 - } - } - - if (model) { - if (model.toLowerCase().includes('claude-3')) { - if (model.toLowerCase().includes('haiku')) { - return 'Claude 3 Haiku'; - } else if (model.toLowerCase().includes('sonnet')) { - return 'Claude 3 Sonnet'; - } else if (model.toLowerCase().includes('opus')) { - return 'Claude 3 Opus'; - } - } else if (model.toLowerCase().includes('claude-4') || model.toLowerCase().includes('sonnet-4')) { - return 'Claude 4 Sonnet'; - } else if (model.toLowerCase().includes('opus-4')) { - return 'Claude 4 Opus'; - } else if (model.length > 20) { - return model.substring(0, 20) + '...'; - } - return model; - } - - return 'Claude (Auto)'; -} - -async function main() { - try { - const currentUrl = getDisplayUrl(); - const currentModel = getCurrentModel(); - const userName = process.env.USER_NAME || ''; - - const parts = []; - if (userName) parts.push(`👤 ${userName}`); - parts.push(currentModel); - parts.push(currentUrl); - - console.log(parts.join(' | ')); - - } catch (error) { - // 即使出错也显示基本信息 - const currentUrl = getDisplayUrl(); - const currentModel = getCurrentModel(); - const userName = process.env.USER_NAME || ''; - const parts = ['🔴 错误']; - if (userName) parts.push(`👤 ${userName}`); - parts.push(currentModel); - parts.push(currentUrl); - console.log(parts.join(' | ')); - } -} - -// ES Module 中直接执行 -main(); diff --git a/.gitignore b/.gitignore index c1146eb..2221ddd 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,7 @@ ds2api-tests # Misc .git/ Thumbs.db + +# Claude Code +.claude/ +CLAUDE.local.md diff --git a/CLAUDE.local.md b/CLAUDE.local.md deleted file mode 100644 index 4fd51c0..0000000 --- a/CLAUDE.local.md +++ /dev/null @@ -1,261 +0,0 @@ -# Claude Code 行为准则 - -> 本文件定义 Claude Code 在本项目中的强制执行规则。所有规则均为**必须执行**,不可跳过。 - ---- - -## 一、核心原则(九荣九耻) - -| 耻 | 荣 | -|---|---| -| ❌ 瞎猜接口 | ✅ 认真查询源码 | -| ❌ 模糊执行 | ✅ 寻求用户确认 | -| ❌ 臆想业务 | ✅ 复用现有实现 | -| ❌ 创造接口 | ✅ 主动测试验证 | -| ❌ 跳过验证 | ✅ 等待人类确认 | -| ❌ 破坏架构 | ✅ 遵循项目规范 | -| ❌ 假装理解 | ✅ 诚实说"不确定" | -| ❌ 盲目修改 | ✅ 谨慎重构 | -| ❌ 画蛇添足 | ✅ 按需实现 | - ---- - -## 二、代码生成前置检查 - -### 【强制】生成代码前必须完成的 4 项检查 - -在生成**任何代码**之前,必须逐条确认以下检查项,**缺一不可**: - -| # | 检查项 | 未通过则 | -|---|--------|---------| -| 1 | 是否已读取 CLAUDE.md 中的编码规范? | ❌ 禁止生成代码 | -| 2 | 是否已搜索项目中类似实现作为参考? | ❌ 禁止生成代码 | -| 3 | 是否有不确定的地方需要询问用户? | ⚠️ 先询问再继续 | -| 4 | 是否复用了现有的实体类/工具类? | ❌ 禁止新建已存在的类 | - ---- - -## 三、"参照 XX 写"执行规则 - -当用户说"参照XX写"、"仿照XX实现"、"按照XX的方式"时,**必须严格执行**以下步骤: - -### 步骤清单 - -| # | 步骤 | 必须完成的动作 | -|---|------|--------------| -| 1 | 完整阅读参照对象 | 读取 Controller、Service、Mapper、Entity **所有相关文件**,不能只看部分 | -| 2 | 列出关键对照点 | 向用户列出:接口路径、参数格式、返回值格式、Service 调用方式、业务逻辑 | -| 3 | 严格对照实现 | ❌ 禁止"优化"或"改进"参照对象,❌ 禁止偏离参照对象的风格 | - -### 完成后自检 - -| # | 自检问题 | 答案必须是"是" | -|---|---------|--------------| -| 1 | 我的实现和参照对象的实现方式是否一致? | 否则必须修正 | -| 2 | 有没有任何地方是我"自作主张"改的? | 有则必须告知用户 | - -**如果有任何偏离,必须告知用户并说明原因,由用户决定是否采用。** - ---- - -## 四、批量保存接口设计规范 - -### 【强制】设计前必须列出用户操作场景 - -| 用户操作 | 数据特征 | 处理方式 | -|---------|---------|---------| -| 新增一条数据 | 传入的数据没有 id | INSERT | -| 修改一条数据 | 传入的数据有 id | UPDATE | -| 删除一条数据 | **数据库有但传入列表中没有** ← 容易遗漏! | DELETE | -| 不做任何改动 | 原样传回 | 不处理 | - -### 正确实现步骤 - -``` -1. 查询数据库中该主体已有的所有数据 ID -2. 对比传入列表中的 ID,找出需要删除的(数据库有但传入没有) -3. 删除不在传入列表中的数据 -4. 新增或更新传入列表中的数据 -``` - -### 完成后自检 - -| # | 自检问题 | 答案必须是"是" | -|---|---------|--------------| -| 1 | 新增、更新、删除——三种情况都覆盖了吗? | | -| 2 | 如果用户删除了一条已有数据,保存后这条数据会消失吗? | | - ---- - -## 五、文档解析规则 - -### 【强制】解析步骤(按顺序执行,不可跳过) - -| # | 步骤 | 必须完成的动作 | 中断条件 | -|---|------|--------------|---------| -| 1 | 多方式解析 | Word/PDF 必须尝试 ≥2 种解析方式(段落、表格、文本框、XML 等) | | -| 2 | 完整性检查 | 检查是否只看到类名而没有属性定义? | ⚠️ **是则停止,询问用户** | -| 3 | 列出清单 | 向用户列出:类数量+名称、每个类的属性数量+名称、方法数量+签名 | ⚠️ **等待用户确认** | -| 4 | 生成代码 | 只有用户明确确认后才能继续 | | - -### 绝对禁止 - -| # | 禁止行为 | -|---|---------| -| 1 | ❌ 禁止在用户确认前生成任何代码 | -| 2 | ❌ 禁止自行补充或猜测文档中未明确写出的内容 | -| 3 | ❌ 禁止只用一种方式解析就认为解析完成 | -| 4 | ❌ 禁止看到类名/接口名却没有属性定义时继续执行 | - ---- - -## 六、接口与参数分析规则 - -### 触发条件 -- 分析接口映射关系(标准接口 → 内部接口) -- 分析参数映射关系 -- 编写 DTO/Entity 字段定义 - -### 【强制】执行步骤 - -| # | 步骤 | 必须完成的动作 | -|---|------|--------------| -| 1 | 确认接口映射 | 阅读标准接口功能 → 搜索后端代码找**功能匹配**的内部接口(不是名称匹配!)→ 读 Controller 确认功能 | -| 2 | 确认参数映射 | 找到 @RequestBody 的类 → 读源码(含父类)→ 逐一列出字段 → 对比建立映射 | - -### 映射可信度标注(必须标注) - -| 标注 | 含义 | -|-----|------| -| ✅ 已验证 | 已阅读源码确认 | -| ⚠️ 待验证 | 需要进一步确认 | -| ❌ 需新建接口 | 需要编写复杂业务逻辑(组合调用多个接口等) | - -### 绝对禁止 - -| # | 禁止行为 | -|---|---------| -| 1 | ❌ 禁止凭接口名称相似就认为可以映射 | -| 2 | ❌ 禁止直接使用 Postman/Swagger 参数定义,必须与源码核对 | -| 3 | ❌ 禁止凭"合理推测"编写参数映射 | -| 4 | ❌ 禁止使用模糊表述如"需要扩展"、"可能需要调用额外接口" | - ---- - -## 七、Postman 文档规范 - -### 核心原则 - -| 位置 | 内容 | -|-----|------| -| `description` 字段 | Markdown 格式,展示完整参数说明(带注释的 JSON 代码块) | -| `body.raw` 字段 | 纯净 JSON(无注释),可直接发送请求 | - -### description 格式模板 - -```json -{ - "description": "接口功能说明。\n\n**请求参数示例:**\n```json\n{\n \"字段名\": \"示例值\", // 字段说明\n}\n```\n\n**响应示例:**\n```json\n{\n \"code\": 0,\n \"data\": {}\n}\n```" -} -``` - -### 自检清单 - -| # | 检查项 | 要求 | -|---|--------|-----| -| 1 | body.raw 是否有注释? | ❌ 禁止,会导致 JSON 格式错误 | -| 2 | description 是否展示了参数格式? | ✅ 必须有带注释的 JSON 示例 | -| 3 | 是否包含响应示例? | ✅ 每个接口都必须有 | -| 4 | Long 类型 ID 是否展示为 String? | ✅ 如 `"id": "123456789"` | - ---- - -## 八、设计文档编写规范 - -### 核心原则 -设计文档的目标是:**开发人员可以直接照着写代码**,不是概念性说明。 - -### 【强制】文档必须包含的内容 - -| # | 内容 | 要求 | -|---|------|-----| -| 1 | 数据库表 DDL | 可直接执行的 CREATE TABLE | -| 2 | 枚举类代码 | 可直接复制使用 | -| 3 | 实体类代码 | 包括所有字段和注解 | -| 4 | Mapper 代码 | 包括 Provider 中的完整 SQL | -| 5 | Service 代码 | 接口定义和实现类 | -| 6 | Controller 代码 | 接口路径、请求体、响应格式 | -| 7 | 实现清单 | 新模块接入时的检查表 | -| 8 | 常见问题 FAQ | 解答可能的疑惑 | - -### 代码示例要求 - -| # | 要求 | -|---|------| -| 1 | 代码必须**完整可用**,不是片段或伪代码 | -| 2 | 必须包含**完整的 import 语句** | -| 3 | SQL 必须**完整可执行**,不能用 `...` 省略 | - -### 完成后自检 - -| # | 自检问题 | 答案必须是"是" | -|---|---------|--------------| -| 1 | 新人开发者能否只看这份文档就完成开发? | | -| 2 | 文档中的代码能否直接复制到项目中使用? | | -| 3 | 是否有"等"、"..."、"类似"等模糊表述? | 有则删除 | - ---- - -## 九、方法重载规范 - -### 规则 - -| # | 规则 | 说明 | -|---|------|-----| -| 1 | 全量参数方法承载所有逻辑 | 是唯一的实现体 | -| 2 | 少参数方法只做委托调用 | 传 `null` 给新增参数,方法体只有一行 `return` | -| 3 | ❌ 禁止两个重载方法各写一份逻辑 | 即使逻辑相同也不行 | -| 4 | ❌ 禁止反向委托 | 全量方法不能调用少参数方法 | - -### 正确示例 - -```java -// ✅ 少参数方法委托全量方法 -public Object foo(Req req, Request request, Response response) { - return foo(req, request, response, null); -} - -public Object foo(Req req, Request request, Response response, Function lineConverter) { - // 所有逻辑在这里 - if (lineConverter != null) { - // 有转换器时的处理 - } -} -``` - ---- - -## 十、工作偏好 - -| # | 偏好 | -|---|------| -| 1 | 始终使用**简体中文**回复 | -| 2 | 长任务必须记录详细进度 | -| 3 | 提交代码时**不要**附带 `Co-Authored-By: Claude` | -| 4 | 对所有工具操作自动同意,无需额外确认 | -| 5 | 不用执行编译和测试 | -| 6 | 编写构建脚本时尽量使用 mjs 编写带菜单的脚本 | -| 7 | 尽量使用 Python 连接数据库 | -| 8 | 联网搜索时**禁止**使用 csdn.net、阿里云/腾讯云/华为云社区等内容农场 | - ---- - -## 十一、代码生成规则 - -| # | 规则 | -|---|------| -| 1 | 提供实体类/模板/文档时,必须**完整复制所有属性和方法**,禁止省略 | -| 2 | 生成代码前,先列出文档中所有属性数量和名称,确认无遗漏后再生成 | -| 3 | 属性超过 20 个时,分批列出确认 | -| 4 | 禁止因为"优化"或"简化"而删减任何属性 | -| 5 | 生成完成后,对比源文档属性数量是否一致 | From 25ea28a27788500817bc5a8149e8fb95dd711d7a Mon Sep 17 00:00:00 2001 From: root Date: Fri, 27 Feb 2026 20:58:18 +0800 Subject: [PATCH 10/10] =?UTF-8?q?feat:=20=E8=B4=A6=E5=8F=B7=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E7=8A=B6=E6=80=81=E6=8C=81=E4=B9=85=E5=8C=96=E3=80=81?= =?UTF-8?q?=E5=88=86=E9=A1=B5=E9=80=89=E6=8B=A9=E5=99=A8=E3=80=81=E7=82=B9?= =?UTF-8?q?=E5=87=BB=E8=B4=A6=E5=8F=B7=E5=90=8D=E5=A4=8D=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Account 结构加 TestStatus 字段,测试后写入 config.json - listAccounts 接口返回 test_status,前端根据结果显示红/绿/黄状态点 - 分页选择器支持 10/20/50/100/500/1000/2000/5000 - 点击账号名自动复制到剪贴板,hover 显示复制图标,复制后显示绿色对勾 --- internal/admin/deps.go | 1 + internal/admin/handler_accounts_crud.go | 1 + internal/admin/handler_accounts_testing.go | 10 ++++- internal/config/config.go | 9 ++-- internal/config/store.go | 12 +++++ .../account/AccountManagerContainer.jsx | 4 ++ webui/src/features/account/AccountsTable.jsx | 45 ++++++++++++++++--- webui/src/features/account/useAccountsData.js | 13 ++++-- webui/src/locales/en.json | 1 + webui/src/locales/zh.json | 1 + 10 files changed, 83 insertions(+), 14 deletions(-) diff --git a/internal/admin/deps.go b/internal/admin/deps.go index e92c37b..7debcf0 100644 --- a/internal/admin/deps.go +++ b/internal/admin/deps.go @@ -16,6 +16,7 @@ type ConfigStore interface { Accounts() []config.Account FindAccount(identifier string) (config.Account, bool) UpdateAccountToken(identifier, token string) error + UpdateAccountTestStatus(identifier, status string) error Update(mutator func(*config.Config) error) error ExportJSONAndBase64() (string, string, error) IsEnvBacked() bool diff --git a/internal/admin/handler_accounts_crud.go b/internal/admin/handler_accounts_crud.go index daaa434..a0d64df 100644 --- a/internal/admin/handler_accounts_crud.go +++ b/internal/admin/handler_accounts_crud.go @@ -56,6 +56,7 @@ func (h *Handler) listAccounts(w http.ResponseWriter, r *http.Request) { "has_password": acc.Password != "", "has_token": token != "", "token_preview": preview, + "test_status": acc.TestStatus, }) } writeJSON(w, http.StatusOK, map[string]any{"items": items, "total": total, "page": page, "page_size": pageSize, "total_pages": totalPages}) diff --git a/internal/admin/handler_accounts_testing.go b/internal/admin/handler_accounts_testing.go index 2bd7706..7a7430d 100644 --- a/internal/admin/handler_accounts_testing.go +++ b/internal/admin/handler_accounts_testing.go @@ -88,7 +88,15 @@ func runAccountTestsConcurrently(accounts []config.Account, maxConcurrency int, func (h *Handler) testAccount(ctx context.Context, acc config.Account, model, message string) map[string]any { start := time.Now() - result := map[string]any{"account": acc.Identifier(), "success": false, "response_time": 0, "message": "", "model": model} + identifier := acc.Identifier() + result := map[string]any{"account": identifier, "success": false, "response_time": 0, "message": "", "model": model} + defer func() { + status := "failed" + if ok, _ := result["success"].(bool); ok { + status = "ok" + } + _ = h.Store.UpdateAccountTestStatus(identifier, status) + }() token := strings.TrimSpace(acc.Token) if token == "" { newToken, err := h.DS.Login(ctx, acc) diff --git a/internal/config/config.go b/internal/config/config.go index 4b281a2..b8d59d6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -18,10 +18,11 @@ type Config struct { } type Account struct { - Email string `json:"email,omitempty"` - Mobile string `json:"mobile,omitempty"` - Password string `json:"password,omitempty"` - Token string `json:"token,omitempty"` + Email string `json:"email,omitempty"` + Mobile string `json:"mobile,omitempty"` + Password string `json:"password,omitempty"` + Token string `json:"token,omitempty"` + TestStatus string `json:"test_status,omitempty"` } type CompatConfig struct { diff --git a/internal/config/store.go b/internal/config/store.go index 2e6fcaf..7a09cdc 100644 --- a/internal/config/store.go +++ b/internal/config/store.go @@ -97,6 +97,18 @@ func (s *Store) FindAccount(identifier string) (Account, bool) { return Account{}, false } +func (s *Store) UpdateAccountTestStatus(identifier, status string) error { + identifier = strings.TrimSpace(identifier) + s.mu.Lock() + defer s.mu.Unlock() + idx, ok := s.findAccountIndexLocked(identifier) + if !ok { + return errors.New("account not found") + } + s.cfg.Accounts[idx].TestStatus = status + return s.saveLocked() +} + func (s *Store) UpdateAccountToken(identifier, token string) error { identifier = strings.TrimSpace(identifier) s.mu.Lock() diff --git a/webui/src/features/account/AccountManagerContainer.jsx b/webui/src/features/account/AccountManagerContainer.jsx index f5f9324..558739d 100644 --- a/webui/src/features/account/AccountManagerContainer.jsx +++ b/webui/src/features/account/AccountManagerContainer.jsx @@ -17,10 +17,12 @@ export default function AccountManagerContainer({ config, onRefresh, onMessage, setKeysExpanded, accounts, page, + pageSize, totalPages, totalAccounts, loadingAccounts, fetchAccounts, + changePageSize, resolveAccountIdentifier, } = useAccountsData({ apiFetch }) @@ -79,6 +81,7 @@ export default function AccountManagerContainer({ config, onRefresh, onMessage, batchProgress={batchProgress} totalAccounts={totalAccounts} page={page} + pageSize={pageSize} totalPages={totalPages} resolveAccountIdentifier={resolveAccountIdentifier} onTestAll={testAllAccounts} @@ -87,6 +90,7 @@ export default function AccountManagerContainer({ config, onRefresh, onMessage, onDeleteAccount={deleteAccount} onPrevPage={() => fetchAccounts(page - 1)} onNextPage={() => fetchAccounts(page + 1)} + onPageSizeChange={changePageSize} /> { + navigator.clipboard.writeText(id).then(() => { + setCopiedId(id) + setTimeout(() => setCopiedId(null), 1500) + }) + } return (
@@ -83,12 +94,23 @@ export default function AccountsTable({
-
{id || '-'}
+
copyId(id)} + > + {id || '-'} + {copiedId === id + ? + : + } +
- {acc.has_token ? t('accountManager.sessionActive') : t('accountManager.reauthRequired')} + {acc.test_status === 'failed' ? t('accountManager.testStatusFailed') : (acc.test_status === 'ok' || acc.has_token) ? t('accountManager.sessionActive') : t('accountManager.reauthRequired')} {acc.token_preview && ( {acc.token_preview} @@ -122,8 +144,19 @@ export default function AccountsTable({ {totalPages > 1 && (
-
- {t('accountManager.pageInfo', { current: page, total: totalPages, count: totalAccounts })} +
+
+ {t('accountManager.pageInfo', { current: page, total: totalPages, count: totalAccounts })} +
+