refactor(项目结构): 重构项目目录结构并优化代码组织

将常用工具函数移动到utils目录
重构提供商策略模式实现
新增docker-compose构建配置文件
优化UI配置选择器的样式和交互
重构代理工具和API管理模块
更新脚本路径和依赖引用
This commit is contained in:
hex2077 2026-01-10 18:19:06 +08:00
parent 4554a4cfd2
commit 2d317e0333
61 changed files with 1189 additions and 611 deletions

View file

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

View file

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

View file

@ -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
node src\core\master.js

View file

@ -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
node src/core/master.js

View file

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

View file

@ -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 提供商配置

View file

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

View file

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

View file

@ -3,7 +3,7 @@
* 使用工厂模式管理转换器实例的创建和缓存
*/
import { MODEL_PROTOCOL_PREFIX } from '../common.js';
import { MODEL_PROTOCOL_PREFIX } from '../utils/common.js';
/**
* 转换器工厂单例模式 + 工厂模式

View file

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

View file

@ -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转换器类

View file

@ -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转换器类

View file

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

View file

@ -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转换器类

View file

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

View file

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

View file

@ -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, // 主进程管理端口

View file

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

View file

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

View file

@ -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';
/**
* 解析请求体

View file

@ -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服务适配器接口
// 所有的服务适配器都应该实现这些方法

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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;
// 解析过期时间

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,281 @@
/**
* Kiro IDC Token Refresh Tool
* 通过 refreshToken + clientId + clientSecret 获取 accessToken (基于 AWS OIDC/IDC)
*
* 使用方法:
* 1. 位置参数模式:
* node src/kiro-idc-token-refresh.js <refreshToken> <clientId> <clientSecret> [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<Object>} 包含 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 <refreshToken> <clientId> <clientSecret> [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();

View file

@ -0,0 +1,184 @@
/**
* Kiro Token Refresh Tool
* 通过 refreshToken 获取 accessToken 并转换为指定格式
*
* 使用方法:
* node src/kiro-token-refresh.js <refreshToken> [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<Object>} 包含 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 <refreshToken> [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();

View file

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

View file

@ -3,7 +3,7 @@ import {
handleContentGenerationRequest,
API_ACTIONS,
ENDPOINT_TYPE
} from './common.js';
} from '../utils/common.js';
import { getProviderPoolManager } from './service-manager.js';
/**

View file

@ -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';
// 检测是否作为子进程运行

View file

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

View file

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

View file

@ -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';
/**
* 用量查询服务类

View file

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

View file

@ -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';
/**
* 扫描和分析配置文件

View file

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

View file

@ -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';
/**

View file

@ -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';
/**

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -25,41 +25,41 @@
</div>
<div class="form-group pool-section">
<label data-i18n="config.modelProvider">模型提供商 (可多选)</label>
<div id="modelProvider" class="provider-checklist">
<label class="checkbox-item">
<input type="checkbox" value="gemini-cli-oauth">
<div id="modelProvider" class="provider-tags">
<button type="button" class="provider-tag" data-value="gemini-cli-oauth">
<i class="fas fa-robot"></i>
<span>Gemini CLI OAuth</span>
</label>
<label class="checkbox-item">
<input type="checkbox" value="gemini-antigravity">
</button>
<button type="button" class="provider-tag" data-value="gemini-antigravity">
<i class="fas fa-rocket"></i>
<span>Gemini Antigravity</span>
</label>
<label class="checkbox-item">
<input type="checkbox" value="openai-custom">
</button>
<button type="button" class="provider-tag" data-value="openai-custom">
<i class="fas fa-brain"></i>
<span>OpenAI Custom</span>
</label>
<label class="checkbox-item">
<input type="checkbox" value="claude-custom">
</button>
<button type="button" class="provider-tag" data-value="claude-custom">
<i class="fas fa-comment-dots"></i>
<span>Claude Custom</span>
</label>
<label class="checkbox-item">
<input type="checkbox" value="claude-kiro-oauth">
</button>
<button type="button" class="provider-tag" data-value="claude-kiro-oauth">
<i class="fas fa-key"></i>
<span>Claude Kiro OAuth</span>
</label>
<label class="checkbox-item">
<input type="checkbox" value="openai-qwen-oauth">
</button>
<button type="button" class="provider-tag" data-value="openai-qwen-oauth">
<i class="fas fa-cloud"></i>
<span>Qwen OAuth</span>
</label>
<label class="checkbox-item">
<input type="checkbox" value="openaiResponses-custom">
</button>
<button type="button" class="provider-tag" data-value="openaiResponses-custom">
<i class="fas fa-reply"></i>
<span>OpenAI Responses</span>
</label>
<label class="checkbox-item">
<input type="checkbox" value="openai-iflow">
</button>
<button type="button" class="provider-tag" data-value="openai-iflow">
<i class="fas fa-stream"></i>
<span>iFlow OAuth</span>
</label>
</button>
</div>
<small class="form-text" data-i18n="config.modelProviderHelp">勾选启动时初始化的模型提供商 (必须至少选一个)</small>
<small class="form-text" data-i18n="config.modelProviderHelp">点击选择启动时初始化的模型提供商 (必须至少选一个)</small>
</div>
<!-- 高级配置区域 -->
@ -76,41 +76,41 @@
</div>
<div class="form-group pool-section">
<label data-i18n="config.proxy.enabledProviders">启用代理的提供商</label>
<div id="proxyProviders" class="provider-checklist">
<label class="checkbox-item">
<input type="checkbox" name="proxyProvider" value="gemini-cli-oauth">
<div id="proxyProviders" class="provider-tags">
<button type="button" class="provider-tag" data-value="gemini-cli-oauth">
<i class="fas fa-robot"></i>
<span>Gemini CLI OAuth</span>
</label>
<label class="checkbox-item">
<input type="checkbox" name="proxyProvider" value="gemini-antigravity">
</button>
<button type="button" class="provider-tag" data-value="gemini-antigravity">
<i class="fas fa-rocket"></i>
<span>Gemini Antigravity</span>
</label>
<label class="checkbox-item">
<input type="checkbox" name="proxyProvider" value="openai-custom">
</button>
<button type="button" class="provider-tag" data-value="openai-custom">
<i class="fas fa-brain"></i>
<span>OpenAI Custom</span>
</label>
<label class="checkbox-item">
<input type="checkbox" name="proxyProvider" value="claude-custom">
</button>
<button type="button" class="provider-tag" data-value="claude-custom">
<i class="fas fa-comment-dots"></i>
<span>Claude Custom</span>
</label>
<label class="checkbox-item">
<input type="checkbox" name="proxyProvider" value="claude-kiro-oauth">
</button>
<button type="button" class="provider-tag" data-value="claude-kiro-oauth">
<i class="fas fa-key"></i>
<span>Claude Kiro OAuth</span>
</label>
<label class="checkbox-item">
<input type="checkbox" name="proxyProvider" value="openai-qwen-oauth">
</button>
<button type="button" class="provider-tag" data-value="openai-qwen-oauth">
<i class="fas fa-cloud"></i>
<span>Qwen OAuth</span>
</label>
<label class="checkbox-item">
<input type="checkbox" name="proxyProvider" value="openaiResponses-custom">
</button>
<button type="button" class="provider-tag" data-value="openaiResponses-custom">
<i class="fas fa-reply"></i>
<span>OpenAI Responses</span>
</label>
<label class="checkbox-item">
<input type="checkbox" name="proxyProvider" value="openai-iflow">
</button>
<button type="button" class="provider-tag" data-value="openai-iflow">
<i class="fas fa-stream"></i>
<span>iFlow OAuth</span>
</label>
</button>
</div>
<small class="form-text" data-i18n="config.proxy.enabledProvidersNote">选择需要通过代理访问的提供商,未选中的提供商将直接连接</small>
<small class="form-text" data-i18n="config.proxy.enabledProvidersNote">点击选择需要通过代理访问的提供商,未选中的提供商将直接连接</small>
</div>
</div>

View file

@ -181,7 +181,7 @@
}
}
</style>
<link rel="stylesheet" href="app/styles.css">
<link rel="stylesheet" href="app/base.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body>