diff --git a/.github/workflows/auto-release-pipeline.yml b/.github/workflows/auto-release-pipeline.yml index 6e7bb904..7fe6eb37 100644 --- a/.github/workflows/auto-release-pipeline.yml +++ b/.github/workflows/auto-release-pipeline.yml @@ -123,8 +123,6 @@ jobs: echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT echo "new_tag=v$NEW_VERSION" >> $GITHUB_OUTPUT - # 前端构建已移至 Docker 构建流程中 - - name: Update VERSION file if: steps.check.outputs.needs_bump == 'true' run: | @@ -138,6 +136,77 @@ jobs: git add VERSION git commit -m "chore: sync VERSION file with release ${{ steps.next_version.outputs.new_tag }} [skip ci]" + # 构建前端并推送到 web-dist 分支 + - name: Setup Node.js + if: steps.check.outputs.needs_bump == 'true' + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + cache-dependency-path: web/admin-spa/package-lock.json + + - name: Build Frontend + if: steps.check.outputs.needs_bump == 'true' + run: | + echo "Building frontend for version ${{ steps.next_version.outputs.new_version }}..." + cd web/admin-spa + npm ci + npm run build + echo "Frontend build completed" + + - name: Push Frontend Build to web-dist Branch + if: steps.check.outputs.needs_bump == 'true' + run: | + # 创建临时目录 + TEMP_DIR=$(mktemp -d) + echo "Using temp directory: $TEMP_DIR" + + # 复制构建产物到临时目录 + cp -r web/admin-spa/dist/* "$TEMP_DIR/" + + # 检查 web-dist 分支是否存在 + if git ls-remote --heads origin web-dist | grep -q web-dist; then + echo "Checking out existing web-dist branch" + git fetch origin web-dist:web-dist + git checkout web-dist + else + echo "Creating new web-dist branch" + git checkout --orphan web-dist + fi + + # 清空当前目录(保留 .git) + git rm -rf . 2>/dev/null || true + + # 复制构建产物 + cp -r "$TEMP_DIR"/* . + + # 添加 README + cat > README.md << 'EOF' + # Claude Relay Service - Web Frontend Build + + This branch contains the pre-built frontend assets for Claude Relay Service. + + **DO NOT EDIT FILES IN THIS BRANCH DIRECTLY** + + These files are automatically generated by the CI/CD pipeline. + + Version: ${{ steps.next_version.outputs.new_version }} + Build Date: $(date -u +"%Y-%m-%d %H:%M:%S UTC") + EOF + + # 提交并推送 + git add -A + git commit -m "chore: update frontend build for v${{ steps.next_version.outputs.new_version }} [skip ci]" + git push origin web-dist --force + + # 切换回主分支 + git checkout main + + # 清理临时目录 + rm -rf "$TEMP_DIR" + + echo "Frontend build pushed to web-dist branch successfully" + - name: Install git-cliff if: steps.check.outputs.needs_bump == 'true' run: | diff --git a/package.json b/package.json index 08a30615..c2540b3e 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "dev": "nodemon src/app.js", "build:web": "cd web/admin-spa && npm run build", "install:web": "cd web/admin-spa && npm install", + "update:pricing": "node scripts/update-model-pricing.js", "setup": "node scripts/setup.js", "cli": "node cli/index.js", "init:costs": "node src/cli/initCosts.js", diff --git a/resources/model-pricing/README.md b/resources/model-pricing/README.md index fcb58357..3f297bfa 100644 --- a/resources/model-pricing/README.md +++ b/resources/model-pricing/README.md @@ -34,4 +34,4 @@ The file contains JSON data with model pricing information including: - Context window sizes - Model capabilities -Last updated: 2025-07-29 \ No newline at end of file +Last updated: 2025-08-06 \ No newline at end of file diff --git a/resources/model-pricing/model_prices_and_context_window.json b/resources/model-pricing/model_prices_and_context_window.json index 40a07a74..d7ef624a 100644 --- a/resources/model-pricing/model_prices_and_context_window.json +++ b/resources/model-pricing/model_prices_and_context_window.json @@ -607,9 +607,9 @@ "supports_system_messages": true, "supports_tool_choice": true, "search_context_cost_per_query": { - "search_context_size_low": 30.0, - "search_context_size_medium": 35.0, - "search_context_size_high": 50.0 + "search_context_size_low": 0.025, + "search_context_size_medium": 0.0275, + "search_context_size_high": 0.03 } }, "codex-mini-latest": { @@ -3750,7 +3750,7 @@ "max_input_tokens": 131072, "max_output_tokens": 131072, "input_cost_per_token": 3.3e-06, - "output_cost_per_token": 16.5e-06, + "output_cost_per_token": 1.65e-05, "litellm_provider": "azure_ai", "mode": "chat", "supports_function_calling": true, @@ -3764,7 +3764,7 @@ "max_input_tokens": 131072, "max_output_tokens": 131072, "input_cost_per_token": 3e-06, - "output_cost_per_token": 15e-06, + "output_cost_per_token": 1.5e-05, "litellm_provider": "azure_ai", "mode": "chat", "supports_function_calling": true, @@ -3777,7 +3777,7 @@ "max_tokens": 131072, "max_input_tokens": 131072, "max_output_tokens": 131072, - "input_cost_per_token": 0.25e-06, + "input_cost_per_token": 2.5e-07, "output_cost_per_token": 1.27e-06, "litellm_provider": "azure_ai", "mode": "chat", @@ -3792,7 +3792,7 @@ "max_tokens": 131072, "max_input_tokens": 131072, "max_output_tokens": 131072, - "input_cost_per_token": 0.275e-06, + "input_cost_per_token": 2.75e-07, "output_cost_per_token": 1.38e-06, "litellm_provider": "azure_ai", "mode": "chat", @@ -5741,6 +5741,32 @@ "supports_reasoning": true, "supports_computer_use": true }, + "claude-opus-4-1-20250805": { + "max_tokens": 32000, + "max_input_tokens": 200000, + "max_output_tokens": 32000, + "input_cost_per_token": 1.5e-05, + "output_cost_per_token": 7.5e-05, + "search_context_cost_per_query": { + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01, + "search_context_size_high": 0.01 + }, + "cache_creation_input_token_cost": 1.875e-05, + "cache_read_input_token_cost": 1.5e-06, + "litellm_provider": "anthropic", + "mode": "chat", + "supports_function_calling": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159, + "supports_assistant_prefill": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_reasoning": true, + "supports_computer_use": true + }, "claude-sonnet-4-20250514": { "max_tokens": 64000, "max_input_tokens": 200000, @@ -7337,12 +7363,12 @@ "max_audio_length_hours": 8.4, "max_audio_per_prompt": 1, "max_pdf_size_mb": 30, - "input_cost_per_token": 3.5e-07, + "input_cost_per_token": 3.5e-07, "input_cost_per_audio_token": 2.1e-06, "input_cost_per_image": 2.1e-06, "input_cost_per_video_per_second": 2.1e-06, "output_cost_per_token": 1.5e-06, - "output_cost_per_audio_token": 8.5e-06, + "output_cost_per_audio_token": 8.5e-06, "litellm_provider": "gemini", "mode": "chat", "rpm": 10, @@ -9039,9 +9065,9 @@ "supports_tool_choice": true }, "vertex_ai/meta/llama-4-scout-17b-16e-instruct-maas": { - "max_tokens": 10000000.0, - "max_input_tokens": 10000000.0, - "max_output_tokens": 10000000.0, + "max_tokens": 10000000, + "max_input_tokens": 10000000, + "max_output_tokens": 10000000, "input_cost_per_token": 2.5e-07, "output_cost_per_token": 7e-07, "litellm_provider": "vertex_ai-llama_models", @@ -9059,9 +9085,9 @@ ] }, "vertex_ai/meta/llama-4-scout-17b-128e-instruct-maas": { - "max_tokens": 10000000.0, - "max_input_tokens": 10000000.0, - "max_output_tokens": 10000000.0, + "max_tokens": 10000000, + "max_input_tokens": 10000000, + "max_output_tokens": 10000000, "input_cost_per_token": 2.5e-07, "output_cost_per_token": 7e-07, "litellm_provider": "vertex_ai-llama_models", @@ -9079,9 +9105,9 @@ ] }, "vertex_ai/meta/llama-4-maverick-17b-128e-instruct-maas": { - "max_tokens": 1000000.0, - "max_input_tokens": 1000000.0, - "max_output_tokens": 1000000.0, + "max_tokens": 1000000, + "max_input_tokens": 1000000, + "max_output_tokens": 1000000, "input_cost_per_token": 3.5e-07, "output_cost_per_token": 1.15e-06, "litellm_provider": "vertex_ai-llama_models", @@ -9099,9 +9125,9 @@ ] }, "vertex_ai/meta/llama-4-maverick-17b-16e-instruct-maas": { - "max_tokens": 1000000.0, - "max_input_tokens": 1000000.0, - "max_output_tokens": 1000000.0, + "max_tokens": 1000000, + "max_input_tokens": 1000000, + "max_output_tokens": 1000000, "input_cost_per_token": 3.5e-07, "output_cost_per_token": 1.15e-06, "litellm_provider": "vertex_ai-llama_models", @@ -9174,7 +9200,7 @@ "max_input_tokens": 128000, "max_output_tokens": 2048, "input_cost_per_token": 5e-06, - "output_cost_per_token": 16e-06, + "output_cost_per_token": 1.6e-05, "litellm_provider": "vertex_ai-llama_models", "mode": "chat", "supports_system_messages": true, @@ -10480,12 +10506,26 @@ "supports_tool_choice": true, "supports_prompt_caching": true }, - "openrouter/bytedance/ui-tars-1.5-7b":{ + "openrouter/x-ai/grok-4": { + "max_tokens": 256000, + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "input_cost_per_token": 3e-06, + "output_cost_per_token": 1.5e-05, + "litellm_provider": "openrouter", + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_reasoning": true, + "source": "https://openrouter.ai/x-ai/grok-4", + "supports_web_search": true + }, + "openrouter/bytedance/ui-tars-1.5-7b": { "max_tokens": 2048, "max_input_tokens": 131072, "max_output_tokens": 2048, - "input_cost_per_token": 0.1e-06, - "output_cost_per_token": 0.2e-06, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 2e-07, "litellm_provider": "openrouter", "mode": "chat", "source": "https://openrouter.ai/api/v1/models/bytedance/ui-tars-1.5-7b", @@ -11164,8 +11204,8 @@ "max_tokens": 8192, "max_input_tokens": 8192, "max_output_tokens": 2048, - "input_cost_per_token": 0.21e-06, - "output_cost_per_token": 0.63e-06, + "input_cost_per_token": 2.1e-07, + "output_cost_per_token": 6.3e-07, "litellm_provider": "openrouter", "mode": "chat", "supports_tool_choice": true @@ -11877,6 +11917,32 @@ "supports_pdf_input": true, "supports_tool_choice": true }, + "anthropic.claude-opus-4-1-20250805-v1:0": { + "max_tokens": 32000, + "max_input_tokens": 200000, + "max_output_tokens": 32000, + "input_cost_per_token": 1.5e-05, + "output_cost_per_token": 7.5e-05, + "search_context_cost_per_query": { + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01, + "search_context_size_high": 0.01 + }, + "cache_creation_input_token_cost": 1.875e-05, + "cache_read_input_token_cost": 1.5e-06, + "litellm_provider": "bedrock_converse", + "mode": "chat", + "supports_function_calling": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159, + "supports_assistant_prefill": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_reasoning": true, + "supports_computer_use": true + }, "anthropic.claude-opus-4-20250514-v1:0": { "max_tokens": 32000, "max_input_tokens": 200000, @@ -12079,6 +12145,32 @@ "supports_tool_choice": true, "supports_reasoning": true }, + "us.anthropic.claude-opus-4-1-20250805-v1:0": { + "max_tokens": 32000, + "max_input_tokens": 200000, + "max_output_tokens": 32000, + "input_cost_per_token": 1.5e-05, + "output_cost_per_token": 7.5e-05, + "search_context_cost_per_query": { + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01, + "search_context_size_high": 0.01 + }, + "cache_creation_input_token_cost": 1.875e-05, + "cache_read_input_token_cost": 1.5e-06, + "litellm_provider": "bedrock_converse", + "mode": "chat", + "supports_function_calling": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159, + "supports_assistant_prefill": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_reasoning": true, + "supports_computer_use": true + }, "us.anthropic.claude-opus-4-20250514-v1:0": { "max_tokens": 32000, "max_input_tokens": 200000, @@ -12252,6 +12344,32 @@ "supports_pdf_input": true, "supports_tool_choice": true }, + "eu.anthropic.claude-opus-4-1-20250805-v1:0": { + "max_tokens": 32000, + "max_input_tokens": 200000, + "max_output_tokens": 32000, + "input_cost_per_token": 1.5e-05, + "output_cost_per_token": 7.5e-05, + "search_context_cost_per_query": { + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01, + "search_context_size_high": 0.01 + }, + "cache_creation_input_token_cost": 1.875e-05, + "cache_read_input_token_cost": 1.5e-06, + "litellm_provider": "bedrock_converse", + "mode": "chat", + "supports_function_calling": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159, + "supports_assistant_prefill": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_reasoning": true, + "supports_computer_use": true + }, "eu.anthropic.claude-opus-4-20250514-v1:0": { "max_tokens": 32000, "max_input_tokens": 200000, @@ -14749,7 +14867,7 @@ "max_tokens": 131072, "max_input_tokens": 131072, "max_output_tokens": 16384, - "input_cost_per_token": 0.6e-06, + "input_cost_per_token": 6e-07, "output_cost_per_token": 2.5e-06, "litellm_provider": "fireworks_ai", "mode": "chat", @@ -14795,6 +14913,58 @@ "source": "https://fireworks.ai/pricing", "supports_tool_choice": false }, + "fireworks_ai/accounts/fireworks/models/glm-4p5": { + "max_tokens": 128000, + "max_input_tokens": 128000, + "max_output_tokens": 96000, + "input_cost_per_token": 5.5e-07, + "output_cost_per_token": 2.19e-06, + "litellm_provider": "fireworks_ai", + "mode": "chat", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "source": "https://fireworks.ai/models/fireworks/glm-4p5" + }, + "fireworks_ai/accounts/fireworks/models/glm-4p5-air": { + "max_tokens": 128000, + "max_input_tokens": 128000, + "max_output_tokens": 96000, + "input_cost_per_token": 2.2e-07, + "output_cost_per_token": 8.8e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "source": "https://artificialanalysis.ai/models/glm-4-5-air" + }, + "fireworks_ai/accounts/fireworks/models/gpt-oss-120b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 1.5e-07, + "output_cost_per_token": 6e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "source": "https://fireworks.ai/pricing" + }, + "fireworks_ai/accounts/fireworks/models/gpt-oss-20b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 5e-08, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "source": "https://fireworks.ai/pricing" + }, "fireworks_ai/nomic-ai/nomic-embed-text-v1.5": { "max_tokens": 8192, "max_input_tokens": 8192, diff --git a/scripts/manage.sh b/scripts/manage.sh index 5a526887..31c8d38a 100644 --- a/scripts/manage.sh +++ b/scripts/manage.sh @@ -454,13 +454,61 @@ EOF print_info "运行初始化设置..." npm run setup - # 安装Web界面依赖 - print_info "安装Web界面依赖..." - npm run install:web + # 获取预构建的前端文件 + print_info "获取预构建的前端文件..." - # 构建前端 - print_info "构建前端界面..." - npm run build:web + # 创建目标目录 + mkdir -p web/admin-spa/dist + + # 从 web-dist 分支获取构建好的文件 + if git ls-remote --heads origin web-dist | grep -q web-dist; then + print_info "从 web-dist 分支下载前端文件..." + + # 创建临时目录用于 clone + TEMP_CLONE_DIR=$(mktemp -d) + + # 使用 sparse-checkout 来只获取需要的文件 + git clone --depth 1 --branch web-dist --single-branch \ + https://github.com/Wei-Shaw/claude-relay-service.git \ + "$TEMP_CLONE_DIR" 2>/dev/null || { + # 如果 HTTPS 失败,尝试使用当前仓库的 remote URL + REPO_URL=$(git config --get remote.origin.url) + git clone --depth 1 --branch web-dist --single-branch "$REPO_URL" "$TEMP_CLONE_DIR" + } + + # 复制文件到目标目录(排除 .git 和 README.md) + rsync -av --exclude='.git' --exclude='README.md' "$TEMP_CLONE_DIR/" web/admin-spa/dist/ 2>/dev/null || { + # 如果没有 rsync,使用 cp + cp -r "$TEMP_CLONE_DIR"/* web/admin-spa/dist/ 2>/dev/null + rm -rf web/admin-spa/dist/.git 2>/dev/null + rm -f web/admin-spa/dist/README.md 2>/dev/null + } + + # 清理临时目录 + rm -rf "$TEMP_CLONE_DIR" + + print_success "前端文件下载完成" + else + print_warning "web-dist 分支不存在,尝试本地构建..." + + # 检查是否有 Node.js 和 npm + if command_exists npm; then + # 回退到原始构建方式 + if [ -f "web/admin-spa/package.json" ]; then + print_info "开始本地构建前端..." + cd web/admin-spa + npm install + npm run build + cd ../.. + print_success "前端本地构建完成" + else + print_error "无法找到前端项目文件" + fi + else + print_error "无法获取前端文件,且本地环境不支持构建" + print_info "请确保仓库已正确配置 web-dist 分支" + fi + fi # 创建systemd服务文件(Linux) if [[ "$OS" == "debian" || "$OS" == "redhat" || "$OS" == "arch" ]]; then @@ -549,11 +597,65 @@ update_service() { # 更新依赖 print_info "更新依赖..." npm install - npm run install:web - # 构建前端 - print_info "构建前端界面..." - npm run build:web + # 获取最新的预构建前端文件 + print_info "更新前端文件..." + + # 创建目标目录 + mkdir -p web/admin-spa/dist + + # 清理旧的前端文件 + rm -rf web/admin-spa/dist/* + + # 从 web-dist 分支获取构建好的文件 + if git ls-remote --heads origin web-dist | grep -q web-dist; then + print_info "从 web-dist 分支下载最新前端文件..." + + # 创建临时目录用于 clone + TEMP_CLONE_DIR=$(mktemp -d) + + # 使用 sparse-checkout 来只获取需要的文件 + git clone --depth 1 --branch web-dist --single-branch \ + https://github.com/Wei-Shaw/claude-relay-service.git \ + "$TEMP_CLONE_DIR" 2>/dev/null || { + # 如果 HTTPS 失败,尝试使用当前仓库的 remote URL + REPO_URL=$(git config --get remote.origin.url) + git clone --depth 1 --branch web-dist --single-branch "$REPO_URL" "$TEMP_CLONE_DIR" + } + + # 复制文件到目标目录(排除 .git 和 README.md) + rsync -av --exclude='.git' --exclude='README.md' "$TEMP_CLONE_DIR/" web/admin-spa/dist/ 2>/dev/null || { + # 如果没有 rsync,使用 cp + cp -r "$TEMP_CLONE_DIR"/* web/admin-spa/dist/ 2>/dev/null + rm -rf web/admin-spa/dist/.git 2>/dev/null + rm -f web/admin-spa/dist/README.md 2>/dev/null + } + + # 清理临时目录 + rm -rf "$TEMP_CLONE_DIR" + + print_success "前端文件更新完成" + else + print_warning "web-dist 分支不存在,尝试本地构建..." + + # 检查是否有 Node.js 和 npm + if command_exists npm; then + # 回退到原始构建方式 + if [ -f "web/admin-spa/package.json" ]; then + print_info "开始本地构建前端..." + cd web/admin-spa + npm install + npm run build + cd ../.. + print_success "前端本地构建完成" + else + print_error "无法找到前端项目文件" + fi + else + print_error "无法获取前端文件,且本地环境不支持构建" + print_info "请确保仓库已正确配置 web-dist 分支" + fi + fi # 启动服务 start_service @@ -678,6 +780,36 @@ restart_service() { start_service } +# 更新模型价格 +update_model_pricing() { + if ! check_installation; then + print_error "服务未安装,请先运行: $0 install" + return 1 + fi + + print_info "更新模型价格数据..." + + cd "$APP_DIR" + + # 运行更新脚本 + if npm run update:pricing; then + print_success "模型价格数据更新完成" + + # 显示更新后的信息 + if [ -f "data/model_pricing.json" ]; then + local model_count=$(grep -o '"[^"]*"\s*:' data/model_pricing.json | wc -l) + local file_size=$(du -h data/model_pricing.json | cut -f1) + echo -e "\n更新信息:" + echo -e " 模型数量: ${GREEN}$model_count${NC}" + echo -e " 文件大小: ${GREEN}$file_size${NC}" + echo -e " 文件位置: $APP_DIR/data/model_pricing.json" + fi + else + print_error "模型价格数据更新失败" + return 1 + fi +} + # 显示状态 show_status() { echo -e "\n${BLUE}=== Claude Relay Service 状态 ===${NC}" @@ -751,15 +883,16 @@ show_help() { echo "用法: $0 [命令]" echo "" echo "命令:" - echo " install - 安装服务" - echo " update - 更新服务" - echo " uninstall - 卸载服务" - echo " start - 启动服务" - echo " stop - 停止服务" - echo " restart - 重启服务" - echo " status - 查看状态" - echo " symlink - 创建 crs 快捷命令" - echo " help - 显示帮助" + echo " install - 安装服务" + echo " update - 更新服务" + echo " uninstall - 卸载服务" + echo " start - 启动服务" + echo " stop - 停止服务" + echo " restart - 重启服务" + echo " status - 查看状态" + echo " update-pricing - 更新模型价格数据" + echo " symlink - 创建 crs 快捷命令" + echo " help - 显示帮助" echo "" } @@ -834,10 +967,11 @@ show_menu() { echo " 3) 停止服务" echo " 4) 重启服务" echo " 5) 更新服务" - echo " 6) 卸载服务" - echo " 7) 退出" + echo " 6) 更新模型价格" + echo " 7) 卸载服务" + echo " 8) 退出" echo "" - echo -n "请输入选项 [1-7]: " + echo -n "请输入选项 [1-8]: " fi } @@ -924,13 +1058,19 @@ handle_menu_choice() { read ;; 6) + echo "" + update_model_pricing + echo -n "按回车键继续..." + read + ;; + 7) echo "" uninstall_service if [ $? -eq 0 ]; then exit 0 fi ;; - 7) + 8) echo "退出管理工具" exit 0 ;; @@ -1109,6 +1249,9 @@ main() { status) show_status ;; + update-pricing) + update_model_pricing + ;; symlink) # 单独创建软链接 # 确保 APP_DIR 已设置 diff --git a/scripts/test-web-dist.sh b/scripts/test-web-dist.sh new file mode 100644 index 00000000..a2b53364 --- /dev/null +++ b/scripts/test-web-dist.sh @@ -0,0 +1,227 @@ +#!/bin/bash + +# 测试 web-dist 分支构建和获取流程 +# 用于验证 CI/CD 流程和 manage.sh 的修改 + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;36m' +NC='\033[0m' # No Color + +print_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +# 测试构建并推送到 web-dist 分支 +test_build_and_push() { + print_info "开始测试构建和推送流程..." + + # 检查是否在项目根目录 + if [ ! -f "package.json" ] || [ ! -d "web/admin-spa" ]; then + print_error "请在项目根目录运行此脚本" + return 1 + fi + + # 构建前端 + print_info "构建前端..." + cd web/admin-spa + + # 检查 node_modules + if [ ! -d "node_modules" ]; then + print_info "安装前端依赖..." + npm install + fi + + # 执行构建 + npm run build + + if [ ! -d "dist" ]; then + print_error "构建失败,dist 目录不存在" + cd ../.. + return 1 + fi + + print_success "前端构建成功" + cd ../.. + + # 创建临时目录保存构建产物 + TEMP_DIR=$(mktemp -d) + print_info "复制构建产物到临时目录: $TEMP_DIR" + cp -r web/admin-spa/dist/* "$TEMP_DIR/" + + # 配置 git + git config user.name "Test User" + git config user.email "test@example.com" + + # 检查 web-dist 分支是否存在 + print_info "检查 web-dist 分支..." + if git ls-remote --heads origin web-dist | grep -q web-dist; then + print_info "web-dist 分支已存在,获取最新版本" + git fetch origin web-dist:web-dist + git checkout web-dist + else + print_info "创建新的 web-dist 分支" + git checkout --orphan web-dist + fi + + # 清空当前目录(保留 .git) + git rm -rf . 2>/dev/null || true + + # 复制构建产物 + cp -r "$TEMP_DIR"/* . + + # 添加 README + cat > README.md << 'EOF' +# Claude Relay Service - Web Frontend Build + +This branch contains the pre-built frontend assets for Claude Relay Service. + +**DO NOT EDIT FILES IN THIS BRANCH DIRECTLY** + +These files are automatically generated by the CI/CD pipeline. + +Test Build Date: $(date -u +"%Y-%m-%d %H:%M:%S UTC") +EOF + + # 提交 + git add -A + git commit -m "test: frontend build test $(date +%Y%m%d%H%M%S)" + + print_success "本地 web-dist 分支创建成功" + print_warning "注意:这只是本地测试,没有推送到远程仓库" + print_info "如需推送,请运行: git push origin web-dist --force" + + # 切换回主分支 + git checkout main + + # 清理临时目录 + rm -rf "$TEMP_DIR" + + print_success "测试完成" +} + +# 测试从 web-dist 分支获取文件 +test_fetch_from_web_dist() { + print_info "测试从 web-dist 分支获取文件..." + + # 创建测试目录 + TEST_DIR="test-web-dist-fetch" + rm -rf "$TEST_DIR" + mkdir -p "$TEST_DIR" + + # 检查远程 web-dist 分支 + if ! git ls-remote --heads origin web-dist | grep -q web-dist; then + print_warning "远程 web-dist 分支不存在" + print_info "尝试使用本地 web-dist 分支..." + + # 检查本地分支 + if ! git branch | grep -q web-dist; then + print_error "本地和远程都没有 web-dist 分支" + rm -rf "$TEST_DIR" + return 1 + fi + fi + + print_info "克隆 web-dist 分支到测试目录..." + + # 创建临时目录用于 clone + TEMP_CLONE_DIR=$(mktemp -d) + + # 获取仓库 URL + REPO_URL=$(git config --get remote.origin.url) + + # 克隆 web-dist 分支 + if git clone --depth 1 --branch web-dist --single-branch "$REPO_URL" "$TEMP_CLONE_DIR" 2>/dev/null; then + print_success "成功克隆 web-dist 分支" + + # 复制文件(排除 .git 和 README.md) + if command -v rsync >/dev/null 2>&1; then + rsync -av --exclude='.git' --exclude='README.md' "$TEMP_CLONE_DIR/" "$TEST_DIR/" + else + cp -r "$TEMP_CLONE_DIR"/* "$TEST_DIR/" 2>/dev/null + rm -rf "$TEST_DIR/.git" 2>/dev/null + rm -f "$TEST_DIR/README.md" 2>/dev/null + fi + + print_success "文件复制成功" + print_info "测试目录内容:" + ls -la "$TEST_DIR" | head -10 + + # 验证关键文件 + if [ -f "$TEST_DIR/index.html" ]; then + print_success "✓ index.html 文件存在" + else + print_error "✗ index.html 文件不存在" + fi + + if [ -d "$TEST_DIR/assets" ]; then + print_success "✓ assets 目录存在" + else + print_error "✗ assets 目录不存在" + fi + + else + print_error "克隆 web-dist 分支失败" + print_info "可能需要先运行: test_build_and_push" + fi + + # 清理 + rm -rf "$TEMP_CLONE_DIR" + rm -rf "$TEST_DIR" + + print_success "获取测试完成" +} + +# 显示帮助 +show_help() { + echo "用法: $0 [命令]" + echo "" + echo "命令:" + echo " build - 测试构建并创建本地 web-dist 分支" + echo " fetch - 测试从 web-dist 分支获取文件" + echo " all - 运行所有测试" + echo " help - 显示帮助" + echo "" +} + +# 主函数 +main() { + case "$1" in + build) + test_build_and_push + ;; + fetch) + test_fetch_from_web_dist + ;; + all) + test_build_and_push + echo "" + test_fetch_from_web_dist + ;; + help) + show_help + ;; + *) + print_error "未知命令: $1" + echo "" + show_help + ;; + esac +} + +# 运行主函数 +main "$@" \ No newline at end of file diff --git a/scripts/update-model-pricing.js b/scripts/update-model-pricing.js new file mode 100644 index 00000000..2af0fa34 --- /dev/null +++ b/scripts/update-model-pricing.js @@ -0,0 +1,262 @@ +#!/usr/bin/env node + +/** + * 手动更新模型价格数据脚本 + * 从 LiteLLM 仓库下载最新的模型价格和上下文窗口信息 + */ + +const fs = require('fs'); +const path = require('path'); +const https = require('https'); + +// 颜色输出 +const colors = { + reset: '\x1b[0m', + bright: '\x1b[1m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[36m', + magenta: '\x1b[35m' +}; + +// 日志函数 +const log = { + info: (msg) => console.log(`${colors.blue}[INFO]${colors.reset} ${msg}`), + success: (msg) => console.log(`${colors.green}[SUCCESS]${colors.reset} ${msg}`), + error: (msg) => console.error(`${colors.red}[ERROR]${colors.reset} ${msg}`), + warn: (msg) => console.warn(`${colors.yellow}[WARNING]${colors.reset} ${msg}`) +}; + +// 配置 +const config = { + dataDir: path.join(process.cwd(), 'data'), + pricingFile: path.join(process.cwd(), 'data', 'model_pricing.json'), + pricingUrl: 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json', + fallbackFile: path.join(process.cwd(), 'resources', 'model-pricing', 'model_prices_and_context_window.json'), + backupFile: path.join(process.cwd(), 'data', 'model_pricing.backup.json'), + timeout: 30000 // 30秒超时 +}; + +// 创建数据目录 +function ensureDataDir() { + if (!fs.existsSync(config.dataDir)) { + fs.mkdirSync(config.dataDir, { recursive: true }); + log.info('Created data directory'); + } +} + +// 备份现有文件 +function backupExistingFile() { + if (fs.existsSync(config.pricingFile)) { + try { + fs.copyFileSync(config.pricingFile, config.backupFile); + log.info('Backed up existing pricing file'); + return true; + } catch (error) { + log.warn(`Failed to backup existing file: ${error.message}`); + return false; + } + } + return false; +} + +// 恢复备份 +function restoreBackup() { + if (fs.existsSync(config.backupFile)) { + try { + fs.copyFileSync(config.backupFile, config.pricingFile); + log.info('Restored from backup'); + return true; + } catch (error) { + log.error(`Failed to restore backup: ${error.message}`); + return false; + } + } + return false; +} + +// 下载价格数据 +function downloadPricingData() { + return new Promise((resolve, reject) => { + log.info(`Downloading model pricing data from LiteLLM...`); + log.info(`URL: ${config.pricingUrl}`); + + const request = https.get(config.pricingUrl, (response) => { + if (response.statusCode !== 200) { + reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`)); + return; + } + + let data = ''; + let downloadedBytes = 0; + + response.on('data', (chunk) => { + data += chunk; + downloadedBytes += chunk.length; + // 显示下载进度 + process.stdout.write(`\rDownloading... ${Math.round(downloadedBytes / 1024)}KB`); + }); + + response.on('end', () => { + process.stdout.write('\n'); // 换行 + try { + const jsonData = JSON.parse(data); + + // 验证数据结构 + if (typeof jsonData !== 'object' || Object.keys(jsonData).length === 0) { + throw new Error('Invalid pricing data structure'); + } + + // 保存到文件 + fs.writeFileSync(config.pricingFile, JSON.stringify(jsonData, null, 2)); + + const modelCount = Object.keys(jsonData).length; + const fileSize = Math.round(fs.statSync(config.pricingFile).size / 1024); + + log.success(`Downloaded pricing data for ${modelCount} models (${fileSize}KB)`); + + // 显示一些统计信息 + const claudeModels = Object.keys(jsonData).filter(k => k.includes('claude')).length; + const gptModels = Object.keys(jsonData).filter(k => k.includes('gpt')).length; + const geminiModels = Object.keys(jsonData).filter(k => k.includes('gemini')).length; + + log.info(`Model breakdown:`); + log.info(` - Claude models: ${claudeModels}`); + log.info(` - GPT models: ${gptModels}`); + log.info(` - Gemini models: ${geminiModels}`); + log.info(` - Other models: ${modelCount - claudeModels - gptModels - geminiModels}`); + + resolve(jsonData); + } catch (error) { + reject(new Error(`Failed to parse pricing data: ${error.message}`)); + } + }); + }); + + request.on('error', (error) => { + reject(new Error(`Network error: ${error.message}`)); + }); + + request.setTimeout(config.timeout, () => { + request.destroy(); + reject(new Error(`Download timeout after ${config.timeout / 1000} seconds`)); + }); + }); +} + +// 使用 fallback 文件 +function useFallback() { + log.warn('Attempting to use fallback pricing data...'); + + if (!fs.existsSync(config.fallbackFile)) { + log.error(`Fallback file not found: ${config.fallbackFile}`); + return false; + } + + try { + const fallbackData = fs.readFileSync(config.fallbackFile, 'utf8'); + const jsonData = JSON.parse(fallbackData); + + // 保存到data目录 + fs.writeFileSync(config.pricingFile, JSON.stringify(jsonData, null, 2)); + + const modelCount = Object.keys(jsonData).length; + log.warn(`Using fallback pricing data for ${modelCount} models`); + log.info('Note: Fallback data may be outdated. Try updating again later.'); + + return true; + } catch (error) { + log.error(`Failed to use fallback: ${error.message}`); + return false; + } +} + +// 显示当前状态 +function showCurrentStatus() { + if (fs.existsSync(config.pricingFile)) { + const stats = fs.statSync(config.pricingFile); + const fileAge = Date.now() - stats.mtime.getTime(); + const ageInHours = Math.round(fileAge / (60 * 60 * 1000)); + const ageInDays = Math.floor(ageInHours / 24); + + let ageString = ''; + if (ageInDays > 0) { + ageString = `${ageInDays} day${ageInDays > 1 ? 's' : ''} and ${ageInHours % 24} hour${(ageInHours % 24) !== 1 ? 's' : ''}`; + } else { + ageString = `${ageInHours} hour${ageInHours !== 1 ? 's' : ''}`; + } + + log.info(`Current pricing file age: ${ageString}`); + + try { + const data = JSON.parse(fs.readFileSync(config.pricingFile, 'utf8')); + log.info(`Current file contains ${Object.keys(data).length} models`); + } catch (error) { + log.warn('Current file exists but could not be parsed'); + } + } else { + log.info('No existing pricing file found'); + } +} + +// 主函数 +async function main() { + console.log(`${colors.bright}${colors.blue}======================================${colors.reset}`); + console.log(`${colors.bright} Model Pricing Update Tool${colors.reset}`); + console.log(`${colors.bright}${colors.blue}======================================${colors.reset}\n`); + + // 显示当前状态 + showCurrentStatus(); + console.log(''); + + // 确保数据目录存在 + ensureDataDir(); + + // 备份现有文件 + const hasBackup = backupExistingFile(); + + try { + // 尝试下载最新数据 + await downloadPricingData(); + + // 清理备份文件(成功下载后) + if (hasBackup && fs.existsSync(config.backupFile)) { + fs.unlinkSync(config.backupFile); + log.info('Cleaned up backup file'); + } + + console.log(`\n${colors.green}✅ Model pricing updated successfully!${colors.reset}`); + process.exit(0); + } catch (error) { + log.error(`Download failed: ${error.message}`); + + // 尝试恢复备份 + if (hasBackup) { + if (restoreBackup()) { + log.info('Original file restored'); + } + } + + // 尝试使用 fallback + if (useFallback()) { + console.log(`\n${colors.yellow}⚠️ Using fallback data (update completed with warnings)${colors.reset}`); + process.exit(0); + } else { + console.log(`\n${colors.red}❌ Failed to update model pricing${colors.reset}`); + process.exit(1); + } + } +} + +// 处理未捕获的错误 +process.on('unhandledRejection', (error) => { + log.error(`Unhandled error: ${error.message}`); + process.exit(1); +}); + +// 运行主函数 +main().catch((error) => { + log.error(`Fatal error: ${error.message}`); + process.exit(1); +}); \ No newline at end of file