feat: 添加OAuth授权凭据自动生成功能并优化UI
- 实现OAuth授权成功后自动生成凭据文件并填充路径 - 添加"生成凭据"按钮到各提供商配置表单 - 优化文件上传组件的样式和布局 - 将autoLinkProviderConfigs函数导出供服务初始化使用 - 新增oauth_success事件处理逻辑 - 调整授权模态框位置避免遮挡
This commit is contained in:
parent
e943819539
commit
e87d74f517
11 changed files with 217 additions and 66 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import * as http from 'http';
|
||||
import { initializeConfig, CONFIG, logProviderSpecificDetails } from './config-manager.js';
|
||||
import { initApiService } from './service-manager.js';
|
||||
import { initApiService, autoLinkProviderConfigs } from './service-manager.js';
|
||||
import { initializeUIManagement } from './ui-manager.js';
|
||||
import { initializeAPIManagement } from './api-manager.js';
|
||||
import { createRequestHandler } from './request-handler.js';
|
||||
|
|
@ -119,6 +119,10 @@ async function startServer() {
|
|||
// Initialize configuration
|
||||
await initializeConfig();
|
||||
|
||||
// 自动关联 configs 目录中的配置文件到对应的提供商
|
||||
console.log('[Initialization] Checking for unlinked provider configs...');
|
||||
await autoLinkProviderConfigs(CONFIG);
|
||||
|
||||
// Initialize API services
|
||||
const services = await initApiService(CONFIG);
|
||||
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ async function closeActiveServer(port) {
|
|||
* @param {string} provider - 提供商标识
|
||||
* @returns {Promise<http.Server>} HTTP 服务器实例
|
||||
*/
|
||||
async function createOAuthCallbackServer(config, redirectUri, authClient, credPath, provider) {
|
||||
async function createOAuthCallbackServer(config, redirectUri, authClient, credPath, provider, options = {}) {
|
||||
// 先关闭该端口上的旧服务器
|
||||
await closeActiveServer(config.port);
|
||||
|
||||
|
|
@ -123,14 +123,29 @@ async function createOAuthCallbackServer(config, redirectUri, authClient, credPa
|
|||
|
||||
try {
|
||||
const { tokens } = await authClient.getToken(code);
|
||||
await fs.promises.mkdir(path.dirname(credPath), { recursive: true });
|
||||
await fs.promises.writeFile(credPath, JSON.stringify(tokens, null, 2));
|
||||
console.log(`${config.logPrefix} 新令牌已接收并保存到文件`);
|
||||
let finalCredPath = credPath;
|
||||
|
||||
// 如果指定了保存到 configs 目录
|
||||
if (options.saveToConfigs) {
|
||||
const providerDir = options.providerDir;
|
||||
const targetDir = path.join(process.cwd(), 'configs', providerDir);
|
||||
await fs.promises.mkdir(targetDir, { recursive: true });
|
||||
const timestamp = Date.now();
|
||||
const filename = `${timestamp}_oauth_creds.json`;
|
||||
finalCredPath = path.join(targetDir, filename);
|
||||
}
|
||||
|
||||
await fs.promises.mkdir(path.dirname(finalCredPath), { recursive: true });
|
||||
await fs.promises.writeFile(finalCredPath, JSON.stringify(tokens, null, 2));
|
||||
console.log(`${config.logPrefix} 新令牌已接收并保存到文件: ${finalCredPath}`);
|
||||
|
||||
const relativePath = path.relative(process.cwd(), finalCredPath);
|
||||
|
||||
// 广播授权成功事件
|
||||
broadcastEvent('oauth_success', {
|
||||
provider: provider,
|
||||
credPath: credPath,
|
||||
credPath: finalCredPath,
|
||||
relativePath: relativePath,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
|
|
@ -195,9 +210,10 @@ async function createOAuthCallbackServer(config, redirectUri, authClient, credPa
|
|||
* 处理 Google OAuth 授权(通用函数)
|
||||
* @param {string} providerKey - 提供商键名
|
||||
* @param {Object} currentConfig - 当前配置对象
|
||||
* @param {Object} options - 额外选项
|
||||
* @returns {Promise<Object>} 返回授权URL和相关信息
|
||||
*/
|
||||
async function handleGoogleOAuth(providerKey, currentConfig) {
|
||||
async function handleGoogleOAuth(providerKey, currentConfig, options = {}) {
|
||||
const config = OAUTH_PROVIDERS[providerKey];
|
||||
if (!config) {
|
||||
throw new Error(`未知的提供商: ${providerKey}`);
|
||||
|
|
@ -211,6 +227,7 @@ async function handleGoogleOAuth(providerKey, currentConfig) {
|
|||
|
||||
const authUrl = authClient.generateAuthUrl({
|
||||
access_type: 'offline',
|
||||
prompt: 'select_account',
|
||||
scope: config.scope
|
||||
});
|
||||
|
||||
|
|
@ -218,7 +235,7 @@ async function handleGoogleOAuth(providerKey, currentConfig) {
|
|||
const credPath = path.join(os.homedir(), config.credentialsDir, config.credentialsFile);
|
||||
|
||||
try {
|
||||
await createOAuthCallbackServer(config, redirectUri, authClient, credPath, providerKey);
|
||||
await createOAuthCallbackServer(config, redirectUri, authClient, credPath, providerKey, options);
|
||||
} catch (error) {
|
||||
throw new Error(`启动回调服务器失败: ${error.message}`);
|
||||
}
|
||||
|
|
@ -237,19 +254,21 @@ async function handleGoogleOAuth(providerKey, currentConfig) {
|
|||
/**
|
||||
* 处理 Gemini CLI OAuth 授权
|
||||
* @param {Object} currentConfig - 当前配置对象
|
||||
* @param {Object} options - 额外选项
|
||||
* @returns {Promise<Object>} 返回授权URL和相关信息
|
||||
*/
|
||||
export async function handleGeminiCliOAuth(currentConfig) {
|
||||
return handleGoogleOAuth('gemini-cli-oauth', currentConfig);
|
||||
export async function handleGeminiCliOAuth(currentConfig, options = {}) {
|
||||
return handleGoogleOAuth('gemini-cli-oauth', currentConfig, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 Gemini Antigravity OAuth 授权
|
||||
* @param {Object} currentConfig - 当前配置对象
|
||||
* @param {Object} options - 额外选项
|
||||
* @returns {Promise<Object>} 返回授权URL和相关信息
|
||||
*/
|
||||
export async function handleGeminiAntigravityOAuth(currentConfig) {
|
||||
return handleGoogleOAuth('gemini-antigravity', currentConfig);
|
||||
export async function handleGeminiAntigravityOAuth(currentConfig, options = {}) {
|
||||
return handleGoogleOAuth('gemini-antigravity', currentConfig, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -291,10 +310,11 @@ function stopPollingTask(taskId) {
|
|||
* @param {number} interval - 轮询间隔(秒)
|
||||
* @param {number} expiresIn - 过期时间(秒)
|
||||
* @param {string} taskId - 任务标识符
|
||||
* @param {Object} options - 额外选项
|
||||
* @returns {Promise<Object>} 返回令牌信息
|
||||
*/
|
||||
async function pollQwenToken(deviceCode, codeVerifier, interval = 5, expiresIn = 300, taskId = 'default') {
|
||||
const credPath = path.join(os.homedir(), QWEN_OAUTH_CONFIG.credentialsDir, QWEN_OAUTH_CONFIG.credentialsFile);
|
||||
async function pollQwenToken(deviceCode, codeVerifier, interval = 5, expiresIn = 300, taskId = 'default', options = {}) {
|
||||
let credPath = path.join(os.homedir(), QWEN_OAUTH_CONFIG.credentialsDir, QWEN_OAUTH_CONFIG.credentialsFile);
|
||||
const maxAttempts = Math.floor(expiresIn / interval);
|
||||
let attempts = 0;
|
||||
|
||||
|
|
@ -345,11 +365,22 @@ async function pollQwenToken(deviceCode, codeVerifier, interval = 5, expiresIn =
|
|||
// 成功获取令牌
|
||||
console.log(`${QWEN_OAUTH_CONFIG.logPrefix} 成功获取令牌 [${taskId}]`);
|
||||
|
||||
// 如果指定了保存到 configs 目录
|
||||
if (options.saveToConfigs) {
|
||||
const targetDir = path.join(process.cwd(), 'configs', options.providerDir);
|
||||
await fs.promises.mkdir(targetDir, { recursive: true });
|
||||
const timestamp = Date.now();
|
||||
const filename = `${timestamp}_oauth_creds.json`;
|
||||
credPath = path.join(targetDir, filename);
|
||||
}
|
||||
|
||||
// 保存令牌到文件
|
||||
await fs.promises.mkdir(path.dirname(credPath), { recursive: true });
|
||||
await fs.promises.writeFile(credPath, JSON.stringify(data, null, 2));
|
||||
console.log(`${QWEN_OAUTH_CONFIG.logPrefix} 令牌已保存到 ${credPath}`);
|
||||
|
||||
const relativePath = path.relative(process.cwd(), credPath);
|
||||
|
||||
// 清理任务
|
||||
activePollingTasks.delete(taskId);
|
||||
|
||||
|
|
@ -357,6 +388,7 @@ async function pollQwenToken(deviceCode, codeVerifier, interval = 5, expiresIn =
|
|||
broadcastEvent('oauth_success', {
|
||||
provider: 'openai-qwen-oauth',
|
||||
credPath: credPath,
|
||||
relativePath: relativePath,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
|
|
@ -401,9 +433,10 @@ async function pollQwenToken(deviceCode, codeVerifier, interval = 5, expiresIn =
|
|||
/**
|
||||
* 处理 Qwen OAuth 授权(设备授权流程)
|
||||
* @param {Object} currentConfig - 当前配置对象
|
||||
* @param {Object} options - 额外选项
|
||||
* @returns {Promise<Object>} 返回授权URL和相关信息
|
||||
*/
|
||||
export async function handleQwenOAuth(currentConfig) {
|
||||
export async function handleQwenOAuth(currentConfig, options = {}) {
|
||||
const codeVerifier = generateCodeVerifier();
|
||||
const codeChallenge = generateCodeChallenge(codeVerifier);
|
||||
|
||||
|
|
@ -454,7 +487,7 @@ export async function handleQwenOAuth(currentConfig) {
|
|||
}
|
||||
|
||||
// 不等待轮询完成,立即返回授权信息
|
||||
pollQwenToken(deviceAuth.device_code, codeVerifier, interval, expiresIn, taskId)
|
||||
pollQwenToken(deviceAuth.device_code, codeVerifier, interval, expiresIn, taskId, options)
|
||||
.catch(error => {
|
||||
console.error(`${QWEN_OAUTH_CONFIG.logPrefix} 轮询失败 [${taskId}]:`, error);
|
||||
// 广播授权失败事件
|
||||
|
|
|
|||
|
|
@ -296,7 +296,7 @@ export function createProviderConfig(options) {
|
|||
[credPathKey]: credPath,
|
||||
uuid: generateUUID(),
|
||||
checkModelName: defaultCheckModel,
|
||||
checkHealth: true,
|
||||
checkHealth: false,
|
||||
isHealthy: true,
|
||||
isDisabled: false,
|
||||
lastUsed: null,
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ let providerPoolManager = null;
|
|||
* @param {Object} config - 服务器配置对象
|
||||
* @returns {Promise<Object>} 更新后的 providerPools 对象
|
||||
*/
|
||||
async function autoLinkProviderConfigs(config) {
|
||||
export async function autoLinkProviderConfigs(config) {
|
||||
// 确保 providerPools 对象存在
|
||||
if (!config.providerPools) {
|
||||
config.providerPools = {};
|
||||
|
|
@ -159,9 +159,6 @@ async function scanProviderDirectory(dirPath, linkedPaths, newProviders, options
|
|||
* @returns {Promise<Object>} The initialized services
|
||||
*/
|
||||
export async function initApiService(config) {
|
||||
// 自动关联 configs 目录中的配置文件到对应的提供商
|
||||
console.log('[Initialization] Checking for unlinked provider configs...');
|
||||
await autoLinkProviderConfigs(config);
|
||||
|
||||
if (config.providerPools && Object.keys(config.providerPools).length > 0) {
|
||||
providerPoolManager = new ProviderPoolManager(config.providerPools, {
|
||||
|
|
|
|||
|
|
@ -1350,17 +1350,25 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo
|
|||
let authUrl = '';
|
||||
let authInfo = {};
|
||||
|
||||
// 解析 options
|
||||
let options = {};
|
||||
try {
|
||||
options = await getRequestBody(req);
|
||||
} catch (e) {
|
||||
// 如果没有请求体,使用默认空对象
|
||||
}
|
||||
|
||||
// 根据提供商类型生成授权链接并启动回调服务器
|
||||
if (providerType === 'gemini-cli-oauth') {
|
||||
const result = await handleGeminiCliOAuth(currentConfig);
|
||||
const result = await handleGeminiCliOAuth(currentConfig, options);
|
||||
authUrl = result.authUrl;
|
||||
authInfo = result.authInfo;
|
||||
} else if (providerType === 'gemini-antigravity') {
|
||||
const result = await handleGeminiAntigravityOAuth(currentConfig);
|
||||
const result = await handleGeminiAntigravityOAuth(currentConfig, options);
|
||||
authUrl = result.authUrl;
|
||||
authInfo = result.authInfo;
|
||||
} else if (providerType === 'openai-qwen-oauth') {
|
||||
const result = await handleQwenOAuth(currentConfig);
|
||||
const result = await handleQwenOAuth(currentConfig, options);
|
||||
authUrl = result.authUrl;
|
||||
authInfo = result.authInfo;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@ import {
|
|||
loadSystemInfo,
|
||||
updateTimeDisplay,
|
||||
loadProviders,
|
||||
openProviderManager
|
||||
openProviderManager,
|
||||
showAuthModal
|
||||
} from './provider-manager.js';
|
||||
|
||||
import {
|
||||
|
|
@ -141,6 +142,7 @@ window.openProviderManager = openProviderManager;
|
|||
window.showProviderManagerModal = showProviderManagerModal;
|
||||
window.refreshProviderConfig = refreshProviderConfig;
|
||||
window.fileUploadHandler = fileUploadHandler;
|
||||
window.showAuthModal = showAuthModal;
|
||||
|
||||
// 上传配置管理相关全局函数
|
||||
window.viewConfig = viewConfig;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { elements, autoScroll, setAutoScroll, clearLogs } from './constants.js';
|
||||
import { showToast } from './utils.js';
|
||||
import { fileUploadHandler } from './file-upload.js';
|
||||
|
||||
/**
|
||||
* 初始化所有事件监听器
|
||||
|
|
@ -66,6 +67,11 @@ function initEventListeners() {
|
|||
button.addEventListener('click', handlePasswordToggle);
|
||||
});
|
||||
|
||||
// 生成凭据按钮监听
|
||||
document.querySelectorAll('.generate-creds-btn').forEach(button => {
|
||||
button.addEventListener('click', handleGenerateCreds);
|
||||
});
|
||||
|
||||
// 提供商池配置监听
|
||||
// const providerPoolsInput = document.getElementById('providerPoolsFilePath');
|
||||
// if (providerPoolsInput) {
|
||||
|
|
@ -171,6 +177,65 @@ function handlePasswordToggle(event) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理生成凭据逻辑
|
||||
* @param {Event} event - 事件对象
|
||||
*/
|
||||
async function handleGenerateCreds(event) {
|
||||
const button = event.target.closest('.generate-creds-btn');
|
||||
if (!button) return;
|
||||
|
||||
const providerType = button.getAttribute('data-provider');
|
||||
const targetInputId = button.getAttribute('data-target');
|
||||
|
||||
try {
|
||||
showToast('正在初始化凭据生成...', 'info');
|
||||
|
||||
// 使用 fileUploadHandler 中的 getProviderKey 获取目录名称
|
||||
const providerDir = fileUploadHandler.getProviderKey(providerType);
|
||||
|
||||
const response = await window.apiClient.post(
|
||||
`/providers/${encodeURIComponent(providerType)}/generate-auth-url`,
|
||||
{
|
||||
saveToConfigs: true,
|
||||
providerDir: providerDir
|
||||
}
|
||||
);
|
||||
|
||||
if (response.success && response.authUrl) {
|
||||
// 使用自定义事件监听授权成功,以便自动填充路径
|
||||
const handleSuccess = (e) => {
|
||||
const data = e.detail;
|
||||
if (data.provider === providerType && data.relativePath) {
|
||||
const input = document.getElementById(targetInputId);
|
||||
if (input) {
|
||||
input.value = data.relativePath;
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
showToast('凭据已生成并自动填充路径', 'success');
|
||||
}
|
||||
window.removeEventListener('oauth_success_event', handleSuccess);
|
||||
}
|
||||
};
|
||||
window.addEventListener('oauth_success_event', handleSuccess);
|
||||
|
||||
// 调用 provider-manager.js 中的 showAuthModal (假设已在全局作用域或通过某种方式可用)
|
||||
// 如果不可用,我们需要在 app.js 中导出它
|
||||
if (window.showAuthModal) {
|
||||
window.showAuthModal(response.authUrl, response.authInfo);
|
||||
} else {
|
||||
// 降级处理:如果在 app.js 中没导出,尝试直接打开
|
||||
window.open(response.authUrl, '_blank');
|
||||
showToast('请在打开的窗口中完成授权', 'info');
|
||||
}
|
||||
} else {
|
||||
showToast('初始化凭据生成失败', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('生成凭据失败:', error);
|
||||
showToast(`生成凭据失败: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提供商池配置变化处理
|
||||
* @param {Event} event - 事件对象
|
||||
|
|
|
|||
|
|
@ -33,6 +33,22 @@ function initEventStream() {
|
|||
updateProviderStatus(data);
|
||||
});
|
||||
|
||||
newEventSource.addEventListener('oauth_success', (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
showToast(`授权成功 (${data.provider})`, 'success');
|
||||
// 发送自定义事件,以便其他模块(如生成凭据逻辑)可以接收到详细信息
|
||||
window.dispatchEvent(new CustomEvent('oauth_success_event', { detail: data }));
|
||||
|
||||
// 关闭授权窗口和模态框
|
||||
// 查找并关闭所有授权相关的模态框
|
||||
const modals = document.querySelectorAll('.modal-overlay');
|
||||
modals.forEach(modal => modal.remove());
|
||||
|
||||
// 授权成功后刷新配置和提供商列表
|
||||
if (loadProviders) loadProviders();
|
||||
if (loadConfigList) loadConfigList();
|
||||
});
|
||||
|
||||
newEventSource.addEventListener('provider_update', (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
handleProviderUpdate(data);
|
||||
|
|
@ -127,7 +143,7 @@ function handleProviderUpdate(data) {
|
|||
}
|
||||
|
||||
// 导入工具函数
|
||||
import { escapeHtml } from './utils.js';
|
||||
import { escapeHtml, showToast } from './utils.js';
|
||||
|
||||
// 需要从其他模块导入的函数
|
||||
let loadProviders;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
// 提供商管理功能模块
|
||||
|
||||
import { providerStats, updateProviderStats } from './constants.js';
|
||||
import { showToast } from './utils.js';
|
||||
import { showToast, formatUptime } from './utils.js';
|
||||
import { fileUploadHandler } from './file-upload.js';
|
||||
|
||||
// 保存初始服务器时间和运行时间
|
||||
let initialServerTime = null;
|
||||
|
|
@ -342,9 +343,15 @@ async function handleGenerateAuthUrl(providerType) {
|
|||
try {
|
||||
showToast('正在生成授权链接...', 'info');
|
||||
|
||||
// 使用 fileUploadHandler 中的 getProviderKey 获取目录名称
|
||||
const providerDir = fileUploadHandler.getProviderKey(providerType);
|
||||
|
||||
const response = await window.apiClient.post(
|
||||
`/providers/${encodeURIComponent(providerType)}/generate-auth-url`,
|
||||
{}
|
||||
{
|
||||
saveToConfigs: true,
|
||||
providerDir: providerDir
|
||||
}
|
||||
);
|
||||
|
||||
if (response.success && response.authUrl) {
|
||||
|
|
@ -394,7 +401,6 @@ function showAuthModal(authUrl, authInfo) {
|
|||
<h4>授权步骤:</h4>
|
||||
<ol>
|
||||
<li>点击下方按钮在浏览器中打开授权页面</li>
|
||||
<li>在授权页面输入用户码: <strong>${authInfo.userCode}</strong></li>
|
||||
<li>完成授权后,系统会自动获取访问令牌</li>
|
||||
<li>授权有效期: ${Math.floor(authInfo.expiresIn / 60)} 分钟</li>
|
||||
</ol>
|
||||
|
|
@ -491,7 +497,7 @@ function showAuthModal(authUrl, authInfo) {
|
|||
// 使用子窗口打开,以便监听 URL 变化
|
||||
const width = 600;
|
||||
const height = 700;
|
||||
const left = (window.screen.width - width) / 2;
|
||||
const left = (window.screen.width - width) / 2 + 600;
|
||||
const top = (window.screen.height - height) / 2;
|
||||
|
||||
const authWindow = window.open(
|
||||
|
|
@ -500,6 +506,16 @@ function showAuthModal(authUrl, authInfo) {
|
|||
`width=${width},height=${height},left=${left},top=${top},status=no,resizable=yes,scrollbars=yes`
|
||||
);
|
||||
|
||||
// 监听 OAuth 成功事件,自动关闭窗口和模态框
|
||||
const handleOAuthSuccess = () => {
|
||||
if (authWindow && !authWindow.closed) {
|
||||
authWindow.close();
|
||||
}
|
||||
modal.remove();
|
||||
window.removeEventListener('oauth_success_event', handleOAuthSuccess);
|
||||
};
|
||||
window.addEventListener('oauth_success_event', handleOAuthSuccess);
|
||||
|
||||
if (authWindow) {
|
||||
showToast('已打开授权窗口,请在窗口中完成操作', 'info');
|
||||
|
||||
|
|
@ -547,13 +563,6 @@ function showAuthModal(authUrl, authInfo) {
|
|||
img.src = localUrl.href;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
showToast('授权完成!', 'success');
|
||||
if (authWindow && !authWindow.closed) authWindow.close();
|
||||
modal.remove();
|
||||
// 刷新列表以显示新状态
|
||||
loadProviders();
|
||||
}, 2500);
|
||||
} else {
|
||||
showToast('该 URL 似乎不包含有效的授权代码', 'warning');
|
||||
}
|
||||
|
|
@ -596,14 +605,12 @@ function showAuthModal(authUrl, authInfo) {
|
|||
});
|
||||
}
|
||||
|
||||
// 导入工具函数
|
||||
import { formatUptime } from './utils.js';
|
||||
|
||||
export {
|
||||
loadSystemInfo,
|
||||
updateTimeDisplay,
|
||||
loadProviders,
|
||||
renderProviders,
|
||||
updateProviderStatsDisplay,
|
||||
openProviderManager
|
||||
openProviderManager,
|
||||
showAuthModal
|
||||
};
|
||||
|
|
@ -418,33 +418,38 @@ textarea.form-control {
|
|||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.file-input-group .form-control {
|
||||
flex: 1;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
padding-right: 3rem;
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-left: none;
|
||||
padding: 0.75rem 1rem;
|
||||
.file-input-group .btn-outline {
|
||||
height: 38px;
|
||||
width: 38px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
flex-shrink: 0;
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-left: none;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
white-space: nowrap;
|
||||
z-index: 1;
|
||||
background: bottom;
|
||||
}
|
||||
|
||||
.file-input-group .btn-outline:hover {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* 兼容旧的 class 名 */
|
||||
.upload-btn {
|
||||
/* 移除之前的 position: absolute 等样式,改为 inline-flex 配合父级 flex 布局 */
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.upload-btn:hover {
|
||||
|
|
@ -1616,14 +1621,12 @@ input:checked + .toggle-slider:before {
|
|||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.config-item .file-input-group input {
|
||||
flex: 1;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
padding-right: 3rem;
|
||||
padding-right: 0.75rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -527,6 +527,9 @@
|
|||
<button type="button" class="btn btn-outline upload-btn" data-target="geminiOauthCredsFilePath" aria-label="上传文件">
|
||||
<i class="fas fa-upload"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline generate-creds-btn" data-target="geminiOauthCredsFilePath" data-provider="gemini-cli-oauth" title="生成凭据文件">
|
||||
<i class="fas fa-magic"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -548,6 +551,9 @@
|
|||
<button type="button" class="btn btn-outline upload-btn" data-target="antigravityOauthCredsFilePath" aria-label="上传文件">
|
||||
<i class="fas fa-upload"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline generate-creds-btn" data-target="antigravityOauthCredsFilePath" data-provider="gemini-antigravity" title="生成凭据文件">
|
||||
<i class="fas fa-magic"></i>
|
||||
</button>
|
||||
</div>
|
||||
<small class="form-text">Antigravity 使用 Google OAuth 认证,需要提供凭据文件路径</small>
|
||||
</div>
|
||||
|
|
@ -652,6 +658,9 @@
|
|||
<button type="button" class="btn btn-outline upload-btn" data-target="qwenOauthCredsFilePath" aria-label="上传文件">
|
||||
<i class="fas fa-upload"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline generate-creds-btn" data-target="qwenOauthCredsFilePath" data-provider="openai-qwen-oauth" title="生成凭据文件">
|
||||
<i class="fas fa-magic"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -929,8 +938,11 @@
|
|||
<div id="toastContainer" class="toast-container"></div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="app/auth.js"></script>
|
||||
<script>
|
||||
<script type="module" src="app/auth.js"></script>
|
||||
<script type="module">
|
||||
// 导入认证函数
|
||||
import { initAuth, logout } from './app/auth.js';
|
||||
|
||||
// 页面加载时检查登录状态
|
||||
(async function() {
|
||||
const isAuthenticated = await initAuth();
|
||||
|
|
@ -983,6 +995,10 @@
|
|||
element.textContent = updatedCommand;
|
||||
});
|
||||
}
|
||||
|
||||
// 导出到 window 供其他脚本使用
|
||||
window.initAuth = initAuth;
|
||||
window.logout = logout;
|
||||
</script>
|
||||
<script type="module" src="app/app.js"></script>
|
||||
</body>
|
||||
|
|
|
|||
Loading…
Reference in a new issue