From c5cd1ab2c7c431570762e37a13453b55994ee5b9 Mon Sep 17 00:00:00 2001 From: hex2077 Date: Wed, 10 Dec 2025 12:13:11 +0800 Subject: [PATCH] Revert "Merge pull request #103 from leonaii/main" This reverts commit 4f336889fd46193f5d6b32d95738070e4aecb7b0, reversing changes made to 4a645cdf3ffaeb922d97141dbf699807ccc2d240. --- .gitignore | 4 +- .vscode/settings.json | 2 - run-docker.ps1 | 166 ---------- run-docker.sh | 294 +++-------------- .../strategies/OpenAIResponsesConverter.js | 4 - src/provider-models.js | 8 +- src/provider-pool-manager.js | 248 ++++----------- src/service-manager.js | 178 ----------- src/ui-manager.js | 230 -------------- static/app/modal.js | 297 +----------------- static/app/styles.css | 247 --------------- static/app/upload-config-manager.js | 91 ------ static/index.html | 3 - 13 files changed, 116 insertions(+), 1656 deletions(-) delete mode 100644 .vscode/settings.json delete mode 100644 run-docker.ps1 diff --git a/.gitignore b/.gitignore index 3acba33..190bb19 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,4 @@ CLAUDE.md config.json provider_pools.json fetch_system_prompt.txt -input_system_prompt.txt -pwd -import-accounts.ps1 \ No newline at end of file +input_system_prompt.txt \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 7a73a41..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} \ No newline at end of file diff --git a/run-docker.ps1 b/run-docker.ps1 deleted file mode 100644 index 4ff8a5a..0000000 --- a/run-docker.ps1 +++ /dev/null @@ -1,166 +0,0 @@ -# run-docker-with-credentials.ps1 -# 生成指定的Docker运行命令,使用环境变量构建路径 - -Write-Host "正在生成指定的Docker运行命令..." -ForegroundColor Green - -# 设置配置文件路径,使用环境变量 -$AWS_SSO_CACHE_PATH = Join-Path $env:USERPROFILE ".aws\sso\cache" -$GEMINI_CONFIG_PATH = Join-Path $env:USERPROFILE ".gemini\oauth_creds.json" -$CONFIG_JSON_PATH = Join-Path $PSScriptRoot "config.json" -$CONFIG_EXAMPLE_PATH = Join-Path $PSScriptRoot "config.json.example" - -# 设置数据目录映射路径 -$DATA_DIR_PATH = "/home/2api_data" - -# 自动修补config.json -Write-Host "" -Write-Host "检查配置文件..." -ForegroundColor Green - -if (-not (Test-Path $CONFIG_EXAMPLE_PATH)) { - Write-Host "错误:未找到 config.json.example 文件!" -ForegroundColor Red - Read-Host "按Enter键退出" - exit 1 -} - -try { - $exampleConfig = Get-Content $CONFIG_EXAMPLE_PATH -Raw | ConvertFrom-Json - - if (Test-Path $CONFIG_JSON_PATH) { - Write-Host "发现现有配置文件,正在合并新字段..." -ForegroundColor Cyan - $currentConfig = Get-Content $CONFIG_JSON_PATH -Raw | ConvertFrom-Json - - # 合并配置:example中的新字段添加到current中,保留current中已有的值 - $exampleConfig.PSObject.Properties | ForEach-Object { - $key = $_.Name - if (-not ($currentConfig.PSObject.Properties.Name -contains $key)) { - Write-Host " 添加新字段: $key" -ForegroundColor Yellow - $currentConfig | Add-Member -NotePropertyName $key -NotePropertyValue $_.Value -Force - } - } - - # 保存合并后的配置 - $currentConfig | ConvertTo-Json -Depth 10 | Set-Content $CONFIG_JSON_PATH -Encoding UTF8 - Write-Host "配置文件已更新" -ForegroundColor Green - } else { - Write-Host "未找到 config.json,从 config.json.example 创建..." -ForegroundColor Yellow - Copy-Item $CONFIG_EXAMPLE_PATH $CONFIG_JSON_PATH - Write-Host "已创建 config.json,请根据需要修改配置" -ForegroundColor Green - } -} catch { - Write-Host "配置文件处理失败: $_" -ForegroundColor Red - Read-Host "按Enter键退出" - exit 1 -} - -# 检查AWS SSO缓存目录是否存在 -if (Test-Path $AWS_SSO_CACHE_PATH) { - Write-Host "发现AWS SSO缓存目录: $AWS_SSO_CACHE_PATH" -ForegroundColor Cyan -} else { - Write-Host "未找到AWS SSO缓存目录: $AWS_SSO_CACHE_PATH" -ForegroundColor Yellow - Write-Host "注意:AWS SSO缓存目录不存在,Docker容器可能无法访问AWS凭证" -ForegroundColor Yellow -} - -# 检查Gemini配置文件是否存在 -if (Test-Path $GEMINI_CONFIG_PATH) { - Write-Host "发现Gemini配置文件: $GEMINI_CONFIG_PATH" -ForegroundColor Cyan -} else { - Write-Host "未找到Gemini配置文件: $GEMINI_CONFIG_PATH" -ForegroundColor Yellow - Write-Host "注意:Gemini配置文件不存在,Docker容器可能无法访问Gemini API" -ForegroundColor Yellow -} - -# 检查并清理旧容器 -Write-Host "" -Write-Host "检查是否存在旧容器..." -ForegroundColor Green -$containerExists = docker ps -a -q -f name=aiclient2api 2>$null -if ($containerExists) { - Write-Host "发现已存在的容器 'aiclient2api',正在停止并删除..." -ForegroundColor Yellow - docker stop aiclient2api 2>$null | Out-Null - docker rm aiclient2api 2>$null | Out-Null - Write-Host "旧容器已清理" -ForegroundColor Green -} else { - Write-Host "未发现旧容器" -ForegroundColor Cyan -} - -# 检查Docker镜像是否存在 -Write-Host "" -Write-Host "检查Docker镜像..." -ForegroundColor Green -$imageExists = docker images -q aiclient2api 2>$null -if (-not $imageExists) { - Write-Host "未找到Docker镜像 'aiclient2api',开始构建..." -ForegroundColor Yellow - Write-Host "执行: docker build -t aiclient2api ." -ForegroundColor Cyan - docker build -t aiclient2api . - if ($LASTEXITCODE -ne 0) { - Write-Host "Docker镜像构建失败!" -ForegroundColor Red - Read-Host "按Enter键退出" - exit 1 - } - Write-Host "Docker镜像构建成功!" -ForegroundColor Green -} else { - Write-Host "发现Docker镜像 'aiclient2api'" -ForegroundColor Cyan -} - -# 构建Docker运行命令 -# 注意:挂载config.json后,容器会优先使用配置文件中的设置 -# 命令行参数(ARGS)会覆盖config.json中的对应配置 -# 确保数据目录存在 -if (-not (Test-Path $DATA_DIR_PATH)) { - Write-Host "创建数据目录: $DATA_DIR_PATH" -ForegroundColor Yellow - New-Item -ItemType Directory -Path $DATA_DIR_PATH -Force | Out-Null -} - -$volumeMounts = @( - "-v `"${AWS_SSO_CACHE_PATH}:/root/.aws/sso/cache`"" - "-v `"${GEMINI_CONFIG_PATH}:/root/.gemini/oauth_creds.json`"" - "-v `"${DATA_DIR_PATH}:/home/2api_data`"" -) - -# 如果config.json存在,挂载到容器中 -if (Test-Path $CONFIG_JSON_PATH) { - $volumeMounts += "-v `"${CONFIG_JSON_PATH}:/app/config.json`"" - Write-Host "将挂载config.json到容器(可读写)" -ForegroundColor Cyan -} - -Write-Host "将挂载数据目录: $DATA_DIR_PATH -> /home/2api_data" -ForegroundColor Cyan - -$DOCKER_CMD = @( - "docker run -d" - "--restart=always" - "--privileged=true" - "-p 3000:3000" - $volumeMounts -join " " - "--name aiclient2api" - "aiclient2api" -) -join " " - -# 显示将要执行的命令 -Write-Host "" -Write-Host "生成的Docker命令:" -ForegroundColor Green -Write-Host $DOCKER_CMD -ForegroundColor White -Write-Host "" - -# 将命令保存到文件中 -$DOCKER_CMD | Out-File -FilePath "docker-run-command.txt" -Encoding UTF8 -Write-Host "命令已保存到 docker-run-command.txt 文件中,您可以从该文件复制完整的命令。" -ForegroundColor Green - -# 询问用户是否要执行该命令 -Write-Host "" -$EXECUTE_CMD = Read-Host "是否要立即执行该Docker命令?(y/n)" -if ($EXECUTE_CMD -eq "y" -or $EXECUTE_CMD -eq "Y") { - Write-Host "正在执行Docker命令..." -ForegroundColor Green - try { - Invoke-Expression $DOCKER_CMD - if ($LASTEXITCODE -eq 0) { - Write-Host "Docker容器已成功启动!" -ForegroundColor Green - Write-Host "您可以通过 http://localhost:3000 访问API服务" -ForegroundColor Cyan - } else { - Write-Host "Docker命令执行失败,请检查错误信息" -ForegroundColor Red - } - } catch { - Write-Host "Docker命令执行失败: $_" -ForegroundColor Red - } -} else { - Write-Host "命令未执行,您可以手动从docker-run-command.txt文件复制并执行命令" -ForegroundColor Yellow -} - -Write-Host "脚本执行完成" -ForegroundColor Green -Read-Host "按Enter键退出" diff --git a/run-docker.sh b/run-docker.sh index 6684698..c0a0b49 100644 --- a/run-docker.sh +++ b/run-docker.sh @@ -1,281 +1,65 @@ #!/bin/bash -# run-docker.sh -# 本地构建、打包并运行 Docker 容器 +# run-docker-with-credentials.sh +# 生成指定的Docker运行命令,使用HOME环境变量构建路径 -set -e +echo "正在生成指定的Docker运行命令..." -# 颜色定义 -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -CYAN='\033[0;36m' -NC='\033[0m' - -# 日志函数 -log_info() { echo -e "${GREEN}$1${NC}"; } -log_warn() { echo -e "${YELLOW}$1${NC}"; } -log_error() { echo -e "${RED}$1${NC}"; } -log_cyan() { echo -e "${CYAN}$1${NC}"; } - -error_exit() { - log_error "错误: $1" - exit 1 -} - -# 获取脚本目录 -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "$SCRIPT_DIR" || error_exit "无法切换到脚本目录" - -log_info "正在准备 Docker 环境..." -echo "" - -# ========== 前置检查 ========== -log_info "检查前置条件..." - -if ! command -v docker &> /dev/null; then - error_exit "未找到 Docker,请先安装 Docker" -fi - -if ! docker info &> /dev/null; then - error_exit "Docker 未运行,请启动 Docker 服务" -fi - -log_cyan "Docker 环境正常" -echo "" - -# ========== 配置文件处理 ========== -log_info "检查配置文件..." - -DATA_DIR_PATH="/home/2api_data" -CONFIG_JSON_PATH="$DATA_DIR_PATH/config.json" -CONFIG_EXAMPLE_PATH="$SCRIPT_DIR/config.json.example" - -# 确保数据目录存在 -if [ ! -d "$DATA_DIR_PATH" ]; then - log_warn "正在创建数据目录: $DATA_DIR_PATH" - mkdir -p "$DATA_DIR_PATH" || error_exit "创建数据目录失败" -fi - -if [ ! -f "$CONFIG_EXAMPLE_PATH" ]; then - error_exit "未找到 config.json.example 文件!" -fi - -if command -v jq &> /dev/null; then - HAS_JQ=true -else - HAS_JQ=false - log_warn "未安装 jq,使用简单配置处理" -fi - -if [ -f "$CONFIG_JSON_PATH" ]; then - log_cyan "发现现有配置文件" - if [ "$HAS_JQ" = true ]; then - log_cyan "正在合并新字段..." - MERGED=$(jq -s '.[0] * .[1] | .[1] * .' "$CONFIG_EXAMPLE_PATH" "$CONFIG_JSON_PATH" 2>/dev/null) || { - log_warn "配置合并失败,保留现有配置" - MERGED="" - } - if [ -n "$MERGED" ]; then - echo "$MERGED" > "$CONFIG_JSON_PATH" - log_info "配置文件已更新" - fi - else - log_cyan "保留现有配置文件" - fi -else - log_warn "未找到 config.json,从 config.json.example 创建..." - cp "$CONFIG_EXAMPLE_PATH" "$CONFIG_JSON_PATH" || error_exit "无法创建配置文件" - log_info "已创建 config.json,请根据需要修改配置" -fi -echo "" - -# ========== 凭证路径 ========== +# 设置配置文件路径,使用HOME环境变量 AWS_SSO_CACHE_PATH="$HOME/.aws/sso/cache" GEMINI_CONFIG_PATH="$HOME/.gemini/oauth_creds.json" -CONFIGS_DIR_PATH="$DATA_DIR_PATH/configs" -LOGS_DIR_PATH="$DATA_DIR_PATH/logs" +# 检查AWS SSO缓存目录是否存在 if [ -d "$AWS_SSO_CACHE_PATH" ]; then - log_cyan "发现 AWS SSO 缓存: $AWS_SSO_CACHE_PATH" - AWS_MOUNT="-v $AWS_SSO_CACHE_PATH:/root/.aws/sso/cache" + echo "发现AWS SSO缓存目录: $AWS_SSO_CACHE_PATH" else - log_warn "未找到 AWS SSO 缓存: $AWS_SSO_CACHE_PATH" - log_warn "注意: Docker 容器可能无法访问 AWS 凭证" - AWS_MOUNT="" + echo "未找到AWS SSO缓存目录: $AWS_SSO_CACHE_PATH" + echo "注意:AWS SSO缓存目录不存在,Docker容器可能无法访问AWS凭证" fi +# 检查Gemini配置文件是否存在 if [ -f "$GEMINI_CONFIG_PATH" ]; then - log_cyan "发现 Gemini 配置: $GEMINI_CONFIG_PATH" - GEMINI_MOUNT="-v $GEMINI_CONFIG_PATH:/root/.gemini/oauth_creds.json" + echo "发现Gemini配置文件: $GEMINI_CONFIG_PATH" else - log_warn "未找到 Gemini 配置: $GEMINI_CONFIG_PATH" - log_warn "注意: Docker 容器可能无法访问 Gemini API" - GEMINI_MOUNT="" + echo "未找到Gemini配置文件: $GEMINI_CONFIG_PATH" + echo "注意:Gemini配置文件不存在,Docker容器可能无法访问Gemini API" fi -# 数据目录已在上面创建 -log_cyan "发现数据目录: $DATA_DIR_PATH" - -# 确保 configs 目录存在 -if [ ! -d "$CONFIGS_DIR_PATH" ]; then - log_warn "正在创建 configs 目录: $CONFIGS_DIR_PATH" - mkdir -p "$CONFIGS_DIR_PATH" || log_warn "创建 configs 目录失败" -fi - -if [ -d "$CONFIGS_DIR_PATH" ]; then - log_cyan "发现 configs 目录: $CONFIGS_DIR_PATH" - CONFIGS_MOUNT="-v $CONFIGS_DIR_PATH:/app/configs" -else - log_warn "未找到 configs 目录: $CONFIGS_DIR_PATH" - CONFIGS_MOUNT="" -fi - -# 确保 logs 目录存在 -if [ ! -d "$LOGS_DIR_PATH" ]; then - log_warn "正在创建 logs 目录: $LOGS_DIR_PATH" - mkdir -p "$LOGS_DIR_PATH" || log_warn "创建 logs 目录失败" -fi - -if [ -d "$LOGS_DIR_PATH" ]; then - log_cyan "发现 logs 目录: $LOGS_DIR_PATH" - LOGS_MOUNT="-v $LOGS_DIR_PATH:/app/logs" -else - log_warn "未找到 logs 目录: $LOGS_DIR_PATH" - LOGS_MOUNT="" -fi - -# provider_pools.json 文件路径 -PROVIDER_POOLS_PATH="$DATA_DIR_PATH/provider_pools.json" -if [ ! -f "$PROVIDER_POOLS_PATH" ]; then - log_warn "正在创建 provider_pools.json: $PROVIDER_POOLS_PATH" - echo "{}" > "$PROVIDER_POOLS_PATH" || log_warn "创建 provider_pools.json 失败" -fi - -if [ -f "$PROVIDER_POOLS_PATH" ]; then - log_cyan "发现 provider_pools.json: $PROVIDER_POOLS_PATH" - PROVIDER_POOLS_MOUNT="-v $PROVIDER_POOLS_PATH:/app/provider_pools.json" -else - log_warn "未找到 provider_pools.json: $PROVIDER_POOLS_PATH" - PROVIDER_POOLS_MOUNT="" -fi -echo "" - -# ========== 清理旧容器 ========== -log_info "检查是否存在旧容器..." - -CONTAINER_ID=$(docker ps -a -q -f name=aiclient2api 2>/dev/null) -if [ -n "$CONTAINER_ID" ]; then - log_warn "发现已存在的容器 'aiclient2api',正在停止并删除..." - docker stop aiclient2api 2>/dev/null || true - docker rm aiclient2api 2>/dev/null || true - log_info "旧容器已清理" -else - log_cyan "未发现旧容器" -fi -echo "" - -# ========== 构建 Docker 镜像 ========== -log_info "检查 Docker 镜像..." - -FORCE_BUILD=false -if docker images -q aiclient2api 2>/dev/null | grep -q .; then - log_cyan "发现已存在的 Docker 镜像 'aiclient2api'" - read -p "是否强制重新构建镜像?(y/n, 默认 n): " REBUILD_CHOICE - if [ "$REBUILD_CHOICE" = "y" ] || [ "$REBUILD_CHOICE" = "Y" ]; then - FORCE_BUILD=true - log_warn "将删除旧镜像并重新构建..." - docker rmi aiclient2api 2>/dev/null || log_warn "删除旧镜像失败,继续执行..." - fi -else - FORCE_BUILD=true -fi - -if [ "$FORCE_BUILD" = true ]; then - log_info "开始本地构建 Docker 镜像..." - log_cyan "执行: docker build -t aiclient2api ." - echo "" - - if ! docker build -t aiclient2api . ; then - error_exit "Docker 镜像构建失败!请检查 Dockerfile 和源代码" - fi - - log_info "Docker 镜像构建成功!" -fi -echo "" - -# ========== 构建运行命令 ========== -log_info "准备 Docker 运行命令..." - -VOLUME_MOUNTS="" -[ -n "$AWS_MOUNT" ] && VOLUME_MOUNTS="$VOLUME_MOUNTS $AWS_MOUNT" -[ -n "$GEMINI_MOUNT" ] && VOLUME_MOUNTS="$VOLUME_MOUNTS $GEMINI_MOUNT" -[ -n "$CONFIGS_MOUNT" ] && VOLUME_MOUNTS="$VOLUME_MOUNTS $CONFIGS_MOUNT" -[ -n "$LOGS_MOUNT" ] && VOLUME_MOUNTS="$VOLUME_MOUNTS $LOGS_MOUNT" -[ -n "$PROVIDER_POOLS_MOUNT" ] && VOLUME_MOUNTS="$VOLUME_MOUNTS $PROVIDER_POOLS_MOUNT" - -if [ -f "$CONFIG_JSON_PATH" ]; then - VOLUME_MOUNTS="$VOLUME_MOUNTS -v $CONFIG_JSON_PATH:/app/config.json" - log_cyan "将挂载 config.json: $CONFIG_JSON_PATH -> /app/config.json" -fi - -if [ -n "$CONFIGS_MOUNT" ]; then - log_cyan "将挂载 configs: $CONFIGS_DIR_PATH -> /app/configs" -fi - -if [ -n "$LOGS_MOUNT" ]; then - log_cyan "将挂载 logs: $LOGS_DIR_PATH -> /app/logs" -fi - -if [ -n "$PROVIDER_POOLS_MOUNT" ]; then - log_cyan "将挂载 provider_pools.json: $PROVIDER_POOLS_PATH -> /app/provider_pools.json" -fi - -DOCKER_CMD="docker run -d \ - --restart=always \ - --privileged=true \ - -p 3000:3000 \ - $VOLUME_MOUNTS \ - --name aiclient2api \ +# 构建Docker运行命令,使用HOME环境变量构建的路径 +DOCKER_CMD="docker run -d \\ + -u "$(id -u):$(id -g)" \\ + --restart=always \\ + --privileged=true \\ + -p 3000:3000 \\ + -e ARGS=\"--api-key 123456 --host 0.0.0.0\" \\ + -v $AWS_SSO_CACHE_PATH:/root/.aws/sso/cache \\ + -v $GEMINI_CONFIG_PATH:/root/.gemini/oauth_creds.json \\ + --name aiclient2api \\ aiclient2api" -echo "" -log_info "生成的 Docker 命令:" +# 显示将要执行的命令 +echo +echo "生成的Docker命令:" echo "$DOCKER_CMD" -echo "" +echo +# 将命令保存到文件中 echo "$DOCKER_CMD" > docker-run-command.txt -log_info "命令已保存到 docker-run-command.txt" -echo "" - -# ========== 运行容器 ========== -read -p "是否立即执行 Docker 命令?(y/n, 默认 y): " EXECUTE_CMD -EXECUTE_CMD=${EXECUTE_CMD:-y} +echo "命令已保存到 docker-run-command.txt 文件中,您可以从该文件复制完整的命令。" +# 询问用户是否要执行该命令 +echo +read -p "是否要立即执行该Docker命令?(y/n): " EXECUTE_CMD if [ "$EXECUTE_CMD" = "y" ] || [ "$EXECUTE_CMD" = "Y" ]; then - log_info "正在启动 Docker 容器..." - - if eval "$DOCKER_CMD"; then - echo "" - log_info "Docker 容器启动成功!" - log_cyan "访问 API 服务: http://localhost:3000" - echo "" - - log_info "容器状态:" - docker ps -f name=aiclient2api --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" - - sleep 3 - if docker ps -q -f name=aiclient2api -f status=running | grep -q .; then - log_info "容器运行正常" - else - log_warn "容器可能启动失败,正在检查日志:" - docker logs aiclient2api 2>&1 | tail -20 - fi + echo "正在执行Docker命令..." + eval "$DOCKER_CMD" + if [ $? -eq 0 ]; then + echo "Docker容器已成功启动!" + echo "您可以通过 http://localhost:3000 访问API服务" else - error_exit "Docker 容器启动失败!" + echo "Docker命令执行失败,请检查错误信息" fi else - log_warn "命令未执行,您可以从 docker-run-command.txt 复制命令手动执行" + echo "命令未执行,您可以手动从docker-run-command.txt文件复制并执行命令" fi -echo "" -log_info "脚本执行完成" +echo "脚本执行完成" \ No newline at end of file diff --git a/src/converters/strategies/OpenAIResponsesConverter.js b/src/converters/strategies/OpenAIResponsesConverter.js index 1c635d8..dfd7e06 100644 --- a/src/converters/strategies/OpenAIResponsesConverter.js +++ b/src/converters/strategies/OpenAIResponsesConverter.js @@ -257,10 +257,6 @@ export class OpenAIResponsesConverter extends BaseConverter { // 如果有标准的 messages 字段,也支持 if (responsesRequest.messages && Array.isArray(responsesRequest.messages)) { - const { systemMessages, otherMessages } = extractSystemMessages( - responsesRequest.messages - ); - if (!claudeRequest.system && systemMessages.length > 0) { const systemTexts = systemMessages.map(msg => extractText(msg.content)); claudeRequest.system = systemTexts.join('\n'); diff --git a/src/provider-models.js b/src/provider-models.js index c629405..f2de251 100644 --- a/src/provider-models.js +++ b/src/provider-models.js @@ -25,11 +25,11 @@ export const PROVIDER_MODELS = { 'claude-opus-4-5', 'claude-haiku-4-5', 'claude-sonnet-4-5', - // 'claude-sonnet-4-5-20250929', + 'claude-sonnet-4-5-20250929', 'claude-sonnet-4-20250514', - // 'claude-3-7-sonnet-20250219', - // 'amazonq-claude-sonnet-4-20250514', - // 'amazonq-claude-3-7-sonnet-20250219' + 'claude-3-7-sonnet-20250219', + 'amazonq-claude-sonnet-4-20250514', + 'amazonq-claude-3-7-sonnet-20250219' ], 'openai-custom': [], 'openaiResponses-custom': [], diff --git a/src/provider-pool-manager.js b/src/provider-pool-manager.js index f073c74..1a2ad0a 100644 --- a/src/provider-pool-manager.js +++ b/src/provider-pool-manager.js @@ -1,7 +1,6 @@ import * as fs from 'fs'; // Import fs module import { getServiceAdapter } from './adapter.js'; import { MODEL_PROVIDER } from './common.js'; -import axios from 'axios'; /** * Manages a pool of API service providers, handling their health and selection. @@ -12,8 +11,7 @@ export class ProviderPoolManager { 'gemini-cli': 'gemini-2.5-flash', 'openai-custom': 'gpt-3.5-turbo', 'claude-custom': 'claude-3-7-sonnet-20250219', - 'kiro-api': 'claude-haiku-4-5', - 'claude-kiro-oauth': 'claude-haiku-4-5', + 'kiro-api': 'claude-3-7-sonnet-20250219', 'qwen-api': 'qwen3-coder-flash', 'openai-custom-responses': 'gpt-5-low' }; @@ -83,11 +81,6 @@ export class ProviderPoolManager { providerConfig.lastErrorTime = providerConfig.lastErrorTime instanceof Date ? providerConfig.lastErrorTime.toISOString() : (providerConfig.lastErrorTime || null); - - // 健康检测相关字段 - providerConfig.lastHealthCheckTime = providerConfig.lastHealthCheckTime || null; - providerConfig.lastHealthCheckModel = providerConfig.lastHealthCheckModel || null; - providerConfig.lastErrorMessage = providerConfig.lastErrorMessage || null; this.providerStatus[providerType].push({ config: providerConfig, @@ -172,9 +165,8 @@ export class ProviderPoolManager { * Marks a provider as unhealthy (e.g., after an API error). * @param {string} providerType - The type of the provider. * @param {object} providerConfig - The configuration of the provider to mark. - * @param {string} [errorMessage] - Optional error message to store. */ - markProviderUnhealthy(providerType, providerConfig, errorMessage = null) { + markProviderUnhealthy(providerType, providerConfig) { if (!providerConfig?.uuid) { this._log('error', 'Invalid providerConfig in markProviderUnhealthy'); return; @@ -184,11 +176,6 @@ export class ProviderPoolManager { if (provider) { provider.config.errorCount++; provider.config.lastErrorTime = new Date().toISOString(); - - // 保存错误信息 - if (errorMessage) { - provider.config.lastErrorMessage = errorMessage; - } if (provider.config.errorCount >= this.maxErrorCount) { provider.config.isHealthy = false; @@ -205,10 +192,9 @@ export class ProviderPoolManager { * Marks a provider as healthy. * @param {string} providerType - The type of the provider. * @param {object} providerConfig - The configuration of the provider to mark. - * @param {boolean} resetUsageCount - Whether to reset usage count (optional, default: false). - * @param {string} [healthCheckModel] - Optional model name used for health check. + * @param {boolean} isInit - Whether to reset usage count (optional, default: false). */ - markProviderHealthy(providerType, providerConfig, resetUsageCount = false, healthCheckModel = null) { + markProviderHealthy(providerType, providerConfig, resetUsageCount = false) { if (!providerConfig?.uuid) { this._log('error', 'Invalid providerConfig in markProviderHealthy'); return; @@ -219,14 +205,6 @@ export class ProviderPoolManager { provider.config.isHealthy = true; provider.config.errorCount = 0; provider.config.lastErrorTime = null; - provider.config.lastErrorMessage = null; - - // 更新健康检测信息 - provider.config.lastHealthCheckTime = new Date().toISOString(); - if (healthCheckModel) { - provider.config.lastHealthCheckModel = healthCheckModel; - } - // 只有在明确要求重置使用计数时才重置 if (resetUsageCount) { provider.config.usageCount = 0; @@ -317,224 +295,121 @@ export class ProviderPoolManager { try { // Perform actual health check based on provider type - const healthResult = await this._checkProviderHealth(providerType, providerConfig); + const isHealthy = await this._checkProviderHealth(providerType, providerConfig); - if (healthResult === null) { + if (isHealthy === null) { this._log('debug', `Health check for ${providerConfig.uuid} (${providerType}) skipped: Check not implemented.`); this.resetProviderCounters(providerType, providerConfig); continue; } - if (healthResult.success) { + if (isHealthy) { if (!providerStatus.config.isHealthy) { // Provider was unhealthy but is now healthy // 恢复健康时不重置使用计数,保持原有值 - this.markProviderHealthy(providerType, providerConfig, true, healthResult.modelName); + this.markProviderHealthy(providerType, providerConfig, true); this._log('info', `Health check for ${providerConfig.uuid} (${providerType}): Marked Healthy (actual check)`); } else { // Provider was already healthy and still is // 只在初始化时重置使用计数 - this.markProviderHealthy(providerType, providerConfig, true, healthResult.modelName); + this.markProviderHealthy(providerType, providerConfig, true); this._log('debug', `Health check for ${providerConfig.uuid} (${providerType}): Still Healthy`); } } else { // Provider is not healthy - this._log('warn', `Health check for ${providerConfig.uuid} (${providerType}) failed: ${healthResult.errorMessage || 'Provider is not responding correctly.'}`); - this.markProviderUnhealthy(providerType, providerConfig, healthResult.errorMessage); - - // 更新健康检测时间和模型(即使失败也记录) - providerStatus.config.lastHealthCheckTime = new Date().toISOString(); - if (healthResult.modelName) { - providerStatus.config.lastHealthCheckModel = healthResult.modelName; - } + this._log('warn', `Health check for ${providerConfig.uuid} (${providerType}) failed: Provider is not responding correctly.`); + this.markProviderUnhealthy(providerType, providerConfig); } } catch (error) { this._log('error', `Health check for ${providerConfig.uuid} (${providerType}) failed: ${error.message}`); // If a health check fails, mark it unhealthy, which will update error count and lastErrorTime - this.markProviderUnhealthy(providerType, providerConfig, error.message); + this.markProviderUnhealthy(providerType, providerConfig); } } } } /** - * 构建健康检查请求(返回多种格式用于重试) + * 构建健康检查请求 * @private - * @returns {Array} 请求格式数组,按优先级排序 */ - _buildHealthCheckRequests(providerType, modelName) { - const baseMessage = { role: 'user', content: 'Hi' }; - const requests = []; + _buildHealthCheckRequest(providerType, modelName) { + const baseMessage = { role: 'user', content: 'Hello, are you ok?' }; - // Gemini 使用 contents 格式 + // Gemini 使用不同的请求格式 if (providerType.startsWith('gemini')) { - requests.push({ + return { contents: [{ role: 'user', parts: [{ text: baseMessage.content }] }] - }); - return requests; - } - - // Kiro OAuth 同时支持 messages 和 contents 格式 - if (providerType.startsWith('claude-kiro')) { - // 优先使用 messages 格式 - requests.push({ - messages: [baseMessage], - model: modelName, - max_tokens: 1 - }); - // 备用 contents 格式 - requests.push({ - contents: [{ - role: 'user', - parts: [{ text: baseMessage.content }] - }], - max_tokens: 1 - }); - return requests; + }; } // OpenAI Custom Responses 使用特殊格式 if (providerType === MODEL_PROVIDER.OPENAI_CUSTOM_RESPONSES) { - requests.push({ + return { input: [baseMessage], model: modelName - }); - return requests; + }; } - // 其他提供商(OpenAI、Claude、Qwen)使用标准 messages 格式 - requests.push({ + // 其他提供商(OpenAI、Claude、Kiro、Qwen)使用标准格式 + return { messages: [baseMessage], model: modelName - }); - - return requests; + }; } /** * Performs an actual health check for a specific provider. * @param {string} providerType - The type of the provider. * @param {object} providerConfig - The configuration of the provider to check. - * @returns {Promise<{success: boolean, modelName: string, errorMessage: string}|null>} - Health check result object or null if check not implemented. + * @returns {Promise} - True if healthy, false if unhealthy, null if check not implemented. */ async _checkProviderHealth(providerType, providerConfig) { - // 确定健康检查使用的模型名称 - const modelName = providerConfig.checkModelName || - ProviderPoolManager.DEFAULT_HEALTH_CHECK_MODELS[providerType]; - - // 如果未启用健康检查,返回 null - if (!providerConfig.checkHealth) { - return null; - } - - if (!modelName) { - this._log('warn', `Unknown provider type for health check: ${providerType}`); - return { success: false, modelName: null, errorMessage: 'Unknown provider type for health check' }; - } - - // 使用内部服务适配器方式进行健康检查 - const proxyKeys = ['GEMINI', 'OPENAI', 'CLAUDE', 'QWEN', 'KIRO']; - const tempConfig = { - ...providerConfig, - MODEL_PROVIDER: providerType - }; - - proxyKeys.forEach(key => { - const proxyKey = `USE_SYSTEM_PROXY_${key}`; - if (this.globalConfig[proxyKey] !== undefined) { - tempConfig[proxyKey] = this.globalConfig[proxyKey]; - } - }); - - const serviceAdapter = getServiceAdapter(tempConfig); - - // 获取所有可能的请求格式 - const healthCheckRequests = this._buildHealthCheckRequests(providerType, modelName); - - // 重试机制:尝试不同的请求格式 - const maxRetries = healthCheckRequests.length; - let lastError = null; - - for (let i = 0; i < maxRetries; i++) { - const healthCheckRequest = healthCheckRequests[i]; - try { - this._log('debug', `Health check attempt ${i + 1}/${maxRetries} for ${modelName}: ${JSON.stringify(healthCheckRequest)}`); - await serviceAdapter.generateContent(modelName, healthCheckRequest); - return { success: true, modelName, errorMessage: null }; - } catch (error) { - lastError = error; - this._log('debug', `Health check attempt ${i + 1} failed for ${providerType}: ${error.message}`); - // 继续尝试下一个格式 - } - } - - // 所有尝试都失败 - this._log('error', `Health check failed for ${providerType} after ${maxRetries} attempts: ${lastError?.message}`); - return { success: false, modelName, errorMessage: lastError?.message || 'All health check attempts failed' }; - } - - /** - * 通过 HTTP API 进行健康检查(更通用、更节省 tokens) - * @private - */ - async _httpHealthCheck(providerType, providerConfig, modelName) { - // 确定 API URL 和 Key - let baseUrl = null; - let apiKey = null; - - // 根据 provider 类型获取配置 - if (providerType.includes('kiro') || providerType.includes('claude-kiro')) { - baseUrl = providerConfig.KIRO_BASE_URL; - apiKey = providerConfig.KIRO_API_KEY; - } else if (providerType.includes('openai')) { - baseUrl = providerConfig.OPENAI_BASE_URL; - apiKey = providerConfig.OPENAI_API_KEY; - } else if (providerType.includes('claude')) { - baseUrl = providerConfig.CLAUDE_BASE_URL; - apiKey = providerConfig.CLAUDE_API_KEY; - } else if (providerType.includes('qwen')) { - baseUrl = providerConfig.QWEN_BASE_URL; - apiKey = providerConfig.QWEN_API_KEY; - } - - // 如果没有配置 HTTP API,返回 null 让调用方回退到其他方式 - if (!baseUrl || !apiKey) { - return null; - } - - // 确保 URL 格式正确 - const url = baseUrl.endsWith('/') - ? `${baseUrl}chat/completions` - : `${baseUrl}/chat/completions`; - - this._log('debug', `HTTP health check for ${providerType}: ${url}`); - try { - const response = await axios.post(url, { - model: modelName, - messages: [{ role: 'user', content: 'hi' }], - max_tokens: 1 // 最节省 tokens - }, { - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiKey}` - }, - timeout: 30000 // 30秒超时 + // 如果未启用健康检查,返回 null + if (!providerConfig.checkHealth) { + return null; + } + + // 合并全局配置和 provider 配置(简化代理配置) + const proxyKeys = ['GEMINI', 'OPENAI', 'CLAUDE', 'QWEN', 'KIRO']; + const tempConfig = { + ...providerConfig, + MODEL_PROVIDER: providerType + }; + + // 动态添加代理配置 + proxyKeys.forEach(key => { + const proxyKey = `USE_SYSTEM_PROXY_${key}`; + if (this.globalConfig[proxyKey] !== undefined) { + tempConfig[proxyKey] = this.globalConfig[proxyKey]; + } }); - if (response.status === 200) { - return { success: true, modelName, errorMessage: null }; - } else { - return { success: false, modelName, errorMessage: `HTTP ${response.status}` }; + const serviceAdapter = getServiceAdapter(tempConfig); + + // 确定健康检查使用的模型名称 + const modelName = providerConfig.checkModelName || + ProviderPoolManager.DEFAULT_HEALTH_CHECK_MODELS[providerType]; + + if (!modelName) { + this._log('warn', `Unknown provider type for health check: ${providerType}`); + return false; } + + // 构建健康检查请求 + const healthCheckRequest = this._buildHealthCheckRequest(providerType, modelName); + + this._log('debug', `Health check request for ${modelName}: ${JSON.stringify(healthCheckRequest)}`); + await serviceAdapter.generateContent(modelName, healthCheckRequest); + return true; } catch (error) { - const errorMsg = error.response?.data?.error?.message || error.message; - this._log('error', `HTTP health check failed: ${errorMsg}`); - return { success: false, modelName, errorMessage: errorMsg }; + this._log('error', `Health check failed for ${providerType}: ${error.message}`); + return false; } } @@ -597,9 +472,6 @@ export class ProviderPoolManager { if (config.lastErrorTime instanceof Date) { config.lastErrorTime = config.lastErrorTime.toISOString(); } - if (config.lastHealthCheckTime instanceof Date) { - config.lastHealthCheckTime = config.lastHealthCheckTime.toISOString(); - } return config; }); } else { diff --git a/src/service-manager.js b/src/service-manager.js index cb5e414..123cefc 100644 --- a/src/service-manager.js +++ b/src/service-manager.js @@ -1,194 +1,16 @@ import { getServiceAdapter, serviceInstances } from './adapter.js'; import { ProviderPoolManager } from './provider-pool-manager.js'; import deepmerge from 'deepmerge'; -import * as fs from 'fs'; -import { promises as pfs } from 'fs'; -import * as path from 'path'; // 存储 ProviderPoolManager 实例 let providerPoolManager = null; -/** - * 生成 UUID - * @returns {string} UUID 字符串 - */ -function generateUUID() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - const r = Math.random() * 16 | 0; - const v = c === 'x' ? r : (r & 0x3 | 0x8); - return v.toString(16); - }); -} - -/** - * 扫描 configs/kiro 目录并自动关联未关联的配置文件到 claude-kiro-oauth 提供商 - * @param {Object} config - 服务器配置对象 - * @returns {Promise} 更新后的 providerPools 对象 - */ -async function autoLinkKiroConfigs(config) { - const kiroConfigsPath = path.join(process.cwd(), 'configs', 'kiro'); - const providerType = 'claude-kiro-oauth'; - const defaultCheckModel = 'claude-haiku-4-5'; - - // 确保 providerPools 对象存在 - if (!config.providerPools) { - config.providerPools = {}; - } - - // 确保 claude-kiro-oauth 数组存在 - if (!config.providerPools[providerType]) { - config.providerPools[providerType] = []; - } - - // 检查 configs/kiro 目录是否存在 - if (!fs.existsSync(kiroConfigsPath)) { - console.log('[Auto-Link] configs/kiro directory not found, skipping auto-link'); - return config.providerPools; - } - - // 获取已关联的配置文件路径集合 - const linkedPaths = new Set(); - for (const provider of config.providerPools[providerType]) { - if (provider.KIRO_OAUTH_CREDS_FILE_PATH) { - // 标准化路径以便比较 - const normalizedPath = provider.KIRO_OAUTH_CREDS_FILE_PATH.replace(/\\/g, '/'); - linkedPaths.add(normalizedPath); - linkedPaths.add(normalizedPath.replace(/^\.\//, '')); - if (!normalizedPath.startsWith('./')) { - linkedPaths.add('./' + normalizedPath); - } - } - } - - // 递归扫描 configs/kiro 目录 - const newProviders = []; - await scanKiroDirectory(kiroConfigsPath, linkedPaths, newProviders, defaultCheckModel); - - // 如果有新的配置文件需要关联 - if (newProviders.length > 0) { - config.providerPools[providerType].push(...newProviders); - - // 保存更新后的 provider_pools.json - const filePath = config.PROVIDER_POOLS_FILE_PATH || 'provider_pools.json'; - try { - await pfs.writeFile(filePath, JSON.stringify(config.providerPools, null, 2), 'utf8'); - console.log(`[Auto-Link] Added ${newProviders.length} new Kiro config(s) to ${providerType}`); - newProviders.forEach(p => { - console.log(` - ${p.KIRO_OAUTH_CREDS_FILE_PATH}`); - }); - } catch (error) { - console.error(`[Auto-Link] Failed to save provider_pools.json: ${error.message}`); - } - } else { - console.log('[Auto-Link] No new Kiro configs to link'); - } - - return config.providerPools; -} - -/** - * 递归扫描 Kiro 配置目录 - * @param {string} dirPath - 目录路径 - * @param {Set} linkedPaths - 已关联的路径集合 - * @param {Array} newProviders - 新提供商配置数组 - * @param {string} defaultCheckModel - 默认检测模型 - */ -async function scanKiroDirectory(dirPath, linkedPaths, newProviders, defaultCheckModel) { - try { - const files = await pfs.readdir(dirPath, { withFileTypes: true }); - - for (const file of files) { - const fullPath = path.join(dirPath, file.name); - - if (file.isFile()) { - const ext = path.extname(file.name).toLowerCase(); - // 只处理 JSON 文件 - if (ext === '.json') { - const relativePath = path.relative(process.cwd(), fullPath).replace(/\\/g, '/'); - - // 检查是否已关联 - const isLinked = linkedPaths.has(relativePath) || - linkedPaths.has('./' + relativePath) || - linkedPaths.has(relativePath.replace(/^\.\//, '')); - - if (!isLinked) { - // 验证是否是有效的 OAuth 凭据文件 - const isValid = await isValidKiroCredentials(fullPath); - if (isValid) { - // 创建新的提供商配置 - const newProvider = { - KIRO_OAUTH_CREDS_FILE_PATH: './' + relativePath, - uuid: generateUUID(), - checkModelName: defaultCheckModel, - checkHealth: true, - isHealthy: true, - isDisabled: false, - lastUsed: null, - usageCount: 0, - errorCount: 0, - lastErrorTime: null, - lastHealthCheckTime: null, - lastHealthCheckModel: null, - lastErrorMessage: null - }; - newProviders.push(newProvider); - } - } - } - } else if (file.isDirectory()) { - // 递归扫描子目录(限制深度为 3 层) - const relativePath = path.relative(process.cwd(), fullPath); - const depth = relativePath.split(path.sep).length; - if (depth < 5) { // configs/kiro/subfolder/subsubfolder - await scanKiroDirectory(fullPath, linkedPaths, newProviders, defaultCheckModel); - } - } - } - } catch (error) { - console.warn(`[Auto-Link] Failed to scan directory ${dirPath}: ${error.message}`); - } -} - -/** - * 验证文件是否是有效的 Kiro OAuth 凭据文件 - * @param {string} filePath - 文件路径 - * @returns {Promise} 是否有效 - */ -async function isValidKiroCredentials(filePath) { - try { - const content = await pfs.readFile(filePath, 'utf8'); - const jsonData = JSON.parse(content); - - // 检查是否包含 OAuth 相关字段 - // Kiro 凭据通常包含 access_token, refresh_token, client_id 等字段 - if (jsonData.access_token || jsonData.refresh_token || - jsonData.client_id || jsonData.client_secret || - jsonData.token || jsonData.credentials) { - return true; - } - - // 也可能是包含嵌套结构的凭据文件 - if (jsonData.installed || jsonData.web) { - return true; - } - - return false; - } catch (error) { - // 如果无法解析,认为不是有效的凭据文件 - return false; - } -} - /** * Initialize API services and provider pool manager * @param {Object} config - The server configuration * @returns {Promise} The initialized services */ export async function initApiService(config) { - // 自动关联 configs/kiro 目录中的配置文件 - console.log('[Initialization] Checking for unlinked Kiro configs...'); - await autoLinkKiroConfigs(config); - if (config.providerPools && Object.keys(config.providerPools).length > 0) { providerPoolManager = new ProviderPoolManager(config.providerPools, { globalConfig: config, diff --git a/src/ui-manager.js b/src/ui-manager.js index 7e12133..e680593 100644 --- a/src/ui-manager.js +++ b/src/ui-manager.js @@ -1091,117 +1091,6 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo } } - // Perform health check for all providers of a specific type - const healthCheckMatch = pathParam.match(/^\/api\/providers\/([^\/]+)\/health-check$/); - if (method === 'POST' && healthCheckMatch) { - const providerType = decodeURIComponent(healthCheckMatch[1]); - - try { - if (!providerPoolManager) { - res.writeHead(400, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: { message: 'Provider pool manager not initialized' } })); - return true; - } - - const providers = providerPoolManager.providerStatus[providerType] || []; - - if (providers.length === 0) { - res.writeHead(404, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: { message: 'No providers found for this type' } })); - return true; - } - - console.log(`[UI API] Starting health check for ${providers.length} providers in ${providerType}`); - - // 执行健康检测 - const results = []; - for (const providerStatus of providers) { - const providerConfig = providerStatus.config; - try { - const healthResult = await providerPoolManager._checkProviderHealth(providerType, providerConfig); - - if (healthResult === null) { - results.push({ - uuid: providerConfig.uuid, - success: null, - message: '未启用健康检测 (checkHealth=false)' - }); - continue; - } - - if (healthResult.success) { - providerPoolManager.markProviderHealthy(providerType, providerConfig, false, healthResult.modelName); - results.push({ - uuid: providerConfig.uuid, - success: true, - modelName: healthResult.modelName, - message: '健康' - }); - } else { - providerPoolManager.markProviderUnhealthy(providerType, providerConfig, healthResult.errorMessage); - providerStatus.config.lastHealthCheckTime = new Date().toISOString(); - if (healthResult.modelName) { - providerStatus.config.lastHealthCheckModel = healthResult.modelName; - } - results.push({ - uuid: providerConfig.uuid, - success: false, - modelName: healthResult.modelName, - message: healthResult.errorMessage || '检测失败' - }); - } - } catch (error) { - providerPoolManager.markProviderUnhealthy(providerType, providerConfig, error.message); - results.push({ - uuid: providerConfig.uuid, - success: false, - message: error.message - }); - } - } - - // 保存更新后的状态到文件 - const filePath = currentConfig.PROVIDER_POOLS_FILE_PATH || 'provider_pools.json'; - - // 从 providerStatus 构建 providerPools 对象并保存 - const providerPools = {}; - for (const pType in providerPoolManager.providerStatus) { - providerPools[pType] = providerPoolManager.providerStatus[pType].map(ps => ps.config); - } - writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf8'); - - const successCount = results.filter(r => r.success === true).length; - const failCount = results.filter(r => r.success === false).length; - - console.log(`[UI API] Health check completed for ${providerType}: ${successCount} healthy, ${failCount} unhealthy`); - - // 广播更新事件 - broadcastEvent('config_update', { - action: 'health_check', - filePath: filePath, - providerType, - results, - timestamp: new Date().toISOString() - }); - - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - success: true, - message: `健康检测完成: ${successCount} 个健康, ${failCount} 个异常`, - successCount, - failCount, - totalCount: providers.length, - results - })); - return true; - } catch (error) { - console.error('[UI API] Health check error:', error); - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: { message: error.message } })); - return true; - } - } - // Generate OAuth authorization URL for providers const generateAuthUrlMatch = pathParam.match(/^\/api\/providers\/([^\/]+)\/generate-auth-url$/); if (method === 'POST' && generateAuthUrlMatch) { @@ -1420,125 +1309,6 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo } } - // Quick link Kiro config to claude-kiro-oauth - if (method === 'POST' && pathParam === '/api/quick-link-kiro') { - try { - const body = await getRequestBody(req); - const { filePath } = body; - - if (!filePath) { - res.writeHead(400, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: { message: 'filePath is required' } })); - return true; - } - - const providerType = 'claude-kiro-oauth'; - const defaultCheckModel = 'claude-haiku-4-5'; - const poolsFilePath = currentConfig.PROVIDER_POOLS_FILE_PATH || 'provider_pools.json'; - - // Load existing pools - let providerPools = {}; - if (existsSync(poolsFilePath)) { - try { - const fileContent = readFileSync(poolsFilePath, 'utf8'); - providerPools = JSON.parse(fileContent); - } catch (readError) { - console.warn('[UI API] Failed to read existing provider pools:', readError.message); - } - } - - // Ensure claude-kiro-oauth array exists - if (!providerPools[providerType]) { - providerPools[providerType] = []; - } - - // Check if already linked - const normalizedPath = filePath.replace(/\\/g, '/'); - const isAlreadyLinked = providerPools[providerType].some(p => { - if (!p.KIRO_OAUTH_CREDS_FILE_PATH) return false; - const existingPath = p.KIRO_OAUTH_CREDS_FILE_PATH.replace(/\\/g, '/'); - return existingPath === normalizedPath || - existingPath === './' + normalizedPath || - './' + existingPath === normalizedPath; - }); - - if (isAlreadyLinked) { - res.writeHead(400, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: { message: '该配置文件已关联' } })); - return true; - } - - // Generate UUID - const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - const r = Math.random() * 16 | 0; - const v = c == 'x' ? r : (r & 0x3 | 0x8); - return v.toString(16); - }); - - // Create new provider config - const newProvider = { - KIRO_OAUTH_CREDS_FILE_PATH: normalizedPath.startsWith('./') ? normalizedPath : './' + normalizedPath, - uuid: uuid, - checkModelName: defaultCheckModel, - checkHealth: true, - isHealthy: true, - isDisabled: false, - lastUsed: null, - usageCount: 0, - errorCount: 0, - lastErrorTime: null, - lastHealthCheckTime: null, - lastHealthCheckModel: null, - lastErrorMessage: null - }; - - providerPools[providerType].push(newProvider); - - // Save to file - writeFileSync(poolsFilePath, JSON.stringify(providerPools, null, 2), 'utf8'); - console.log(`[UI API] Quick linked Kiro config: ${filePath} -> ${providerType}`); - - // Update provider pool manager if available - if (providerPoolManager) { - providerPoolManager.providerPools = providerPools; - providerPoolManager.initializeProviderStatus(); - } - - // Broadcast update event - broadcastEvent('config_update', { - action: 'quick_link', - filePath: poolsFilePath, - providerType, - newProvider, - timestamp: new Date().toISOString() - }); - - broadcastEvent('provider_update', { - action: 'add', - providerType, - providerConfig: newProvider, - timestamp: new Date().toISOString() - }); - - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - success: true, - message: '配置已成功关联到 claude-kiro-oauth', - provider: newProvider - })); - return true; - } catch (error) { - console.error('[UI API] Quick link failed:', error); - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - error: { - message: '关联失败: ' + error.message - } - })); - return true; - } - } - // Reload configuration files if (method === 'POST' && pathParam === '/api/reload-config') { try { diff --git a/static/app/modal.js b/static/app/modal.js index e0dc45a..f2a717b 100644 --- a/static/app/modal.js +++ b/static/app/modal.js @@ -3,13 +3,6 @@ import { showToast, getFieldLabel, getProviderTypeFields } from './utils.js'; import { handleProviderPasswordToggle } from './event-handlers.js'; -// 分页配置 -const PROVIDERS_PER_PAGE = 20; -let currentPage = 1; -let currentProviders = []; -let currentProviderType = ''; -let cachedModels = []; // 缓存模型列表 - /** * 显示提供商管理模态框 * @param {Object} data - 提供商数据 @@ -17,12 +10,6 @@ let cachedModels = []; // 缓存模型列表 function showProviderManagerModal(data) { const { providerType, providers, totalCount, healthyCount } = data; - // 保存当前数据用于分页 - currentProviders = providers; - currentProviderType = providerType; - currentPage = 1; - cachedModels = []; - // 移除已存在的模态框 const existingModal = document.querySelector('.provider-modal'); if (existingModal) { @@ -33,8 +20,6 @@ function showProviderManagerModal(data) { existingModal.remove(); } - const totalPages = Math.ceil(providers.length / PROVIDERS_PER_PAGE); - // 创建模态框 const modal = document.createElement('div'); modal.className = 'provider-modal'; @@ -64,19 +49,12 @@ function showProviderManagerModal(data) { - - ${totalPages > 1 ? renderPagination(1, totalPages, providers.length) : ''} -
- ${renderProviderListPaginated(providers, 1)} + ${renderProviderList(providers)}
- - ${totalPages > 1 ? renderPagination(1, totalPages, providers.length, 'bottom') : ''} `; @@ -88,158 +66,20 @@ function showProviderManagerModal(data) { addModalEventListeners(modal); // 先获取该提供商类型的模型列表(只调用一次API) - const pageProviders = providers.slice(0, PROVIDERS_PER_PAGE); - loadModelsForProviderType(providerType, pageProviders); + loadModelsForProviderType(providerType, providers); } /** - * 渲染分页控件 - * @param {number} currentPage - 当前页码 - * @param {number} totalPages - 总页数 - * @param {number} totalItems - 总条目数 - * @param {string} position - 位置标识 (top/bottom) - * @returns {string} HTML字符串 - */ -function renderPagination(page, totalPages, totalItems, position = 'top') { - const startItem = (page - 1) * PROVIDERS_PER_PAGE + 1; - const endItem = Math.min(page * PROVIDERS_PER_PAGE, totalItems); - - // 生成页码按钮 - let pageButtons = ''; - const maxVisiblePages = 5; - let startPage = Math.max(1, page - Math.floor(maxVisiblePages / 2)); - let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1); - - if (endPage - startPage < maxVisiblePages - 1) { - startPage = Math.max(1, endPage - maxVisiblePages + 1); - } - - if (startPage > 1) { - pageButtons += ``; - if (startPage > 2) { - pageButtons += `...`; - } - } - - for (let i = startPage; i <= endPage; i++) { - pageButtons += ``; - } - - if (endPage < totalPages) { - if (endPage < totalPages - 1) { - pageButtons += `...`; - } - pageButtons += ``; - } - - return ` -
-
- 显示 ${startItem}-${endItem} / 共 ${totalItems} 条 -
-
- - ${pageButtons} - -
-
- 跳转到 - - -
-
- `; -} - -/** - * 跳转到指定页 - * @param {number} page - 目标页码 - */ -function goToProviderPage(page) { - const totalPages = Math.ceil(currentProviders.length / PROVIDERS_PER_PAGE); - - // 验证页码范围 - if (page < 1) page = 1; - if (page > totalPages) page = totalPages; - - currentPage = page; - - // 更新提供商列表 - const providerList = document.getElementById('providerList'); - if (providerList) { - providerList.innerHTML = renderProviderListPaginated(currentProviders, page); - } - - // 更新分页控件 - const paginationContainers = document.querySelectorAll('.pagination-container'); - paginationContainers.forEach(container => { - const position = container.getAttribute('data-position'); - container.outerHTML = renderPagination(page, totalPages, currentProviders.length, position); - }); - - // 滚动到顶部 - const modalBody = document.querySelector('.provider-modal-body'); - if (modalBody) { - modalBody.scrollTop = 0; - } - - // 为当前页的提供商加载模型列表 - const startIndex = (page - 1) * PROVIDERS_PER_PAGE; - const endIndex = Math.min(startIndex + PROVIDERS_PER_PAGE, currentProviders.length); - const pageProviders = currentProviders.slice(startIndex, endIndex); - - // 如果已缓存模型列表,直接使用 - if (cachedModels.length > 0) { - pageProviders.forEach(provider => { - renderNotSupportedModelsSelector(provider.uuid, cachedModels, provider.notSupportedModels || []); - }); - } else { - loadModelsForProviderType(currentProviderType, pageProviders); - } -} - -/** - * 渲染分页后的提供商列表 - * @param {Array} providers - 提供商数组 - * @param {number} page - 当前页码 - * @returns {string} HTML字符串 - */ -function renderProviderListPaginated(providers, page) { - const startIndex = (page - 1) * PROVIDERS_PER_PAGE; - const endIndex = Math.min(startIndex + PROVIDERS_PER_PAGE, providers.length); - const pageProviders = providers.slice(startIndex, endIndex); - - return renderProviderList(pageProviders); -} - -/** - * 为提供商类型加载模型列表(优化:只调用一次API,并缓存结果) + * 为提供商类型加载模型列表(优化:只调用一次API) * @param {string} providerType - 提供商类型 * @param {Array} providers - 提供商列表 */ async function loadModelsForProviderType(providerType, providers) { try { - // 如果已有缓存,直接使用 - if (cachedModels.length > 0) { - providers.forEach(provider => { - renderNotSupportedModelsSelector(provider.uuid, cachedModels, provider.notSupportedModels || []); - }); - return; - } - // 只调用一次API获取模型列表 const response = await window.apiClient.get(`/provider-models/${encodeURIComponent(providerType)}`); const models = response.models || []; - // 缓存模型列表 - cachedModels = models; - // 为每个提供商渲染模型选择器 providers.forEach(provider => { renderNotSupportedModelsSelector(provider.uuid, models, provider.notSupportedModels || []); @@ -352,8 +192,6 @@ function renderProviderList(providers) { const isHealthy = provider.isHealthy; const isDisabled = provider.isDisabled || false; const lastUsed = provider.lastUsed ? new Date(provider.lastUsed).toLocaleString() : '从未使用'; - const lastHealthCheckTime = provider.lastHealthCheckTime ? new Date(provider.lastHealthCheckTime).toLocaleString() : '从未检测'; - const lastHealthCheckModel = provider.lastHealthCheckModel || '-'; const healthClass = isHealthy ? 'healthy' : 'unhealthy'; const disabledClass = isDisabled ? 'disabled' : ''; const healthIcon = isHealthy ? 'fas fa-check-circle text-success' : 'fas fa-exclamation-triangle text-warning'; @@ -364,19 +202,6 @@ function renderProviderList(providers) { const toggleButtonIcon = isDisabled ? 'fas fa-play' : 'fas fa-ban'; const toggleButtonClass = isDisabled ? 'btn-success' : 'btn-warning'; - // 构建错误信息显示 - let errorInfoHtml = ''; - if (!isHealthy && provider.lastErrorMessage) { - const escapedErrorMsg = provider.lastErrorMessage.replace(//g, '>'); - errorInfoHtml = ` -
- - 最后错误: - ${escapedErrorMsg} -
- `; - } - return `
@@ -395,17 +220,6 @@ function renderProviderList(providers) { 失败次数: ${provider.errorCount || 0} | 最后使用: ${lastUsed}
-
- - - 最后检测: ${lastHealthCheckTime} - | - - - 检测模型: ${lastHealthCheckModel} - -
- ${errorInfoHtml}
` : ''; item.innerHTML = `
@@ -97,7 +90,6 @@ function createConfigItemElement(config, index) {
${statusText} - ${quickLinkBtnHtml}
@@ -149,15 +141,6 @@ function createConfigItemElement(config, index) { }); } - // 一键关联按钮事件 - const quickLinkBtn = item.querySelector('.btn-quick-link'); - if (quickLinkBtn) { - quickLinkBtn.addEventListener('click', (e) => { - e.stopPropagation(); - quickLinkKiroConfig(config.path); - }); - } - // 添加点击事件展开/折叠详情 item.addEventListener('click', (e) => { if (!e.target.closest('.config-item-actions')) { @@ -723,12 +706,6 @@ function initUploadConfigManager() { refreshBtn.addEventListener('click', loadConfigList); } - // 批量关联 kiro 配置按钮 - const batchLinkKiroBtn = document.getElementById('batchLinkKiroBtn'); - if (batchLinkKiroBtn) { - batchLinkKiroBtn.addEventListener('click', batchLinkKiroConfigs); - } - // 初始加载配置列表 loadConfigList(); } @@ -761,74 +738,6 @@ async function reloadConfig() { } } -/** - * 一键关联 Kiro 配置到 claude-kiro-oauth - * @param {string} filePath - 配置文件路径 - */ -async function quickLinkKiroConfig(filePath) { - try { - showToast('正在关联配置...', 'info'); - - const result = await window.apiClient.post('/quick-link-kiro', { - filePath: filePath - }); - - showToast(result.message || '配置关联成功', 'success'); - - // 刷新配置列表 - await loadConfigList(); - } catch (error) { - console.error('一键关联失败:', error); - showToast('关联失败: ' + error.message, 'error'); - } -} - -/** - * 批量关联 configs/kiro/ 下的未关联配置到 kiro-oauth - */ -async function batchLinkKiroConfigs() { - // 筛选出 configs/kiro/ 下的未关联配置 - const kiroUnlinkedConfigs = allConfigs.filter(config => - !config.isUsed && config.path.toLowerCase().includes('configs/kiro/') - ); - - if (kiroUnlinkedConfigs.length === 0) { - showToast('没有需要关联的 configs/kiro/ 配置', 'info'); - return; - } - - const confirmMsg = `确定要批量关联 ${kiroUnlinkedConfigs.length} 个 configs/kiro/ 下的配置吗?`; - if (!confirm(confirmMsg)) { - return; - } - - showToast(`正在批量关联 ${kiroUnlinkedConfigs.length} 个配置...`, 'info'); - - let successCount = 0; - let failCount = 0; - - for (const config of kiroUnlinkedConfigs) { - try { - await window.apiClient.post('/quick-link-kiro', { - filePath: config.path - }); - successCount++; - } catch (error) { - console.error(`关联失败: ${config.path}`, error); - failCount++; - } - } - - // 刷新配置列表 - await loadConfigList(); - - if (failCount === 0) { - showToast(`成功关联 ${successCount} 个配置`, 'success'); - } else { - showToast(`关联完成: 成功 ${successCount} 个, 失败 ${failCount} 个`, 'warning'); - } -} - /** * 防抖函数 * @param {Function} func - 要防抖的函数 diff --git a/static/index.html b/static/index.html index 4ceca10..7d99511 100644 --- a/static/index.html +++ b/static/index.html @@ -768,9 +768,6 @@ 共 0 个配置文件 已关联: 0 未关联: 0 -