refactor(项目结构): 重构项目目录结构并优化代码组织
将常用工具函数移动到utils目录 重构提供商策略模式实现 新增docker-compose构建配置文件 优化UI配置选择器的样式和交互 重构代理工具和API管理模块 更新脚本路径和依赖引用
This commit is contained in:
parent
4554a4cfd2
commit
2d317e0333
61 changed files with 1189 additions and 611 deletions
23
docker/docker-compose.build.yml
Normal file
23
docker/docker-compose.build.yml
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 提供商配置
|
||||
|
|
@ -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,
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
* 使用工厂模式管理转换器实例的创建和缓存
|
||||
*/
|
||||
|
||||
import { MODEL_PROTOCOL_PREFIX } from '../common.js';
|
||||
import { MODEL_PROTOCOL_PREFIX } from '../utils/common.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';
|
||||
|
|
|
|||
|
|
@ -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转换器类
|
||||
|
|
|
|||
|
|
@ -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转换器类
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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转换器类
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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, // 主进程管理端口
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
|
|
@ -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';
|
||||
|
||||
/**
|
||||
* 解析请求体
|
||||
|
|
|
|||
|
|
@ -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服务适配器接口
|
||||
// 所有的服务适配器都应该实现这些方法
|
||||
|
|
@ -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.
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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.
|
||||
|
|
@ -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({
|
||||
|
|
@ -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({
|
||||
|
|
@ -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.
|
||||
|
|
@ -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;
|
||||
|
||||
// 解析过期时间
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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';
|
||||
|
|
@ -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';
|
||||
|
||||
281
src/scripts/kiro-idc-token-refresh.js
Normal file
281
src/scripts/kiro-idc-token-refresh.js
Normal 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();
|
||||
184
src/scripts/kiro-token-refresh.js
Normal file
184
src/scripts/kiro-token-refresh.js
Normal 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();
|
||||
125
src/scripts/merge-json-files.js
Normal file
125
src/scripts/merge-json-files.js
Normal 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();
|
||||
|
|
@ -3,7 +3,7 @@ import {
|
|||
handleContentGenerationRequest,
|
||||
API_ACTIONS,
|
||||
ENDPOINT_TYPE
|
||||
} from './common.js';
|
||||
} from '../utils/common.js';
|
||||
import { getProviderPoolManager } from './service-manager.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';
|
||||
|
||||
// 检测是否作为子进程运行
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
|
|
@ -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';
|
||||
|
||||
/**
|
||||
* 用量查询服务类
|
||||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
/**
|
||||
* 扫描和分析配置文件
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue