feat(配置管理): 添加提供商URL配置支持并优化UI
扩展配置系统以支持自定义提供商API端点URL,包括Gemini、Kiro、Qwen和Antigravity服务的可配置基础URL。同时优化了配置表单的UI,添加了可选标记和占位符提示。 - 在config.json.example中添加各提供商URL配置项 - 修改各提供商核心服务以支持自定义URL - 更新配置管理器和UI管理器以处理新增URL字段 - 优化前端配置表单,添加可选标记和占位符 - 扩展字段映射和顺序定义以包含URL配置项 - 改进授权模态框,添加手动URL输入支持
This commit is contained in:
parent
5af83e4473
commit
e943819539
14 changed files with 456 additions and 141 deletions
|
|
@ -23,5 +23,15 @@
|
|||
"CRON_NEAR_MINUTES": 1,
|
||||
"CRON_REFRESH_TOKEN": false,
|
||||
"PROVIDER_POOLS_FILE_PATH": "provider_pools.json",
|
||||
"MAX_ERROR_COUNT": 3
|
||||
}
|
||||
"MAX_ERROR_COUNT": 3,
|
||||
"QWEN_BASE_URL": "https://portal.qwen.ai/v1",
|
||||
"QWEN_OAUTH_BASE_URL": "https://chat.qwen.ai",
|
||||
"GEMINI_BASE_URL": "https://cloudcode-pa.googleapis.com",
|
||||
"ANTIGRAVITY_BASE_URL_DAILY": "https://daily-cloudcode-pa.sandbox.googleapis.com",
|
||||
"ANTIGRAVITY_BASE_URL_AUTOPUSH": "https://autopush-cloudcode-pa.sandbox.googleapis.com",
|
||||
"KIRO_REFRESH_URL": "https://prod.{{region}}.auth.desktop.kiro.dev/refreshToken",
|
||||
"KIRO_REFRESH_IDC_URL": "https://oidc.{{region}}.amazonaws.com/token",
|
||||
"KIRO_BASE_URL": "https://codewhisperer.{{region}}.amazonaws.com/generateAssistantResponse",
|
||||
"KIRO_AMAZON_Q_URL": "https://codewhisperer.{{region}}.amazonaws.com/SendMessageStreaming",
|
||||
"KIRO_USAGE_LIMITS_URL": "https://q.{{region}}.amazonaws.com/getUsageLimits"
|
||||
}
|
||||
|
|
@ -451,10 +451,10 @@ async initializeAuth(forceRefresh = false) {
|
|||
this.region = 'us-east-1'; // Set default region
|
||||
}
|
||||
|
||||
this.refreshUrl = KIRO_CONSTANTS.REFRESH_URL.replace("{{region}}", this.region);
|
||||
this.refreshIDCUrl = KIRO_CONSTANTS.REFRESH_IDC_URL.replace("{{region}}", this.region);
|
||||
this.baseUrl = KIRO_CONSTANTS.BASE_URL.replace("{{region}}", this.region);
|
||||
this.amazonQUrl = KIRO_CONSTANTS.AMAZON_Q_URL.replace("{{region}}", this.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}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,6 +77,15 @@ export async function initializeConfig(args = process.argv.slice(2), configFileP
|
|||
KIRO_OAUTH_CREDS_FILE_PATH: null,
|
||||
QWEN_OAUTH_CREDS_FILE_PATH: null,
|
||||
PROJECT_ID: null,
|
||||
// Provider URLs
|
||||
QWEN_BASE_URL: null,
|
||||
QWEN_OAUTH_BASE_URL: null,
|
||||
GEMINI_BASE_URL: null,
|
||||
ANTIGRAVITY_BASE_URL_DAILY: null,
|
||||
ANTIGRAVITY_BASE_URL_AUTOPUSH: null,
|
||||
KIRO_REFRESH_URL: null,
|
||||
KIRO_REFRESH_IDC_URL: null,
|
||||
KIRO_BASE_URL: null,
|
||||
SYSTEM_PROMPT_FILE_PATH: INPUT_SYSTEM_PROMPT_FILE, // Default value
|
||||
SYSTEM_PROMPT_MODE: 'append',
|
||||
PROMPT_LOG_BASE_NAME: "prompt_log",
|
||||
|
|
@ -155,6 +164,48 @@ export async function initializeConfig(args = process.argv.slice(2), configFileP
|
|||
console.warn(`[Config Warning] --claude-base-url flag requires a value.`);
|
||||
}
|
||||
}
|
||||
// Provider URL arguments
|
||||
else if (args[i] === '--qwen-base-url') {
|
||||
if (i + 1 < args.length) {
|
||||
currentConfig.QWEN_BASE_URL = args[i + 1];
|
||||
i++;
|
||||
}
|
||||
} else if (args[i] === '--qwen-oauth-base-url') {
|
||||
if (i + 1 < args.length) {
|
||||
currentConfig.QWEN_OAUTH_BASE_URL = args[i + 1];
|
||||
i++;
|
||||
}
|
||||
} else if (args[i] === '--gemini-base-url') {
|
||||
if (i + 1 < args.length) {
|
||||
currentConfig.GEMINI_BASE_URL = args[i + 1];
|
||||
i++;
|
||||
}
|
||||
} else if (args[i] === '--antigravity-base-url-daily') {
|
||||
if (i + 1 < args.length) {
|
||||
currentConfig.ANTIGRAVITY_BASE_URL_DAILY = args[i + 1];
|
||||
i++;
|
||||
}
|
||||
} else if (args[i] === '--antigravity-base-url-autopush') {
|
||||
if (i + 1 < args.length) {
|
||||
currentConfig.ANTIGRAVITY_BASE_URL_AUTOPUSH = args[i + 1];
|
||||
i++;
|
||||
}
|
||||
} else if (args[i] === '--kiro-refresh-url') {
|
||||
if (i + 1 < args.length) {
|
||||
currentConfig.KIRO_REFRESH_URL = args[i + 1];
|
||||
i++;
|
||||
}
|
||||
} else if (args[i] === '--kiro-refresh-idc-url') {
|
||||
if (i + 1 < args.length) {
|
||||
currentConfig.KIRO_REFRESH_IDC_URL = args[i + 1];
|
||||
i++;
|
||||
}
|
||||
} else if (args[i] === '--kiro-base-url') {
|
||||
if (i + 1 < args.length) {
|
||||
currentConfig.KIRO_BASE_URL = args[i + 1];
|
||||
i++;
|
||||
}
|
||||
}
|
||||
// Gemini-specific arguments
|
||||
else if (args[i] === '--gemini-oauth-creds-base64') {
|
||||
if (i + 1 < args.length) {
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ const httpsAgent = new https.Agent({
|
|||
const AUTH_REDIRECT_PORT = 8086;
|
||||
const CREDENTIALS_DIR = '.antigravity';
|
||||
const CREDENTIALS_FILE = 'oauth_creds.json';
|
||||
const ANTIGRAVITY_BASE_URL_DAILY = 'https://daily-cloudcode-pa.sandbox.googleapis.com';
|
||||
const ANTIGRAVITY_BASE_URL_AUTOPUSH = 'https://autopush-cloudcode-pa.sandbox.googleapis.com';
|
||||
const DEFAULT_ANTIGRAVITY_BASE_URL_DAILY = 'https://daily-cloudcode-pa.sandbox.googleapis.com';
|
||||
const DEFAULT_ANTIGRAVITY_BASE_URL_AUTOPUSH = 'https://autopush-cloudcode-pa.sandbox.googleapis.com';
|
||||
const ANTIGRAVITY_API_VERSION = 'v1internal';
|
||||
const OAUTH_CLIENT_ID = '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com';
|
||||
const OAUTH_CLIENT_SECRET = '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';
|
||||
|
|
@ -240,14 +240,18 @@ export class AntigravityApiService {
|
|||
this.config = config;
|
||||
this.host = config.HOST;
|
||||
this.oauthCredsFilePath = config.ANTIGRAVITY_OAUTH_CREDS_FILE_PATH;
|
||||
this.baseURL = ANTIGRAVITY_BASE_URL_DAILY; // 使用通用 GEMINI_BASE_URL 配置
|
||||
this.baseURL = DEFAULT_ANTIGRAVITY_BASE_URL_DAILY; // 使用通用 GEMINI_BASE_URL 配置
|
||||
this.userAgent = DEFAULT_USER_AGENT; // 支持通用 USER_AGENT 配置
|
||||
this.projectId = config.PROJECT_ID;
|
||||
|
||||
// Initialize instance-specific endpoints
|
||||
this.baseUrlDaily = config.ANTIGRAVITY_BASE_URL_DAILY || DEFAULT_ANTIGRAVITY_BASE_URL_DAILY;
|
||||
this.baseUrlAutopush = config.ANTIGRAVITY_BASE_URL_AUTOPUSH || DEFAULT_ANTIGRAVITY_BASE_URL_AUTOPUSH;
|
||||
|
||||
// 多环境降级顺序
|
||||
this.baseURLs = this.baseURL ? [this.baseURL] : [
|
||||
ANTIGRAVITY_BASE_URL_DAILY,
|
||||
ANTIGRAVITY_BASE_URL_AUTOPUSH
|
||||
this.baseUrlDaily,
|
||||
this.baseUrlAutopush
|
||||
// ANTIGRAVITY_BASE_URL_PROD // 生产环境已注释
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ const httpsAgent = new https.Agent({
|
|||
const AUTH_REDIRECT_PORT = 8085;
|
||||
const CREDENTIALS_DIR = '.gemini';
|
||||
const CREDENTIALS_FILE = 'oauth_creds.json';
|
||||
const CODE_ASSIST_ENDPOINT = 'https://cloudcode-pa.googleapis.com';
|
||||
const CODE_ASSIST_API_VERSION = 'v1internal';
|
||||
const DEFAULT_CODE_ASSIST_ENDPOINT = 'https://cloudcode-pa.googleapis.com';
|
||||
const DEFAULT_CODE_ASSIST_API_VERSION = 'v1internal';
|
||||
const OAUTH_CLIENT_ID = '681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com';
|
||||
const OAUTH_CLIENT_SECRET = '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';
|
||||
const GEMINI_MODELS = getProviderModels('gemini-cli-oauth');
|
||||
|
|
@ -209,6 +209,9 @@ export class GeminiApiService {
|
|||
this.oauthCredsBase64 = config.GEMINI_OAUTH_CREDS_BASE64;
|
||||
this.oauthCredsFilePath = config.GEMINI_OAUTH_CREDS_FILE_PATH;
|
||||
this.projectId = config.PROJECT_ID;
|
||||
|
||||
this.codeAssistEndpoint = config.GEMINI_BASE_URL || DEFAULT_CODE_ASSIST_ENDPOINT;
|
||||
this.apiVersion = DEFAULT_CODE_ASSIST_API_VERSION;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
|
|
@ -409,7 +412,7 @@ export class GeminiApiService {
|
|||
|
||||
try {
|
||||
const requestOptions = {
|
||||
url: `${CODE_ASSIST_ENDPOINT}/${CODE_ASSIST_API_VERSION}:${method}`,
|
||||
url: `${this.codeAssistEndpoint}/${this.apiVersion}:${method}`,
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
responseType: "json",
|
||||
|
|
@ -453,7 +456,7 @@ export class GeminiApiService {
|
|||
|
||||
try {
|
||||
const requestOptions = {
|
||||
url: `${CODE_ASSIST_ENDPOINT}/${CODE_ASSIST_API_VERSION}:${method}`,
|
||||
url: `${this.codeAssistEndpoint}/${this.apiVersion}:${method}`,
|
||||
method: "POST",
|
||||
params: { alt: "sse" },
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
|
@ -606,7 +609,7 @@ export class GeminiApiService {
|
|||
|
||||
// 调用 retrieveUserQuota 接口获取用户配额信息
|
||||
try {
|
||||
const quotaURL = `${CODE_ASSIST_ENDPOINT}/${CODE_ASSIST_API_VERSION}:retrieveUserQuota`;
|
||||
const quotaURL = `${this.codeAssistEndpoint}/${this.apiVersion}:retrieveUserQuota`;
|
||||
const requestBody = {
|
||||
project: `projects/${this.projectId}`
|
||||
};
|
||||
|
|
|
|||
|
|
@ -30,16 +30,12 @@ const DEFAULT_LOCK_CONFIG = {
|
|||
attemptInterval: 200,
|
||||
};
|
||||
|
||||
const QWEN_OAUTH_BASE_URL = 'https://chat.qwen.ai';
|
||||
const QWEN_OAUTH_DEVICE_CODE_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/device/code`;
|
||||
const QWEN_OAUTH_TOKEN_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/token`;
|
||||
const DEFAULT_QWEN_OAUTH_BASE_URL = 'https://chat.qwen.ai';
|
||||
const DEFAULT_QWEN_BASE_URL = 'https://portal.qwen.ai/v1';
|
||||
const QWEN_OAUTH_CLIENT_ID = 'f0304373b74a44d2b584a3fb70ca9e56';
|
||||
const QWEN_OAUTH_SCOPE = 'openid profile email model.completion';
|
||||
const QWEN_OAUTH_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';
|
||||
|
||||
// const DEFAULT_QWEN_BASE_URL = 'https://dashscope.aliyuncs.com/compatible-mode/v1';
|
||||
const DEFAULT_QWEN_BASE_URL = 'https://portal.qwen.ai/v1';
|
||||
|
||||
export const QwenOAuth2Event = {
|
||||
AuthUri: 'auth-uri',
|
||||
AuthProgress: 'auth-progress',
|
||||
|
|
@ -181,6 +177,13 @@ export class QwenApiService {
|
|||
this.currentAxiosInstance = null;
|
||||
this.tokenManagerOptions = { credentialFilePath: this._getQwenCachedCredentialPath() };
|
||||
this.useSystemProxy = config?.USE_SYSTEM_PROXY_QWEN ?? false;
|
||||
|
||||
// Initialize instance-specific endpoints
|
||||
this.baseUrl = config.QWEN_BASE_URL || DEFAULT_QWEN_BASE_URL;
|
||||
const oauthBaseUrl = config.QWEN_OAUTH_BASE_URL || DEFAULT_QWEN_OAUTH_BASE_URL;
|
||||
this.oauthDeviceCodeEndpoint = `${oauthBaseUrl}/api/v1/oauth2/device/code`;
|
||||
this.oauthTokenEndpoint = `${oauthBaseUrl}/api/v1/oauth2/token`;
|
||||
|
||||
console.log(`[Qwen] System proxy ${this.useSystemProxy ? 'enabled' : 'disabled'}`);
|
||||
this.qwenClient = new QwenOAuth2Client(config, this.useSystemProxy);
|
||||
}
|
||||
|
|
@ -205,7 +208,7 @@ export class QwenApiService {
|
|||
});
|
||||
|
||||
const axiosConfig = {
|
||||
baseURL: DEFAULT_QWEN_BASE_URL,
|
||||
baseURL: this.baseUrl,
|
||||
httpAgent,
|
||||
httpsAgent,
|
||||
headers: {
|
||||
|
|
@ -386,7 +389,7 @@ export class QwenApiService {
|
|||
}
|
||||
|
||||
getCurrentEndpoint(resourceUrl) {
|
||||
const baseEndpoint = resourceUrl || DEFAULT_QWEN_BASE_URL;
|
||||
const baseEndpoint = resourceUrl || this.baseUrl;
|
||||
const suffix = '/v1';
|
||||
|
||||
const normalizedUrl = baseEndpoint.startsWith('http') ?
|
||||
|
|
@ -887,7 +890,8 @@ class QwenOAuth2Client {
|
|||
client_id: QWEN_OAUTH_CLIENT_ID,
|
||||
};
|
||||
try {
|
||||
const response = await commonFetch(QWEN_OAUTH_TOKEN_ENDPOINT, {
|
||||
const endpoint = this.oauthTokenEndpoint;
|
||||
const response = await commonFetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
|
||||
body: objectToUrlEncoded(bodyData),
|
||||
|
|
@ -917,7 +921,8 @@ class QwenOAuth2Client {
|
|||
code_challenge_method: options.code_challenge_method,
|
||||
};
|
||||
try {
|
||||
const response = await commonFetch(QWEN_OAUTH_DEVICE_CODE_ENDPOINT, {
|
||||
const endpoint = this.oauthDeviceCodeEndpoint;
|
||||
const response = await commonFetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
|
||||
body: objectToUrlEncoded(bodyData),
|
||||
|
|
@ -936,7 +941,8 @@ class QwenOAuth2Client {
|
|||
code_verifier: options.code_verifier,
|
||||
};
|
||||
try {
|
||||
const response = await commonFetch(QWEN_OAUTH_TOKEN_ENDPOINT, {
|
||||
const endpoint = this.oauthTokenEndpoint;
|
||||
const response = await commonFetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
|
||||
body: objectToUrlEncoded(bodyData),
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ export const PROVIDER_MAPPINGS = [
|
|||
credPathKey: 'KIRO_OAUTH_CREDS_FILE_PATH',
|
||||
defaultCheckModel: 'claude-haiku-4-5',
|
||||
displayName: 'Claude Kiro OAuth',
|
||||
needsProjectId: false
|
||||
needsProjectId: false,
|
||||
urlKeys: ['KIRO_BASE_URL', 'KIRO_REFRESH_URL', 'KIRO_REFRESH_IDC_URL']
|
||||
},
|
||||
{
|
||||
// Gemini CLI OAuth 配置
|
||||
|
|
@ -29,7 +30,8 @@ export const PROVIDER_MAPPINGS = [
|
|||
credPathKey: 'GEMINI_OAUTH_CREDS_FILE_PATH',
|
||||
defaultCheckModel: 'gemini-2.5-flash',
|
||||
displayName: 'Gemini CLI OAuth',
|
||||
needsProjectId: true
|
||||
needsProjectId: true,
|
||||
urlKeys: ['GEMINI_BASE_URL']
|
||||
},
|
||||
{
|
||||
// Qwen OAuth 配置
|
||||
|
|
@ -39,7 +41,8 @@ export const PROVIDER_MAPPINGS = [
|
|||
credPathKey: 'QWEN_OAUTH_CREDS_FILE_PATH',
|
||||
defaultCheckModel: 'qwen3-coder-plus',
|
||||
displayName: 'Qwen OAuth',
|
||||
needsProjectId: false
|
||||
needsProjectId: false,
|
||||
urlKeys: ['QWEN_BASE_URL', 'QWEN_OAUTH_BASE_URL']
|
||||
},
|
||||
{
|
||||
// Antigravity OAuth 配置
|
||||
|
|
@ -49,7 +52,8 @@ export const PROVIDER_MAPPINGS = [
|
|||
credPathKey: 'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH',
|
||||
defaultCheckModel: 'gemini-2.5-computer-use-preview-10-2025',
|
||||
displayName: 'Gemini Antigravity',
|
||||
needsProjectId: true
|
||||
needsProjectId: true,
|
||||
urlKeys: ['ANTIGRAVITY_BASE_URL_DAILY', 'ANTIGRAVITY_BASE_URL_AUTOPUSH']
|
||||
}
|
||||
];
|
||||
|
||||
|
|
@ -282,10 +286,11 @@ export async function isValidOAuthCredentials(filePath) {
|
|||
* @param {string} options.credPath - 凭据文件路径
|
||||
* @param {string} options.defaultCheckModel - 默认检测模型
|
||||
* @param {boolean} options.needsProjectId - 是否需要 PROJECT_ID
|
||||
* @param {Array} options.urlKeys - 可选的 URL 配置项键名列表
|
||||
* @returns {Object} 新的提供商配置对象
|
||||
*/
|
||||
export function createProviderConfig(options) {
|
||||
const { credPathKey, credPath, defaultCheckModel, needsProjectId } = options;
|
||||
const { credPathKey, credPath, defaultCheckModel, needsProjectId, urlKeys } = options;
|
||||
|
||||
const newProvider = {
|
||||
[credPathKey]: credPath,
|
||||
|
|
@ -307,6 +312,13 @@ export function createProviderConfig(options) {
|
|||
if (needsProjectId) {
|
||||
newProvider.PROJECT_ID = '';
|
||||
}
|
||||
|
||||
// 初始化可选的 URL 配置项
|
||||
if (urlKeys && Array.isArray(urlKeys)) {
|
||||
urlKeys.forEach(key => {
|
||||
newProvider[key] = '';
|
||||
});
|
||||
}
|
||||
|
||||
return newProvider;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -628,6 +628,16 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo
|
|||
if (newConfig.KIRO_OAUTH_CREDS_BASE64 !== undefined) currentConfig.KIRO_OAUTH_CREDS_BASE64 = newConfig.KIRO_OAUTH_CREDS_BASE64;
|
||||
if (newConfig.KIRO_OAUTH_CREDS_FILE_PATH !== undefined) currentConfig.KIRO_OAUTH_CREDS_FILE_PATH = newConfig.KIRO_OAUTH_CREDS_FILE_PATH;
|
||||
if (newConfig.QWEN_OAUTH_CREDS_FILE_PATH !== undefined) currentConfig.QWEN_OAUTH_CREDS_FILE_PATH = newConfig.QWEN_OAUTH_CREDS_FILE_PATH;
|
||||
|
||||
// New Provider URLs
|
||||
if (newConfig.QWEN_BASE_URL !== undefined) currentConfig.QWEN_BASE_URL = newConfig.QWEN_BASE_URL;
|
||||
if (newConfig.QWEN_OAUTH_BASE_URL !== undefined) currentConfig.QWEN_OAUTH_BASE_URL = newConfig.QWEN_OAUTH_BASE_URL;
|
||||
if (newConfig.GEMINI_BASE_URL !== undefined) currentConfig.GEMINI_BASE_URL = newConfig.GEMINI_BASE_URL;
|
||||
if (newConfig.ANTIGRAVITY_BASE_URL_DAILY !== undefined) currentConfig.ANTIGRAVITY_BASE_URL_DAILY = newConfig.ANTIGRAVITY_BASE_URL_DAILY;
|
||||
if (newConfig.ANTIGRAVITY_BASE_URL_AUTOPUSH !== undefined) currentConfig.ANTIGRAVITY_BASE_URL_AUTOPUSH = newConfig.ANTIGRAVITY_BASE_URL_AUTOPUSH;
|
||||
if (newConfig.KIRO_REFRESH_URL !== undefined) currentConfig.KIRO_REFRESH_URL = newConfig.KIRO_REFRESH_URL;
|
||||
if (newConfig.KIRO_REFRESH_IDC_URL !== undefined) currentConfig.KIRO_REFRESH_IDC_URL = newConfig.KIRO_REFRESH_IDC_URL;
|
||||
if (newConfig.KIRO_BASE_URL !== undefined) currentConfig.KIRO_BASE_URL = newConfig.KIRO_BASE_URL;
|
||||
if (newConfig.SYSTEM_PROMPT_FILE_PATH !== undefined) currentConfig.SYSTEM_PROMPT_FILE_PATH = newConfig.SYSTEM_PROMPT_FILE_PATH;
|
||||
if (newConfig.SYSTEM_PROMPT_MODE !== undefined) currentConfig.SYSTEM_PROMPT_MODE = newConfig.SYSTEM_PROMPT_MODE;
|
||||
if (newConfig.PROMPT_LOG_BASE_NAME !== undefined) currentConfig.PROMPT_LOG_BASE_NAME = newConfig.PROMPT_LOG_BASE_NAME;
|
||||
|
|
@ -680,6 +690,17 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo
|
|||
KIRO_OAUTH_CREDS_BASE64: currentConfig.KIRO_OAUTH_CREDS_BASE64,
|
||||
KIRO_OAUTH_CREDS_FILE_PATH: currentConfig.KIRO_OAUTH_CREDS_FILE_PATH,
|
||||
QWEN_OAUTH_CREDS_FILE_PATH: currentConfig.QWEN_OAUTH_CREDS_FILE_PATH,
|
||||
// Provider URLs
|
||||
QWEN_BASE_URL: currentConfig.QWEN_BASE_URL,
|
||||
QWEN_OAUTH_BASE_URL: currentConfig.QWEN_OAUTH_BASE_URL,
|
||||
GEMINI_BASE_URL: currentConfig.GEMINI_BASE_URL,
|
||||
ANTIGRAVITY_BASE_URL_DAILY: currentConfig.ANTIGRAVITY_BASE_URL_DAILY,
|
||||
ANTIGRAVITY_BASE_URL_AUTOPUSH: currentConfig.ANTIGRAVITY_BASE_URL_AUTOPUSH,
|
||||
KIRO_REFRESH_URL: currentConfig.KIRO_REFRESH_URL,
|
||||
KIRO_REFRESH_IDC_URL: currentConfig.KIRO_REFRESH_IDC_URL,
|
||||
KIRO_BASE_URL: currentConfig.KIRO_BASE_URL,
|
||||
KIRO_AMAZON_Q_URL: currentConfig.KIRO_AMAZON_Q_URL,
|
||||
KIRO_USAGE_LIMITS_URL: currentConfig.KIRO_USAGE_LIMITS_URL,
|
||||
SYSTEM_PROMPT_FILE_PATH: currentConfig.SYSTEM_PROMPT_FILE_PATH,
|
||||
SYSTEM_PROMPT_MODE: currentConfig.SYSTEM_PROMPT_MODE,
|
||||
PROMPT_LOG_BASE_NAME: currentConfig.PROMPT_LOG_BASE_NAME,
|
||||
|
|
|
|||
|
|
@ -32,6 +32,12 @@ async function loadConfiguration() {
|
|||
if (projectIdEl) projectIdEl.value = data.PROJECT_ID || '';
|
||||
if (geminiOauthCredsBase64El) geminiOauthCredsBase64El.value = data.GEMINI_OAUTH_CREDS_BASE64 || '';
|
||||
if (geminiOauthCredsFilePathEl) geminiOauthCredsFilePathEl.value = data.GEMINI_OAUTH_CREDS_FILE_PATH || '';
|
||||
const geminiBaseUrlEl = document.getElementById('geminiBaseUrl');
|
||||
if (geminiBaseUrlEl) geminiBaseUrlEl.value = data.GEMINI_BASE_URL || '';
|
||||
const antigravityBaseUrlDailyEl = document.getElementById('antigravityBaseUrlDaily');
|
||||
if (antigravityBaseUrlDailyEl) antigravityBaseUrlDailyEl.value = data.ANTIGRAVITY_BASE_URL_DAILY || '';
|
||||
const antigravityBaseUrlAutopushEl = document.getElementById('antigravityBaseUrlAutopush');
|
||||
if (antigravityBaseUrlAutopushEl) antigravityBaseUrlAutopushEl.value = data.ANTIGRAVITY_BASE_URL_AUTOPUSH || '';
|
||||
|
||||
// OpenAI Custom
|
||||
const openaiApiKeyEl = document.getElementById('openaiApiKey');
|
||||
|
|
@ -53,10 +59,20 @@ async function loadConfiguration() {
|
|||
|
||||
if (kiroOauthCredsBase64El) kiroOauthCredsBase64El.value = data.KIRO_OAUTH_CREDS_BASE64 || '';
|
||||
if (kiroOauthCredsFilePathEl) kiroOauthCredsFilePathEl.value = data.KIRO_OAUTH_CREDS_FILE_PATH || '';
|
||||
const kiroBaseUrlEl = document.getElementById('kiroBaseUrl');
|
||||
if (kiroBaseUrlEl) kiroBaseUrlEl.value = data.KIRO_BASE_URL || '';
|
||||
const kiroRefreshUrlEl = document.getElementById('kiroRefreshUrl');
|
||||
if (kiroRefreshUrlEl) kiroRefreshUrlEl.value = data.KIRO_REFRESH_URL || '';
|
||||
const kiroRefreshIdcUrlEl = document.getElementById('kiroRefreshIdcUrl');
|
||||
if (kiroRefreshIdcUrlEl) kiroRefreshIdcUrlEl.value = data.KIRO_REFRESH_IDC_URL || '';
|
||||
|
||||
// Qwen OAuth
|
||||
const qwenOauthCredsFilePathEl = document.getElementById('qwenOauthCredsFilePath');
|
||||
if (qwenOauthCredsFilePathEl) qwenOauthCredsFilePathEl.value = data.QWEN_OAUTH_CREDS_FILE_PATH || '';
|
||||
const qwenBaseUrlEl = document.getElementById('qwenBaseUrl');
|
||||
if (qwenBaseUrlEl) qwenBaseUrlEl.value = data.QWEN_BASE_URL || '';
|
||||
const qwenOauthBaseUrlEl = document.getElementById('qwenOauthBaseUrl');
|
||||
if (qwenOauthBaseUrlEl) qwenOauthBaseUrlEl.value = data.QWEN_OAUTH_BASE_URL || '';
|
||||
|
||||
// OpenAI Responses
|
||||
const openaiResponsesApiKeyEl = document.getElementById('openaiResponsesApiKey');
|
||||
|
|
@ -150,6 +166,13 @@ async function saveConfiguration() {
|
|||
config.GEMINI_OAUTH_CREDS_BASE64 = null;
|
||||
config.GEMINI_OAUTH_CREDS_FILE_PATH = document.getElementById('geminiOauthCredsFilePath')?.value || '';
|
||||
}
|
||||
config.GEMINI_BASE_URL = document.getElementById('geminiBaseUrl')?.value || null;
|
||||
break;
|
||||
|
||||
case 'gemini-antigravity':
|
||||
config.ANTIGRAVITY_BASE_URL_DAILY = document.getElementById('antigravityBaseUrlDaily')?.value || null;
|
||||
config.ANTIGRAVITY_BASE_URL_AUTOPUSH = document.getElementById('antigravityBaseUrlAutopush')?.value || null;
|
||||
config.ANTIGRAVITY_OAUTH_CREDS_FILE_PATH = document.getElementById('antigravityOauthCredsFilePath')?.value || '';
|
||||
break;
|
||||
|
||||
case 'openai-custom':
|
||||
|
|
@ -171,10 +194,15 @@ async function saveConfiguration() {
|
|||
config.KIRO_OAUTH_CREDS_BASE64 = null;
|
||||
config.KIRO_OAUTH_CREDS_FILE_PATH = document.getElementById('kiroOauthCredsFilePath')?.value || '';
|
||||
}
|
||||
config.KIRO_BASE_URL = document.getElementById('kiroBaseUrl')?.value || null;
|
||||
config.KIRO_REFRESH_URL = document.getElementById('kiroRefreshUrl')?.value || null;
|
||||
config.KIRO_REFRESH_IDC_URL = document.getElementById('kiroRefreshIdcUrl')?.value || null;
|
||||
break;
|
||||
|
||||
case 'openai-qwen-oauth':
|
||||
config.QWEN_OAUTH_CREDS_FILE_PATH = document.getElementById('qwenOauthCredsFilePath')?.value || '';
|
||||
config.QWEN_BASE_URL = document.getElementById('qwenBaseUrl')?.value || null;
|
||||
config.QWEN_OAUTH_BASE_URL = document.getElementById('qwenOauthBaseUrl')?.value || null;
|
||||
break;
|
||||
|
||||
case 'openaiResponses-custom':
|
||||
|
|
|
|||
|
|
@ -435,7 +435,10 @@ function renderProviderList(providers) {
|
|||
* @returns {string} HTML字符串
|
||||
*/
|
||||
function renderProviderConfig(provider) {
|
||||
// 获取字段映射,确保顺序一致
|
||||
// 获取该提供商类型的所有字段定义(从 utils.js)
|
||||
const fieldConfigs = getProviderTypeFields(currentProviderType);
|
||||
|
||||
// 获取字段显示顺序
|
||||
const fieldOrder = getFieldOrder(provider);
|
||||
|
||||
// 先渲染基础配置字段(customName、checkModelName 和 checkHealth)
|
||||
|
|
@ -447,6 +450,10 @@ function renderProviderConfig(provider) {
|
|||
const value = provider[fieldKey];
|
||||
const displayValue = value || '';
|
||||
|
||||
// 查找字段定义以获取 placeholder
|
||||
const fieldDef = fieldConfigs.find(f => f.id === fieldKey) || fieldConfigs.find(f => f.id.toUpperCase() === fieldKey.toUpperCase()) || {};
|
||||
const placeholder = fieldDef.placeholder || (fieldKey === 'customName' ? '节点自定义名称' : (fieldKey === 'checkModelName' ? '例如: gpt-3.5-turbo' : ''));
|
||||
|
||||
// 如果是 customName 字段,使用普通文本输入框
|
||||
if (fieldKey === 'customName') {
|
||||
html += `
|
||||
|
|
@ -457,7 +464,7 @@ function renderProviderConfig(provider) {
|
|||
readonly
|
||||
data-config-key="${fieldKey}"
|
||||
data-config-value="${value || ''}"
|
||||
placeholder="节点自定义名称">
|
||||
placeholder="${placeholder}">
|
||||
</div>
|
||||
`;
|
||||
} else if (fieldKey === 'checkHealth') {
|
||||
|
|
@ -485,7 +492,8 @@ function renderProviderConfig(provider) {
|
|||
value="${displayValue}"
|
||||
readonly
|
||||
data-config-key="${fieldKey}"
|
||||
data-config-value="${value || ''}">
|
||||
data-config-value="${value || ''}"
|
||||
placeholder="${placeholder}">
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
@ -504,6 +512,7 @@ function renderProviderConfig(provider) {
|
|||
const field1IsPassword = field1Key.toLowerCase().includes('key') || field1Key.toLowerCase().includes('password');
|
||||
const field1IsOAuthFilePath = field1Key.includes('OAUTH_CREDS_FILE_PATH');
|
||||
const field1DisplayValue = field1IsPassword && field1Value ? '••••••••' : (field1Value || '');
|
||||
const field1Def = fieldConfigs.find(f => f.id === field1Key) || fieldConfigs.find(f => f.id.toUpperCase() === field1Key.toUpperCase()) || {};
|
||||
|
||||
if (field1IsPassword) {
|
||||
html += `
|
||||
|
|
@ -514,8 +523,9 @@ function renderProviderConfig(provider) {
|
|||
value="${field1DisplayValue}"
|
||||
readonly
|
||||
data-config-key="${field1Key}"
|
||||
data-config-value="${field1Value || ''}">
|
||||
<button type="button" class="password-toggle" data-target="${field1Key}">
|
||||
data-config-value="${field1Value || ''}"
|
||||
placeholder="${field1Def.placeholder || ''}">
|
||||
<button type="button" class="password-toggle" data-target="${field1Key}">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -532,8 +542,9 @@ function renderProviderConfig(provider) {
|
|||
value="${field1Value || ''}"
|
||||
readonly
|
||||
data-config-key="${field1Key}"
|
||||
data-config-value="${field1Value || ''}">
|
||||
<button type="button" class="btn btn-outline upload-btn" data-target="edit-${provider.uuid}-${field1Key}" aria-label="上传文件" disabled>
|
||||
data-config-value="${field1Value || ''}"
|
||||
placeholder="${field1Def.placeholder || ''}">
|
||||
<button type="button" class="btn btn-outline upload-btn" data-target="edit-${provider.uuid}-${field1Key}" aria-label="上传文件" disabled>
|
||||
<i class="fas fa-upload"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -547,7 +558,8 @@ function renderProviderConfig(provider) {
|
|||
value="${field1DisplayValue}"
|
||||
readonly
|
||||
data-config-key="${field1Key}"
|
||||
data-config-value="${field1Value || ''}">
|
||||
data-config-value="${field1Value || ''}"
|
||||
placeholder="${field1Def.placeholder || ''}">
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
@ -560,6 +572,7 @@ function renderProviderConfig(provider) {
|
|||
const field2IsPassword = field2Key.toLowerCase().includes('key') || field2Key.toLowerCase().includes('password');
|
||||
const field2IsOAuthFilePath = field2Key.includes('OAUTH_CREDS_FILE_PATH');
|
||||
const field2DisplayValue = field2IsPassword && field2Value ? '••••••••' : (field2Value || '');
|
||||
const field2Def = fieldConfigs.find(f => f.id === field2Key) || fieldConfigs.find(f => f.id.toUpperCase() === field2Key.toUpperCase()) || {};
|
||||
|
||||
if (field2IsPassword) {
|
||||
html += `
|
||||
|
|
@ -570,7 +583,8 @@ function renderProviderConfig(provider) {
|
|||
value="${field2DisplayValue}"
|
||||
readonly
|
||||
data-config-key="${field2Key}"
|
||||
data-config-value="${field2Value || ''}">
|
||||
data-config-value="${field2Value || ''}"
|
||||
placeholder="${field2Def.placeholder || ''}">
|
||||
<button type="button" class="password-toggle" data-target="${field2Key}">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
|
|
@ -588,7 +602,8 @@ function renderProviderConfig(provider) {
|
|||
value="${field2Value || ''}"
|
||||
readonly
|
||||
data-config-key="${field2Key}"
|
||||
data-config-value="${field2Value || ''}">
|
||||
data-config-value="${field2Value || ''}"
|
||||
placeholder="${field2Def.placeholder || ''}">
|
||||
<button type="button" class="btn btn-outline upload-btn" data-target="edit-${provider.uuid}-${field2Key}" aria-label="上传文件" disabled>
|
||||
<i class="fas fa-upload"></i>
|
||||
</button>
|
||||
|
|
@ -603,7 +618,8 @@ function renderProviderConfig(provider) {
|
|||
value="${field2DisplayValue}"
|
||||
readonly
|
||||
data-config-key="${field2Key}"
|
||||
data-config-value="${field2Value || ''}">
|
||||
data-config-value="${field2Value || ''}"
|
||||
placeholder="${field2Def.placeholder || ''}">
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
@ -652,54 +668,48 @@ function getFieldOrder(provider) {
|
|||
'openai-custom': ['OPENAI_API_KEY', 'OPENAI_BASE_URL'],
|
||||
'openaiResponses-custom': ['OPENAI_API_KEY', 'OPENAI_BASE_URL'],
|
||||
'claude-custom': ['CLAUDE_API_KEY', 'CLAUDE_BASE_URL'],
|
||||
'gemini-cli-oauth': ['PROJECT_ID', 'GEMINI_OAUTH_CREDS_FILE_PATH'],
|
||||
'claude-kiro-oauth': ['KIRO_OAUTH_CREDS_FILE_PATH'],
|
||||
'openai-qwen-oauth': ['QWEN_OAUTH_CREDS_FILE_PATH'],
|
||||
'gemini-antigravity': ['PROJECT_ID', 'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH']
|
||||
'gemini-cli-oauth': ['PROJECT_ID', 'GEMINI_OAUTH_CREDS_FILE_PATH', 'GEMINI_BASE_URL'],
|
||||
'claude-kiro-oauth': ['KIRO_OAUTH_CREDS_FILE_PATH', 'KIRO_BASE_URL', 'KIRO_REFRESH_URL'],
|
||||
'openai-qwen-oauth': ['QWEN_OAUTH_CREDS_FILE_PATH', 'QWEN_BASE_URL', 'QWEN_OAUTH_BASE_URL'],
|
||||
'gemini-antigravity': ['PROJECT_ID', 'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH', 'ANTIGRAVITY_BASE_URL_DAILY', 'ANTIGRAVITY_BASE_URL_AUTOPUSH']
|
||||
};
|
||||
|
||||
// 获取所有其他配置项
|
||||
// 尝试从全局或当前模态框上下文中推断提供商类型
|
||||
let providerType = currentProviderType;
|
||||
if (!providerType) {
|
||||
if (provider.OPENAI_API_KEY && provider.OPENAI_BASE_URL) {
|
||||
providerType = 'openai-custom';
|
||||
} else if (provider.CLAUDE_API_KEY && provider.CLAUDE_BASE_URL) {
|
||||
providerType = 'claude-custom';
|
||||
} else if (provider.GEMINI_OAUTH_CREDS_FILE_PATH) {
|
||||
providerType = 'gemini-cli-oauth';
|
||||
} else if (provider.KIRO_OAUTH_CREDS_FILE_PATH) {
|
||||
providerType = 'claude-kiro-oauth';
|
||||
} else if (provider.QWEN_OAUTH_CREDS_FILE_PATH) {
|
||||
providerType = 'openai-qwen-oauth';
|
||||
} else if (provider.ANTIGRAVITY_OAUTH_CREDS_FILE_PATH) {
|
||||
providerType = 'gemini-antigravity';
|
||||
}
|
||||
}
|
||||
|
||||
// 获取该类型应该具有的所有字段(预定义顺序)
|
||||
const predefinedOrder = providerType ? (fieldOrderMap[providerType] || []) : [];
|
||||
|
||||
// 获取当前对象中存在且不在预定义列表中的其他字段
|
||||
const otherFields = Object.keys(provider).filter(key =>
|
||||
!excludedFields.includes(key) && !orderedFields.includes(key)
|
||||
!excludedFields.includes(key) &&
|
||||
!orderedFields.includes(key) &&
|
||||
!predefinedOrder.includes(key)
|
||||
);
|
||||
otherFields.sort();
|
||||
|
||||
// 合并所有要显示的字段
|
||||
const allExpectedFields = [...orderedFields, ...predefinedOrder, ...otherFields];
|
||||
|
||||
// 尝试从 provider 中推断提供商类型
|
||||
let providerType = null;
|
||||
if (provider.OPENAI_API_KEY && provider.OPENAI_BASE_URL) {
|
||||
providerType = 'openai-custom'; // 或 openaiResponses-custom,顺序相同
|
||||
} else if (provider.CLAUDE_API_KEY && provider.CLAUDE_BASE_URL) {
|
||||
providerType = 'claude-custom';
|
||||
} else if (provider.GEMINI_OAUTH_CREDS_FILE_PATH) {
|
||||
providerType = 'gemini-cli-oauth';
|
||||
} else if (provider.KIRO_OAUTH_CREDS_FILE_PATH) {
|
||||
providerType = 'claude-kiro-oauth';
|
||||
} else if (provider.QWEN_OAUTH_CREDS_FILE_PATH) {
|
||||
providerType = 'openai-qwen-oauth';
|
||||
} else if (provider.ANTIGRAVITY_OAUTH_CREDS_FILE_PATH) {
|
||||
providerType = 'gemini-antigravity';
|
||||
}
|
||||
|
||||
// 如果能识别提供商类型,使用预定义的顺序
|
||||
if (providerType && fieldOrderMap[providerType]) {
|
||||
const predefinedOrder = fieldOrderMap[providerType];
|
||||
const orderedOtherFields = [];
|
||||
const remainingFields = [...otherFields];
|
||||
|
||||
// 先按预定义顺序添加字段
|
||||
predefinedOrder.forEach(fieldKey => {
|
||||
const index = remainingFields.indexOf(fieldKey);
|
||||
if (index !== -1) {
|
||||
orderedOtherFields.push(fieldKey);
|
||||
remainingFields.splice(index, 1);
|
||||
}
|
||||
});
|
||||
|
||||
// 剩余字段按字母顺序添加
|
||||
remainingFields.sort();
|
||||
orderedOtherFields.push(...remainingFields);
|
||||
|
||||
return [...orderedFields, ...orderedOtherFields].filter(key => provider.hasOwnProperty(key));
|
||||
}
|
||||
// 只有在字段确实存在于 provider 中,或者它是该提供商类型的预定义字段时才显示
|
||||
return allExpectedFields.filter(key =>
|
||||
provider.hasOwnProperty(key) || predefinedOrder.includes(key)
|
||||
);
|
||||
|
||||
// 如果无法识别提供商类型,按字母顺序排序
|
||||
otherFields.sort();
|
||||
|
|
@ -1071,16 +1081,21 @@ function showAddProviderForm(providerType) {
|
|||
function addDynamicConfigFields(form, providerType) {
|
||||
const configFields = form.querySelector('#dynamicConfigFields');
|
||||
|
||||
// 获取该提供商类型的字段配置
|
||||
const providerFields = getProviderTypeFields(providerType);
|
||||
// 获取该提供商类型的字段配置(已经在 utils.js 中包含了 URL 字段)
|
||||
const allFields = getProviderTypeFields(providerType);
|
||||
|
||||
// 过滤掉已经在 form-grid 中硬编码显示的三个基础字段,避免重复
|
||||
const baseFields = ['customName', 'checkModelName', 'checkHealth'];
|
||||
const filteredFields = allFields.filter(f => !baseFields.some(bf => f.id.toLowerCase().includes(bf.toLowerCase())));
|
||||
|
||||
let fields = '';
|
||||
|
||||
if (providerFields.length > 0) {
|
||||
if (filteredFields.length > 0) {
|
||||
// 分组显示,每行两个字段
|
||||
for (let i = 0; i < providerFields.length; i += 2) {
|
||||
for (let i = 0; i < filteredFields.length; i += 2) {
|
||||
fields += '<div class="form-grid">';
|
||||
|
||||
const field1 = providerFields[i];
|
||||
const field1 = filteredFields[i];
|
||||
// 检查是否为密码类型字段
|
||||
const isPassword1 = field1.type === 'password';
|
||||
// 检查是否为OAuth凭据文件路径字段
|
||||
|
|
@ -1122,7 +1137,7 @@ function addDynamicConfigFields(form, providerType) {
|
|||
`;
|
||||
}
|
||||
|
||||
const field2 = providerFields[i + 1];
|
||||
const field2 = filteredFields[i + 1];
|
||||
if (field2) {
|
||||
// 检查是否为密码类型字段
|
||||
const isPassword2 = field2.type === 'password';
|
||||
|
|
@ -1215,35 +1230,14 @@ async function addProvider(providerType) {
|
|||
checkHealth
|
||||
};
|
||||
|
||||
// 根据提供商类型收集配置
|
||||
switch (providerType) {
|
||||
case 'openai-custom':
|
||||
providerConfig.OPENAI_API_KEY = document.getElementById('newOpenaiApiKey')?.value || '';
|
||||
providerConfig.OPENAI_BASE_URL = document.getElementById('newOpenaiBaseUrl')?.value || '';
|
||||
break;
|
||||
case 'openaiResponses-custom':
|
||||
providerConfig.OPENAI_API_KEY = document.getElementById('newOpenaiApiKey')?.value || '';
|
||||
providerConfig.OPENAI_BASE_URL = document.getElementById('newOpenaiBaseUrl')?.value || '';
|
||||
break;
|
||||
case 'claude-custom':
|
||||
providerConfig.CLAUDE_API_KEY = document.getElementById('newClaudeApiKey')?.value || '';
|
||||
providerConfig.CLAUDE_BASE_URL = document.getElementById('newClaudeBaseUrl')?.value || '';
|
||||
break;
|
||||
case 'gemini-cli-oauth':
|
||||
providerConfig.PROJECT_ID = document.getElementById('newProjectId')?.value || '';
|
||||
providerConfig.GEMINI_OAUTH_CREDS_FILE_PATH = document.getElementById('newGeminiOauthCredsFilePath')?.value || '';
|
||||
break;
|
||||
case 'claude-kiro-oauth':
|
||||
providerConfig.KIRO_OAUTH_CREDS_FILE_PATH = document.getElementById('newKiroOauthCredsFilePath')?.value || '';
|
||||
break;
|
||||
case 'openai-qwen-oauth':
|
||||
providerConfig.QWEN_OAUTH_CREDS_FILE_PATH = document.getElementById('newQwenOauthCredsFilePath')?.value || '';
|
||||
break;
|
||||
case 'gemini-antigravity':
|
||||
providerConfig.PROJECT_ID = document.getElementById('newProjectId')?.value || '';
|
||||
providerConfig.ANTIGRAVITY_OAUTH_CREDS_FILE_PATH = document.getElementById('newAntigravityOauthCredsFilePath')?.value || '';
|
||||
break;
|
||||
}
|
||||
// 根据提供商类型动态收集配置字段(自动匹配 utils.js 中的定义)
|
||||
const allFields = getProviderTypeFields(providerType);
|
||||
allFields.forEach(field => {
|
||||
const element = document.getElementById(`new${field.id}`);
|
||||
if (element) {
|
||||
providerConfig[field.id] = element.value || '';
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await window.apiClient.post('/providers', {
|
||||
|
|
|
|||
|
|
@ -488,8 +488,104 @@ function showAuthModal(authUrl, authInfo) {
|
|||
// 在浏览器中打开按钮
|
||||
const openBtn = modal.querySelector('.open-auth-btn');
|
||||
openBtn.addEventListener('click', () => {
|
||||
window.open(authUrl, '_blank');
|
||||
showToast('已在新标签页中打开授权页面', 'success');
|
||||
// 使用子窗口打开,以便监听 URL 变化
|
||||
const width = 600;
|
||||
const height = 700;
|
||||
const left = (window.screen.width - width) / 2;
|
||||
const top = (window.screen.height - height) / 2;
|
||||
|
||||
const authWindow = window.open(
|
||||
authUrl,
|
||||
'OAuthAuthWindow',
|
||||
`width=${width},height=${height},left=${left},top=${top},status=no,resizable=yes,scrollbars=yes`
|
||||
);
|
||||
|
||||
if (authWindow) {
|
||||
showToast('已打开授权窗口,请在窗口中完成操作', 'info');
|
||||
|
||||
// 添加手动输入回调 URL 的 UI
|
||||
const urlSection = modal.querySelector('.auth-url-section');
|
||||
const manualInputHtml = `
|
||||
<div class="manual-callback-section" style="margin-top: 20px; padding: 15px; background: #fffbeb; border: 1px solid #fef3c7; border-radius: 8px;">
|
||||
<h4 style="color: #92400e; margin-bottom: 8px;"><i class="fas fa-exclamation-circle"></i> 自动监听受阻?</h4>
|
||||
<p style="font-size: 0.875rem; color: #b45309; margin-bottom: 10px;">如果授权窗口重定向后显示“无法访问”,请将该窗口地址栏的 <strong>完整 URL</strong> 粘贴到下方:</p>
|
||||
<div class="auth-url-container" style="display: flex; gap: 5px;">
|
||||
<input type="text" class="manual-callback-input" placeholder="粘贴回调 URL (包含 code=...)" style="flex: 1; padding: 8px; border: 1px solid #fcd34d; border-radius: 4px; background: white; color: black;">
|
||||
<button class="btn btn-success apply-callback-btn" style="padding: 8px 15px; white-space: nowrap; background: #059669; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||||
<i class="fas fa-check"></i> 提交
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
urlSection.insertAdjacentHTML('afterend', manualInputHtml);
|
||||
|
||||
const manualInput = modal.querySelector('.manual-callback-input');
|
||||
const applyBtn = modal.querySelector('.apply-callback-btn');
|
||||
|
||||
// 处理回调 URL 的核心逻辑
|
||||
const processCallback = (urlStr) => {
|
||||
try {
|
||||
// 尝试清理 URL(有些用户可能会复制多余的文字)
|
||||
const cleanUrlStr = urlStr.trim().match(/https?:\/\/[^\s]+/)?.[0] || urlStr.trim();
|
||||
const url = new URL(cleanUrlStr);
|
||||
|
||||
if (url.searchParams.has('code') || url.searchParams.has('token')) {
|
||||
clearInterval(pollTimer);
|
||||
// 构造本地可处理的 URL,只修改 hostname,保持原始 URL 的端口号不变
|
||||
const localUrl = new URL(url.href);
|
||||
localUrl.hostname = window.location.hostname;
|
||||
localUrl.protocol = window.location.protocol;
|
||||
|
||||
showToast('正在完成授权...', 'info');
|
||||
|
||||
// 优先在子窗口中跳转(如果没关)
|
||||
if (authWindow && !authWindow.closed) {
|
||||
authWindow.location.href = localUrl.href;
|
||||
} else {
|
||||
// 备选方案:通过隐藏 iframe 或者是 fetch
|
||||
const img = new Image();
|
||||
img.src = localUrl.href;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
showToast('授权完成!', 'success');
|
||||
if (authWindow && !authWindow.closed) authWindow.close();
|
||||
modal.remove();
|
||||
// 刷新列表以显示新状态
|
||||
loadProviders();
|
||||
}, 2500);
|
||||
} else {
|
||||
showToast('该 URL 似乎不包含有效的授权代码', 'warning');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('处理回调失败:', err);
|
||||
showToast('无效的 URL 格式', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
applyBtn.addEventListener('click', () => {
|
||||
processCallback(manualInput.value);
|
||||
});
|
||||
|
||||
// 启动定时器轮询子窗口 URL
|
||||
const pollTimer = setInterval(() => {
|
||||
try {
|
||||
if (authWindow.closed) {
|
||||
clearInterval(pollTimer);
|
||||
return;
|
||||
}
|
||||
// 如果能读到说明回到了同域
|
||||
const currentUrl = authWindow.location.href;
|
||||
if (currentUrl && (currentUrl.includes('code=') || currentUrl.includes('token='))) {
|
||||
processCallback(currentUrl);
|
||||
}
|
||||
} catch (e) {
|
||||
// 跨域受限是正常的
|
||||
}
|
||||
}, 1000);
|
||||
} else {
|
||||
showToast('授权窗口被浏览器拦截,请允许弹出窗口', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// 点击遮罩层关闭
|
||||
|
|
|
|||
|
|
@ -309,11 +309,12 @@ body {
|
|||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.form-group label .optional-mark {
|
||||
.optional-tag, .form-group label .optional-mark {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 400;
|
||||
margin-left: 0.25rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
|
|
|
|||
|
|
@ -65,7 +65,14 @@ function getFieldLabel(key) {
|
|||
'GEMINI_OAUTH_CREDS_FILE_PATH': 'OAuth凭据文件路径',
|
||||
'KIRO_OAUTH_CREDS_FILE_PATH': 'OAuth凭据文件路径',
|
||||
'QWEN_OAUTH_CREDS_FILE_PATH': 'OAuth凭据文件路径',
|
||||
'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH': 'OAuth凭据文件路径'
|
||||
'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH': 'OAuth凭据文件路径',
|
||||
'GEMINI_BASE_URL': 'Gemini Base URL',
|
||||
'KIRO_BASE_URL': 'Base URL',
|
||||
'KIRO_REFRESH_URL': 'Refresh URL',
|
||||
'QWEN_BASE_URL': 'Qwen Base URL',
|
||||
'QWEN_OAUTH_BASE_URL': 'OAuth Base URL',
|
||||
'ANTIGRAVITY_BASE_URL_DAILY': 'Daily Base URL',
|
||||
'ANTIGRAVITY_BASE_URL_AUTOPUSH': 'Autopush Base URL'
|
||||
};
|
||||
|
||||
return labelMap[key] || key;
|
||||
|
|
@ -80,88 +87,136 @@ function getProviderTypeFields(providerType) {
|
|||
const fieldConfigs = {
|
||||
'openai-custom': [
|
||||
{
|
||||
id: 'OpenaiApiKey',
|
||||
id: 'OPENAI_API_KEY',
|
||||
label: 'OpenAI API Key',
|
||||
type: 'password',
|
||||
placeholder: 'sk-...'
|
||||
},
|
||||
{
|
||||
id: 'OpenaiBaseUrl',
|
||||
id: 'OPENAI_BASE_URL',
|
||||
label: 'OpenAI Base URL',
|
||||
type: 'text',
|
||||
value: 'https://api.openai.com/v1'
|
||||
placeholder: 'https://api.openai.com/v1'
|
||||
}
|
||||
],
|
||||
'openaiResponses-custom': [
|
||||
{
|
||||
id: 'OpenaiApiKey',
|
||||
id: 'OPENAI_API_KEY',
|
||||
label: 'OpenAI API Key',
|
||||
type: 'password',
|
||||
placeholder: 'sk-...'
|
||||
},
|
||||
{
|
||||
id: 'OpenaiBaseUrl',
|
||||
id: 'OPENAI_BASE_URL',
|
||||
label: 'OpenAI Base URL',
|
||||
type: 'text',
|
||||
value: 'https://api.openai.com/v1'
|
||||
placeholder: 'https://api.openai.com/v1'
|
||||
}
|
||||
],
|
||||
'claude-custom': [
|
||||
{
|
||||
id: 'ClaudeApiKey',
|
||||
id: 'CLAUDE_API_KEY',
|
||||
label: 'Claude API Key',
|
||||
type: 'password',
|
||||
placeholder: 'sk-ant-...'
|
||||
},
|
||||
{
|
||||
id: 'ClaudeBaseUrl',
|
||||
id: 'CLAUDE_BASE_URL',
|
||||
label: 'Claude Base URL',
|
||||
type: 'text',
|
||||
value: 'https://api.anthropic.com'
|
||||
placeholder: 'https://api.anthropic.com'
|
||||
}
|
||||
],
|
||||
'gemini-cli-oauth': [
|
||||
{
|
||||
id: 'ProjectId',
|
||||
id: 'PROJECT_ID',
|
||||
label: '项目ID',
|
||||
type: 'text',
|
||||
placeholder: 'Google Cloud项目ID'
|
||||
},
|
||||
{
|
||||
id: 'GeminiOauthCredsFilePath',
|
||||
id: 'GEMINI_OAUTH_CREDS_FILE_PATH',
|
||||
label: 'OAuth凭据文件路径',
|
||||
type: 'text',
|
||||
placeholder: '例如: ~/.gemini/oauth_creds.json'
|
||||
},
|
||||
{
|
||||
id: 'GEMINI_BASE_URL',
|
||||
label: 'Gemini Base URL <span class="optional-tag">(选填)</span>',
|
||||
type: 'text',
|
||||
placeholder: 'https://cloudcode-pa.googleapis.com'
|
||||
}
|
||||
],
|
||||
'claude-kiro-oauth': [
|
||||
{
|
||||
id: 'KiroOauthCredsFilePath',
|
||||
id: 'KIRO_OAUTH_CREDS_FILE_PATH',
|
||||
label: 'OAuth凭据文件路径',
|
||||
type: 'text',
|
||||
placeholder: '例如: ~/.aws/sso/cache/kiro-auth-token.json'
|
||||
},
|
||||
{
|
||||
id: 'KIRO_BASE_URL',
|
||||
label: 'Base URL <span class="optional-tag">(选填)</span>',
|
||||
type: 'text',
|
||||
placeholder: 'https://codewhisperer.{{region}}.amazonaws.com/generateAssistantResponse'
|
||||
},
|
||||
{
|
||||
id: 'KIRO_REFRESH_URL',
|
||||
label: 'Refresh URL <span class="optional-tag">(选填)</span>',
|
||||
type: 'text',
|
||||
placeholder: 'https://prod.{{region}}.auth.desktop.kiro.dev/refreshToken'
|
||||
},
|
||||
{
|
||||
id: 'KIRO_REFRESH_IDC_URL',
|
||||
label: 'Refresh IDC URL <span class="optional-tag">(选填)</span>',
|
||||
type: 'text',
|
||||
placeholder: 'https://oidc.{{region}}.amazonaws.com/token'
|
||||
}
|
||||
],
|
||||
'openai-qwen-oauth': [
|
||||
{
|
||||
id: 'QwenOauthCredsFilePath',
|
||||
id: 'QWEN_OAUTH_CREDS_FILE_PATH',
|
||||
label: 'OAuth凭据文件路径',
|
||||
type: 'text',
|
||||
placeholder: '例如: ~/.qwen/oauth_creds.json'
|
||||
},
|
||||
{
|
||||
id: 'QWEN_BASE_URL',
|
||||
label: 'Qwen Base URL <span class="optional-tag">(选填)</span>',
|
||||
type: 'text',
|
||||
placeholder: 'https://portal.qwen.ai/v1'
|
||||
},
|
||||
{
|
||||
id: 'QWEN_OAUTH_BASE_URL',
|
||||
label: 'OAuth Base URL <span class="optional-tag">(选填)</span>',
|
||||
type: 'text',
|
||||
placeholder: 'https://chat.qwen.ai'
|
||||
}
|
||||
],
|
||||
'gemini-antigravity': [
|
||||
{
|
||||
id: 'ProjectId',
|
||||
id: 'PROJECT_ID',
|
||||
label: '项目ID (选填)',
|
||||
type: 'text',
|
||||
placeholder: 'Google Cloud项目ID (留空自动发现)'
|
||||
},
|
||||
{
|
||||
id: 'AntigravityOauthCredsFilePath',
|
||||
id: 'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH',
|
||||
label: 'OAuth凭据文件路径',
|
||||
type: 'text',
|
||||
placeholder: '例如: ~/.antigravity/oauth_creds.json'
|
||||
},
|
||||
{
|
||||
id: 'ANTIGRAVITY_BASE_URL_DAILY',
|
||||
label: 'Daily Base URL <span class="optional-tag">(选填)</span>',
|
||||
type: 'text',
|
||||
placeholder: 'https://daily-cloudcode-pa.sandbox.googleapis.com'
|
||||
},
|
||||
{
|
||||
id: 'ANTIGRAVITY_BASE_URL_AUTOPUSH',
|
||||
label: 'Autopush Base URL <span class="optional-tag">(选填)</span>',
|
||||
type: 'text',
|
||||
placeholder: 'https://autopush-cloudcode-pa.sandbox.googleapis.com'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
|
|||
|
|
@ -496,7 +496,11 @@
|
|||
<!-- Gemini CLI OAuth 配置 -->
|
||||
<div class="provider-config" data-provider="gemini-cli-oauth">
|
||||
<div class="form-group">
|
||||
<label for="projectId">项目ID</label>
|
||||
<label for="geminiBaseUrl">Gemini Base URL <span class="optional-tag">(选填)</span></label>
|
||||
<input type="text" id="geminiBaseUrl" class="form-control" placeholder="https://cloudcode-pa.googleapis.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="projectId">项目ID <span class="optional-tag">(选填)</span></label>
|
||||
<input type="text" id="projectId" class="form-control" placeholder="Google Cloud项目ID">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
|
@ -529,6 +533,14 @@
|
|||
|
||||
<!-- Gemini Antigravity 配置 -->
|
||||
<div class="provider-config" data-provider="gemini-antigravity" style="display: none;">
|
||||
<div class="form-group">
|
||||
<label for="antigravityBaseUrlDaily">Daily Base URL <span class="optional-tag">(选填)</span></label>
|
||||
<input type="text" id="antigravityBaseUrlDaily" class="form-control" placeholder="https://daily-cloudcode-pa.sandbox.googleapis.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="antigravityBaseUrlAutopush">Autopush Base URL <span class="optional-tag">(选填)</span></label>
|
||||
<input type="text" id="antigravityBaseUrlAutopush" class="form-control" placeholder="https://autopush-cloudcode-pa.sandbox.googleapis.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="antigravityOauthCredsFilePath">OAuth凭据文件路径</label>
|
||||
<div class="file-input-group">
|
||||
|
|
@ -577,6 +589,20 @@
|
|||
|
||||
<!-- Claude Kiro OAuth 配置 -->
|
||||
<div class="provider-config" data-provider="claude-kiro-oauth" style="display: none;">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="kiroBaseUrl">Base URL <span class="optional-tag">(选填)</span></label>
|
||||
<input type="text" id="kiroBaseUrl" class="form-control" placeholder="https://codewhisperer.{{region}}.amazonaws.com/generateAssistantResponse">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="kiroRefreshUrl">Refresh URL <span class="optional-tag">(选填)</span></label>
|
||||
<input type="text" id="kiroRefreshUrl" class="form-control" placeholder="https://prod.{{region}}.auth.desktop.kiro.dev/refreshToken">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="kiroRefreshIdcUrl">Refresh IDC URL <span class="optional-tag">(选填)</span></label>
|
||||
<input type="text" id="kiroRefreshIdcUrl" class="form-control" placeholder="https://oidc.{{region}}.amazonaws.com/token">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>OAuth凭据</label>
|
||||
<div class="radio-group">
|
||||
|
|
@ -611,6 +637,14 @@
|
|||
|
||||
<!-- Qwen OAuth 配置 -->
|
||||
<div class="provider-config" data-provider="openai-qwen-oauth" style="display: none;">
|
||||
<div class="form-group">
|
||||
<label for="qwenBaseUrl">Qwen Base URL <span class="optional-tag">(选填)</span></label>
|
||||
<input type="text" id="qwenBaseUrl" class="form-control" placeholder="https://portal.qwen.ai/v1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="qwenOauthBaseUrl">OAuth Base URL <span class="optional-tag">(选填)</span></label>
|
||||
<input type="text" id="qwenOauthBaseUrl" class="form-control" placeholder="https://chat.qwen.ai">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="qwenOauthCredsFilePath">OAuth凭据文件路径</label>
|
||||
<div class="file-input-group">
|
||||
|
|
|
|||
Loading…
Reference in a new issue