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: services:
aiclient-api: aiclient-api:
# 方式一:使用 Docker Hub 预构建镜像(默认,推荐) # 方式一:使用 Docker Hub 预构建镜像(默认,推荐)
# 使用方法: docker compose up -d
image: justlikemaki/aiclient-2-api:latest image: justlikemaki/aiclient-2-api:latest
# 方式二:从 Dockerfile 本地构建(取消下面注释并注释掉上面的 image 行)
# build:
# context: ..
# dockerfile: Dockerfile
container_name: aiclient2api container_name: aiclient2api
restart: unless-stopped restart: unless-stopped
ports: ports:
- "3000:3000" - "3000:3000"
- "8085:8085" - "8085-8087:8085-8087"
- "8086:8086"
- "19876-19880:19876-19880" - "19876-19880:19876-19880"
volumes: volumes:
- ./configs:/app/configs - ./configs:/app/configs

View file

@ -69,8 +69,8 @@ if !errorlevel! neq 0 (
echo [成功] 依赖安装/更新完成 echo [成功] 依赖安装/更新完成
:: 检查src目录和master.js是否存在 :: 检查src目录和master.js是否存在
if not exist "src\master.js" ( if not exist "src\core\master.js" (
echo [错误] 未找到src\master.js文件 echo [错误] 未找到src\core\master.js文件
pause pause
exit /b 1 exit /b 1
) )
@ -89,4 +89,4 @@ echo 按 Ctrl+C 停止服务器
echo. echo.
:: 启动服务器 :: 启动服务器
node src\master.js node src\core\master.js

View file

@ -76,8 +76,8 @@ fi
echo "[成功] 依赖安装/更新完成" echo "[成功] 依赖安装/更新完成"
# 检查src目录和master.js是否存在 # 检查src目录和master.js是否存在
if [ ! -f "src/master.js" ]; then if [ ! -f "src/core/master.js" ]; then
echo "[错误] 未找到src/master.js文件" echo "[错误] 未找到src/core/master.js文件"
exit 1 exit 1
fi fi
@ -95,4 +95,4 @@ echo "按 Ctrl+C 停止服务器"
echo echo
# 启动服务器 # 启动服务器
node src/master.js node src/core/master.js

View file

@ -26,8 +26,8 @@
"supertest": "^6.3.3" "supertest": "^6.3.3"
}, },
"scripts": { "scripts": {
"start": "node src/master.js", "start": "node src/core/master.js",
"start:standalone": "node src/api-server.js", "start:standalone": "node src/services/api-server.js",
"start:dev": "node src/master.js --dev", "start:dev": "node src/master.js --dev",
"test": "jest", "test": "jest",
"test:watch": "jest --watch", "test:watch": "jest --watch",

View file

@ -5,9 +5,9 @@ import path from 'path';
import os from 'os'; import os from 'os';
import crypto from 'crypto'; import crypto from 'crypto';
import open from 'open'; import open from 'open';
import { broadcastEvent } from './ui-manager.js'; import { broadcastEvent } from '../services/ui-manager.js';
import { autoLinkProviderConfigs } from './service-manager.js'; import { autoLinkProviderConfigs } from '../services/service-manager.js';
import { CONFIG } from './config-manager.js'; import { CONFIG } from '../core/config-manager.js';
/** /**
* OAuth 提供商配置 * OAuth 提供商配置

View file

@ -1,5 +1,5 @@
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { MODEL_PROTOCOL_PREFIX, getProtocolPrefix } from './common.js'; import { MODEL_PROTOCOL_PREFIX, getProtocolPrefix } from '../utils/common.js';
import { import {
streamStateManager, streamStateManager,
generateResponseCreated, generateResponseCreated,

View file

@ -7,8 +7,8 @@
*/ */
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { MODEL_PROTOCOL_PREFIX, getProtocolPrefix } from './common.js'; import { MODEL_PROTOCOL_PREFIX, getProtocolPrefix } from '../utils/common.js';
import { ConverterFactory } from './converters/ConverterFactory.js'; import { ConverterFactory } from '../converters/ConverterFactory.js';
import { import {
generateResponseCreated, generateResponseCreated,
generateResponseInProgress, generateResponseInProgress,
@ -18,7 +18,7 @@ import {
generateContentPartDone, generateContentPartDone,
generateOutputItemDone, generateOutputItemDone,
generateResponseCompleted 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) { export async function extractAndProcessSystemMessages(messages) {
const { Utils } = await import('./converters/utils.js'); const { Utils } = await import('../converters/utils.js');
return Utils.extractSystemMessages(messages); return Utils.extractSystemMessages(messages);
} }
export async function extractTextFromMessageContent(content) { export async function extractTextFromMessageContent(content) {
const { Utils } = await import('./converters/utils.js'); const { Utils } = await import('../converters/utils.js');
return Utils.extractText(content); 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 { ConverterFactory } from './ConverterFactory.js';
import { OpenAIConverter } from './strategies/OpenAIConverter.js'; import { OpenAIConverter } from './strategies/OpenAIConverter.js';
import { OpenAIResponsesConverter } from './strategies/OpenAIResponsesConverter.js'; import { OpenAIResponsesConverter } from './strategies/OpenAIResponsesConverter.js';

View file

@ -18,7 +18,7 @@ import {
GEMINI_DEFAULT_INPUT_TOKEN_LIMIT, GEMINI_DEFAULT_INPUT_TOKEN_LIMIT,
GEMINI_DEFAULT_OUTPUT_TOKEN_LIMIT GEMINI_DEFAULT_OUTPUT_TOKEN_LIMIT
} from '../utils.js'; } from '../utils.js';
import { MODEL_PROTOCOL_PREFIX } from '../../common.js'; import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
import { import {
generateResponseCreated, generateResponseCreated,
generateResponseInProgress, generateResponseInProgress,
@ -28,7 +28,7 @@ import {
generateContentPartDone, generateContentPartDone,
generateOutputItemDone, generateOutputItemDone,
generateResponseCompleted generateResponseCompleted
} from '../../openai/openai-responses-core.mjs'; } from '../../providers/openai/openai-responses-core.mjs';
/** /**
* Claude转换器类 * Claude转换器类

View file

@ -14,7 +14,7 @@ import {
CLAUDE_DEFAULT_TEMPERATURE, CLAUDE_DEFAULT_TEMPERATURE,
CLAUDE_DEFAULT_TOP_P CLAUDE_DEFAULT_TOP_P
} from '../utils.js'; } from '../utils.js';
import { MODEL_PROTOCOL_PREFIX } from '../../common.js'; import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
import { import {
generateResponseCreated, generateResponseCreated,
generateResponseInProgress, generateResponseInProgress,
@ -24,7 +24,7 @@ import {
generateContentPartDone, generateContentPartDone,
generateOutputItemDone, generateOutputItemDone,
generateResponseCompleted generateResponseCompleted
} from '../../openai/openai-responses-core.mjs'; } from '../../providers/openai/openai-responses-core.mjs';
/** /**
* Gemini转换器类 * Gemini转换器类

View file

@ -6,7 +6,7 @@
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import { BaseConverter } from '../BaseConverter.js'; import { BaseConverter } from '../BaseConverter.js';
import { MODEL_PROTOCOL_PREFIX } from '../../common.js'; import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
import { import {
OLLAMA_DEFAULT_CONTEXT_LENGTH, OLLAMA_DEFAULT_CONTEXT_LENGTH,
OLLAMA_DEFAULT_MAX_OUTPUT_TOKENS, OLLAMA_DEFAULT_MAX_OUTPUT_TOKENS,

View file

@ -22,7 +22,7 @@ import {
OPENAI_DEFAULT_INPUT_TOKEN_LIMIT, OPENAI_DEFAULT_INPUT_TOKEN_LIMIT,
OPENAI_DEFAULT_OUTPUT_TOKEN_LIMIT OPENAI_DEFAULT_OUTPUT_TOKEN_LIMIT
} from '../utils.js'; } from '../utils.js';
import { MODEL_PROTOCOL_PREFIX } from '../../common.js'; import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
import { import {
generateResponseCreated, generateResponseCreated,
generateResponseInProgress, generateResponseInProgress,
@ -32,7 +32,7 @@ import {
generateContentPartDone, generateContentPartDone,
generateOutputItemDone, generateOutputItemDone,
generateResponseCompleted generateResponseCompleted
} from '../../openai/openai-responses-core.mjs'; } from '../../providers/openai/openai-responses-core.mjs';
/** /**
* OpenAI转换器类 * OpenAI转换器类

View file

@ -4,7 +4,7 @@
*/ */
import { BaseConverter } from '../BaseConverter.js'; import { BaseConverter } from '../BaseConverter.js';
import { MODEL_PROTOCOL_PREFIX } from '../../common.js'; import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
import { import {
extractAndProcessSystemMessages as extractSystemMessages, extractAndProcessSystemMessages as extractSystemMessages,
extractTextFromMessageContent as extractText, extractTextFromMessageContent as extractText,

View file

@ -1,6 +1,6 @@
import * as fs from 'fs'; import * as fs from 'fs';
import { promises as pfs } 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 CONFIG = {}; // Make CONFIG exportable
export let PROMPT_LOG_FILENAME = ''; // Make PROMPT_LOG_FILENAME exportable export let PROMPT_LOG_FILENAME = ''; // Make PROMPT_LOG_FILENAME exportable

View file

@ -33,7 +33,7 @@ let workerStatus = {
// 配置 // 配置
const config = { const config = {
workerScript: path.join(__dirname, 'api-server.js'), workerScript: path.join(__dirname, '../services/api-server.js'),
maxRestartAttempts: 10, maxRestartAttempts: 10,
restartDelay: 1000, // 重启延迟(毫秒) restartDelay: 1000, // 重启延迟(毫秒)
masterPort: parseInt(process.env.MASTER_PORT) || 3100, // 主进程管理端口 masterPort: parseInt(process.env.MASTER_PORT) || 3100, // 主进程管理端口

View file

@ -3,9 +3,9 @@
* 处理Ollama特定的端点并在后端协议之间进行转换 * 处理Ollama特定的端点并在后端协议之间进行转换
*/ */
import { getRequestBody, handleError, MODEL_PROTOCOL_PREFIX, MODEL_PROVIDER, getProtocolPrefix } from './common.js'; import { getRequestBody, handleError, MODEL_PROTOCOL_PREFIX, MODEL_PROVIDER, getProtocolPrefix } from '../utils/common.js';
import { convertData } from './convert.js'; import { convertData } from '../convert/convert.js';
import { ConverterFactory } from './converters/ConverterFactory.js'; import { ConverterFactory } from '../converters/ConverterFactory.js';
// Ollama版本号 // Ollama版本号
/** /**
@ -385,7 +385,7 @@ export async function handleOllamaTags(req, res, apiService, currentConfig, prov
console.log('[Ollama] Handling /api/tags request'); console.log('[Ollama] Handling /api/tags request');
const ollamaConverter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OLLAMA); 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 // Helper to fetch and convert models from a provider
const fetchProviderModels = async (providerType, service) => { 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'); console.log('[Ollama] Handling /api/chat request');
const ollamaRequest = await getRequestBody(req); const ollamaRequest = await getRequestBody(req);
const { getServiceAdapter } = await import('./adapter.js'); const { getServiceAdapter } = await import('../providers/adapter.js');
// Determine provider based on model name // Determine provider based on model name
const rawModelName = ollamaRequest.model; const rawModelName = ollamaRequest.model;
@ -623,7 +623,7 @@ export async function handleOllamaGenerate(req, res, apiService, currentConfig,
console.log('[Ollama] Handling /api/generate request'); console.log('[Ollama] Handling /api/generate request');
const ollamaRequest = await getRequestBody(req); const ollamaRequest = await getRequestBody(req);
const { getServiceAdapter } = await import('./adapter.js'); const { getServiceAdapter } = await import('../providers/adapter.js');
// Determine provider based on model name // Determine provider based on model name
const rawModelName = ollamaRequest.model; const rawModelName = ollamaRequest.model;

View file

@ -1,13 +1,13 @@
import deepmerge from 'deepmerge'; import deepmerge from 'deepmerge';
import { handleError } from './common.js'; import { handleError } from '../utils/common.js';
import { handleUIApiRequests, serveStaticFiles } from './ui-manager.js'; import { handleUIApiRequests, serveStaticFiles } from '../services/ui-manager.js';
import { handleAPIRequests } from './api-manager.js'; import { handleAPIRequests } from '../services/api-manager.js';
import { getApiService, getProviderStatus } from './service-manager.js'; import { getApiService, getProviderStatus } from '../services/service-manager.js';
import { getProviderPoolManager } from './service-manager.js'; import { getProviderPoolManager } from '../services/service-manager.js';
import { MODEL_PROVIDER } from './common.js'; import { MODEL_PROVIDER } from '../utils/common.js';
import { PROMPT_LOG_FILENAME } from './config-manager.js'; import { PROMPT_LOG_FILENAME } from '../core/config-manager.js';
import { handleOllamaRequest, handleOllamaShow } from './ollama-handler.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 * Parse request body as JSON

View file

@ -36,9 +36,9 @@ import path from 'path';
import { existsSync } from 'fs'; import { existsSync } from 'fs';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import multer from 'multer'; import multer from 'multer';
import { batchImportKiroRefreshTokensStream, importAwsCredentials } from '../../oauth-handlers.js'; import { batchImportKiroRefreshTokensStream, importAwsCredentials } from '../../auth/oauth-handlers.js';
import { autoLinkProviderConfigs, getProviderPoolManager } from '../../service-manager.js'; import { autoLinkProviderConfigs, getProviderPoolManager } from '../../services/service-manager.js';
import { CONFIG } from '../../config-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 { KiroApiService } from './claude/claude-kiro.js'; // 导入KiroApiService
import { QwenApiService } from './openai/qwen-core.js'; // 导入QwenApiService import { QwenApiService } from './openai/qwen-core.js'; // 导入QwenApiService
import { IFlowApiService } from './openai/iflow-core.js'; // 导入IFlowApiService 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服务适配器接口 // 定义AI服务适配器接口
// 所有的服务适配器都应该实现这些方法 // 所有的服务适配器都应该实现这些方法

View file

@ -1,8 +1,8 @@
import axios from 'axios'; import axios from 'axios';
import * as http from 'http'; import * as http from 'http';
import * as https from 'https'; import * as https from 'https';
import { configureAxiosProxy } from '../proxy-utils.js'; import { configureAxiosProxy } from '../../utils/proxy-utils.js';
import { isRetryableNetworkError } from '../common.js'; import { isRetryableNetworkError } from '../../utils/common.js';
/** /**
* Claude API Core Service Class. * Claude API Core Service Class.

View file

@ -8,14 +8,13 @@ import * as http from 'http';
import * as https from 'https'; import * as https from 'https';
import { getProviderModels } from '../provider-models.js'; import { getProviderModels } from '../provider-models.js';
import { countTokens } from '@anthropic-ai/tokenizer'; import { countTokens } from '@anthropic-ai/tokenizer';
import { configureAxiosProxy } from '../proxy-utils.js'; import { configureAxiosProxy } from '../../utils/proxy-utils.js';
import { isRetryableNetworkError } from '../common.js'; import { isRetryableNetworkError } from '../../utils/common.js';
import { CLAUDE_DEFAULT_MAX_TOKENS } from '../converters/utils.js';
const KIRO_CONSTANTS = { const KIRO_CONSTANTS = {
REFRESH_URL: 'https://prod.{{region}}.auth.desktop.kiro.dev/refreshToken', REFRESH_URL: 'https://prod.{{region}}.auth.desktop.kiro.dev/refreshToken',
REFRESH_IDC_URL: 'https://oidc.{{region}}.amazonaws.com/token', REFRESH_IDC_URL: 'https://oidc.{{region}}.amazonaws.com/token',
BASE_URL: 'https://codewhisperer.{{region}}.amazonaws.com/generateAssistantResponse', BASE_URL: 'https://q.{{region}}.amazonaws.com/generateAssistantResponse',
AMAZON_Q_URL: 'https://codewhisperer.{{region}}.amazonaws.com/SendMessageStreaming', AMAZON_Q_URL: 'https://codewhisperer.{{region}}.amazonaws.com/SendMessageStreaming',
USAGE_LIMITS_URL: 'https://q.{{region}}.amazonaws.com/getUsageLimits', USAGE_LIMITS_URL: 'https://q.{{region}}.amazonaws.com/getUsageLimits',
DEFAULT_MODEL_NAME: 'claude-opus-4-5', DEFAULT_MODEL_NAME: 'claude-opus-4-5',
@ -27,6 +26,7 @@ const KIRO_CONSTANTS = {
AUTH_METHOD_SOCIAL: 'social', AUTH_METHOD_SOCIAL: 'social',
CHAT_TRIGGER_TYPE_MANUAL: 'MANUAL', CHAT_TRIGGER_TYPE_MANUAL: 'MANUAL',
ORIGIN_AI_EDITOR: 'AI_EDITOR', ORIGIN_AI_EDITOR: 'AI_EDITOR',
TOTAL_CONTEXT_TOKENS: 200000, // 总上下文 200k tokens
}; };
// 从 provider-models.js 获取支持的模型列表 // 从 provider-models.js 获取支持的模型列表
@ -34,9 +34,9 @@ const KIRO_MODELS = getProviderModels('claude-kiro-oauth');
// 完整的模型映射表 // 完整的模型映射表
const FULL_MODEL_MAPPING = { const FULL_MODEL_MAPPING = {
"claude-opus-4-5": "claude-opus-4.5", "claude-opus-4-5":"claude-opus-4.5",
"claude-opus-4-5-20251101": "claude-opus-4.5", "claude-opus-4-5-20251101":"claude-opus-4.5",
"claude-haiku-4-5": "claude-haiku-4.5", "claude-haiku-4-5":"claude-haiku-4.5",
"claude-sonnet-4-5": "CLAUDE_SONNET_4_5_20250929_V1_0", "claude-sonnet-4-5": "CLAUDE_SONNET_4_5_20250929_V1_0",
"claude-sonnet-4-5-20250929": "CLAUDE_SONNET_4_5_20250929_V1_0", "claude-sonnet-4-5-20250929": "CLAUDE_SONNET_4_5_20250929_V1_0",
"claude-sonnet-4-20250514": "CLAUDE_SONNET_4_20250514_V1_0", "claude-sonnet-4-20250514": "CLAUDE_SONNET_4_20250514_V1_0",
@ -371,170 +371,126 @@ export class KiroApiService {
this.isInitialized = true; this.isInitialized = true;
} }
async initializeAuth(forceRefresh = false) { async initializeAuth(forceRefresh = false) {
// 如果已有 accessToken 且不是强制刷新,直接返回 if (this.accessToken && !forceRefresh) {
if (this.accessToken && !forceRefresh) { console.debug('[Kiro Auth] Access token already available and not forced refresh.');
console.debug('[Kiro Auth] Access token already available and not forced refresh.'); return;
return;
}
// 如果是强制刷新且已有 refreshToken跳过凭证加载直接刷新
if (forceRefresh && this.refreshToken) {
console.debug('[Kiro Auth] Force refresh requested, skipping credential loading.');
// 直接跳转到刷新逻辑
return this._refreshAccessToken();
}
// Helper to load credentials from a file
const loadCredentialsFromFile = async (filePath) => {
try {
const fileContent = await fs.readFile(filePath, 'utf8');
return JSON.parse(fileContent);
} catch (error) {
if (error.code === 'ENOENT') {
console.debug(`[Kiro Auth] Credential file not found: ${filePath}`);
} else if (error instanceof SyntaxError) {
console.warn(`[Kiro Auth] Failed to parse JSON from ${filePath}: ${error.message}`);
} else {
console.warn(`[Kiro Auth] Failed to read credential file ${filePath}: ${error.message}`);
}
return null;
}
};
// Helper to save credentials to a file
const saveCredentialsToFile = async (filePath, newData) => {
try {
let existingData = {};
try {
const fileContent = await fs.readFile(filePath, 'utf8');
existingData = JSON.parse(fileContent);
} catch (readError) {
if (readError.code === 'ENOENT') {
console.debug(`[Kiro Auth] Token file not found, creating new one: ${filePath}`);
} else {
console.warn(`[Kiro Auth] Could not read existing token file ${filePath}: ${readError.message}`);
}
}
const mergedData = { ...existingData, ...newData };
await fs.writeFile(filePath, JSON.stringify(mergedData, null, 2), 'utf8');
console.info(`[Kiro Auth] Updated token file: ${filePath}`);
} catch (error) {
console.error(`[Kiro Auth] Failed to write token to file ${filePath}: ${error.message}`);
}
};
try {
let mergedCredentials = {};
// Priority 1: Load from Base64 credentials if available
if (this.base64Creds) {
Object.assign(mergedCredentials, this.base64Creds);
console.info('[Kiro Auth] Successfully loaded credentials from Base64 (constructor).');
// Clear base64Creds after use to prevent re-processing
this.base64Creds = null;
}
// Priority 2 & 3 合并: 从指定文件路径或目录加载凭证
// 读取指定的 credPath 文件以及目录下的其他 JSON 文件(排除当前文件)
const targetFilePath = this.credsFilePath || path.join(this.credPath, KIRO_AUTH_TOKEN_FILE);
const dirPath = path.dirname(targetFilePath);
const targetFileName = path.basename(targetFilePath);
console.debug(`[Kiro Auth] Attempting to load credentials from directory: ${dirPath}`);
try {
// 首先尝试读取目标文件
const targetCredentials = await loadCredentialsFromFile(targetFilePath);
if (targetCredentials) {
Object.assign(mergedCredentials, targetCredentials);
console.info(`[Kiro Auth] Successfully loaded OAuth credentials from ${targetFilePath}`);
}
// 然后读取目录下的其他 JSON 文件(排除目标文件本身)
const files = await fs.readdir(dirPath);
for (const file of files) {
if (file.endsWith('.json') && file !== targetFileName) {
const filePath = path.join(dirPath, file);
const credentials = await loadCredentialsFromFile(filePath);
if (credentials) {
// 保留已有的 expiresAt,避免被覆盖
credentials.expiresAt = mergedCredentials.expiresAt;
Object.assign(mergedCredentials, credentials);
console.debug(`[Kiro Auth] Loaded Client credentials from ${file}`);
}
}
}
} catch (error) {
console.warn(`[Kiro Auth] Error loading credentials from directory ${dirPath}: ${error.message}`);
}
// console.log('[Kiro Auth] Merged credentials:', mergedCredentials);
// Apply loaded credentials, prioritizing existing values if they are not null/undefined
this.accessToken = this.accessToken || mergedCredentials.accessToken;
this.refreshToken = this.refreshToken || mergedCredentials.refreshToken;
this.clientId = this.clientId || mergedCredentials.clientId;
this.clientSecret = this.clientSecret || mergedCredentials.clientSecret;
this.authMethod = this.authMethod || mergedCredentials.authMethod;
this.expiresAt = this.expiresAt || mergedCredentials.expiresAt;
this.profileArn = this.profileArn || mergedCredentials.profileArn;
this.region = this.region || mergedCredentials.region;
// Ensure region is set before using it in URLs
if (!this.region) {
console.warn('[Kiro Auth] Region not found in credentials. Using default region us-east-1 for URLs.');
this.region = 'us-east-1'; // Set default region
}
this.refreshUrl = (this.config.KIRO_REFRESH_URL || KIRO_CONSTANTS.REFRESH_URL).replace("{{region}}", this.region);
this.refreshIDCUrl = (this.config.KIRO_REFRESH_IDC_URL || KIRO_CONSTANTS.REFRESH_IDC_URL).replace("{{region}}", this.region);
this.baseUrl = (this.config.KIRO_BASE_URL || KIRO_CONSTANTS.BASE_URL).replace("{{region}}", this.region);
this.amazonQUrl = (KIRO_CONSTANTS.AMAZON_Q_URL).replace("{{region}}", this.region);
} catch (error) {
console.warn(`[Kiro Auth] Error during credential loading: ${error.message}`);
}
// Refresh token if access token is missing but refresh token is available
if (!this.accessToken && this.refreshToken) {
await this._refreshAccessToken();
}
if (!this.accessToken) {
throw new Error('No access token available after initialization and refresh attempts.');
}
} }
/** // Helper to load credentials from a file
* 内部方法刷新 access token const loadCredentialsFromFile = async (filePath) => {
* @private try {
*/ const fileContent = await fs.readFile(filePath, 'utf8');
async _refreshAccessToken() { return JSON.parse(fileContent);
} catch (error) {
if (error.code === 'ENOENT') {
console.debug(`[Kiro Auth] Credential file not found: ${filePath}`);
} else if (error instanceof SyntaxError) {
console.warn(`[Kiro Auth] Failed to parse JSON from ${filePath}: ${error.message}`);
} else {
console.warn(`[Kiro Auth] Failed to read credential file ${filePath}: ${error.message}`);
}
return null;
}
};
// Helper to save credentials to a file
const saveCredentialsToFile = async (filePath, newData) => {
try {
let existingData = {};
try {
const fileContent = await fs.readFile(filePath, 'utf8');
existingData = JSON.parse(fileContent);
} catch (readError) {
if (readError.code === 'ENOENT') {
console.debug(`[Kiro Auth] Token file not found, creating new one: ${filePath}`);
} else {
console.warn(`[Kiro Auth] Could not read existing token file ${filePath}: ${readError.message}`);
}
}
const mergedData = { ...existingData, ...newData };
await fs.writeFile(filePath, JSON.stringify(mergedData, null, 2), 'utf8');
console.info(`[Kiro Auth] Updated token file: ${filePath}`);
} catch (error) {
console.error(`[Kiro Auth] Failed to write token to file ${filePath}: ${error.message}`);
}
};
try {
let mergedCredentials = {};
// Priority 1: Load from Base64 credentials if available
if (this.base64Creds) {
Object.assign(mergedCredentials, this.base64Creds);
console.info('[Kiro Auth] Successfully loaded credentials from Base64 (constructor).');
// Clear base64Creds after use to prevent re-processing
this.base64Creds = null;
}
// Priority 2 & 3 合并: 从指定文件路径或目录加载凭证
// 读取指定的 credPath 文件以及目录下的其他 JSON 文件(排除当前文件)
const targetFilePath = this.credsFilePath || path.join(this.credPath, KIRO_AUTH_TOKEN_FILE);
const dirPath = path.dirname(targetFilePath);
const targetFileName = path.basename(targetFilePath);
console.debug(`[Kiro Auth] Attempting to load credentials from directory: ${dirPath}`);
try {
// 首先尝试读取目标文件
const targetCredentials = await loadCredentialsFromFile(targetFilePath);
if (targetCredentials) {
Object.assign(mergedCredentials, targetCredentials);
console.info(`[Kiro Auth] Successfully loaded OAuth credentials from ${targetFilePath}`);
}
// 然后读取目录下的其他 JSON 文件(排除目标文件本身)
const files = await fs.readdir(dirPath);
for (const file of files) {
if (file.endsWith('.json') && file !== targetFileName) {
const filePath = path.join(dirPath, file);
const credentials = await loadCredentialsFromFile(filePath);
if (credentials) {
// 保留已有的 expiresAt,避免被覆盖
credentials.expiresAt = mergedCredentials.expiresAt;
Object.assign(mergedCredentials, credentials);
console.debug(`[Kiro Auth] Loaded Client credentials from ${file}`);
}
}
}
} catch (error) {
console.warn(`[Kiro Auth] Error loading credentials from directory ${dirPath}: ${error.message}`);
}
// console.log('[Kiro Auth] Merged credentials:', mergedCredentials);
// Apply loaded credentials, prioritizing existing values if they are not null/undefined
this.accessToken = this.accessToken || mergedCredentials.accessToken;
this.refreshToken = this.refreshToken || mergedCredentials.refreshToken;
this.clientId = this.clientId || mergedCredentials.clientId;
this.clientSecret = this.clientSecret || mergedCredentials.clientSecret;
this.authMethod = this.authMethod || mergedCredentials.authMethod;
this.expiresAt = this.expiresAt || mergedCredentials.expiresAt;
this.profileArn = this.profileArn || mergedCredentials.profileArn;
this.region = this.region || mergedCredentials.region;
// Ensure region is set before using it in URLs
if (!this.region) {
console.warn('[Kiro Auth] Region not found in credentials. Using default region us-east-1 for URLs.');
this.region = 'us-east-1'; // Set default region
}
this.refreshUrl = (this.config.KIRO_REFRESH_URL || KIRO_CONSTANTS.REFRESH_URL).replace("{{region}}", this.region);
this.refreshIDCUrl = (this.config.KIRO_REFRESH_IDC_URL || KIRO_CONSTANTS.REFRESH_IDC_URL).replace("{{region}}", this.region);
this.baseUrl = (this.config.KIRO_BASE_URL || KIRO_CONSTANTS.BASE_URL).replace("{{region}}", this.region);
this.amazonQUrl = (KIRO_CONSTANTS.AMAZON_Q_URL).replace("{{region}}", this.region);
} catch (error) {
console.warn(`[Kiro Auth] Error during credential loading: ${error.message}`);
}
// Refresh token if forced or if access token is missing but refresh token is available
if (forceRefresh || (!this.accessToken && this.refreshToken)) {
if (!this.refreshToken) { if (!this.refreshToken) {
throw new Error('No refresh token available to refresh access token.'); throw new Error('No refresh token available to refresh access token.');
} }
// Helper to save credentials to a file
const saveCredentialsToFile = async (filePath, newData) => {
try {
let existingData = {};
try {
const fileContent = await fs.readFile(filePath, 'utf8');
existingData = JSON.parse(fileContent);
} catch (readError) {
if (readError.code === 'ENOENT') {
console.debug(`[Kiro Auth] Token file not found, creating new one: ${filePath}`);
} else {
console.warn(`[Kiro Auth] Could not read existing token file ${filePath}: ${readError.message}`);
}
}
const mergedData = { ...existingData, ...newData };
await fs.writeFile(filePath, JSON.stringify(mergedData, null, 2), 'utf8');
console.info(`[Kiro Auth] Updated token file: ${filePath}`);
} catch (error) {
console.error(`[Kiro Auth] Failed to write token to file ${filePath}: ${error.message}`);
}
};
try { try {
const requestBody = { const requestBody = {
refreshToken: this.refreshToken, refreshToken: this.refreshToken,
@ -552,7 +508,7 @@ export class KiroApiService {
if (this.authMethod === KIRO_CONSTANTS.AUTH_METHOD_SOCIAL) { if (this.authMethod === KIRO_CONSTANTS.AUTH_METHOD_SOCIAL) {
response = await this.axiosSocialRefreshInstance.post(refreshUrl, requestBody); response = await this.axiosSocialRefreshInstance.post(refreshUrl, requestBody);
console.log('[Kiro Auth] Token refresh social response: ok'); console.log('[Kiro Auth] Token refresh social response: ok');
} else { }else{
response = await this.axiosInstance.post(refreshUrl, requestBody); response = await this.axiosInstance.post(refreshUrl, requestBody);
console.log('[Kiro Auth] Token refresh idc response: ok'); console.log('[Kiro Auth] Token refresh idc response: ok');
} }
@ -573,7 +529,7 @@ export class KiroApiService {
refreshToken: this.refreshToken, refreshToken: this.refreshToken,
expiresAt: expiresAt, expiresAt: expiresAt,
}; };
if (this.profileArn) { if(this.profileArn){
updatedTokenData.profileArn = this.profileArn; updatedTokenData.profileArn = this.profileArn;
} }
await saveCredentialsToFile(tokenFilePath, updatedTokenData); await saveCredentialsToFile(tokenFilePath, updatedTokenData);
@ -586,21 +542,26 @@ export class KiroApiService {
} }
} }
if (!this.accessToken) {
throw new Error('No access token available after initialization and refresh attempts.');
}
}
/** /**
* Extract text content from OpenAI message format * Extract text content from OpenAI message format
*/ */
getContentText(message) { getContentText(message) {
if (message == null) { if(message==null){
return ""; return "";
} }
if (Array.isArray(message)) { if (Array.isArray(message) ) {
return message return message
.filter(part => part.type === 'text' && part.text) .filter(part => part.type === 'text' && part.text)
.map(part => part.text) .map(part => part.text)
.join(''); .join('');
} else if (typeof message.content === 'string') { } else if (typeof message.content === 'string') {
return message.content; return message.content;
} else if (Array.isArray(message.content)) { } else if (Array.isArray(message.content) ) {
return message.content return message.content
.filter(part => part.type === 'text' && part.text) .filter(part => part.type === 'text' && part.text)
.map(part => part.text) .map(part => part.text)
@ -951,39 +912,11 @@ export class KiroApiService {
} }
userInputMessageContext.toolResults = uniqueToolResults; userInputMessageContext.toolResults = uniqueToolResults;
} }
const historyHasToolCalling = history.some(h =>
h.assistantResponseMessage?.toolUses ||
h.userInputMessage?.userInputMessageContext?.toolResults
);
if (Object.keys(toolsContext).length > 0 && toolsContext.tools) { if (Object.keys(toolsContext).length > 0 && toolsContext.tools) {
userInputMessageContext.tools = toolsContext.tools; userInputMessageContext.tools = toolsContext.tools;
} else if (historyHasToolCalling && !toolsContext.tools) {
const toolNamesInHistory = new Set();
history.forEach(h => {
if (h.assistantResponseMessage?.toolUses) {
h.assistantResponseMessage.toolUses.forEach(tu => {
toolNamesInHistory.add(tu.name);
});
}
});
if (toolNamesInHistory.size > 0) {
userInputMessageContext.tools = Array.from(toolNamesInHistory).map(name => ({
toolSpecification: {
name: name,
description: "Tool",
inputSchema: {
json: {
type: "object",
properties: {}
}
}
}
}));
}
} }
// 只有当 userInputMessageContext 有内容时才添加
if (Object.keys(userInputMessageContext).length > 0) { if (Object.keys(userInputMessageContext).length > 0) {
userInputMessage.userInputMessageContext = userInputMessageContext; userInputMessage.userInputMessageContext = userInputMessageContext;
} }
@ -1170,6 +1103,7 @@ export class KiroApiService {
_processApiResponse(response) { _processApiResponse(response) {
const rawResponseText = Buffer.isBuffer(response.data) ? response.data.toString('utf8') : String(response.data); const rawResponseText = Buffer.isBuffer(response.data) ? response.data.toString('utf8') : String(response.data);
//console.log(`[Kiro] Raw response length: ${rawResponseText.length}`);
if (rawResponseText.includes("[Called")) { if (rawResponseText.includes("[Called")) {
console.log("[Kiro] Raw response contains [Called marker."); console.log("[Kiro] Raw response contains [Called marker.");
} }
@ -1221,22 +1155,13 @@ export class KiroApiService {
const finalModel = MODEL_MAPPING[model] ? model : this.modelName; const finalModel = MODEL_MAPPING[model] ? model : this.modelName;
console.log(`[Kiro] Calling generateContent with model: ${finalModel}`); console.log(`[Kiro] Calling generateContent with model: ${finalModel}`);
// Estimate input tokens before making the API call
const inputTokens = this.estimateInputTokens(requestBody);
const response = await this.callApi('', finalModel, requestBody); const response = await this.callApi('', finalModel, requestBody);
try { try {
const { responseText, toolCalls } = this._processApiResponse(response); const { responseText, toolCalls } = this._processApiResponse(response);
let inputTokens = 0;
const rawResponseText = Buffer.isBuffer(response.data)
? response.data.toString('utf8')
: String(response.data);
const contextUsageMatch = rawResponseText.match(/"contextUsagePercentage":\s*([\d.]+)/);
if (contextUsageMatch) {
const percentage = parseFloat(contextUsageMatch[1]);
inputTokens = this.calculateInputTokensFromPercentage(percentage);
}
return this.buildClaudeResponse(responseText, false, 'assistant', model, toolCalls, inputTokens); return this.buildClaudeResponse(responseText, false, 'assistant', model, toolCalls, inputTokens);
} catch (error) { } catch (error) {
console.error('[Kiro] Error in generateContent:', error); console.error('[Kiro] Error in generateContent:', error);
@ -1263,6 +1188,7 @@ export class KiroApiService {
// 1. {"name":"xxx","toolUseId":"xxx"} - 开始 // 1. {"name":"xxx","toolUseId":"xxx"} - 开始
// 2. {"input":"..."} - input 数据(可能多次) // 2. {"input":"..."} - input 数据(可能多次)
// 3. {"stop":true} - 结束 // 3. {"stop":true} - 结束
// 4. {"contextUsagePercentage":...} - 上下文使用百分比(最后一条消息)
const contentStart = remaining.indexOf('{"content":', searchStart); const contentStart = remaining.indexOf('{"content":', searchStart);
const nameStart = remaining.indexOf('{"name":', searchStart); const nameStart = remaining.indexOf('{"name":', searchStart);
const followupStart = remaining.indexOf('{"followupPrompt":', searchStart); const followupStart = remaining.indexOf('{"followupPrompt":', searchStart);
@ -1352,8 +1278,8 @@ export class KiroApiService {
} }
}); });
} }
// 处理工具调用的结束事件(只有 stop 字段 // 处理工具调用的结束事件(只有 stop 字段,且不包含 contextUsagePercentage
else if (parsed.stop !== undefined) { else if (parsed.stop !== undefined && parsed.contextUsagePercentage === undefined) {
events.push({ events.push({
type: 'toolUseStop', type: 'toolUseStop',
data: { data: {
@ -1361,12 +1287,12 @@ export class KiroApiService {
} }
}); });
} }
// 处理 context usage percentage 事件 // 处理上下文使用百分比事件(最后一条消息)
else if (parsed.contextUsagePercentage !== undefined) { else if (parsed.contextUsagePercentage !== undefined) {
events.push({ events.push({
type: 'contextUsage', type: 'contextUsage',
data: { data: {
percentage: parsed.contextUsagePercentage contextUsagePercentage: parsed.contextUsagePercentage
} }
}); });
} }
@ -1416,7 +1342,7 @@ export class KiroApiService {
stream = response.data; stream = response.data;
let buffer = ''; let buffer = '';
let lastContentEvent = null; let lastContentEvent = null; // 用于检测连续重复的 content 事件
for await (const chunk of stream) { for await (const chunk of stream) {
buffer += chunk.toString(); buffer += chunk.toString();
@ -1442,7 +1368,7 @@ export class KiroApiService {
} else if (event.type === 'toolUseStop') { } else if (event.type === 'toolUseStop') {
yield { type: 'toolUseStop', stop: event.data.stop }; yield { type: 'toolUseStop', stop: event.data.stop };
} else if (event.type === 'contextUsage') { } else if (event.type === 'contextUsage') {
yield { type: 'contextUsage', percentage: event.data.percentage }; yield { type: 'contextUsage', contextUsagePercentage: event.data.contextUsagePercentage };
} }
} }
} }
@ -1526,52 +1452,51 @@ export class KiroApiService {
const finalModel = MODEL_MAPPING[model] ? model : this.modelName; const finalModel = MODEL_MAPPING[model] ? model : this.modelName;
console.log(`[Kiro] Calling generateContentStream with model: ${finalModel} (real streaming)`); console.log(`[Kiro] Calling generateContentStream with model: ${finalModel} (real streaming)`);
// 预先估算 inputTokens 作为保底值,如果收到 contextUsagePercentage 会被覆盖 const inputTokens = this.estimateInputTokens(requestBody);
let inputTokens = this.estimateInputTokens(requestBody);
const messageId = `${uuidv4()}`; const messageId = `${uuidv4()}`;
// 立即发送 message_start不等待 contextUsagePercentage
yield {
type: "message_start",
message: {
id: messageId,
type: "message",
role: "assistant",
model: model,
usage: {
input_tokens: 0,
output_tokens: 0,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0
},
content: []
}
};
yield {
type: "content_block_start",
index: 0,
content_block: { type: "text", text: "" }
};
try { try {
// 1. 先发送 message_start 事件
yield {
type: "message_start",
message: {
id: messageId,
type: "message",
role: "assistant",
model: model,
usage: { input_tokens: inputTokens, output_tokens: 0 },
content: []
}
};
// 2. 发送 content_block_start 事件
yield {
type: "content_block_start",
index: 0,
content_block: { type: "text", text: "" }
};
let totalContent = ''; let totalContent = '';
let outputTokens = 0; let outputTokens = 0;
const toolCalls = []; const toolCalls = [];
let currentToolCall = null; let currentToolCall = null; // 用于累积结构化工具调用
let contextUsagePercentage = null; // 用于存储上下文使用百分比
// 3. 流式接收并发送每个 content_block_delta
for await (const event of this.streamApiReal('', finalModel, requestBody)) { for await (const event of this.streamApiReal('', finalModel, requestBody)) {
if (event.type === 'contextUsage' && event.percentage) { if (event.type === 'content' && event.content) {
// 收到 contextUsagePercentage 时更新 inputTokens用于最终的 message_delta
inputTokens = this.calculateInputTokensFromPercentage(event.percentage);
} else if (event.type === 'content' && event.content) {
totalContent += event.content; totalContent += event.content;
// 不再每个 chunk 都计算 token改为最后统一计算避免阻塞事件循环
yield { yield {
type: "content_block_delta", type: "content_block_delta",
index: 0, index: 0,
delta: { type: "text_delta", text: event.content } delta: { type: "text_delta", text: event.content }
}; };
} else if (event.type === 'contextUsage') {
// 捕获上下文使用百分比
contextUsagePercentage = event.contextUsagePercentage;
console.log(`[Kiro] Received contextUsagePercentage: ${contextUsagePercentage}%`);
} else if (event.type === 'toolUse') { } else if (event.type === 'toolUse') {
const tc = event.toolUse; const tc = event.toolUse;
// 工具调用事件(包含 name 和 toolUseId // 工具调用事件(包含 name 和 toolUseId
@ -1602,7 +1527,7 @@ export class KiroApiService {
if (tc.stop) { if (tc.stop) {
try { try {
currentToolCall.input = JSON.parse(currentToolCall.input); currentToolCall.input = JSON.parse(currentToolCall.input);
} catch (e) { } } catch (e) {}
toolCalls.push(currentToolCall); toolCalls.push(currentToolCall);
currentToolCall = null; currentToolCall = null;
} }
@ -1630,7 +1555,7 @@ export class KiroApiService {
if (currentToolCall) { if (currentToolCall) {
try { try {
currentToolCall.input = JSON.parse(currentToolCall.input); currentToolCall.input = JSON.parse(currentToolCall.input);
} catch (e) { } } catch (e) {}
toolCalls.push(currentToolCall); toolCalls.push(currentToolCall);
currentToolCall = null; currentToolCall = null;
} }
@ -1681,24 +1606,28 @@ export class KiroApiService {
} }
// 6. 发送 message_delta 事件 // 6. 发送 message_delta 事件
// 在流结束后统一计算 output tokens避免在流式循环中阻塞事件循环 // 如果有 contextUsagePercentage使用它来计算 token
outputTokens = this.countTextTokens(totalContent); // 总上下文 200k tokens通过百分比计算总使用量再减去输入 token 得到输出 token
for (const tc of toolCalls) { let totalTokens = 0;
outputTokens += this.countTextTokens(JSON.stringify(tc.input || {})); if (contextUsagePercentage !== null && contextUsagePercentage > 0) {
const totalContextTokens = KIRO_CONSTANTS.TOTAL_CONTEXT_TOKENS;
// totalUsedTokens 就是通过百分比计算出的总使用量,直接作为 total_tokens
totalTokens = Math.round(totalContextTokens * contextUsagePercentage / 100);
outputTokens = Math.max(0, totalTokens - inputTokens);
console.log(`[Kiro] Token calculation from contextUsagePercentage: total=${totalTokens}, input=${inputTokens}, output=${outputTokens}`);
} else {
// 回退到原来的计算方式
outputTokens = this.countTextTokens(totalContent);
for (const tc of toolCalls) {
outputTokens += this.countTextTokens(JSON.stringify(tc.input || {}));
}
totalTokens = inputTokens + outputTokens;
} }
yield { yield {
type: "message_delta", type: "message_delta",
delta: { delta: { stop_reason: toolCalls.length > 0 ? "tool_use" : "end_turn" },
stop_reason: toolCalls.length > 0 ? "tool_use" : "end_turn", usage: { input_tokens: inputTokens, output_tokens: outputTokens, total_tokens: totalTokens }
stop_sequence: null
},
usage: {
input_tokens: inputTokens,
output_tokens: outputTokens,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0
}
}; };
// 7. 发送 message_stop 事件 // 7. 发送 message_stop 事件
@ -1725,24 +1654,7 @@ export class KiroApiService {
} }
/** /**
* Convert context usage percentage to actual input tokens * Calculate input tokens from request body using Claude's official tokenizer
* @param {number} percentage - Context usage percentage (0-100)
* @returns {number} Actual input tokens
*/
calculateInputTokensFromPercentage(percentage) {
if (!percentage || percentage <= 0) {
return 0;
}
const contextWindow = CLAUDE_DEFAULT_MAX_TOKENS;
const inputTokens = Math.round((percentage / 100) * contextWindow);
return inputTokens;
}
/**
* Estimate input tokens from request body using Claude's official tokenizer
* Used as fallback when contextUsagePercentage is not available from API
*/ */
estimateInputTokens(requestBody) { estimateInputTokens(requestBody) {
let totalTokens = 0; let totalTokens = 0;
@ -1943,9 +1855,7 @@ export class KiroApiService {
stop_sequence: null, stop_sequence: null,
usage: { usage: {
input_tokens: inputTokens, input_tokens: inputTokens,
output_tokens: outputTokens, output_tokens: outputTokens
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0
}, },
content: contentArray content: contentArray
}; };
@ -2071,7 +1981,7 @@ export class KiroApiService {
origin: KIRO_CONSTANTS.ORIGIN_AI_EDITOR, origin: KIRO_CONSTANTS.ORIGIN_AI_EDITOR,
resourceType: resourceType resourceType: resourceType
}); });
if (this.authMethod === KIRO_CONSTANTS.AUTH_METHOD_SOCIAL && this.profileArn) { if (this.authMethod === KIRO_CONSTANTS.AUTH_METHOD_SOCIAL && this.profileArn) {
params.append('profileArn', this.profileArn); params.append('profileArn', this.profileArn);
} }
const fullUrl = `${usageLimitsUrl}?${params.toString()}`; const fullUrl = `${usageLimitsUrl}?${params.toString()}`;

View file

@ -1,5 +1,5 @@
import { ProviderStrategy } from '../provider-strategy.js'; import { ProviderStrategy } from '../../utils/provider-strategy.js';
import { extractSystemPromptFromRequestBody, MODEL_PROTOCOL_PREFIX } from '../common.js'; import { extractSystemPromptFromRequestBody, MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
/** /**
* Claude provider strategy implementation. * Claude provider strategy implementation.

View file

@ -9,11 +9,11 @@ import * as os from 'os';
import * as readline from 'readline'; import * as readline from 'readline';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import open from 'open'; import open from 'open';
import { formatExpiryTime, isRetryableNetworkError } from '../common.js'; import { formatExpiryTime, isRetryableNetworkError } from '../../utils/common.js';
import { getProviderModels } from '../provider-models.js'; import { getProviderModels } from '../provider-models.js';
import { handleGeminiAntigravityOAuth } from '../oauth-handlers.js'; import { handleGeminiAntigravityOAuth } from '../../auth/oauth-handlers.js';
import { getProxyConfigForProvider, getGoogleAuthProxyConfig } from '../proxy-utils.js'; import { getProxyConfigForProvider, getGoogleAuthProxyConfig } from '../../utils/proxy-utils.js';
import { cleanJsonSchemaProperties } from '../converters/utils.js'; import { cleanJsonSchemaProperties } from '../../converters/utils.js';
// 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏 // 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏
const httpAgent = new http.Agent({ const httpAgent = new http.Agent({

View file

@ -6,10 +6,10 @@ import * as path from 'path';
import * as os from 'os'; import * as os from 'os';
import * as readline from 'readline'; import * as readline from 'readline';
import open from 'open'; 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 { getProviderModels } from '../provider-models.js';
import { handleGeminiCliOAuth } from '../oauth-handlers.js'; import { handleGeminiCliOAuth } from '../../auth/oauth-handlers.js';
import { getProxyConfigForProvider, getGoogleAuthProxyConfig } from '../proxy-utils.js'; import { getProxyConfigForProvider, getGoogleAuthProxyConfig } from '../../utils/proxy-utils.js';
// 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏 // 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏
const httpAgent = new http.Agent({ const httpAgent = new http.Agent({

View file

@ -1,5 +1,5 @@
import { API_ACTIONS, extractSystemPromptFromRequestBody, MODEL_PROTOCOL_PREFIX } from '../common.js'; import { API_ACTIONS, extractSystemPromptFromRequestBody, MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
import { ProviderStrategy } from '../provider-strategy.js'; import { ProviderStrategy } from '../../utils/provider-strategy.js';
/** /**
* Gemini provider strategy implementation. * Gemini provider strategy implementation.

View file

@ -22,8 +22,8 @@ import * as https from 'https';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import * as path from 'path'; import * as path from 'path';
import * as os from 'os'; import * as os from 'os';
import { configureAxiosProxy } from '../proxy-utils.js'; import { configureAxiosProxy } from '../../utils/proxy-utils.js';
import { isRetryableNetworkError } from '../common.js'; import { isRetryableNetworkError } from '../../utils/common.js';
// iFlow API 端点 // iFlow API 端点
const IFLOW_API_BASE_URL = 'https://apis.iflow.cn/v1'; const IFLOW_API_BASE_URL = 'https://apis.iflow.cn/v1';
@ -643,8 +643,8 @@ export class IFlowApiService {
} }
const currentTime = Date.now(); const currentTime = Date.now();
// 默认 10 分钟,可通过配置覆盖 // 授权文件时效48小时判断是否过期或接近过期 45小时
const cronNearMinutes = this.config.CRON_NEAR_MINUTES || 10; const cronNearMinutes = 60 * 45;
const cronNearMinutesInMillis = cronNearMinutes * 60 * 1000; const cronNearMinutesInMillis = cronNearMinutes * 60 * 1000;
// 解析过期时间 // 解析过期时间

View file

@ -1,8 +1,8 @@
import axios from 'axios'; import axios from 'axios';
import * as http from 'http'; import * as http from 'http';
import * as https from 'https'; import * as https from 'https';
import { configureAxiosProxy } from '../proxy-utils.js'; import { configureAxiosProxy } from '../../utils/proxy-utils.js';
import { isRetryableNetworkError } from '../common.js'; import { isRetryableNetworkError } from '../../utils/common.js';
// Assumed OpenAI API specification service for interacting with third-party models // Assumed OpenAI API specification service for interacting with third-party models
export class OpenAIApiService { export class OpenAIApiService {

View file

@ -1,7 +1,7 @@
import axios from 'axios'; import axios from 'axios';
import * as http from 'http'; import * as http from 'http';
import * as https from 'https'; 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 // OpenAI Responses API specification service for interacting with third-party models
export class OpenAIResponsesApiService { export class OpenAIResponsesApiService {

View file

@ -1,5 +1,5 @@
import { ProviderStrategy } from '../provider-strategy.js'; import { ProviderStrategy } from '../../utils/provider-strategy.js';
import { extractSystemPromptFromRequestBody, MODEL_PROTOCOL_PREFIX } from '../common.js'; import { extractSystemPromptFromRequestBody, MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
/** /**
* OpenAI Responses API strategy implementation. * OpenAI Responses API strategy implementation.

View file

@ -1,5 +1,5 @@
import { ProviderStrategy } from '../provider-strategy.js'; import { ProviderStrategy } from '../../utils/provider-strategy.js';
import { extractSystemPromptFromRequestBody, MODEL_PROTOCOL_PREFIX } from '../common.js'; import { extractSystemPromptFromRequestBody, MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
/** /**
* OpenAI provider strategy implementation. * OpenAI provider strategy implementation.

View file

@ -9,9 +9,9 @@ import open from 'open';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { randomUUID } from 'node:crypto'; import { randomUUID } from 'node:crypto';
import { getProviderModels } from '../provider-models.js'; import { getProviderModels } from '../provider-models.js';
import { handleQwenOAuth } from '../oauth-handlers.js'; import { handleQwenOAuth } from '../../auth/oauth-handlers.js';
import { configureAxiosProxy } from '../proxy-utils.js'; import { configureAxiosProxy } from '../../utils/proxy-utils.js';
import { isRetryableNetworkError } from '../common.js'; import { isRetryableNetworkError } from '../../utils/common.js';
// --- Constants --- // --- Constants ---
const QWEN_DIR = '.qwen'; const QWEN_DIR = '.qwen';

View file

@ -1,6 +1,6 @@
import * as fs from 'fs'; // Import fs module import * as fs from 'fs'; // Import fs module
import { getServiceAdapter } from './adapter.js'; 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 { getProviderModels } from './provider-models.js';
import axios from 'axios'; 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, handleContentGenerationRequest,
API_ACTIONS, API_ACTIONS,
ENDPOINT_TYPE ENDPOINT_TYPE
} from './common.js'; } from '../utils/common.js';
import { getProviderPoolManager } from './service-manager.js'; import { getProviderPoolManager } from './service-manager.js';
/** /**

View file

@ -1,10 +1,10 @@
import * as http from 'http'; 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 { initApiService, autoLinkProviderConfigs } from './service-manager.js';
import { initializeUIManagement } from './ui-manager.js'; import { initializeUIManagement } from './ui-manager.js';
import { initializeAPIManagement } from './api-manager.js'; import { initializeAPIManagement } from './api-manager.js';
import { createRequestHandler } from './request-handler.js'; import { createRequestHandler } from '../handlers/request-handler.js';
import { discoverPlugins, getPluginManager } from './plugin-manager.js'; import { discoverPlugins, getPluginManager } from '../core/plugin-manager.js';
/** /**
* @license * @license
@ -112,7 +112,7 @@ import { discoverPlugins, getPluginManager } from './plugin-manager.js';
*/ */
import 'dotenv/config'; // Import dotenv and configure it import 'dotenv/config'; // Import dotenv and configure it
import './converters/register-converters.js'; // 注册所有转换器 import '../converters/register-converters.js'; // 注册所有转换器
import { getProviderPoolManager } from './service-manager.js'; import { getProviderPoolManager } from './service-manager.js';
// 检测是否作为子进程运行 // 检测是否作为子进程运行

View file

@ -1,5 +1,5 @@
import { getServiceAdapter, serviceInstances } from './adapter.js'; import { getServiceAdapter, serviceInstances } from '../providers/adapter.js';
import { ProviderPoolManager } from './provider-pool-manager.js'; import { ProviderPoolManager } from '../providers/provider-pool-manager.js';
import deepmerge from 'deepmerge'; import deepmerge from 'deepmerge';
import * as fs from 'fs'; import * as fs from 'fs';
import { promises as pfs } from 'fs'; import { promises as pfs } from 'fs';
@ -11,7 +11,7 @@ import {
isPathUsed, isPathUsed,
getFileName, getFileName,
formatSystemPath formatSystemPath
} from './provider-utils.js'; } from '../utils/provider-utils.js';
// 存储 ProviderPoolManager 实例 // 存储 ProviderPoolManager 实例
let providerPoolManager = null; let providerPoolManager = null;

View file

@ -2,19 +2,19 @@ import { existsSync, readFileSync } from 'fs';
import path from 'path'; import path from 'path';
// Import UI modules // Import UI modules
import * as auth from './ui-modules/auth.js'; import * as auth from '../ui-modules/auth.js';
import * as configApi from './ui-modules/config-api.js'; import * as configApi from '../ui-modules/config-api.js';
import * as providerApi from './ui-modules/provider-api.js'; import * as providerApi from '../ui-modules/provider-api.js';
import * as usageApi from './ui-modules/usage-api.js'; import * as usageApi from '../ui-modules/usage-api.js';
import * as pluginApi from './ui-modules/plugin-api.js'; import * as pluginApi from '../ui-modules/plugin-api.js';
import * as uploadConfigApi from './ui-modules/upload-config-api.js'; import * as uploadConfigApi from '../ui-modules/upload-config-api.js';
import * as systemApi from './ui-modules/system-api.js'; import * as systemApi from '../ui-modules/system-api.js';
import * as updateApi from './ui-modules/update-api.js'; import * as updateApi from '../ui-modules/update-api.js';
import * as oauthApi from './ui-modules/oauth-api.js'; import * as oauthApi from '../ui-modules/oauth-api.js';
import * as eventBroadcast from './ui-modules/event-broadcast.js'; import * as eventBroadcast from '../ui-modules/event-broadcast.js';
// Re-export from event-broadcast module // 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 * Serve static files for the UI

View file

@ -4,8 +4,8 @@
*/ */
import { getProviderPoolManager } from './service-manager.js'; import { getProviderPoolManager } from './service-manager.js';
import { serviceInstances } from './adapter.js'; import { serviceInstances } from '../providers/adapter.js';
import { MODEL_PROVIDER } from './common.js'; import { MODEL_PROVIDER } from '../utils/common.js';
/** /**
* 用量查询服务类 * 用量查询服务类

View file

@ -1,11 +1,11 @@
import { existsSync, readFileSync, writeFileSync } from 'fs'; import { existsSync, readFileSync, writeFileSync } from 'fs';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import path from 'path'; import path from 'path';
import { CONFIG } from '../config-manager.js'; import { CONFIG } from '../core/config-manager.js';
import { serviceInstances } from '../adapter.js'; import { serviceInstances } from '../providers/adapter.js';
import { initApiService } from '../service-manager.js'; import { initApiService } from '../services/service-manager.js';
import { getRequestBody } from '../common.js'; import { getRequestBody } from '../utils/common.js';
import { broadcastEvent } from './event-broadcast.js'; import { broadcastEvent } from '../ui-modules/event-broadcast.js';
/** /**
* 重载配置文件 * 重载配置文件
@ -15,7 +15,7 @@ import { broadcastEvent } from './event-broadcast.js';
export async function reloadConfig(providerPoolManager) { export async function reloadConfig(providerPoolManager) {
try { try {
// Import config manager dynamically // Import config manager dynamically
const { initializeConfig } = await import('../config-manager.js'); const { initializeConfig } = await import('../core/config-manager.js');
// Reload main config // Reload main config
const newConfig = await initializeConfig(process.argv.slice(2), 'configs/config.json'); const newConfig = await initializeConfig(process.argv.slice(2), 'configs/config.json');

View file

@ -1,7 +1,7 @@
import { existsSync } from 'fs'; import { existsSync } from 'fs';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import path from 'path'; 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 { import {
handleGeminiCliOAuth, handleGeminiCliOAuth,
handleGeminiAntigravityOAuth, handleGeminiAntigravityOAuth,
@ -7,7 +7,7 @@ import {
handleIFlowOAuth, handleIFlowOAuth,
batchImportKiroRefreshTokensStream, batchImportKiroRefreshTokensStream,
importAwsCredentials importAwsCredentials
} from '../oauth-handlers.js'; } from '../auth/oauth-handlers.js';
/** /**
* 生成 OAuth 授权 URL * 生成 OAuth 授权 URL

View file

@ -1,5 +1,5 @@
import { getPluginManager } from '../plugin-manager.js'; import { getPluginManager } from '../core/plugin-manager.js';
import { getRequestBody } from '../common.js'; import { getRequestBody } from '../utils/common.js';
import { broadcastEvent } from './event-broadcast.js'; import { broadcastEvent } from './event-broadcast.js';
/** /**

View file

@ -1,7 +1,7 @@
import { existsSync, readFileSync, writeFileSync } from 'fs'; import { existsSync, readFileSync, writeFileSync } from 'fs';
import { getRequestBody } from '../common.js'; import { getRequestBody } from '../utils/common.js';
import { getAllProviderModels, getProviderModels } from '../provider-models.js'; import { getAllProviderModels, getProviderModels } from '../providers/provider-models.js';
import { generateUUID, createProviderConfig, formatSystemPath, detectProviderFromPath, addToUsedPaths, isPathUsed, pathsEqual } from '../provider-utils.js'; import { generateUUID, createProviderConfig, formatSystemPath, detectProviderFromPath, addToUsedPaths, isPathUsed, pathsEqual } from '../utils/provider-utils.js';
import { broadcastEvent } from './event-broadcast.js'; import { broadcastEvent } from './event-broadcast.js';
/** /**

View file

@ -1,6 +1,6 @@
import { CONFIG } from '../config-manager.js'; import { CONFIG } from '../core/config-manager.js';
import { serviceInstances, getServiceAdapter } from '../adapter.js'; import { serviceInstances, getServiceAdapter } from '../providers/adapter.js';
import { formatKiroUsage, formatGeminiUsage, formatAntigravityUsage } from '../usage-service.js'; import { formatKiroUsage, formatGeminiUsage, formatAntigravityUsage } from '../services/usage-service.js';
import { readUsageCache, writeUsageCache, readProviderUsageCache, updateProviderUsageCache } from './usage-cache.js'; import { readUsageCache, writeUsageCache, readProviderUsageCache, updateProviderUsageCache } from './usage-cache.js';
import path from 'path'; import path from 'path';

View file

@ -2,9 +2,9 @@ import { promises as fs } from 'fs';
import * as path from 'path'; import * as path from 'path';
import * as http from 'http'; // Add http for IncomingMessage and ServerResponse types import * as http from 'http'; // Add http for IncomingMessage and ServerResponse types
import * as crypto from 'crypto'; // Import crypto for MD5 hashing 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 { 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 // 2.5. 如果使用了提供商池,根据模型重新选择提供商(支持 Fallback
// 注意:这里使用 skipUsageCount: true因为初次选择时已经增加了 usageCount // 注意:这里使用 skipUsageCount: true因为初次选择时已经增加了 usageCount
if (providerPoolManager && CONFIG.providerPools && CONFIG.providerPools[CONFIG.MODEL_PROVIDER]) { 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); const result = await getApiServiceWithFallback(CONFIG, model);
service = result.service; service = result.service;

View file

@ -1,8 +1,8 @@
import { MODEL_PROTOCOL_PREFIX } from './common.js'; import { MODEL_PROTOCOL_PREFIX } from '../utils/common.js';
import { GeminiStrategy } from './gemini/gemini-strategy.js'; import { GeminiStrategy } from '../providers/gemini/gemini-strategy.js';
import { OpenAIStrategy } from './openai/openai-strategy.js'; import { OpenAIStrategy } from '../providers/openai/openai-strategy.js';
import { ClaudeStrategy } from './claude/claude-strategy.js'; import { ClaudeStrategy } from '../providers/claude/claude-strategy.js';
import { ResponsesAPIStrategy } from './openai/openai-responses-strategy.js'; import { ResponsesAPIStrategy } from '../providers/openai/openai-responses-strategy.js';
/** /**
* Strategy factory that returns the appropriate strategy instance based on the provider protocol. * 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 { 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. * 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 (portEl) portEl.value = data.SERVER_PORT || 3000;
if (modelProviderEl) { if (modelProviderEl) {
// 处理多选 MODEL_PROVIDER (复选框) // 处理多选 MODEL_PROVIDER (标签按钮)
const providers = Array.isArray(data.DEFAULT_MODEL_PROVIDERS) const providers = Array.isArray(data.DEFAULT_MODEL_PROVIDERS)
? data.DEFAULT_MODEL_PROVIDERS ? data.DEFAULT_MODEL_PROVIDERS
: (typeof data.MODEL_PROVIDER === 'string' ? data.MODEL_PROVIDER.split(',') : []); : (typeof data.MODEL_PROVIDER === 'string' ? data.MODEL_PROVIDER.split(',') : []);
const checkboxes = modelProviderEl.querySelectorAll('input[type="checkbox"]'); const tags = modelProviderEl.querySelectorAll('.provider-tag');
checkboxes.forEach(checkbox => { tags.forEach(tag => {
checkbox.checked = providers.includes(checkbox.value); 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); const anySelected = Array.from(tags).some(tag => tag.classList.contains('selected'));
if (!anyChecked && checkboxes.length > 0) { if (!anySelected && tags.length > 0) {
checkboxes[0].checked = true; tags[0].classList.add('selected');
} }
// 为复选框添加事件监听,防止取消勾选最后一个 // 为标签按钮添加点击事件监听
checkboxes.forEach(checkbox => { tags.forEach(tag => {
// 移除旧的监听器(如果有的话,虽然这里大概率没有 // 移除旧的监听器(通过克隆节点
const newCheckbox = checkbox.cloneNode(true); const newTag = tag.cloneNode(true);
checkbox.parentNode.replaceChild(newCheckbox, checkbox); tag.parentNode.replaceChild(newTag, tag);
newCheckbox.addEventListener('change', (e) => { newTag.addEventListener('click', (e) => {
const checkedCount = modelProviderEl.querySelectorAll('input[type="checkbox"]:checked').length; e.preventDefault();
if (checkedCount === 0) { const isSelected = newTag.classList.contains('selected');
newCheckbox.checked = true; const selectedCount = modelProviderEl.querySelectorAll('.provider-tag.selected').length;
// 如果当前是选中状态且只剩一个选中的,不允许取消
if (isSelected && selectedCount === 1) {
showToast(t('common.warning'), t('config.modelProviderRequired'), 'warning'); 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'); const proxyUrlEl = document.getElementById('proxyUrl');
if (proxyUrlEl) proxyUrlEl.value = data.PROXY_URL || ''; if (proxyUrlEl) proxyUrlEl.value = data.PROXY_URL || '';
// 加载启用代理的提供商 // 加载启用代理的提供商 (标签按钮)
const proxyProviderCheckboxes = document.querySelectorAll('input[name="proxyProvider"]'); const proxyProvidersEl = document.getElementById('proxyProviders');
const enabledProviders = data.PROXY_ENABLED_PROVIDERS || []; if (proxyProvidersEl) {
proxyProviderCheckboxes.forEach(checkbox => { const enabledProviders = data.PROXY_ENABLED_PROVIDERS || [];
checkbox.checked = enabledProviders.includes(checkbox.value); 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) { } catch (error) {
console.error('Failed to load configuration:', error); console.error('Failed to load configuration:', error);
@ -124,9 +158,9 @@ async function saveConfiguration() {
const modelProviderEl = document.getElementById('modelProvider'); const modelProviderEl = document.getElementById('modelProvider');
let selectedProviders = []; let selectedProviders = [];
if (modelProviderEl) { if (modelProviderEl) {
// 从复选框中获取选中的提供商 // 从标签按钮中获取选中的提供商
selectedProviders = Array.from(modelProviderEl.querySelectorAll('input[type="checkbox"]:checked')) selectedProviders = Array.from(modelProviderEl.querySelectorAll('.provider-tag.selected'))
.map(cb => cb.value); .map(tag => tag.getAttribute('data-value'));
} }
// 校验:必须至少勾选一个 // 校验:必须至少勾选一个
@ -187,9 +221,14 @@ async function saveConfiguration() {
// 保存代理配置 // 保存代理配置
config.PROXY_URL = document.getElementById('proxyUrl')?.value?.trim() || null; config.PROXY_URL = document.getElementById('proxyUrl')?.value?.trim() || null;
// 获取启用代理的提供商列表 // 获取启用代理的提供商列表 (从标签按钮)
const proxyProviderCheckboxes = document.querySelectorAll('input[name="proxyProvider"]:checked'); const proxyProvidersEl = document.getElementById('proxyProviders');
config.PROXY_ENABLED_PROVIDERS = Array.from(proxyProviderCheckboxes).map(cb => cb.value); 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 { try {
await window.apiClient.post('/config', config); await window.apiClient.post('/config', config);

View file

@ -455,7 +455,7 @@ const translations = {
'plugins.toggle.success': '插件 {name} 已{status}', 'plugins.toggle.success': '插件 {name} 已{status}',
'plugins.toggle.failed': '切换插件状态失败', 'plugins.toggle.failed': '切换插件状态失败',
'plugins.load.failed': '加载插件列表失败', 'plugins.load.failed': '加载插件列表失败',
'plugins.restart.required': '更改已保存,请重启服务以生效', 'plugins.restart.required': '更改已保存',
// Common // Common
'common.confirm': '确定', 'common.confirm': '确定',
@ -950,7 +950,7 @@ const translations = {
'plugins.toggle.success': 'Plugin {name} {status}', 'plugins.toggle.success': 'Plugin {name} {status}',
'plugins.toggle.failed': 'Failed to toggle plugin status', 'plugins.toggle.failed': 'Failed to toggle plugin status',
'plugins.load.failed': 'Failed to load plugins list', '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
'common.togglePassword': 'Show/Hide Password', 'common.togglePassword': 'Show/Hide Password',

View file

@ -262,47 +262,69 @@ input:checked + .toggle-slider:before {
box-shadow: 0 4px 12px var(--primary-40); box-shadow: 0 4px 12px var(--primary-40);
} }
/* 复选框列表样式 */ /* 提供商标签选择器样式 */
.provider-checklist { .provider-tags {
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 {
display: flex; display: flex;
align-items: center; flex-wrap: wrap;
gap: 10px; gap: 0.75rem;
padding: 10px 12px; padding: 1rem;
background: var(--bg-primary); background: var(--bg-tertiary);
border-radius: 12px;
border: 1px solid var(--border-color); 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); background: var(--bg-secondary);
border-color: var(--primary-color); 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"] { .provider-tag.selected {
margin: 0; 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; font-size: 0.875rem;
color: var(--text-primary); opacity: 0.8;
font-weight: 500;
} }
.provider-tag.selected i {
opacity: 1;
}
.provider-tag span {
white-space: nowrap;
}
/* 高级配置区域 */ /* 高级配置区域 */
.advanced-config-section { .advanced-config-section {
border: 1px solid var(--border-color); border: 1px solid var(--border-color);

View file

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

View file

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