From 2d317e03330a4a770c541cfd06aa7871deb43ffa Mon Sep 17 00:00:00 2001 From: hex2077 Date: Sat, 10 Jan 2026 18:19:06 +0800 Subject: [PATCH] =?UTF-8?q?refactor(=E9=A1=B9=E7=9B=AE=E7=BB=93=E6=9E=84):?= =?UTF-8?q?=20=E9=87=8D=E6=9E=84=E9=A1=B9=E7=9B=AE=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=E7=BB=93=E6=9E=84=E5=B9=B6=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E7=BB=84=E7=BB=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将常用工具函数移动到utils目录 重构提供商策略模式实现 新增docker-compose构建配置文件 优化UI配置选择器的样式和交互 重构代理工具和API管理模块 更新脚本路径和依赖引用 --- docker/docker-compose.build.yml | 23 + docker/docker-compose.yml | 10 +- install-and-run.bat | 6 +- install-and-run.sh | 6 +- package.json | 4 +- src/{ => auth}/oauth-handlers.js | 6 +- src/{ => convert}/convert-old.js | 2 +- src/{ => convert}/convert.js | 10 +- src/converters/ConverterFactory.js | 2 +- src/converters/register-converters.js | 2 +- src/converters/strategies/ClaudeConverter.js | 4 +- src/converters/strategies/GeminiConverter.js | 4 +- src/converters/strategies/OllamaConverter.js | 2 +- src/converters/strategies/OpenAIConverter.js | 4 +- .../strategies/OpenAIResponsesConverter.js | 2 +- src/{ => core}/config-manager.js | 2 +- src/{ => core}/master.js | 2 +- src/{ => core}/plugin-manager.js | 0 src/{ => handlers}/ollama-handler.js | 12 +- src/{ => handlers}/request-handler.js | 16 +- src/plugins/api-potluck/api-routes.js | 6 +- src/{ => providers}/adapter.js | 2 +- src/{ => providers}/claude/claude-core.js | 4 +- src/{ => providers}/claude/claude-kiro.js | 650 ++++++++---------- src/{ => providers}/claude/claude-strategy.js | 4 +- .../gemini/antigravity-core.js | 8 +- src/{ => providers}/gemini/gemini-core.js | 6 +- src/{ => providers}/gemini/gemini-strategy.js | 4 +- src/{ => providers}/openai/iflow-core.js | 8 +- src/{ => providers}/openai/openai-core.js | 4 +- .../openai/openai-responses-core.js | 2 +- .../openai/openai-responses-core.mjs | 0 .../openai/openai-responses-strategy.js | 4 +- src/{ => providers}/openai/openai-strategy.js | 4 +- src/{ => providers}/openai/qwen-core.js | 6 +- src/{ => providers}/provider-models.js | 0 src/{ => providers}/provider-pool-manager.js | 2 +- src/scripts/kiro-idc-token-refresh.js | 281 ++++++++ src/scripts/kiro-token-refresh.js | 184 +++++ src/scripts/merge-json-files.js | 125 ++++ src/{ => services}/api-manager.js | 2 +- src/{ => services}/api-server.js | 8 +- src/{ => services}/service-manager.js | 6 +- src/{ => services}/ui-manager.js | 22 +- src/{ => services}/usage-service.js | 4 +- src/ui-modules/config-api.js | 12 +- src/ui-modules/config-scanner.js | 2 +- src/ui-modules/oauth-api.js | 4 +- src/ui-modules/plugin-api.js | 4 +- src/ui-modules/provider-api.js | 6 +- src/ui-modules/usage-api.js | 6 +- src/{ => utils}/common.js | 6 +- src/{ => utils}/provider-strategies.js | 10 +- src/{ => utils}/provider-strategy.js | 2 +- src/{ => utils}/provider-utils.js | 0 src/{ => utils}/proxy-utils.js | 0 static/app/config-manager.js | 95 ++- static/app/i18n.js | 4 +- static/components/section-config.css | 78 ++- static/components/section-config.html | 104 +-- static/login.html | 2 +- 61 files changed, 1189 insertions(+), 611 deletions(-) create mode 100644 docker/docker-compose.build.yml rename src/{ => auth}/oauth-handlers.js (99%) rename src/{ => convert}/convert-old.js (99%) rename src/{ => convert}/convert.js (97%) rename src/{ => core}/config-manager.js (99%) rename src/{ => core}/master.js (99%) rename src/{ => core}/plugin-manager.js (100%) rename src/{ => handlers}/ollama-handler.js (98%) rename src/{ => handlers}/request-handler.js (95%) rename src/{ => providers}/adapter.js (99%) rename src/{ => providers}/claude/claude-core.js (98%) rename src/{ => providers}/claude/claude-kiro.js (85%) rename src/{ => providers}/claude/claude-strategy.js (96%) rename src/{ => providers}/gemini/antigravity-core.js (99%) rename src/{ => providers}/gemini/gemini-core.js (99%) rename src/{ => providers}/gemini/gemini-strategy.js (95%) rename src/{ => providers}/openai/iflow-core.js (99%) rename src/{ => providers}/openai/openai-core.js (98%) rename src/{ => providers}/openai/openai-responses-core.js (99%) rename src/{ => providers}/openai/openai-responses-core.mjs (100%) rename src/{ => providers}/openai/openai-responses-strategy.js (97%) rename src/{ => providers}/openai/openai-strategy.js (96%) rename src/{ => providers}/openai/qwen-core.js (99%) rename src/{ => providers}/provider-models.js (100%) rename src/{ => providers}/provider-pool-manager.js (99%) create mode 100644 src/scripts/kiro-idc-token-refresh.js create mode 100644 src/scripts/kiro-token-refresh.js create mode 100644 src/scripts/merge-json-files.js rename src/{ => services}/api-manager.js (99%) rename src/{ => services}/api-server.js (98%) rename src/{ => services}/service-manager.js (98%) rename src/{ => services}/ui-manager.js (94%) rename src/{ => services}/usage-service.js (99%) rename src/{ => utils}/common.js (99%) rename src/{ => utils}/provider-strategies.js (67%) rename src/{ => utils}/provider-strategy.js (98%) rename src/{ => utils}/provider-utils.js (100%) rename src/{ => utils}/proxy-utils.js (100%) diff --git a/docker/docker-compose.build.yml b/docker/docker-compose.build.yml new file mode 100644 index 0000000..d30ee02 --- /dev/null +++ b/docker/docker-compose.build.yml @@ -0,0 +1,23 @@ +services: + aiclient-api: + # 方式二:从 Dockerfile 本地构建 + # 使用方法: docker compose -f docker-compose.build.yml up -d --build + build: + context: .. + dockerfile: Dockerfile + container_name: aiclient2api + restart: unless-stopped + ports: + - "3000:3000" + - "8085-8087:8085-8087" + - "19876-19880:19876-19880" + volumes: + - ./configs:/app/configs + environment: + - ARGS= + healthcheck: + test: ["CMD", "node", "healthcheck.js"] + interval: 30s + timeout: 3s + start_period: 5s + retries: 3 \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 5c082da..a295400 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,19 +1,13 @@ services: aiclient-api: # 方式一:使用 Docker Hub 预构建镜像(默认,推荐) + # 使用方法: docker compose up -d image: justlikemaki/aiclient-2-api:latest - - # 方式二:从 Dockerfile 本地构建(取消下面注释并注释掉上面的 image 行) - # build: - # context: .. - # dockerfile: Dockerfile - container_name: aiclient2api restart: unless-stopped ports: - "3000:3000" - - "8085:8085" - - "8086:8086" + - "8085-8087:8085-8087" - "19876-19880:19876-19880" volumes: - ./configs:/app/configs diff --git a/install-and-run.bat b/install-and-run.bat index d9922a3..f44cada 100755 --- a/install-and-run.bat +++ b/install-and-run.bat @@ -69,8 +69,8 @@ if !errorlevel! neq 0 ( echo [成功] 依赖安装/更新完成 :: 检查src目录和master.js是否存在 -if not exist "src\master.js" ( - echo [错误] 未找到src\master.js文件 +if not exist "src\core\master.js" ( + echo [错误] 未找到src\core\master.js文件 pause exit /b 1 ) @@ -89,4 +89,4 @@ echo 按 Ctrl+C 停止服务器 echo. :: 启动服务器 -node src\master.js \ No newline at end of file +node src\core\master.js \ No newline at end of file diff --git a/install-and-run.sh b/install-and-run.sh index 2aa7e18..5806076 100755 --- a/install-and-run.sh +++ b/install-and-run.sh @@ -76,8 +76,8 @@ fi echo "[成功] 依赖安装/更新完成" # 检查src目录和master.js是否存在 -if [ ! -f "src/master.js" ]; then - echo "[错误] 未找到src/master.js文件" +if [ ! -f "src/core/master.js" ]; then + echo "[错误] 未找到src/core/master.js文件" exit 1 fi @@ -95,4 +95,4 @@ echo "按 Ctrl+C 停止服务器" echo # 启动服务器 -node src/master.js \ No newline at end of file +node src/core/master.js \ No newline at end of file diff --git a/package.json b/package.json index f6c1f57..50538b0 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,8 @@ "supertest": "^6.3.3" }, "scripts": { - "start": "node src/master.js", - "start:standalone": "node src/api-server.js", + "start": "node src/core/master.js", + "start:standalone": "node src/services/api-server.js", "start:dev": "node src/master.js --dev", "test": "jest", "test:watch": "jest --watch", diff --git a/src/oauth-handlers.js b/src/auth/oauth-handlers.js similarity index 99% rename from src/oauth-handlers.js rename to src/auth/oauth-handlers.js index 0c42c87..4c5ff8f 100644 --- a/src/oauth-handlers.js +++ b/src/auth/oauth-handlers.js @@ -5,9 +5,9 @@ import path from 'path'; import os from 'os'; import crypto from 'crypto'; import open from 'open'; -import { broadcastEvent } from './ui-manager.js'; -import { autoLinkProviderConfigs } from './service-manager.js'; -import { CONFIG } from './config-manager.js'; +import { broadcastEvent } from '../services/ui-manager.js'; +import { autoLinkProviderConfigs } from '../services/service-manager.js'; +import { CONFIG } from '../core/config-manager.js'; /** * OAuth 提供商配置 diff --git a/src/convert-old.js b/src/convert/convert-old.js similarity index 99% rename from src/convert-old.js rename to src/convert/convert-old.js index a4a4ae0..f4b89e8 100644 --- a/src/convert-old.js +++ b/src/convert/convert-old.js @@ -1,5 +1,5 @@ import { v4 as uuidv4 } from 'uuid'; -import { MODEL_PROTOCOL_PREFIX, getProtocolPrefix } from './common.js'; +import { MODEL_PROTOCOL_PREFIX, getProtocolPrefix } from '../utils/common.js'; import { streamStateManager, generateResponseCreated, diff --git a/src/convert.js b/src/convert/convert.js similarity index 97% rename from src/convert.js rename to src/convert/convert.js index 6703162..72dd8ab 100644 --- a/src/convert.js +++ b/src/convert/convert.js @@ -7,8 +7,8 @@ */ import { v4 as uuidv4 } from 'uuid'; -import { MODEL_PROTOCOL_PREFIX, getProtocolPrefix } from './common.js'; -import { ConverterFactory } from './converters/ConverterFactory.js'; +import { MODEL_PROTOCOL_PREFIX, getProtocolPrefix } from '../utils/common.js'; +import { ConverterFactory } from '../converters/ConverterFactory.js'; import { generateResponseCreated, generateResponseInProgress, @@ -18,7 +18,7 @@ import { generateContentPartDone, generateOutputItemDone, generateResponseCompleted -} from './openai/openai-responses-core.mjs'; +} from '../providers/openai/openai-responses-core.mjs'; // ============================================================================= // 初始化:注册所有转换器 @@ -230,12 +230,12 @@ export function toOpenAIStreamChunkFromOpenAIResponses(responsesChunk, model) { // 辅助函数导出 export async function extractAndProcessSystemMessages(messages) { - const { Utils } = await import('./converters/utils.js'); + const { Utils } = await import('../converters/utils.js'); return Utils.extractSystemMessages(messages); } export async function extractTextFromMessageContent(content) { - const { Utils } = await import('./converters/utils.js'); + const { Utils } = await import('../converters/utils.js'); return Utils.extractText(content); } diff --git a/src/converters/ConverterFactory.js b/src/converters/ConverterFactory.js index 58d13cb..cabddd5 100644 --- a/src/converters/ConverterFactory.js +++ b/src/converters/ConverterFactory.js @@ -3,7 +3,7 @@ * 使用工厂模式管理转换器实例的创建和缓存 */ -import { MODEL_PROTOCOL_PREFIX } from '../common.js'; +import { MODEL_PROTOCOL_PREFIX } from '../utils/common.js'; /** * 转换器工厂(单例模式 + 工厂模式) diff --git a/src/converters/register-converters.js b/src/converters/register-converters.js index 3b3c565..c386cea 100644 --- a/src/converters/register-converters.js +++ b/src/converters/register-converters.js @@ -3,7 +3,7 @@ * 用于注册所有转换器到工厂,避免循环依赖问题 */ -import { MODEL_PROTOCOL_PREFIX } from '../common.js'; +import { MODEL_PROTOCOL_PREFIX } from '../utils/common.js'; import { ConverterFactory } from './ConverterFactory.js'; import { OpenAIConverter } from './strategies/OpenAIConverter.js'; import { OpenAIResponsesConverter } from './strategies/OpenAIResponsesConverter.js'; diff --git a/src/converters/strategies/ClaudeConverter.js b/src/converters/strategies/ClaudeConverter.js index 329bdee..08242a2 100644 --- a/src/converters/strategies/ClaudeConverter.js +++ b/src/converters/strategies/ClaudeConverter.js @@ -18,7 +18,7 @@ import { GEMINI_DEFAULT_INPUT_TOKEN_LIMIT, GEMINI_DEFAULT_OUTPUT_TOKEN_LIMIT } from '../utils.js'; -import { MODEL_PROTOCOL_PREFIX } from '../../common.js'; +import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js'; import { generateResponseCreated, generateResponseInProgress, @@ -28,7 +28,7 @@ import { generateContentPartDone, generateOutputItemDone, generateResponseCompleted -} from '../../openai/openai-responses-core.mjs'; +} from '../../providers/openai/openai-responses-core.mjs'; /** * Claude转换器类 diff --git a/src/converters/strategies/GeminiConverter.js b/src/converters/strategies/GeminiConverter.js index ad3638d..9364058 100644 --- a/src/converters/strategies/GeminiConverter.js +++ b/src/converters/strategies/GeminiConverter.js @@ -14,7 +14,7 @@ import { CLAUDE_DEFAULT_TEMPERATURE, CLAUDE_DEFAULT_TOP_P } from '../utils.js'; -import { MODEL_PROTOCOL_PREFIX } from '../../common.js'; +import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js'; import { generateResponseCreated, generateResponseInProgress, @@ -24,7 +24,7 @@ import { generateContentPartDone, generateOutputItemDone, generateResponseCompleted -} from '../../openai/openai-responses-core.mjs'; +} from '../../providers/openai/openai-responses-core.mjs'; /** * Gemini转换器类 diff --git a/src/converters/strategies/OllamaConverter.js b/src/converters/strategies/OllamaConverter.js index 733592e..870e1e7 100644 --- a/src/converters/strategies/OllamaConverter.js +++ b/src/converters/strategies/OllamaConverter.js @@ -6,7 +6,7 @@ import { v4 as uuidv4 } from 'uuid'; import { createHash } from 'crypto'; import { BaseConverter } from '../BaseConverter.js'; -import { MODEL_PROTOCOL_PREFIX } from '../../common.js'; +import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js'; import { OLLAMA_DEFAULT_CONTEXT_LENGTH, OLLAMA_DEFAULT_MAX_OUTPUT_TOKENS, diff --git a/src/converters/strategies/OpenAIConverter.js b/src/converters/strategies/OpenAIConverter.js index 700ca41..9a4f128 100644 --- a/src/converters/strategies/OpenAIConverter.js +++ b/src/converters/strategies/OpenAIConverter.js @@ -22,7 +22,7 @@ import { OPENAI_DEFAULT_INPUT_TOKEN_LIMIT, OPENAI_DEFAULT_OUTPUT_TOKEN_LIMIT } from '../utils.js'; -import { MODEL_PROTOCOL_PREFIX } from '../../common.js'; +import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js'; import { generateResponseCreated, generateResponseInProgress, @@ -32,7 +32,7 @@ import { generateContentPartDone, generateOutputItemDone, generateResponseCompleted -} from '../../openai/openai-responses-core.mjs'; +} from '../../providers/openai/openai-responses-core.mjs'; /** * OpenAI转换器类 diff --git a/src/converters/strategies/OpenAIResponsesConverter.js b/src/converters/strategies/OpenAIResponsesConverter.js index 5847320..83e3ec5 100644 --- a/src/converters/strategies/OpenAIResponsesConverter.js +++ b/src/converters/strategies/OpenAIResponsesConverter.js @@ -4,7 +4,7 @@ */ import { BaseConverter } from '../BaseConverter.js'; -import { MODEL_PROTOCOL_PREFIX } from '../../common.js'; +import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js'; import { extractAndProcessSystemMessages as extractSystemMessages, extractTextFromMessageContent as extractText, diff --git a/src/config-manager.js b/src/core/config-manager.js similarity index 99% rename from src/config-manager.js rename to src/core/config-manager.js index 0432bf8..0b6d460 100644 --- a/src/config-manager.js +++ b/src/core/config-manager.js @@ -1,6 +1,6 @@ import * as fs from 'fs'; import { promises as pfs } from 'fs'; -import { INPUT_SYSTEM_PROMPT_FILE, MODEL_PROVIDER } from './common.js'; +import { INPUT_SYSTEM_PROMPT_FILE, MODEL_PROVIDER } from '../utils/common.js'; export let CONFIG = {}; // Make CONFIG exportable export let PROMPT_LOG_FILENAME = ''; // Make PROMPT_LOG_FILENAME exportable diff --git a/src/master.js b/src/core/master.js similarity index 99% rename from src/master.js rename to src/core/master.js index 39e4a84..b55eeda 100644 --- a/src/master.js +++ b/src/core/master.js @@ -33,7 +33,7 @@ let workerStatus = { // 配置 const config = { - workerScript: path.join(__dirname, 'api-server.js'), + workerScript: path.join(__dirname, '../services/api-server.js'), maxRestartAttempts: 10, restartDelay: 1000, // 重启延迟(毫秒) masterPort: parseInt(process.env.MASTER_PORT) || 3100, // 主进程管理端口 diff --git a/src/plugin-manager.js b/src/core/plugin-manager.js similarity index 100% rename from src/plugin-manager.js rename to src/core/plugin-manager.js diff --git a/src/ollama-handler.js b/src/handlers/ollama-handler.js similarity index 98% rename from src/ollama-handler.js rename to src/handlers/ollama-handler.js index 8f78086..48fff52 100644 --- a/src/ollama-handler.js +++ b/src/handlers/ollama-handler.js @@ -3,9 +3,9 @@ * 处理Ollama特定的端点并在后端协议之间进行转换 */ -import { getRequestBody, handleError, MODEL_PROTOCOL_PREFIX, MODEL_PROVIDER, getProtocolPrefix } from './common.js'; -import { convertData } from './convert.js'; -import { ConverterFactory } from './converters/ConverterFactory.js'; +import { getRequestBody, handleError, MODEL_PROTOCOL_PREFIX, MODEL_PROVIDER, getProtocolPrefix } from '../utils/common.js'; +import { convertData } from '../convert/convert.js'; +import { ConverterFactory } from '../converters/ConverterFactory.js'; // Ollama版本号 /** @@ -385,7 +385,7 @@ export async function handleOllamaTags(req, res, apiService, currentConfig, prov console.log('[Ollama] Handling /api/tags request'); const ollamaConverter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OLLAMA); - const { getServiceAdapter } = await import('./adapter.js'); + const { getServiceAdapter } = await import('../providers/adapter.js'); // Helper to fetch and convert models from a provider const fetchProviderModels = async (providerType, service) => { @@ -516,7 +516,7 @@ export async function handleOllamaChat(req, res, apiService, currentConfig, prov console.log('[Ollama] Handling /api/chat request'); const ollamaRequest = await getRequestBody(req); - const { getServiceAdapter } = await import('./adapter.js'); + const { getServiceAdapter } = await import('../providers/adapter.js'); // Determine provider based on model name const rawModelName = ollamaRequest.model; @@ -623,7 +623,7 @@ export async function handleOllamaGenerate(req, res, apiService, currentConfig, console.log('[Ollama] Handling /api/generate request'); const ollamaRequest = await getRequestBody(req); - const { getServiceAdapter } = await import('./adapter.js'); + const { getServiceAdapter } = await import('../providers/adapter.js'); // Determine provider based on model name const rawModelName = ollamaRequest.model; diff --git a/src/request-handler.js b/src/handlers/request-handler.js similarity index 95% rename from src/request-handler.js rename to src/handlers/request-handler.js index 6106f8a..ec3cd41 100644 --- a/src/request-handler.js +++ b/src/handlers/request-handler.js @@ -1,13 +1,13 @@ import deepmerge from 'deepmerge'; -import { handleError } from './common.js'; -import { handleUIApiRequests, serveStaticFiles } from './ui-manager.js'; -import { handleAPIRequests } from './api-manager.js'; -import { getApiService, getProviderStatus } from './service-manager.js'; -import { getProviderPoolManager } from './service-manager.js'; -import { MODEL_PROVIDER } from './common.js'; -import { PROMPT_LOG_FILENAME } from './config-manager.js'; +import { handleError } from '../utils/common.js'; +import { handleUIApiRequests, serveStaticFiles } from '../services/ui-manager.js'; +import { handleAPIRequests } from '../services/api-manager.js'; +import { getApiService, getProviderStatus } from '../services/service-manager.js'; +import { getProviderPoolManager } from '../services/service-manager.js'; +import { MODEL_PROVIDER } from '../utils/common.js'; +import { PROMPT_LOG_FILENAME } from '../core/config-manager.js'; import { handleOllamaRequest, handleOllamaShow } from './ollama-handler.js'; -import { getPluginManager } from './plugin-manager.js'; +import { getPluginManager } from '../core/plugin-manager.js'; /** * Parse request body as JSON diff --git a/src/plugins/api-potluck/api-routes.js b/src/plugins/api-potluck/api-routes.js index 18b37cd..d9b924f 100644 --- a/src/plugins/api-potluck/api-routes.js +++ b/src/plugins/api-potluck/api-routes.js @@ -36,9 +36,9 @@ import path from 'path'; import { existsSync } from 'fs'; import { promises as fs } from 'fs'; import multer from 'multer'; -import { batchImportKiroRefreshTokensStream, importAwsCredentials } from '../../oauth-handlers.js'; -import { autoLinkProviderConfigs, getProviderPoolManager } from '../../service-manager.js'; -import { CONFIG } from '../../config-manager.js'; +import { batchImportKiroRefreshTokensStream, importAwsCredentials } from '../../auth/oauth-handlers.js'; +import { autoLinkProviderConfigs, getProviderPoolManager } from '../../services/service-manager.js'; +import { CONFIG } from '../../core/config-manager.js'; /** * 解析请求体 diff --git a/src/adapter.js b/src/providers/adapter.js similarity index 99% rename from src/adapter.js rename to src/providers/adapter.js index 9465b14..44afdd9 100644 --- a/src/adapter.js +++ b/src/providers/adapter.js @@ -6,7 +6,7 @@ import { ClaudeApiService } from './claude/claude-core.js'; // 导入ClaudeApiSe import { KiroApiService } from './claude/claude-kiro.js'; // 导入KiroApiService import { QwenApiService } from './openai/qwen-core.js'; // 导入QwenApiService import { IFlowApiService } from './openai/iflow-core.js'; // 导入IFlowApiService -import { MODEL_PROVIDER } from './common.js'; // 导入 MODEL_PROVIDER +import { MODEL_PROVIDER } from '../utils/common.js'; // 导入 MODEL_PROVIDER // 定义AI服务适配器接口 // 所有的服务适配器都应该实现这些方法 diff --git a/src/claude/claude-core.js b/src/providers/claude/claude-core.js similarity index 98% rename from src/claude/claude-core.js rename to src/providers/claude/claude-core.js index 44bb45f..9826171 100644 --- a/src/claude/claude-core.js +++ b/src/providers/claude/claude-core.js @@ -1,8 +1,8 @@ import axios from 'axios'; import * as http from 'http'; import * as https from 'https'; -import { configureAxiosProxy } from '../proxy-utils.js'; -import { isRetryableNetworkError } from '../common.js'; +import { configureAxiosProxy } from '../../utils/proxy-utils.js'; +import { isRetryableNetworkError } from '../../utils/common.js'; /** * Claude API Core Service Class. diff --git a/src/claude/claude-kiro.js b/src/providers/claude/claude-kiro.js similarity index 85% rename from src/claude/claude-kiro.js rename to src/providers/claude/claude-kiro.js index 1929fa8..c348e29 100644 --- a/src/claude/claude-kiro.js +++ b/src/providers/claude/claude-kiro.js @@ -8,14 +8,13 @@ import * as http from 'http'; import * as https from 'https'; import { getProviderModels } from '../provider-models.js'; import { countTokens } from '@anthropic-ai/tokenizer'; -import { configureAxiosProxy } from '../proxy-utils.js'; -import { isRetryableNetworkError } from '../common.js'; -import { CLAUDE_DEFAULT_MAX_TOKENS } from '../converters/utils.js'; +import { configureAxiosProxy } from '../../utils/proxy-utils.js'; +import { isRetryableNetworkError } from '../../utils/common.js'; const KIRO_CONSTANTS = { REFRESH_URL: 'https://prod.{{region}}.auth.desktop.kiro.dev/refreshToken', REFRESH_IDC_URL: 'https://oidc.{{region}}.amazonaws.com/token', - BASE_URL: 'https://codewhisperer.{{region}}.amazonaws.com/generateAssistantResponse', + BASE_URL: 'https://q.{{region}}.amazonaws.com/generateAssistantResponse', AMAZON_Q_URL: 'https://codewhisperer.{{region}}.amazonaws.com/SendMessageStreaming', USAGE_LIMITS_URL: 'https://q.{{region}}.amazonaws.com/getUsageLimits', DEFAULT_MODEL_NAME: 'claude-opus-4-5', @@ -27,6 +26,7 @@ const KIRO_CONSTANTS = { AUTH_METHOD_SOCIAL: 'social', CHAT_TRIGGER_TYPE_MANUAL: 'MANUAL', ORIGIN_AI_EDITOR: 'AI_EDITOR', + TOTAL_CONTEXT_TOKENS: 200000, // 总上下文 200k tokens }; // 从 provider-models.js 获取支持的模型列表 @@ -34,9 +34,9 @@ const KIRO_MODELS = getProviderModels('claude-kiro-oauth'); // 完整的模型映射表 const FULL_MODEL_MAPPING = { - "claude-opus-4-5": "claude-opus-4.5", - "claude-opus-4-5-20251101": "claude-opus-4.5", - "claude-haiku-4-5": "claude-haiku-4.5", + "claude-opus-4-5":"claude-opus-4.5", + "claude-opus-4-5-20251101":"claude-opus-4.5", + "claude-haiku-4-5":"claude-haiku-4.5", "claude-sonnet-4-5": "CLAUDE_SONNET_4_5_20250929_V1_0", "claude-sonnet-4-5-20250929": "CLAUDE_SONNET_4_5_20250929_V1_0", "claude-sonnet-4-20250514": "CLAUDE_SONNET_4_20250514_V1_0", @@ -75,7 +75,7 @@ function getSystemRuntimeInfo() { const osPlatform = os.platform(); const osRelease = os.release(); const nodeVersion = process.version.replace('v', ''); - + let osName = osPlatform; if (osPlatform === 'win32') osName = `windows#${osRelease}`; else if (osPlatform === 'darwin') osName = `macos#${osRelease}`; @@ -249,7 +249,7 @@ function parseBracketToolCalls(responseText) { continue; // Skip this one if no closing bracket found } } - + const parsedCall = parseSingleToolCall(toolCallText); if (parsedCall) { toolCalls.push(parsedCall); @@ -312,7 +312,7 @@ export class KiroApiService { this.axiosInstance = null; // Initialize later in async method this.axiosSocialRefreshInstance = null; } - + async initialize() { if (this.isInitialized) return; console.log('[Kiro] Initializing Kiro API Service...'); @@ -339,7 +339,7 @@ export class KiroApiService { maxFreeSockets: 5, timeout: KIRO_CONSTANTS.AXIOS_TIMEOUT, }); - + const axiosConfig = { timeout: KIRO_CONSTANTS.AXIOS_TIMEOUT, httpAgent, @@ -354,15 +354,15 @@ export class KiroApiService { 'Connection': 'close' }, }; - + // 根据 useSystemProxy 配置代理设置 if (!this.useSystemProxy) { axiosConfig.proxy = false; } - + // 配置自定义代理 configureAxiosProxy(axiosConfig, this.config, 'claude-kiro-oauth'); - + this.axiosInstance = axios.create(axiosConfig); axiosConfig.headers = new Headers(); @@ -371,170 +371,126 @@ export class KiroApiService { this.isInitialized = true; } - async initializeAuth(forceRefresh = false) { - // 如果已有 accessToken 且不是强制刷新,直接返回 - if (this.accessToken && !forceRefresh) { - console.debug('[Kiro Auth] Access token already available and not forced refresh.'); - return; - } - - // 如果是强制刷新且已有 refreshToken,跳过凭证加载,直接刷新 - if (forceRefresh && this.refreshToken) { - console.debug('[Kiro Auth] Force refresh requested, skipping credential loading.'); - // 直接跳转到刷新逻辑 - return this._refreshAccessToken(); - } - - // Helper to load credentials from a file - const loadCredentialsFromFile = async (filePath) => { - try { - const fileContent = await fs.readFile(filePath, 'utf8'); - return JSON.parse(fileContent); - } catch (error) { - if (error.code === 'ENOENT') { - console.debug(`[Kiro Auth] Credential file not found: ${filePath}`); - } else if (error instanceof SyntaxError) { - console.warn(`[Kiro Auth] Failed to parse JSON from ${filePath}: ${error.message}`); - } else { - console.warn(`[Kiro Auth] Failed to read credential file ${filePath}: ${error.message}`); - } - return null; - } - }; - - // Helper to save credentials to a file - const saveCredentialsToFile = async (filePath, newData) => { - try { - let existingData = {}; - try { - const fileContent = await fs.readFile(filePath, 'utf8'); - existingData = JSON.parse(fileContent); - } catch (readError) { - if (readError.code === 'ENOENT') { - console.debug(`[Kiro Auth] Token file not found, creating new one: ${filePath}`); - } else { - console.warn(`[Kiro Auth] Could not read existing token file ${filePath}: ${readError.message}`); - } - } - const mergedData = { ...existingData, ...newData }; - await fs.writeFile(filePath, JSON.stringify(mergedData, null, 2), 'utf8'); - console.info(`[Kiro Auth] Updated token file: ${filePath}`); - } catch (error) { - console.error(`[Kiro Auth] Failed to write token to file ${filePath}: ${error.message}`); - } - }; - - try { - let mergedCredentials = {}; - - // Priority 1: Load from Base64 credentials if available - if (this.base64Creds) { - Object.assign(mergedCredentials, this.base64Creds); - console.info('[Kiro Auth] Successfully loaded credentials from Base64 (constructor).'); - // Clear base64Creds after use to prevent re-processing - this.base64Creds = null; - } - - // Priority 2 & 3 合并: 从指定文件路径或目录加载凭证 - // 读取指定的 credPath 文件以及目录下的其他 JSON 文件(排除当前文件) - const targetFilePath = this.credsFilePath || path.join(this.credPath, KIRO_AUTH_TOKEN_FILE); - const dirPath = path.dirname(targetFilePath); - const targetFileName = path.basename(targetFilePath); - - console.debug(`[Kiro Auth] Attempting to load credentials from directory: ${dirPath}`); - - try { - // 首先尝试读取目标文件 - const targetCredentials = await loadCredentialsFromFile(targetFilePath); - if (targetCredentials) { - Object.assign(mergedCredentials, targetCredentials); - console.info(`[Kiro Auth] Successfully loaded OAuth credentials from ${targetFilePath}`); - } - - // 然后读取目录下的其他 JSON 文件(排除目标文件本身) - const files = await fs.readdir(dirPath); - for (const file of files) { - if (file.endsWith('.json') && file !== targetFileName) { - const filePath = path.join(dirPath, file); - const credentials = await loadCredentialsFromFile(filePath); - if (credentials) { - // 保留已有的 expiresAt,避免被覆盖 - credentials.expiresAt = mergedCredentials.expiresAt; - Object.assign(mergedCredentials, credentials); - console.debug(`[Kiro Auth] Loaded Client credentials from ${file}`); - } - } - } - } catch (error) { - console.warn(`[Kiro Auth] Error loading credentials from directory ${dirPath}: ${error.message}`); - } - - // console.log('[Kiro Auth] Merged credentials:', mergedCredentials); - // Apply loaded credentials, prioritizing existing values if they are not null/undefined - this.accessToken = this.accessToken || mergedCredentials.accessToken; - this.refreshToken = this.refreshToken || mergedCredentials.refreshToken; - this.clientId = this.clientId || mergedCredentials.clientId; - this.clientSecret = this.clientSecret || mergedCredentials.clientSecret; - this.authMethod = this.authMethod || mergedCredentials.authMethod; - this.expiresAt = this.expiresAt || mergedCredentials.expiresAt; - this.profileArn = this.profileArn || mergedCredentials.profileArn; - this.region = this.region || mergedCredentials.region; - - // Ensure region is set before using it in URLs - if (!this.region) { - console.warn('[Kiro Auth] Region not found in credentials. Using default region us-east-1 for URLs.'); - this.region = 'us-east-1'; // Set default region - } - - this.refreshUrl = (this.config.KIRO_REFRESH_URL || KIRO_CONSTANTS.REFRESH_URL).replace("{{region}}", this.region); - this.refreshIDCUrl = (this.config.KIRO_REFRESH_IDC_URL || KIRO_CONSTANTS.REFRESH_IDC_URL).replace("{{region}}", this.region); - this.baseUrl = (this.config.KIRO_BASE_URL || KIRO_CONSTANTS.BASE_URL).replace("{{region}}", this.region); - this.amazonQUrl = (KIRO_CONSTANTS.AMAZON_Q_URL).replace("{{region}}", this.region); - } catch (error) { - console.warn(`[Kiro Auth] Error during credential loading: ${error.message}`); - } - - // Refresh token if access token is missing but refresh token is available - if (!this.accessToken && this.refreshToken) { - await this._refreshAccessToken(); - } - - if (!this.accessToken) { - throw new Error('No access token available after initialization and refresh attempts.'); - } +async initializeAuth(forceRefresh = false) { + if (this.accessToken && !forceRefresh) { + console.debug('[Kiro Auth] Access token already available and not forced refresh.'); + return; } - /** - * 内部方法:刷新 access token - * @private - */ - async _refreshAccessToken() { + // Helper to load credentials from a file + const loadCredentialsFromFile = async (filePath) => { + try { + const fileContent = await fs.readFile(filePath, 'utf8'); + return JSON.parse(fileContent); + } catch (error) { + if (error.code === 'ENOENT') { + console.debug(`[Kiro Auth] Credential file not found: ${filePath}`); + } else if (error instanceof SyntaxError) { + console.warn(`[Kiro Auth] Failed to parse JSON from ${filePath}: ${error.message}`); + } else { + console.warn(`[Kiro Auth] Failed to read credential file ${filePath}: ${error.message}`); + } + return null; + } + }; + + // Helper to save credentials to a file + const saveCredentialsToFile = async (filePath, newData) => { + try { + let existingData = {}; + try { + const fileContent = await fs.readFile(filePath, 'utf8'); + existingData = JSON.parse(fileContent); + } catch (readError) { + if (readError.code === 'ENOENT') { + console.debug(`[Kiro Auth] Token file not found, creating new one: ${filePath}`); + } else { + console.warn(`[Kiro Auth] Could not read existing token file ${filePath}: ${readError.message}`); + } + } + const mergedData = { ...existingData, ...newData }; + await fs.writeFile(filePath, JSON.stringify(mergedData, null, 2), 'utf8'); + console.info(`[Kiro Auth] Updated token file: ${filePath}`); + } catch (error) { + console.error(`[Kiro Auth] Failed to write token to file ${filePath}: ${error.message}`); + } + }; + + try { + let mergedCredentials = {}; + + // Priority 1: Load from Base64 credentials if available + if (this.base64Creds) { + Object.assign(mergedCredentials, this.base64Creds); + console.info('[Kiro Auth] Successfully loaded credentials from Base64 (constructor).'); + // Clear base64Creds after use to prevent re-processing + this.base64Creds = null; + } + + // Priority 2 & 3 合并: 从指定文件路径或目录加载凭证 + // 读取指定的 credPath 文件以及目录下的其他 JSON 文件(排除当前文件) + const targetFilePath = this.credsFilePath || path.join(this.credPath, KIRO_AUTH_TOKEN_FILE); + const dirPath = path.dirname(targetFilePath); + const targetFileName = path.basename(targetFilePath); + + console.debug(`[Kiro Auth] Attempting to load credentials from directory: ${dirPath}`); + + try { + // 首先尝试读取目标文件 + const targetCredentials = await loadCredentialsFromFile(targetFilePath); + if (targetCredentials) { + Object.assign(mergedCredentials, targetCredentials); + console.info(`[Kiro Auth] Successfully loaded OAuth credentials from ${targetFilePath}`); + } + + // 然后读取目录下的其他 JSON 文件(排除目标文件本身) + const files = await fs.readdir(dirPath); + for (const file of files) { + if (file.endsWith('.json') && file !== targetFileName) { + const filePath = path.join(dirPath, file); + const credentials = await loadCredentialsFromFile(filePath); + if (credentials) { + // 保留已有的 expiresAt,避免被覆盖 + credentials.expiresAt = mergedCredentials.expiresAt; + Object.assign(mergedCredentials, credentials); + console.debug(`[Kiro Auth] Loaded Client credentials from ${file}`); + } + } + } + } catch (error) { + console.warn(`[Kiro Auth] Error loading credentials from directory ${dirPath}: ${error.message}`); + } + + // console.log('[Kiro Auth] Merged credentials:', mergedCredentials); + // Apply loaded credentials, prioritizing existing values if they are not null/undefined + this.accessToken = this.accessToken || mergedCredentials.accessToken; + this.refreshToken = this.refreshToken || mergedCredentials.refreshToken; + this.clientId = this.clientId || mergedCredentials.clientId; + this.clientSecret = this.clientSecret || mergedCredentials.clientSecret; + this.authMethod = this.authMethod || mergedCredentials.authMethod; + this.expiresAt = this.expiresAt || mergedCredentials.expiresAt; + this.profileArn = this.profileArn || mergedCredentials.profileArn; + this.region = this.region || mergedCredentials.region; + + // Ensure region is set before using it in URLs + if (!this.region) { + console.warn('[Kiro Auth] Region not found in credentials. Using default region us-east-1 for URLs.'); + this.region = 'us-east-1'; // Set default region + } + + this.refreshUrl = (this.config.KIRO_REFRESH_URL || KIRO_CONSTANTS.REFRESH_URL).replace("{{region}}", this.region); + this.refreshIDCUrl = (this.config.KIRO_REFRESH_IDC_URL || KIRO_CONSTANTS.REFRESH_IDC_URL).replace("{{region}}", this.region); + this.baseUrl = (this.config.KIRO_BASE_URL || KIRO_CONSTANTS.BASE_URL).replace("{{region}}", this.region); + this.amazonQUrl = (KIRO_CONSTANTS.AMAZON_Q_URL).replace("{{region}}", this.region); + } catch (error) { + console.warn(`[Kiro Auth] Error during credential loading: ${error.message}`); + } + + // Refresh token if forced or if access token is missing but refresh token is available + if (forceRefresh || (!this.accessToken && this.refreshToken)) { if (!this.refreshToken) { throw new Error('No refresh token available to refresh access token.'); } - - // Helper to save credentials to a file - const saveCredentialsToFile = async (filePath, newData) => { - try { - let existingData = {}; - try { - const fileContent = await fs.readFile(filePath, 'utf8'); - existingData = JSON.parse(fileContent); - } catch (readError) { - if (readError.code === 'ENOENT') { - console.debug(`[Kiro Auth] Token file not found, creating new one: ${filePath}`); - } else { - console.warn(`[Kiro Auth] Could not read existing token file ${filePath}: ${readError.message}`); - } - } - const mergedData = { ...existingData, ...newData }; - await fs.writeFile(filePath, JSON.stringify(mergedData, null, 2), 'utf8'); - console.info(`[Kiro Auth] Updated token file: ${filePath}`); - } catch (error) { - console.error(`[Kiro Auth] Failed to write token to file ${filePath}: ${error.message}`); - } - }; - try { const requestBody = { refreshToken: this.refreshToken, @@ -552,7 +508,7 @@ export class KiroApiService { if (this.authMethod === KIRO_CONSTANTS.AUTH_METHOD_SOCIAL) { response = await this.axiosSocialRefreshInstance.post(refreshUrl, requestBody); console.log('[Kiro Auth] Token refresh social response: ok'); - } else { + }else{ response = await this.axiosInstance.post(refreshUrl, requestBody); console.log('[Kiro Auth] Token refresh idc response: ok'); } @@ -573,7 +529,7 @@ export class KiroApiService { refreshToken: this.refreshToken, expiresAt: expiresAt, }; - if (this.profileArn) { + if(this.profileArn){ updatedTokenData.profileArn = this.profileArn; } await saveCredentialsToFile(tokenFilePath, updatedTokenData); @@ -586,26 +542,31 @@ export class KiroApiService { } } + if (!this.accessToken) { + throw new Error('No access token available after initialization and refresh attempts.'); + } + } + /** * Extract text content from OpenAI message format */ getContentText(message) { - if (message == null) { + if(message==null){ return ""; } - if (Array.isArray(message)) { + if (Array.isArray(message) ) { return message .filter(part => part.type === 'text' && part.text) .map(part => part.text) .join(''); } else if (typeof message.content === 'string') { return message.content; - } else if (Array.isArray(message.content)) { + } else if (Array.isArray(message.content) ) { return message.content .filter(part => part.type === 'text' && part.text) .map(part => part.text) .join(''); - } + } return String(message.content || message); } @@ -614,7 +575,7 @@ export class KiroApiService { */ buildCodewhispererRequest(messages, model, tools = null, inSystemPrompt = null) { const conversationId = uuidv4(); - + let systemPrompt = this.getContentText(inSystemPrompt); const processedMessages = messages; @@ -635,12 +596,12 @@ export class KiroApiService { const mergedMessages = []; for (let i = 0; i < processedMessages.length; i++) { const currentMsg = processedMessages[i]; - + if (mergedMessages.length === 0) { mergedMessages.push(currentMsg); } else { const lastMsg = mergedMessages[mergedMessages.length - 1]; - + // 判断当前消息和上一条消息是否为相同 role if (currentMsg.role === lastMsg.role) { // 合并消息内容 @@ -663,13 +624,13 @@ export class KiroApiService { } } } - + // 用合并后的消息替换原消息数组 processedMessages.length = 0; processedMessages.push(...mergedMessages); const codewhispererModel = MODEL_MAPPING[model] || MODEL_MAPPING[this.modelName]; - + let toolsContext = {}; if (tools && Array.isArray(tools) && tools.length > 0) { toolsContext = { @@ -713,14 +674,14 @@ export class KiroApiService { } // 保留最近 5 条历史消息中的图片 - const keepImageThreshold = 5; + const keepImageThreshold = 5; for (let i = startIndex; i < processedMessages.length - 1; i++) { const message = processedMessages[i]; // 计算当前消息距离最后一条消息的位置(从后往前数) const distanceFromEnd = (processedMessages.length - 1) - i; // 如果距离末尾不超过 5 条,则保留图片 const shouldKeepImages = distanceFromEnd <= keepImageThreshold; - + if (message.role === 'user') { let userInputMessage = { content: '', @@ -730,7 +691,7 @@ export class KiroApiService { let imageCount = 0; let toolResults = []; let images = []; - + if (Array.isArray(message.content)) { for (const part of message.content) { if (part.type === 'text') { @@ -759,13 +720,13 @@ export class KiroApiService { } else { userInputMessage.content = this.getContentText(message); } - + // 如果有保留的图片,添加到消息中 if (images.length > 0) { userInputMessage.images = images; console.log(`[Kiro] Kept ${images.length} image(s) in recent history message (distance from end: ${distanceFromEnd})`); } - + // 如果有被替换的图片,添加占位符说明 if (imageCount > 0) { const imagePlaceholder = `[此消息包含 ${imageCount} 张图片,已在历史记录中省略]`; @@ -774,7 +735,7 @@ export class KiroApiService { : imagePlaceholder; console.log(`[Kiro] Replaced ${imageCount} image(s) with placeholder in old history message (distance from end: ${distanceFromEnd})`); } - + if (toolResults.length > 0) { // 去重 toolResults - Kiro API 不接受重复的 toolUseId const uniqueToolResults = []; @@ -787,14 +748,14 @@ export class KiroApiService { } userInputMessage.userInputMessageContext = { toolResults: uniqueToolResults }; } - + history.push({ userInputMessage }); } else if (message.role === 'assistant') { let assistantResponseMessage = { content: '' }; let toolUses = []; - + if (Array.isArray(message.content)) { for (const part of message.content) { if (part.type === 'text') { @@ -810,12 +771,12 @@ export class KiroApiService { } else { assistantResponseMessage.content = this.getContentText(message); } - + // 只添加非空字段 if (toolUses.length > 0) { assistantResponseMessage.toolUses = toolUses; } - + history.push({ assistantResponseMessage }); } } @@ -831,7 +792,7 @@ export class KiroApiService { // 因为 CodeWhisperer API 的 currentMessage 必须是 userInputMessage 类型 if (currentMessage.role === 'assistant') { console.log('[Kiro] Last message is assistant, moving it to history and creating user currentMessage'); - + // 构建 assistant 消息并加入 history let assistantResponseMessage = { content: '', @@ -856,7 +817,7 @@ export class KiroApiService { delete assistantResponseMessage.toolUses; } history.push({ assistantResponseMessage }); - + // 设置 currentContent 为 "Continue",因为我们需要一个 user 消息来触发 AI 继续 currentContent = 'Continue'; } else { @@ -874,7 +835,7 @@ export class KiroApiService { }); } } - + // 处理 user 消息 if (Array.isArray(currentMessage.content)) { for (const part of currentMessage.content) { @@ -918,7 +879,7 @@ export class KiroApiService { currentMessage: {} // Will be populated as userInputMessage } }; - + // 只有当 history 非空时才添加(API 可能不接受空数组) if (history.length > 0) { request.conversationState.history = history; @@ -951,39 +912,11 @@ export class KiroApiService { } userInputMessageContext.toolResults = uniqueToolResults; } - const historyHasToolCalling = history.some(h => - h.assistantResponseMessage?.toolUses || - h.userInputMessage?.userInputMessageContext?.toolResults - ); - if (Object.keys(toolsContext).length > 0 && toolsContext.tools) { userInputMessageContext.tools = toolsContext.tools; - } else if (historyHasToolCalling && !toolsContext.tools) { - const toolNamesInHistory = new Set(); - history.forEach(h => { - if (h.assistantResponseMessage?.toolUses) { - h.assistantResponseMessage.toolUses.forEach(tu => { - toolNamesInHistory.add(tu.name); - }); - } - }); - - if (toolNamesInHistory.size > 0) { - userInputMessageContext.tools = Array.from(toolNamesInHistory).map(name => ({ - toolSpecification: { - name: name, - description: "Tool", - inputSchema: { - json: { - type: "object", - properties: {} - } - } - } - })); - } } + // 只有当 userInputMessageContext 有内容时才添加 if (Object.keys(userInputMessageContext).length > 0) { userInputMessage.userInputMessageContext = userInputMessageContext; } @@ -993,7 +926,7 @@ export class KiroApiService { if (this.authMethod === KIRO_CONSTANTS.AUTH_METHOD_SOCIAL) { request.profileArn = this.profileArn; } - + // fs.writeFile('claude-kiro-request'+Date.now()+'.json', JSON.stringify(request)); return request; } @@ -1009,10 +942,10 @@ export class KiroApiService { // 使用更精确的正则来匹配 SSE 格式的事件 const sseEventRegex = /:message-typeevent(\{[^]*?(?=:event-type|$))/g; const legacyEventRegex = /event(\{.*?(?=event\{|$))/gs; - + // 首先尝试使用 SSE 格式解析 let matches = [...rawStr.matchAll(sseEventRegex)]; - + // 如果 SSE 格式没有匹配到,回退到旧的格式 if (matches.length === 0) { matches = [...rawStr.matchAll(legacyEventRegex)]; @@ -1073,7 +1006,7 @@ export class KiroApiService { } } } - + // 如果还有未完成的工具调用,添加到列表中 if (currentToolCallDict) { toolCalls.push(currentToolCallDict); @@ -1096,7 +1029,7 @@ export class KiroApiService { const uniqueToolCalls = deduplicateToolCalls(toolCalls); return { content: fullContent || '', toolCalls: uniqueToolCalls }; } - + /** * 调用 API 并处理错误重试 @@ -1123,10 +1056,10 @@ export class KiroApiService { const status = error.response?.status; const errorCode = error.code; const errorMessage = error.message || ''; - + // 检查是否为可重试的网络错误 const isNetworkError = isRetryableNetworkError(error); - + if (status === 403 && !isRetry) { console.log('[Kiro] Received 403. Attempting token refresh and retrying...'); try { @@ -1137,7 +1070,7 @@ export class KiroApiService { throw refreshError; } } - + // Handle 429 (Too Many Requests) with exponential backoff if (status === 429 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); @@ -1170,6 +1103,7 @@ export class KiroApiService { _processApiResponse(response) { const rawResponseText = Buffer.isBuffer(response.data) ? response.data.toString('utf8') : String(response.data); + //console.log(`[Kiro] Raw response length: ${rawResponseText.length}`); if (rawResponseText.includes("[Called")) { console.log("[Kiro] Raw response contains [Called marker."); } @@ -1203,7 +1137,7 @@ export class KiroApiService { } fullResponseText = fullResponseText.replace(/\s+/g, ' ').trim(); } - + //console.log(`[Kiro] Final response text after tool call cleanup: ${fullResponseText}`); //console.log(`[Kiro] Final tool calls after deduplication: ${JSON.stringify(uniqueToolCalls)}`); return { responseText: fullResponseText, toolCalls: uniqueToolCalls }; @@ -1211,32 +1145,23 @@ export class KiroApiService { async generateContent(model, requestBody) { if (!this.isInitialized) await this.initialize(); - + // 检查 token 是否即将过期,如果是则先刷新 if (this.isExpiryDateNear()) { console.log('[Kiro] Token is near expiry, refreshing before generateContent request...'); await this.initializeAuth(true); } - + const finalModel = MODEL_MAPPING[model] ? model : this.modelName; console.log(`[Kiro] Calling generateContent with model: ${finalModel}`); - + + // Estimate input tokens before making the API call + const inputTokens = this.estimateInputTokens(requestBody); + const response = await this.callApi('', finalModel, requestBody); try { const { responseText, toolCalls } = this._processApiResponse(response); - - let inputTokens = 0; - const rawResponseText = Buffer.isBuffer(response.data) - ? response.data.toString('utf8') - : String(response.data); - - const contextUsageMatch = rawResponseText.match(/"contextUsagePercentage":\s*([\d.]+)/); - if (contextUsageMatch) { - const percentage = parseFloat(contextUsageMatch[1]); - inputTokens = this.calculateInputTokensFromPercentage(percentage); - } - return this.buildClaudeResponse(responseText, false, 'assistant', model, toolCalls, inputTokens); } catch (error) { console.error('[Kiro] Error in generateContent:', error); @@ -1252,55 +1177,56 @@ export class KiroApiService { const events = []; let remaining = buffer; let searchStart = 0; - + while (true) { // 查找真正的 JSON payload 起始位置 // AWS Event Stream 包含二进制头部,我们只搜索有效的 JSON 模式 // Kiro 返回格式: {"content":"..."} 或 {"name":"xxx","toolUseId":"xxx",...} 或 {"followupPrompt":"..."} - + // 搜索所有可能的 JSON payload 开头模式 // Kiro 返回的 toolUse 可能分多个事件: // 1. {"name":"xxx","toolUseId":"xxx"} - 开始 // 2. {"input":"..."} - input 数据(可能多次) // 3. {"stop":true} - 结束 + // 4. {"contextUsagePercentage":...} - 上下文使用百分比(最后一条消息) const contentStart = remaining.indexOf('{"content":', searchStart); const nameStart = remaining.indexOf('{"name":', searchStart); const followupStart = remaining.indexOf('{"followupPrompt":', searchStart); const inputStart = remaining.indexOf('{"input":', searchStart); const stopStart = remaining.indexOf('{"stop":', searchStart); const contextUsageStart = remaining.indexOf('{"contextUsagePercentage":', searchStart); - + // 找到最早出现的有效 JSON 模式 const candidates = [contentStart, nameStart, followupStart, inputStart, stopStart, contextUsageStart].filter(pos => pos >= 0); if (candidates.length === 0) break; - + const jsonStart = Math.min(...candidates); if (jsonStart < 0) break; - + // 正确处理嵌套的 {} - 使用括号计数法 let braceCount = 0; let jsonEnd = -1; let inString = false; let escapeNext = false; - + for (let i = jsonStart; i < remaining.length; i++) { const char = remaining[i]; - + if (escapeNext) { escapeNext = false; continue; } - + if (char === '\\') { escapeNext = true; continue; } - + if (char === '"') { inString = !inString; continue; } - + if (!inString) { if (char === '{') { braceCount++; @@ -1313,13 +1239,13 @@ export class KiroApiService { } } } - + if (jsonEnd < 0) { // 不完整的 JSON,保留在缓冲区等待更多数据 remaining = remaining.substring(jsonStart); break; } - + const jsonStr = remaining.substring(jsonStart, jsonEnd + 1); try { const parsed = JSON.parse(jsonStr); @@ -1333,8 +1259,8 @@ export class KiroApiService { } // 处理结构化工具调用事件 - 开始事件(包含 name 和 toolUseId) else if (parsed.name && parsed.toolUseId) { - events.push({ - type: 'toolUse', + events.push({ + type: 'toolUse', data: { name: parsed.name, toolUseId: parsed.toolUseId, @@ -1352,8 +1278,8 @@ export class KiroApiService { } }); } - // 处理工具调用的结束事件(只有 stop 字段) - else if (parsed.stop !== undefined) { + // 处理工具调用的结束事件(只有 stop 字段,且不包含 contextUsagePercentage) + else if (parsed.stop !== undefined && parsed.contextUsagePercentage === undefined) { events.push({ type: 'toolUseStop', data: { @@ -1361,31 +1287,31 @@ export class KiroApiService { } }); } - // 处理 context usage percentage 事件 + // 处理上下文使用百分比事件(最后一条消息) else if (parsed.contextUsagePercentage !== undefined) { events.push({ type: 'contextUsage', data: { - percentage: parsed.contextUsagePercentage + contextUsagePercentage: parsed.contextUsagePercentage } }); } } catch (e) { // JSON 解析失败,跳过这个位置继续搜索 } - + searchStart = jsonEnd + 1; if (searchStart >= remaining.length) { remaining = ''; break; } } - + // 如果 searchStart 有进展,截取剩余部分 if (searchStart > 0 && remaining.length > 0) { remaining = remaining.substring(searchStart); } - + return { events, remaining }; } @@ -1409,22 +1335,22 @@ export class KiroApiService { let stream = null; try { - const response = await this.axiosInstance.post(requestUrl, requestData, { + const response = await this.axiosInstance.post(requestUrl, requestData, { headers, responseType: 'stream' }); stream = response.data; let buffer = ''; - let lastContentEvent = null; + let lastContentEvent = null; // 用于检测连续重复的 content 事件 for await (const chunk of stream) { buffer += chunk.toString(); - + // 解析缓冲区中的事件 const { events, remaining } = this.parseAwsEventStreamBuffer(buffer); buffer = remaining; - + // yield 所有事件,但过滤连续完全相同的 content 事件(Kiro API 有时会重复发送) for (const event of events) { if (event.type === 'content' && event.data) { @@ -1442,7 +1368,7 @@ export class KiroApiService { } else if (event.type === 'toolUseStop') { yield { type: 'toolUseStop', stop: event.data.stop }; } else if (event.type === 'contextUsage') { - yield { type: 'contextUsage', percentage: event.data.percentage }; + yield { type: 'contextUsage', contextUsagePercentage: event.data.contextUsagePercentage }; } } } @@ -1451,21 +1377,21 @@ export class KiroApiService { if (stream && typeof stream.destroy === 'function') { stream.destroy(); } - + const status = error.response?.status; const errorCode = error.code; const errorMessage = error.message || ''; - + // 检查是否为可重试的网络错误 const isNetworkError = isRetryableNetworkError(error); - + if (status === 403 && !isRetry) { console.log('[Kiro] Received 403 in stream. Attempting token refresh and retrying...'); await this.initializeAuth(true); yield* this.streamApiReal(method, model, body, true, retryCount); return; } - + if (status === 429 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); console.log(`[Kiro] Received 429 in stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); @@ -1516,62 +1442,61 @@ export class KiroApiService { // 真正的流式传输实现 async * generateContentStream(model, requestBody) { if (!this.isInitialized) await this.initialize(); - + // 检查 token 是否即将过期,如果是则先刷新 if (this.isExpiryDateNear()) { console.log('[Kiro] Token is near expiry, refreshing before generateContentStream request...'); await this.initializeAuth(true); } - + const finalModel = MODEL_MAPPING[model] ? model : this.modelName; console.log(`[Kiro] Calling generateContentStream with model: ${finalModel} (real streaming)`); - - // 预先估算 inputTokens 作为保底值,如果收到 contextUsagePercentage 会被覆盖 - let inputTokens = this.estimateInputTokens(requestBody); + + const inputTokens = this.estimateInputTokens(requestBody); const messageId = `${uuidv4()}`; - - // 立即发送 message_start,不等待 contextUsagePercentage - yield { - type: "message_start", - message: { - id: messageId, - type: "message", - role: "assistant", - model: model, - usage: { - input_tokens: 0, - output_tokens: 0, - cache_creation_input_tokens: 0, - cache_read_input_tokens: 0 - }, - content: [] - } - }; - - yield { - type: "content_block_start", - index: 0, - content_block: { type: "text", text: "" } - }; - + try { + // 1. 先发送 message_start 事件 + yield { + type: "message_start", + message: { + id: messageId, + type: "message", + role: "assistant", + model: model, + usage: { input_tokens: inputTokens, output_tokens: 0 }, + content: [] + } + }; + + // 2. 发送 content_block_start 事件 + yield { + type: "content_block_start", + index: 0, + content_block: { type: "text", text: "" } + }; + let totalContent = ''; let outputTokens = 0; const toolCalls = []; - let currentToolCall = null; + let currentToolCall = null; // 用于累积结构化工具调用 + let contextUsagePercentage = null; // 用于存储上下文使用百分比 + // 3. 流式接收并发送每个 content_block_delta for await (const event of this.streamApiReal('', finalModel, requestBody)) { - if (event.type === 'contextUsage' && event.percentage) { - // 收到 contextUsagePercentage 时更新 inputTokens,用于最终的 message_delta - inputTokens = this.calculateInputTokensFromPercentage(event.percentage); - } else if (event.type === 'content' && event.content) { + if (event.type === 'content' && event.content) { totalContent += event.content; - + // 不再每个 chunk 都计算 token,改为最后统一计算,避免阻塞事件循环 + yield { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: event.content } }; + } else if (event.type === 'contextUsage') { + // 捕获上下文使用百分比 + contextUsagePercentage = event.contextUsagePercentage; + console.log(`[Kiro] Received contextUsagePercentage: ${contextUsagePercentage}%`); } else if (event.type === 'toolUse') { const tc = event.toolUse; // 工具调用事件(包含 name 和 toolUseId) @@ -1602,7 +1527,7 @@ export class KiroApiService { if (tc.stop) { try { currentToolCall.input = JSON.parse(currentToolCall.input); - } catch (e) { } + } catch (e) {} toolCalls.push(currentToolCall); currentToolCall = null; } @@ -1625,16 +1550,16 @@ export class KiroApiService { } } } - + // 处理未完成的工具调用(如果流提前结束) if (currentToolCall) { try { currentToolCall.input = JSON.parse(currentToolCall.input); - } catch (e) { } + } catch (e) {} toolCalls.push(currentToolCall); currentToolCall = null; } - + // 检查文本内容中的 bracket 格式工具调用 const bracketToolCalls = parseBracketToolCalls(totalContent); if (bracketToolCalls && bracketToolCalls.length > 0) { @@ -1655,7 +1580,7 @@ export class KiroApiService { for (let i = 0; i < toolCalls.length; i++) { const tc = toolCalls[i]; const blockIndex = i + 1; - + yield { type: "content_block_start", index: blockIndex, @@ -1666,7 +1591,7 @@ export class KiroApiService { input: {} } }; - + yield { type: "content_block_delta", index: blockIndex, @@ -1675,30 +1600,34 @@ export class KiroApiService { partial_json: typeof tc.input === 'string' ? tc.input : JSON.stringify(tc.input || {}) } }; - + yield { type: "content_block_stop", index: blockIndex }; } } // 6. 发送 message_delta 事件 - // 在流结束后统一计算 output tokens,避免在流式循环中阻塞事件循环 - outputTokens = this.countTextTokens(totalContent); - for (const tc of toolCalls) { - outputTokens += this.countTextTokens(JSON.stringify(tc.input || {})); + // 如果有 contextUsagePercentage,使用它来计算 token + // 总上下文 200k tokens,通过百分比计算总使用量,再减去输入 token 得到输出 token + let totalTokens = 0; + if (contextUsagePercentage !== null && contextUsagePercentage > 0) { + const totalContextTokens = KIRO_CONSTANTS.TOTAL_CONTEXT_TOKENS; + // totalUsedTokens 就是通过百分比计算出的总使用量,直接作为 total_tokens + totalTokens = Math.round(totalContextTokens * contextUsagePercentage / 100); + outputTokens = Math.max(0, totalTokens - inputTokens); + console.log(`[Kiro] Token calculation from contextUsagePercentage: total=${totalTokens}, input=${inputTokens}, output=${outputTokens}`); + } else { + // 回退到原来的计算方式 + outputTokens = this.countTextTokens(totalContent); + for (const tc of toolCalls) { + outputTokens += this.countTextTokens(JSON.stringify(tc.input || {})); + } + totalTokens = inputTokens + outputTokens; } - + yield { type: "message_delta", - delta: { - stop_reason: toolCalls.length > 0 ? "tool_use" : "end_turn", - stop_sequence: null - }, - usage: { - input_tokens: inputTokens, - output_tokens: outputTokens, - cache_creation_input_tokens: 0, - cache_read_input_tokens: 0 - } + delta: { stop_reason: toolCalls.length > 0 ? "tool_use" : "end_turn" }, + usage: { input_tokens: inputTokens, output_tokens: outputTokens, total_tokens: totalTokens } }; // 7. 发送 message_stop 事件 @@ -1725,34 +1654,17 @@ export class KiroApiService { } /** - * Convert context usage percentage to actual input tokens - * @param {number} percentage - Context usage percentage (0-100) - * @returns {number} Actual input tokens - */ - calculateInputTokensFromPercentage(percentage) { - if (!percentage || percentage <= 0) { - return 0; - } - - const contextWindow = CLAUDE_DEFAULT_MAX_TOKENS; - const inputTokens = Math.round((percentage / 100) * contextWindow); - - return inputTokens; - } - - /** - * Estimate input tokens from request body using Claude's official tokenizer - * Used as fallback when contextUsagePercentage is not available from API + * Calculate input tokens from request body using Claude's official tokenizer */ estimateInputTokens(requestBody) { let totalTokens = 0; - + // Count system prompt tokens if (requestBody.system) { const systemText = this.getContentText(requestBody.system); totalTokens += this.countTextTokens(systemText); } - + // Count all messages tokens if (requestBody.messages && Array.isArray(requestBody.messages)) { for (const message of requestBody.messages) { @@ -1762,12 +1674,12 @@ export class KiroApiService { } } } - + // Count tools definitions tokens if present if (requestBody.tools && Array.isArray(requestBody.tools)) { totalTokens += this.countTextTokens(JSON.stringify(requestBody.tools)); } - + return totalTokens; } @@ -1797,7 +1709,7 @@ export class KiroApiService { content: [] // Content will be streamed via content_block_delta } }); - + let totalOutputTokens = 0; let stopReason = "end_turn"; @@ -1943,9 +1855,7 @@ export class KiroApiService { stop_sequence: null, usage: { input_tokens: inputTokens, - output_tokens: outputTokens, - cache_creation_input_tokens: 0, - cache_read_input_tokens: 0 + output_tokens: outputTokens }, content: contentArray }; @@ -1959,7 +1869,7 @@ export class KiroApiService { const models = KIRO_MODELS.map(id => ({ name: id })); - + return { models: models }; } @@ -2054,16 +1964,16 @@ export class KiroApiService { */ async getUsageLimits() { if (!this.isInitialized) await this.initialize(); - + // 检查 token 是否即将过期,如果是则先刷新 if (this.isExpiryDateNear()) { console.log('[Kiro] Token is near expiry, refreshing before getUsageLimits request...'); await this.initializeAuth(true); } - + // 内部固定的资源类型 const resourceType = 'AGENTIC_REQUEST'; - + // 构建请求 URL const usageLimitsUrl = KIRO_CONSTANTS.USAGE_LIMITS_URL.replace('{{region}}', this.region); const params = new URLSearchParams({ @@ -2071,7 +1981,7 @@ export class KiroApiService { origin: KIRO_CONSTANTS.ORIGIN_AI_EDITOR, resourceType: resourceType }); - if (this.authMethod === KIRO_CONSTANTS.AUTH_METHOD_SOCIAL && this.profileArn) { + if (this.authMethod === KIRO_CONSTANTS.AUTH_METHOD_SOCIAL && this.profileArn) { params.append('profileArn', this.profileArn); } const fullUrl = `${usageLimitsUrl}?${params.toString()}`; diff --git a/src/claude/claude-strategy.js b/src/providers/claude/claude-strategy.js similarity index 96% rename from src/claude/claude-strategy.js rename to src/providers/claude/claude-strategy.js index 0152113..e9c4981 100644 --- a/src/claude/claude-strategy.js +++ b/src/providers/claude/claude-strategy.js @@ -1,5 +1,5 @@ -import { ProviderStrategy } from '../provider-strategy.js'; -import { extractSystemPromptFromRequestBody, MODEL_PROTOCOL_PREFIX } from '../common.js'; +import { ProviderStrategy } from '../../utils/provider-strategy.js'; +import { extractSystemPromptFromRequestBody, MODEL_PROTOCOL_PREFIX } from '../../utils/common.js'; /** * Claude provider strategy implementation. diff --git a/src/gemini/antigravity-core.js b/src/providers/gemini/antigravity-core.js similarity index 99% rename from src/gemini/antigravity-core.js rename to src/providers/gemini/antigravity-core.js index 29d3acf..ba46567 100644 --- a/src/gemini/antigravity-core.js +++ b/src/providers/gemini/antigravity-core.js @@ -9,11 +9,11 @@ import * as os from 'os'; import * as readline from 'readline'; import { v4 as uuidv4 } from 'uuid'; import open from 'open'; -import { formatExpiryTime, isRetryableNetworkError } from '../common.js'; +import { formatExpiryTime, isRetryableNetworkError } from '../../utils/common.js'; import { getProviderModels } from '../provider-models.js'; -import { handleGeminiAntigravityOAuth } from '../oauth-handlers.js'; -import { getProxyConfigForProvider, getGoogleAuthProxyConfig } from '../proxy-utils.js'; -import { cleanJsonSchemaProperties } from '../converters/utils.js'; +import { handleGeminiAntigravityOAuth } from '../../auth/oauth-handlers.js'; +import { getProxyConfigForProvider, getGoogleAuthProxyConfig } from '../../utils/proxy-utils.js'; +import { cleanJsonSchemaProperties } from '../../converters/utils.js'; // 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏 const httpAgent = new http.Agent({ diff --git a/src/gemini/gemini-core.js b/src/providers/gemini/gemini-core.js similarity index 99% rename from src/gemini/gemini-core.js rename to src/providers/gemini/gemini-core.js index da494d4..a8bb5d7 100644 --- a/src/gemini/gemini-core.js +++ b/src/providers/gemini/gemini-core.js @@ -6,10 +6,10 @@ import * as path from 'path'; import * as os from 'os'; import * as readline from 'readline'; import open from 'open'; -import { API_ACTIONS, formatExpiryTime, isRetryableNetworkError } from '../common.js'; +import { API_ACTIONS, formatExpiryTime, isRetryableNetworkError } from '../../utils/common.js'; import { getProviderModels } from '../provider-models.js'; -import { handleGeminiCliOAuth } from '../oauth-handlers.js'; -import { getProxyConfigForProvider, getGoogleAuthProxyConfig } from '../proxy-utils.js'; +import { handleGeminiCliOAuth } from '../../auth/oauth-handlers.js'; +import { getProxyConfigForProvider, getGoogleAuthProxyConfig } from '../../utils/proxy-utils.js'; // 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏 const httpAgent = new http.Agent({ diff --git a/src/gemini/gemini-strategy.js b/src/providers/gemini/gemini-strategy.js similarity index 95% rename from src/gemini/gemini-strategy.js rename to src/providers/gemini/gemini-strategy.js index e4c0c3b..4a175be 100644 --- a/src/gemini/gemini-strategy.js +++ b/src/providers/gemini/gemini-strategy.js @@ -1,5 +1,5 @@ -import { API_ACTIONS, extractSystemPromptFromRequestBody, MODEL_PROTOCOL_PREFIX } from '../common.js'; -import { ProviderStrategy } from '../provider-strategy.js'; +import { API_ACTIONS, extractSystemPromptFromRequestBody, MODEL_PROTOCOL_PREFIX } from '../../utils/common.js'; +import { ProviderStrategy } from '../../utils/provider-strategy.js'; /** * Gemini provider strategy implementation. diff --git a/src/openai/iflow-core.js b/src/providers/openai/iflow-core.js similarity index 99% rename from src/openai/iflow-core.js rename to src/providers/openai/iflow-core.js index 345a199..c02b9a4 100644 --- a/src/openai/iflow-core.js +++ b/src/providers/openai/iflow-core.js @@ -22,8 +22,8 @@ import * as https from 'https'; import { promises as fs } from 'fs'; import * as path from 'path'; import * as os from 'os'; -import { configureAxiosProxy } from '../proxy-utils.js'; -import { isRetryableNetworkError } from '../common.js'; +import { configureAxiosProxy } from '../../utils/proxy-utils.js'; +import { isRetryableNetworkError } from '../../utils/common.js'; // iFlow API 端点 const IFLOW_API_BASE_URL = 'https://apis.iflow.cn/v1'; @@ -643,8 +643,8 @@ export class IFlowApiService { } const currentTime = Date.now(); - // 默认 10 分钟,可通过配置覆盖 - const cronNearMinutes = this.config.CRON_NEAR_MINUTES || 10; + // 授权文件时效48小时,判断是否过期或接近过期 (45小时) + const cronNearMinutes = 60 * 45; const cronNearMinutesInMillis = cronNearMinutes * 60 * 1000; // 解析过期时间 diff --git a/src/openai/openai-core.js b/src/providers/openai/openai-core.js similarity index 98% rename from src/openai/openai-core.js rename to src/providers/openai/openai-core.js index ca4812d..fa7dabc 100644 --- a/src/openai/openai-core.js +++ b/src/providers/openai/openai-core.js @@ -1,8 +1,8 @@ import axios from 'axios'; import * as http from 'http'; import * as https from 'https'; -import { configureAxiosProxy } from '../proxy-utils.js'; -import { isRetryableNetworkError } from '../common.js'; +import { configureAxiosProxy } from '../../utils/proxy-utils.js'; +import { isRetryableNetworkError } from '../../utils/common.js'; // Assumed OpenAI API specification service for interacting with third-party models export class OpenAIApiService { diff --git a/src/openai/openai-responses-core.js b/src/providers/openai/openai-responses-core.js similarity index 99% rename from src/openai/openai-responses-core.js rename to src/providers/openai/openai-responses-core.js index b753bc6..0af2b77 100644 --- a/src/openai/openai-responses-core.js +++ b/src/providers/openai/openai-responses-core.js @@ -1,7 +1,7 @@ import axios from 'axios'; import * as http from 'http'; import * as https from 'https'; -import { configureAxiosProxy } from '../proxy-utils.js'; +import { configureAxiosProxy } from '../../utils/proxy-utils.js'; // OpenAI Responses API specification service for interacting with third-party models export class OpenAIResponsesApiService { diff --git a/src/openai/openai-responses-core.mjs b/src/providers/openai/openai-responses-core.mjs similarity index 100% rename from src/openai/openai-responses-core.mjs rename to src/providers/openai/openai-responses-core.mjs diff --git a/src/openai/openai-responses-strategy.js b/src/providers/openai/openai-responses-strategy.js similarity index 97% rename from src/openai/openai-responses-strategy.js rename to src/providers/openai/openai-responses-strategy.js index cd1d6be..74a679e 100644 --- a/src/openai/openai-responses-strategy.js +++ b/src/providers/openai/openai-responses-strategy.js @@ -1,5 +1,5 @@ -import { ProviderStrategy } from '../provider-strategy.js'; -import { extractSystemPromptFromRequestBody, MODEL_PROTOCOL_PREFIX } from '../common.js'; +import { ProviderStrategy } from '../../utils/provider-strategy.js'; +import { extractSystemPromptFromRequestBody, MODEL_PROTOCOL_PREFIX } from '../../utils/common.js'; /** * OpenAI Responses API strategy implementation. diff --git a/src/openai/openai-strategy.js b/src/providers/openai/openai-strategy.js similarity index 96% rename from src/openai/openai-strategy.js rename to src/providers/openai/openai-strategy.js index 5201db4..b5231d9 100644 --- a/src/openai/openai-strategy.js +++ b/src/providers/openai/openai-strategy.js @@ -1,5 +1,5 @@ -import { ProviderStrategy } from '../provider-strategy.js'; -import { extractSystemPromptFromRequestBody, MODEL_PROTOCOL_PREFIX } from '../common.js'; +import { ProviderStrategy } from '../../utils/provider-strategy.js'; +import { extractSystemPromptFromRequestBody, MODEL_PROTOCOL_PREFIX } from '../../utils/common.js'; /** * OpenAI provider strategy implementation. diff --git a/src/openai/qwen-core.js b/src/providers/openai/qwen-core.js similarity index 99% rename from src/openai/qwen-core.js rename to src/providers/openai/qwen-core.js index 43dd8bd..f6cfc9b 100644 --- a/src/openai/qwen-core.js +++ b/src/providers/openai/qwen-core.js @@ -9,9 +9,9 @@ import open from 'open'; import { EventEmitter } from 'events'; import { randomUUID } from 'node:crypto'; import { getProviderModels } from '../provider-models.js'; -import { handleQwenOAuth } from '../oauth-handlers.js'; -import { configureAxiosProxy } from '../proxy-utils.js'; -import { isRetryableNetworkError } from '../common.js'; +import { handleQwenOAuth } from '../../auth/oauth-handlers.js'; +import { configureAxiosProxy } from '../../utils/proxy-utils.js'; +import { isRetryableNetworkError } from '../../utils/common.js'; // --- Constants --- const QWEN_DIR = '.qwen'; diff --git a/src/provider-models.js b/src/providers/provider-models.js similarity index 100% rename from src/provider-models.js rename to src/providers/provider-models.js diff --git a/src/provider-pool-manager.js b/src/providers/provider-pool-manager.js similarity index 99% rename from src/provider-pool-manager.js rename to src/providers/provider-pool-manager.js index b40bc97..8c777a3 100644 --- a/src/provider-pool-manager.js +++ b/src/providers/provider-pool-manager.js @@ -1,6 +1,6 @@ import * as fs from 'fs'; // Import fs module import { getServiceAdapter } from './adapter.js'; -import { MODEL_PROVIDER, getProtocolPrefix } from './common.js'; +import { MODEL_PROVIDER, getProtocolPrefix } from '../utils/common.js'; import { getProviderModels } from './provider-models.js'; import axios from 'axios'; diff --git a/src/scripts/kiro-idc-token-refresh.js b/src/scripts/kiro-idc-token-refresh.js new file mode 100644 index 0000000..66c0be7 --- /dev/null +++ b/src/scripts/kiro-idc-token-refresh.js @@ -0,0 +1,281 @@ +/** + * Kiro IDC Token Refresh Tool + * 通过 refreshToken + clientId + clientSecret 获取 accessToken (基于 AWS OIDC/IDC) + * + * 使用方法: + * 1. 位置参数模式: + * node src/kiro-idc-token-refresh.js [authMethod] [provider] + * 2. JSON 文件模式: + * node src/kiro-idc-token-refresh.js ./config.json + * 3. JSON 字符串模式: + * node src/kiro-idc-token-refresh.js '{"refreshToken": "...", "clientId": "...", "clientSecret": "..."}' + * + * 参数: + * refreshToken - Kiro 的 refresh token + * clientId - AWS OIDC client ID + * clientSecret - AWS OIDC client secret + * authMethod - 认证方法 (可选,默认: IdC) + * provider - 提供商 (可选,默认: BuilderId) + * + * 输出格式: + * { + * "accessToken": "aoaAAAA", + * "refreshToken": "aorAAAAAGnTpTMP_mR", + * "expiresAt": "2026-01-06T14:22:16.130Z", + * "authMethod": "IdC", + * "provider": "BuilderId", + * "clientId": "e8pqSrALVjvbqaW", + * "clientSecret": "eyJraWQiOiJrZXktMTU2NDAy", + * "region": "us-east-1" + * } + */ + +import axios from 'axios'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +// 获取当前脚本所在目录 +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const KIRO_IDC_CONSTANTS = { + REFRESH_IDC_URL: 'https://oidc.{{region}}.amazonaws.com/token', + CONTENT_TYPE_JSON: 'application/json', + DEFAULT_AUTH_METHOD: 'IdC', + DEFAULT_PROVIDER: 'BuilderId', + DEFAULT_REGION: 'us-east-1', + AXIOS_TIMEOUT: 30000, // 30 seconds timeout +}; + +/** + * 通过 IDC (AWS OIDC) 刷新 token + * @param {string} refreshToken - Kiro 的 refresh token + * @param {string} clientId - AWS OIDC client ID + * @param {string} clientSecret - AWS OIDC client secret + * @param {Object} options - 可选参数 + * @param {string} options.authMethod - 认证方法 (默认: IdC) + * @param {string} options.provider - 提供商 (默认: BuilderId) + * @param {string} options.region - AWS 区域 (默认: us-east-1) + * @returns {Promise} 包含 accessToken 等信息的对象 + */ +async function refreshKiroIdcToken(refreshToken, clientId, clientSecret, options = {}) { + const authMethod = options.authMethod || KIRO_IDC_CONSTANTS.DEFAULT_AUTH_METHOD; + const provider = options.provider || KIRO_IDC_CONSTANTS.DEFAULT_PROVIDER; + const region = options.region || KIRO_IDC_CONSTANTS.DEFAULT_REGION; + + const refreshUrl = KIRO_IDC_CONSTANTS.REFRESH_IDC_URL.replace('{{region}}', region); + + // IDC/OIDC 使用 form-urlencoded 格式 + const requestBody = { + grantType: 'refresh_token', + refreshToken: refreshToken, + clientId: clientId, + clientSecret: clientSecret, + }; + + const axiosConfig = { + timeout: KIRO_IDC_CONSTANTS.AXIOS_TIMEOUT, + headers: { + 'Content-Type': KIRO_IDC_CONSTANTS.CONTENT_TYPE_JSON, + 'User-Agent': 'KiroIDE' + }, + }; + + try { + console.log(`[Kiro IDC Token Refresh] 正在请求: ${refreshUrl}`); + const response = await axios.post(refreshUrl, requestBody, axiosConfig); + + // AWS OIDC 返回格式: { access_token, refresh_token, expires_in, token_type } + if (response.data && response.data.accessToken) { + const expiresIn = response.data.expiresIn; + const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString(); + + const result = { + accessToken: response.data.accessToken, + refreshToken: response.data.refreshToken || refreshToken, + expiresAt: expiresAt, + authMethod: authMethod, + provider: provider, + clientId: clientId, + clientSecret: clientSecret, + region: region, + }; + + return result; + } else { + throw new Error('Invalid refresh response: Missing access_token'); + } + } catch (error) { + if (error.response) { + console.error(`[Kiro IDC Token Refresh] 请求失败: HTTP ${error.response.status}`); + console.error(`[Kiro IDC Token Refresh] 响应内容:`, error.response.data); + } else if (error.request) { + console.error(`[Kiro IDC Token Refresh] 请求失败: 无响应`); + } else { + console.error(`[Kiro IDC Token Refresh] 请求失败:`, error.message); + } + throw error; + } +} + +/** + * 主函数 - 命令行入口 + */ +async function main() { + const args = process.argv.slice(2); + + let refreshToken, clientId, clientSecret, authMethod, provider; + + // 1. 尝试解析第一个参数为 JSON 文件路径 + if (args.length === 1 && args[0].toLowerCase().endsWith('.json')) { + try { + const jsonPath = path.isAbsolute(args[0]) ? args[0] : path.resolve(process.cwd(), args[0]); + if (fs.existsSync(jsonPath)) { + console.log(`[Kiro IDC Token Refresh] 正在从文件读取配置: ${jsonPath}`); + const fileContent = fs.readFileSync(jsonPath, 'utf-8'); + const parsed = JSON.parse(fileContent); + refreshToken = parsed.refreshToken; + clientId = parsed.clientId; + clientSecret = parsed.clientSecret; + authMethod = parsed.authMethod; + provider = parsed.provider; + } else { + console.error(`错误: 找不到文件 ${jsonPath}`); + process.exit(1); + } + } catch (e) { + console.error(`错误: 读取或解析 JSON 文件失败: ${e.message}`); + process.exit(1); + } + } + // 2. 尝试解析第一个参数为 JSON 字符串 + else if (args.length === 1 && args[0].trim().startsWith('{')) { + try { + const parsed = JSON.parse(args[0]); + refreshToken = parsed.refreshToken; + clientId = parsed.clientId; + clientSecret = parsed.clientSecret; + authMethod = parsed.authMethod; + provider = parsed.provider; + } catch (e) { + // JSON 解析失败,将回退到位置参数处理 + } + } + + // 如果没有通过 JSON 成功获取参数,则尝试位置参数 + if (!refreshToken) { + if (args.length === 0 || args.length < 3) { + console.log('Kiro IDC Token Refresh Tool'); + console.log('============================'); + console.log(''); + console.log('使用方法:'); + console.log(' 1. 位置参数模式:'); + console.log(' node src/kiro-idc-token-refresh.js [authMethod] [provider]'); + console.log(' 2. JSON 文件模式:'); + console.log(' node src/kiro-idc-token-refresh.js ./config.json'); + console.log(' 3. JSON 字符串模式:'); + console.log(' node src/kiro-idc-token-refresh.js \'{"refreshToken": "...", "clientId": "...", "clientSecret": "...", "authMethod": "...", "provider": "..."}\''); + console.log(''); + console.log('参数:'); + console.log(' refreshToken - Kiro 的 refresh token (必需)'); + console.log(' clientId - AWS OIDC client ID (必需)'); + console.log(' clientSecret - AWS OIDC client secret (必需)'); + console.log(' authMethod - 认证方法 (可选,默认: IdC)'); + console.log(' provider - 提供商 (可选,默认: BuilderId)'); + console.log(''); + console.log('示例:'); + console.log(' node src/kiro-idc-token-refresh.js aorAxxxxxxxx e8pqSrALVjvbqaW eyJraWQiOiJrZXktMTU2NDAy'); + console.log(' node src/kiro-idc-token-refresh.js aorAxxxxxxxx e8pqSrALVjvbqaW eyJraWQiOiJrZXktMTU2NDAy IdC Enterprise'); + console.log(''); + console.log('输出格式:'); + console.log(JSON.stringify({ + accessToken: "aoaAAAA...", + refreshToken: "aorAAAAAGnTpTMP_mR...", + expiresAt: "2026-01-06T14:22:16.130Z", + authMethod: "IdC", + provider: "BuilderId", + clientId: "e8pqSrALVjvbqaW", + clientSecret: "eyJraWQiOiJrZXktMTU2NDAy", + region: "us-east-1" + }, null, 2)); + process.exit(0); + } + + refreshToken = args[0]; + clientId = args[1]; + clientSecret = args[2]; + authMethod = args[3]; + provider = args[4]; + } + + // 设置默认值 + authMethod = authMethod || KIRO_IDC_CONSTANTS.DEFAULT_AUTH_METHOD; + provider = provider || KIRO_IDC_CONSTANTS.DEFAULT_PROVIDER; + + if (!refreshToken) { + console.error('错误: 请提供 refreshToken'); + process.exit(1); + } + + if (!clientId) { + console.error('错误: 请提供 clientId'); + process.exit(1); + } + + if (!clientSecret) { + console.error('错误: 请提供 clientSecret'); + process.exit(1); + } + + try { + console.log(`[Kiro IDC Token Refresh] 开始刷新 token...`); + console.log(`[Kiro IDC Token Refresh] 认证方法: ${authMethod}`); + console.log(`[Kiro IDC Token Refresh] 提供商: ${provider}`); + console.log(`[Kiro IDC Token Refresh] Client ID: ${clientId.substring(0, 8)}...`); + + const result = await refreshKiroIdcToken(refreshToken, clientId, clientSecret, { + authMethod, + provider, + region: KIRO_IDC_CONSTANTS.DEFAULT_REGION + }); + + console.log(''); + console.log('=== Token 刷新成功 ==='); + console.log(''); + console.log(JSON.stringify(result, null, 2)); + + // 输出过期时间信息 + const expiresDate = new Date(result.expiresAt); + const now = new Date(); + const diffMs = expiresDate - now; + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMins / 60); + + console.log(''); + console.log(`[Kiro IDC Token Refresh] Token 将在 ${diffHours} 小时 ${diffMins % 60} 分钟后过期`); + console.log(`[Kiro IDC Token Refresh] 过期时间: ${result.expiresAt}`); + + // 写入 JSON 文件到脚本执行目录 + const timestamp = Date.now(); + const outputFileName = `kiro-idc-${timestamp}-auth-token.json`; + const outputFilePath = path.join(__dirname, outputFileName); + + fs.writeFileSync(outputFilePath, JSON.stringify(result, null, 2), 'utf-8'); + + console.log(''); + console.log(`[Kiro IDC Token Refresh] Token 已保存到文件: ${outputFilePath}`); + + } catch (error) { + console.error(''); + console.error('=== Token 刷新失败 ==='); + console.error(`错误: ${error.message}`); + process.exit(1); + } +} + +// 导出函数供其他模块使用 +export { refreshKiroIdcToken }; + +// 如果直接运行此脚本,执行主函数 +main(); \ No newline at end of file diff --git a/src/scripts/kiro-token-refresh.js b/src/scripts/kiro-token-refresh.js new file mode 100644 index 0000000..88c735e --- /dev/null +++ b/src/scripts/kiro-token-refresh.js @@ -0,0 +1,184 @@ +/** + * Kiro Token Refresh Tool + * 通过 refreshToken 获取 accessToken 并转换为指定格式 + * + * 使用方法: + * node src/kiro-token-refresh.js [region] + * + * 参数: + * refreshToken - Kiro 的 refresh token + * region - AWS 区域 (可选,默认: us-east-1) + * + * 输出格式: + * { + * "accessToken": "aoaAAAAAGlfTyA8C4c", + * "refreshToken": "aorA", + * "profileArn": "arn:aws:codewhisperer:us-east-1:699475941385:profile/EHGA3GRVQMUK", + * "expiresAt": "2026-01-08T06:30:59.065Z", + * "authMethod": "social", + * "provider": "Google" + * } + */ + +import axios from 'axios'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +// 获取当前脚本所在目录 +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const KIRO_CONSTANTS = { + REFRESH_URL: 'https://prod.{{region}}.auth.desktop.kiro.dev/refreshToken', + CONTENT_TYPE_JSON: 'application/json', + AUTH_METHOD_SOCIAL: 'social', + DEFAULT_PROVIDER: 'Google', + AXIOS_TIMEOUT: 30000, // 30 seconds timeout +}; + +/** + * 通过 refreshToken 获取 accessToken + * @param {string} refreshToken - Kiro 的 refresh token + * @param {string} region - AWS 区域 (默认: us-east-1) + * @returns {Promise} 包含 accessToken 等信息的对象 + */ +async function refreshKiroToken(refreshToken, region = 'us-east-1') { + const refreshUrl = KIRO_CONSTANTS.REFRESH_URL.replace('{{region}}', region); + + const requestBody = { + refreshToken: refreshToken, + }; + + const axiosConfig = { + timeout: KIRO_CONSTANTS.AXIOS_TIMEOUT, + headers: { + 'Content-Type': KIRO_CONSTANTS.CONTENT_TYPE_JSON, + }, + }; + + try { + console.log(`[Kiro Token Refresh] 正在请求: ${refreshUrl}`); + const response = await axios.post(refreshUrl, requestBody, axiosConfig); + + if (response.data && response.data.accessToken) { + const expiresIn = response.data.expiresIn; + const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString(); + + const result = { + accessToken: response.data.accessToken, + refreshToken: response.data.refreshToken || refreshToken, + profileArn: response.data.profileArn || '', + expiresAt: expiresAt, + authMethod: KIRO_CONSTANTS.AUTH_METHOD_SOCIAL, + provider: KIRO_CONSTANTS.DEFAULT_PROVIDER, + }; + + // 如果响应中包含 region 信息,添加到结果中 + if (region) { + result.region = region; + } + + return result; + } else { + throw new Error('Invalid refresh response: Missing accessToken'); + } + } catch (error) { + if (error.response) { + console.error(`[Kiro Token Refresh] 请求失败: HTTP ${error.response.status}`); + console.error(`[Kiro Token Refresh] 响应内容:`, error.response.data); + } else if (error.request) { + console.error(`[Kiro Token Refresh] 请求失败: 无响应`); + } else { + console.error(`[Kiro Token Refresh] 请求失败:`, error.message); + } + throw error; + } +} + +/** + * 主函数 - 命令行入口 + */ +async function main() { + const args = process.argv.slice(2); + + if (args.length === 0) { + console.log('Kiro Token Refresh Tool'); + console.log('========================'); + console.log(''); + console.log('使用方法:'); + console.log(' node src/kiro-token-refresh.js [region]'); + console.log(''); + console.log('参数:'); + console.log(' refreshToken - Kiro 的 refresh token (必需)'); + console.log(' region - AWS 区域 (可选,默认: us-east-1)'); + console.log(''); + console.log('示例:'); + console.log(' node src/kiro-token-refresh.js aorAxxxxxxxx'); + console.log(' node src/kiro-token-refresh.js aorAxxxxxxxx us-west-2'); + console.log(''); + console.log('输出格式:'); + console.log(JSON.stringify({ + accessToken: "aoaAAAAAGlfTyA8C4c...", + refreshToken: "aorA...", + profileArn: "arn:aws:codewhisperer:us-east-1:699475941385:profile/EHGA3GRVQMUK", + expiresAt: "2026-01-08T06:30:59.065Z", + authMethod: "social", + provider: "Google" + }, null, 2)); + process.exit(0); + } + + const refreshToken = args[0]; + const region = args[1] || 'us-east-1'; + + if (!refreshToken) { + console.error('错误: 请提供 refreshToken'); + process.exit(1); + } + + try { + console.log(`[Kiro Token Refresh] 开始刷新 token...`); + console.log(`[Kiro Token Refresh] 区域: ${region}`); + + const result = await refreshKiroToken(refreshToken, region); + + console.log(''); + console.log('=== Token 刷新成功 ==='); + console.log(''); + console.log(JSON.stringify(result, null, 2)); + + // 输出过期时间信息 + const expiresDate = new Date(result.expiresAt); + const now = new Date(); + const diffMs = expiresDate - now; + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMins / 60); + + console.log(''); + console.log(`[Kiro Token Refresh] Token 将在 ${diffHours} 小时 ${diffMins % 60} 分钟后过期`); + console.log(`[Kiro Token Refresh] 过期时间: ${result.expiresAt}`); + + // 写入 JSON 文件到脚本执行目录 + const timestamp = Date.now(); + const outputFileName = `kiro-${timestamp}-auth-token.json`; + const outputFilePath = path.join(__dirname, outputFileName); + + fs.writeFileSync(outputFilePath, JSON.stringify(result, null, 2), 'utf-8'); + + console.log(''); + console.log(`[Kiro Token Refresh] Token 已保存到文件: ${outputFilePath}`); + + } catch (error) { + console.error(''); + console.error('=== Token 刷新失败 ==='); + console.error(`错误: ${error.message}`); + process.exit(1); + } +} + +// 导出函数供其他模块使用 +export { refreshKiroToken }; + +// 如果直接运行此脚本,执行主函数 +main(); \ No newline at end of file diff --git a/src/scripts/merge-json-files.js b/src/scripts/merge-json-files.js new file mode 100644 index 0000000..b0f8416 --- /dev/null +++ b/src/scripts/merge-json-files.js @@ -0,0 +1,125 @@ +/** + * JSON File Merger Tool + * 解析当前或指定目录的 .json 文件,合并为一个 JSON 对象,并保存到执行脚本的目录下。 + * + * 功能: + * 1. 扫描目录下的所有 .json 文件。 + * 2. 读取并解析每个文件。 + * 3. 过滤掉非对象 JSON 内容。 + * 4. 将所有对象属性合并到一个大对象中。 + * 5. 特殊处理: 如果合并后的对象中包含 clientSecret 字段,则移除 expiresAt 字段。 + * 6. 输出文件名为: merge-kiro-<时间戳>-auth-token.json + * + * 使用方法: + * node src/merge-json-files.js [directory] + * + * 参数: + * directory - 要扫描的目录路径 (可选,默认: 当前脚本执行目录) + */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +// 获取当前脚本所在目录 +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +/** + * 主函数 + */ +async function main() { + // 获取命令行参数中的目录,如果未提供则使用当前工作目录 (process.cwd()) + // 注意:用户需求是"解析当前或指定目录",这里的"当前"通常指用户运行命令时的目录 + const args = process.argv.slice(2); + const targetDir = args[0] ? path.resolve(process.cwd(), args[0]) : process.cwd(); + + console.log(`[JSON Merger] 扫描目录: ${targetDir}`); + + if (!fs.existsSync(targetDir)) { + console.error(`错误: 目录不存在 ${targetDir}`); + process.exit(1); + } + + try { + const files = fs.readdirSync(targetDir); + const jsonFiles = files.filter(file => file.toLowerCase().endsWith('.json')); + + if (jsonFiles.length === 0) { + console.log('[JSON Merger] 未找到 JSON 文件。'); + process.exit(0); + } + + console.log(`[JSON Merger] 找到 ${jsonFiles.length} 个 JSON 文件`); + + let mergedData = {}; + let successCount = 0; + let skipCount = 0; + + for (const file of jsonFiles) { + const filePath = path.join(targetDir, file); + + // 跳过自身生成的合并文件,防止递归合并垃圾数据 (简单的名字检查) + if (file.startsWith('merge-kiro-') && file.endsWith('-auth-token.json')) { + console.log(`[JSON Merger] 跳过之前的合并文件: ${file}`); + skipCount++; + continue; + } + + try { + const content = fs.readFileSync(filePath, 'utf-8'); + const jsonData = JSON.parse(content); + + // 处理逻辑: + // 仅处理对象类型,如果是数组则跳过或尝试合并数组中的对象(通常合并对象意味着所有字段平铺到一个对象中) + // 鉴于用户要求合并为一个 JSON 对象,假设所有文件内容都是部分配置,需要合并到一起。 + + if (typeof jsonData === 'object' && jsonData !== null && !Array.isArray(jsonData)) { + Object.assign(mergedData, jsonData); + successCount++; + } else { + console.log(`[JSON Merger] 文件 ${file} 内容格式不符合要求 (非纯对象),跳过`); + skipCount++; + continue; + } + + } catch (error) { + console.warn(`[JSON Merger] 解析文件 ${file} 失败: ${error.message}`); + skipCount++; + } + } + + // 特殊处理: 如果包含 clientSecret,移除 expiresAt + // 注意:这是在合并后的对象上进行处理,因为 clientSecret 和 expiresAt 可能来自不同文件,或者合并后才决定 + if (mergedData.clientSecret && mergedData.expiresAt) { + delete mergedData.expiresAt; + } + + if (Object.keys(mergedData).length === 0) { + console.log('[JSON Merger] 没有有效的数据需要合并。'); + process.exit(0); + } + + // 生成输出文件名 + const timestamp = Date.now(); + const outputFileName = `merge-kiro-${timestamp}-auth-token.json`; + // 用户需求: "保存到执行脚本的目录下" -> 即 __dirname + const outputFilePath = path.join(__dirname, outputFileName); + + fs.writeFileSync(outputFilePath, JSON.stringify(mergedData, null, 2), 'utf-8'); + + console.log(''); + console.log('=== 合并完成 ==='); + console.log(`扫描文件数: ${jsonFiles.length}`); + console.log(`成功处理: ${successCount}`); + console.log(`跳过/失败: ${skipCount}`); + console.log(`合并字段数: ${Object.keys(mergedData).length}`); + console.log(`输出文件: ${outputFilePath}`); + + } catch (error) { + console.error(`[JSON Merger] 处理过程中发生错误: ${error.message}`); + process.exit(1); + } +} + +main(); \ No newline at end of file diff --git a/src/api-manager.js b/src/services/api-manager.js similarity index 99% rename from src/api-manager.js rename to src/services/api-manager.js index 98d660b..4303710 100644 --- a/src/api-manager.js +++ b/src/services/api-manager.js @@ -3,7 +3,7 @@ import { handleContentGenerationRequest, API_ACTIONS, ENDPOINT_TYPE -} from './common.js'; +} from '../utils/common.js'; import { getProviderPoolManager } from './service-manager.js'; /** diff --git a/src/api-server.js b/src/services/api-server.js similarity index 98% rename from src/api-server.js rename to src/services/api-server.js index 3a305eb..dabd95e 100644 --- a/src/api-server.js +++ b/src/services/api-server.js @@ -1,10 +1,10 @@ import * as http from 'http'; -import { initializeConfig, CONFIG } from './config-manager.js'; +import { initializeConfig, CONFIG } from '../core/config-manager.js'; import { initApiService, autoLinkProviderConfigs } from './service-manager.js'; import { initializeUIManagement } from './ui-manager.js'; import { initializeAPIManagement } from './api-manager.js'; -import { createRequestHandler } from './request-handler.js'; -import { discoverPlugins, getPluginManager } from './plugin-manager.js'; +import { createRequestHandler } from '../handlers/request-handler.js'; +import { discoverPlugins, getPluginManager } from '../core/plugin-manager.js'; /** * @license @@ -112,7 +112,7 @@ import { discoverPlugins, getPluginManager } from './plugin-manager.js'; */ import 'dotenv/config'; // Import dotenv and configure it -import './converters/register-converters.js'; // 注册所有转换器 +import '../converters/register-converters.js'; // 注册所有转换器 import { getProviderPoolManager } from './service-manager.js'; // 检测是否作为子进程运行 diff --git a/src/service-manager.js b/src/services/service-manager.js similarity index 98% rename from src/service-manager.js rename to src/services/service-manager.js index e18405f..3bdba07 100644 --- a/src/service-manager.js +++ b/src/services/service-manager.js @@ -1,5 +1,5 @@ -import { getServiceAdapter, serviceInstances } from './adapter.js'; -import { ProviderPoolManager } from './provider-pool-manager.js'; +import { getServiceAdapter, serviceInstances } from '../providers/adapter.js'; +import { ProviderPoolManager } from '../providers/provider-pool-manager.js'; import deepmerge from 'deepmerge'; import * as fs from 'fs'; import { promises as pfs } from 'fs'; @@ -11,7 +11,7 @@ import { isPathUsed, getFileName, formatSystemPath -} from './provider-utils.js'; +} from '../utils/provider-utils.js'; // 存储 ProviderPoolManager 实例 let providerPoolManager = null; diff --git a/src/ui-manager.js b/src/services/ui-manager.js similarity index 94% rename from src/ui-manager.js rename to src/services/ui-manager.js index 030cd07..2fa9f93 100644 --- a/src/ui-manager.js +++ b/src/services/ui-manager.js @@ -2,19 +2,19 @@ import { existsSync, readFileSync } from 'fs'; import path from 'path'; // Import UI modules -import * as auth from './ui-modules/auth.js'; -import * as configApi from './ui-modules/config-api.js'; -import * as providerApi from './ui-modules/provider-api.js'; -import * as usageApi from './ui-modules/usage-api.js'; -import * as pluginApi from './ui-modules/plugin-api.js'; -import * as uploadConfigApi from './ui-modules/upload-config-api.js'; -import * as systemApi from './ui-modules/system-api.js'; -import * as updateApi from './ui-modules/update-api.js'; -import * as oauthApi from './ui-modules/oauth-api.js'; -import * as eventBroadcast from './ui-modules/event-broadcast.js'; +import * as auth from '../ui-modules/auth.js'; +import * as configApi from '../ui-modules/config-api.js'; +import * as providerApi from '../ui-modules/provider-api.js'; +import * as usageApi from '../ui-modules/usage-api.js'; +import * as pluginApi from '../ui-modules/plugin-api.js'; +import * as uploadConfigApi from '../ui-modules/upload-config-api.js'; +import * as systemApi from '../ui-modules/system-api.js'; +import * as updateApi from '../ui-modules/update-api.js'; +import * as oauthApi from '../ui-modules/oauth-api.js'; +import * as eventBroadcast from '../ui-modules/event-broadcast.js'; // Re-export from event-broadcast module -export { broadcastEvent, initializeUIManagement, handleUploadOAuthCredentials, upload } from './ui-modules/event-broadcast.js'; +export { broadcastEvent, initializeUIManagement, handleUploadOAuthCredentials, upload } from '../ui-modules/event-broadcast.js'; /** * Serve static files for the UI diff --git a/src/usage-service.js b/src/services/usage-service.js similarity index 99% rename from src/usage-service.js rename to src/services/usage-service.js index 8f23965..77fcb07 100644 --- a/src/usage-service.js +++ b/src/services/usage-service.js @@ -4,8 +4,8 @@ */ import { getProviderPoolManager } from './service-manager.js'; -import { serviceInstances } from './adapter.js'; -import { MODEL_PROVIDER } from './common.js'; +import { serviceInstances } from '../providers/adapter.js'; +import { MODEL_PROVIDER } from '../utils/common.js'; /** * 用量查询服务类 diff --git a/src/ui-modules/config-api.js b/src/ui-modules/config-api.js index f07e5d4..4d36c23 100644 --- a/src/ui-modules/config-api.js +++ b/src/ui-modules/config-api.js @@ -1,11 +1,11 @@ import { existsSync, readFileSync, writeFileSync } from 'fs'; import { promises as fs } from 'fs'; import path from 'path'; -import { CONFIG } from '../config-manager.js'; -import { serviceInstances } from '../adapter.js'; -import { initApiService } from '../service-manager.js'; -import { getRequestBody } from '../common.js'; -import { broadcastEvent } from './event-broadcast.js'; +import { CONFIG } from '../core/config-manager.js'; +import { serviceInstances } from '../providers/adapter.js'; +import { initApiService } from '../services/service-manager.js'; +import { getRequestBody } from '../utils/common.js'; +import { broadcastEvent } from '../ui-modules/event-broadcast.js'; /** * 重载配置文件 @@ -15,7 +15,7 @@ import { broadcastEvent } from './event-broadcast.js'; export async function reloadConfig(providerPoolManager) { try { // Import config manager dynamically - const { initializeConfig } = await import('../config-manager.js'); + const { initializeConfig } = await import('../core/config-manager.js'); // Reload main config const newConfig = await initializeConfig(process.argv.slice(2), 'configs/config.json'); diff --git a/src/ui-modules/config-scanner.js b/src/ui-modules/config-scanner.js index 97cf8b7..e9021d0 100644 --- a/src/ui-modules/config-scanner.js +++ b/src/ui-modules/config-scanner.js @@ -1,7 +1,7 @@ import { existsSync } from 'fs'; import { promises as fs } from 'fs'; import path from 'path'; -import { addToUsedPaths, isPathUsed, pathsEqual } from '../provider-utils.js'; +import { addToUsedPaths, isPathUsed, pathsEqual } from '../utils/provider-utils.js'; /** * 扫描和分析配置文件 diff --git a/src/ui-modules/oauth-api.js b/src/ui-modules/oauth-api.js index f2de003..f16704c 100644 --- a/src/ui-modules/oauth-api.js +++ b/src/ui-modules/oauth-api.js @@ -1,4 +1,4 @@ -import { getRequestBody } from '../common.js'; +import { getRequestBody } from '../utils/common.js'; import { handleGeminiCliOAuth, handleGeminiAntigravityOAuth, @@ -7,7 +7,7 @@ import { handleIFlowOAuth, batchImportKiroRefreshTokensStream, importAwsCredentials -} from '../oauth-handlers.js'; +} from '../auth/oauth-handlers.js'; /** * 生成 OAuth 授权 URL diff --git a/src/ui-modules/plugin-api.js b/src/ui-modules/plugin-api.js index a3bf5fc..2d30b79 100644 --- a/src/ui-modules/plugin-api.js +++ b/src/ui-modules/plugin-api.js @@ -1,5 +1,5 @@ -import { getPluginManager } from '../plugin-manager.js'; -import { getRequestBody } from '../common.js'; +import { getPluginManager } from '../core/plugin-manager.js'; +import { getRequestBody } from '../utils/common.js'; import { broadcastEvent } from './event-broadcast.js'; /** diff --git a/src/ui-modules/provider-api.js b/src/ui-modules/provider-api.js index 9400b17..1da20b4 100644 --- a/src/ui-modules/provider-api.js +++ b/src/ui-modules/provider-api.js @@ -1,7 +1,7 @@ import { existsSync, readFileSync, writeFileSync } from 'fs'; -import { getRequestBody } from '../common.js'; -import { getAllProviderModels, getProviderModels } from '../provider-models.js'; -import { generateUUID, createProviderConfig, formatSystemPath, detectProviderFromPath, addToUsedPaths, isPathUsed, pathsEqual } from '../provider-utils.js'; +import { getRequestBody } from '../utils/common.js'; +import { getAllProviderModels, getProviderModels } from '../providers/provider-models.js'; +import { generateUUID, createProviderConfig, formatSystemPath, detectProviderFromPath, addToUsedPaths, isPathUsed, pathsEqual } from '../utils/provider-utils.js'; import { broadcastEvent } from './event-broadcast.js'; /** diff --git a/src/ui-modules/usage-api.js b/src/ui-modules/usage-api.js index 164434c..de7d25e 100644 --- a/src/ui-modules/usage-api.js +++ b/src/ui-modules/usage-api.js @@ -1,6 +1,6 @@ -import { CONFIG } from '../config-manager.js'; -import { serviceInstances, getServiceAdapter } from '../adapter.js'; -import { formatKiroUsage, formatGeminiUsage, formatAntigravityUsage } from '../usage-service.js'; +import { CONFIG } from '../core/config-manager.js'; +import { serviceInstances, getServiceAdapter } from '../providers/adapter.js'; +import { formatKiroUsage, formatGeminiUsage, formatAntigravityUsage } from '../services/usage-service.js'; import { readUsageCache, writeUsageCache, readProviderUsageCache, updateProviderUsageCache } from './usage-cache.js'; import path from 'path'; diff --git a/src/common.js b/src/utils/common.js similarity index 99% rename from src/common.js rename to src/utils/common.js index 1376444..1ea4439 100644 --- a/src/common.js +++ b/src/utils/common.js @@ -2,9 +2,9 @@ import { promises as fs } from 'fs'; import * as path from 'path'; import * as http from 'http'; // Add http for IncomingMessage and ServerResponse types import * as crypto from 'crypto'; // Import crypto for MD5 hashing -import { convertData, getOpenAIStreamChunkStop } from './convert.js'; +import { convertData, getOpenAIStreamChunkStop } from '../convert/convert.js'; import { ProviderStrategyFactory } from './provider-strategies.js'; -import { getPluginManager } from './plugin-manager.js'; +import { getPluginManager } from '../core/plugin-manager.js'; // ==================== 网络错误处理 ==================== @@ -446,7 +446,7 @@ export async function handleContentGenerationRequest(req, res, service, endpoint // 2.5. 如果使用了提供商池,根据模型重新选择提供商(支持 Fallback) // 注意:这里使用 skipUsageCount: true,因为初次选择时已经增加了 usageCount if (providerPoolManager && CONFIG.providerPools && CONFIG.providerPools[CONFIG.MODEL_PROVIDER]) { - const { getApiServiceWithFallback } = await import('./service-manager.js'); + const { getApiServiceWithFallback } = await import('../services/service-manager.js'); const result = await getApiServiceWithFallback(CONFIG, model); service = result.service; diff --git a/src/provider-strategies.js b/src/utils/provider-strategies.js similarity index 67% rename from src/provider-strategies.js rename to src/utils/provider-strategies.js index 644d2ed..3567b6f 100644 --- a/src/provider-strategies.js +++ b/src/utils/provider-strategies.js @@ -1,8 +1,8 @@ -import { MODEL_PROTOCOL_PREFIX } from './common.js'; -import { GeminiStrategy } from './gemini/gemini-strategy.js'; -import { OpenAIStrategy } from './openai/openai-strategy.js'; -import { ClaudeStrategy } from './claude/claude-strategy.js'; -import { ResponsesAPIStrategy } from './openai/openai-responses-strategy.js'; +import { MODEL_PROTOCOL_PREFIX } from '../utils/common.js'; +import { GeminiStrategy } from '../providers/gemini/gemini-strategy.js'; +import { OpenAIStrategy } from '../providers/openai/openai-strategy.js'; +import { ClaudeStrategy } from '../providers/claude/claude-strategy.js'; +import { ResponsesAPIStrategy } from '../providers/openai/openai-responses-strategy.js'; /** * Strategy factory that returns the appropriate strategy instance based on the provider protocol. diff --git a/src/provider-strategy.js b/src/utils/provider-strategy.js similarity index 98% rename from src/provider-strategy.js rename to src/utils/provider-strategy.js index 973ddc4..e2c2725 100644 --- a/src/provider-strategy.js +++ b/src/utils/provider-strategy.js @@ -1,5 +1,5 @@ import { promises as fs } from 'fs'; -import { FETCH_SYSTEM_PROMPT_FILE } from './common.js'; +import { FETCH_SYSTEM_PROMPT_FILE } from '../utils/common.js'; /** * Abstract provider strategy class, defining the interface for handling different model providers. diff --git a/src/provider-utils.js b/src/utils/provider-utils.js similarity index 100% rename from src/provider-utils.js rename to src/utils/provider-utils.js diff --git a/src/proxy-utils.js b/src/utils/proxy-utils.js similarity index 100% rename from src/proxy-utils.js rename to src/utils/proxy-utils.js diff --git a/static/app/config-manager.js b/static/app/config-manager.js index 3b12635..4371c7f 100644 --- a/static/app/config-manager.js +++ b/static/app/config-manager.js @@ -24,34 +24,46 @@ async function loadConfiguration() { if (portEl) portEl.value = data.SERVER_PORT || 3000; if (modelProviderEl) { - // 处理多选 MODEL_PROVIDER (复选框) + // 处理多选 MODEL_PROVIDER (标签按钮) const providers = Array.isArray(data.DEFAULT_MODEL_PROVIDERS) ? data.DEFAULT_MODEL_PROVIDERS : (typeof data.MODEL_PROVIDER === 'string' ? data.MODEL_PROVIDER.split(',') : []); - const checkboxes = modelProviderEl.querySelectorAll('input[type="checkbox"]'); - checkboxes.forEach(checkbox => { - checkbox.checked = providers.includes(checkbox.value); + const tags = modelProviderEl.querySelectorAll('.provider-tag'); + tags.forEach(tag => { + const value = tag.getAttribute('data-value'); + if (providers.includes(value)) { + tag.classList.add('selected'); + } else { + tag.classList.remove('selected'); + } }); // 如果没有任何选中的,默认选中第一个(保持兼容性) - const anyChecked = Array.from(checkboxes).some(cb => cb.checked); - if (!anyChecked && checkboxes.length > 0) { - checkboxes[0].checked = true; + const anySelected = Array.from(tags).some(tag => tag.classList.contains('selected')); + if (!anySelected && tags.length > 0) { + tags[0].classList.add('selected'); } - // 为复选框添加事件监听,防止取消勾选最后一个 - checkboxes.forEach(checkbox => { - // 移除旧的监听器(如果有的话,虽然这里大概率没有) - const newCheckbox = checkbox.cloneNode(true); - checkbox.parentNode.replaceChild(newCheckbox, checkbox); + // 为标签按钮添加点击事件监听 + tags.forEach(tag => { + // 移除旧的监听器(通过克隆节点) + const newTag = tag.cloneNode(true); + tag.parentNode.replaceChild(newTag, tag); - newCheckbox.addEventListener('change', (e) => { - const checkedCount = modelProviderEl.querySelectorAll('input[type="checkbox"]:checked').length; - if (checkedCount === 0) { - newCheckbox.checked = true; + newTag.addEventListener('click', (e) => { + e.preventDefault(); + const isSelected = newTag.classList.contains('selected'); + const selectedCount = modelProviderEl.querySelectorAll('.provider-tag.selected').length; + + // 如果当前是选中状态且只剩一个选中的,不允许取消 + if (isSelected && selectedCount === 1) { showToast(t('common.warning'), t('config.modelProviderRequired'), 'warning'); + return; } + + // 切换选中状态 + newTag.classList.toggle('selected'); }); }); } @@ -105,12 +117,34 @@ async function loadConfiguration() { const proxyUrlEl = document.getElementById('proxyUrl'); if (proxyUrlEl) proxyUrlEl.value = data.PROXY_URL || ''; - // 加载启用代理的提供商 - const proxyProviderCheckboxes = document.querySelectorAll('input[name="proxyProvider"]'); - const enabledProviders = data.PROXY_ENABLED_PROVIDERS || []; - proxyProviderCheckboxes.forEach(checkbox => { - checkbox.checked = enabledProviders.includes(checkbox.value); - }); + // 加载启用代理的提供商 (标签按钮) + const proxyProvidersEl = document.getElementById('proxyProviders'); + if (proxyProvidersEl) { + const enabledProviders = data.PROXY_ENABLED_PROVIDERS || []; + const proxyTags = proxyProvidersEl.querySelectorAll('.provider-tag'); + + proxyTags.forEach(tag => { + const value = tag.getAttribute('data-value'); + if (enabledProviders.includes(value)) { + tag.classList.add('selected'); + } else { + tag.classList.remove('selected'); + } + }); + + // 为代理提供商标签按钮添加点击事件监听 + proxyTags.forEach(tag => { + // 移除旧的监听器(通过克隆节点) + const newTag = tag.cloneNode(true); + tag.parentNode.replaceChild(newTag, tag); + + newTag.addEventListener('click', (e) => { + e.preventDefault(); + // 代理提供商可以全部取消选择,所以不需要检查最少选择数量 + newTag.classList.toggle('selected'); + }); + }); + } } catch (error) { console.error('Failed to load configuration:', error); @@ -124,9 +158,9 @@ async function saveConfiguration() { const modelProviderEl = document.getElementById('modelProvider'); let selectedProviders = []; if (modelProviderEl) { - // 从复选框中获取选中的提供商 - selectedProviders = Array.from(modelProviderEl.querySelectorAll('input[type="checkbox"]:checked')) - .map(cb => cb.value); + // 从标签按钮中获取选中的提供商 + selectedProviders = Array.from(modelProviderEl.querySelectorAll('.provider-tag.selected')) + .map(tag => tag.getAttribute('data-value')); } // 校验:必须至少勾选一个 @@ -187,9 +221,14 @@ async function saveConfiguration() { // 保存代理配置 config.PROXY_URL = document.getElementById('proxyUrl')?.value?.trim() || null; - // 获取启用代理的提供商列表 - const proxyProviderCheckboxes = document.querySelectorAll('input[name="proxyProvider"]:checked'); - config.PROXY_ENABLED_PROVIDERS = Array.from(proxyProviderCheckboxes).map(cb => cb.value); + // 获取启用代理的提供商列表 (从标签按钮) + const proxyProvidersEl = document.getElementById('proxyProviders'); + if (proxyProvidersEl) { + config.PROXY_ENABLED_PROVIDERS = Array.from(proxyProvidersEl.querySelectorAll('.provider-tag.selected')) + .map(tag => tag.getAttribute('data-value')); + } else { + config.PROXY_ENABLED_PROVIDERS = []; + } try { await window.apiClient.post('/config', config); diff --git a/static/app/i18n.js b/static/app/i18n.js index e6134ed..56296e7 100644 --- a/static/app/i18n.js +++ b/static/app/i18n.js @@ -455,7 +455,7 @@ const translations = { 'plugins.toggle.success': '插件 {name} 已{status}', 'plugins.toggle.failed': '切换插件状态失败', 'plugins.load.failed': '加载插件列表失败', - 'plugins.restart.required': '更改已保存,请重启服务以生效', + 'plugins.restart.required': '更改已保存', // Common 'common.confirm': '确定', @@ -950,7 +950,7 @@ const translations = { 'plugins.toggle.success': 'Plugin {name} {status}', 'plugins.toggle.failed': 'Failed to toggle plugin status', 'plugins.load.failed': 'Failed to load plugins list', - 'plugins.restart.required': 'Changes saved, please restart service to take effect', + 'plugins.restart.required': 'Changes saved', // Common 'common.togglePassword': 'Show/Hide Password', diff --git a/static/components/section-config.css b/static/components/section-config.css index d8ca61c..cd63a37 100644 --- a/static/components/section-config.css +++ b/static/components/section-config.css @@ -262,47 +262,69 @@ input:checked + .toggle-slider:before { box-shadow: 0 4px 12px var(--primary-40); } -/* 复选框列表样式 */ -.provider-checklist { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - gap: 10px; - padding: 15px; - background: var(--bg-tertiary); - border-radius: 8px; - border: 1px solid var(--border-color); - max-height: 300px; - overflow-y: auto; -} - -.checkbox-item { +/* 提供商标签选择器样式 */ +.provider-tags { display: flex; - align-items: center; - gap: 10px; - padding: 10px 12px; - background: var(--bg-primary); + flex-wrap: wrap; + gap: 0.75rem; + padding: 1rem; + background: var(--bg-tertiary); + border-radius: 12px; border: 1px solid var(--border-color); - border-radius: 6px; - cursor: pointer; - transition: var(--transition); } -.checkbox-item:hover { +.provider-tag { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.625rem 1rem; + background: var(--bg-primary); + border: 2px solid var(--border-color); + border-radius: 50px; + cursor: pointer; + transition: all 0.2s ease; + font-size: 0.875rem; + font-weight: 500; + color: var(--text-secondary); + user-select: none; +} + +.provider-tag:hover { background: var(--bg-secondary); border-color: var(--primary-color); - box-shadow: var(--shadow-sm); + color: var(--primary-color); + transform: translateY(-1px); + box-shadow: 0 4px 12px var(--neutral-shadow-10); } -.checkbox-item input[type="checkbox"] { - margin: 0; +.provider-tag.selected { + background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%); + border-color: transparent; + color: white; + box-shadow: 0 4px 12px var(--primary-30); } -.checkbox-item span { +.provider-tag.selected:hover { + background: linear-gradient(135deg, var(--btn-primary-hover) 0%, var(--primary-color) 100%); + transform: translateY(-1px); + box-shadow: 0 6px 16px var(--primary-40); + color: white; +} + +.provider-tag i { font-size: 0.875rem; - color: var(--text-primary); - font-weight: 500; + opacity: 0.8; } +.provider-tag.selected i { + opacity: 1; +} + +.provider-tag span { + white-space: nowrap; +} + + /* 高级配置区域 */ .advanced-config-section { border: 1px solid var(--border-color); diff --git a/static/components/section-config.html b/static/components/section-config.html index 6bfbf6a..3f2412b 100644 --- a/static/components/section-config.html +++ b/static/components/section-config.html @@ -25,41 +25,41 @@
-
-
@@ -76,41 +76,41 @@
-
-
diff --git a/static/login.html b/static/login.html index 5ad696e..314941a 100644 --- a/static/login.html +++ b/static/login.html @@ -181,7 +181,7 @@ } } - +