feat(认证流程): 添加浏览器自动打开功能并改进授权流程
添加自动打开浏览器功能到Gemini和Antigravity认证流程,当自动打开失败时显示备用提示信息 在提供商管理界面添加重置健康状态功能 调整CSS布局增加元素间距
This commit is contained in:
parent
cb76d85ecd
commit
7420c88a6f
5 changed files with 159 additions and 7 deletions
|
|
@ -5,6 +5,7 @@ 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 { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import open from 'open';
|
||||||
import { API_ACTIONS, formatExpiryTime } from '../common.js';
|
import { API_ACTIONS, formatExpiryTime } from '../common.js';
|
||||||
import { getProviderModels } from '../provider-models.js';
|
import { getProviderModels } from '../provider-models.js';
|
||||||
|
|
||||||
|
|
@ -293,14 +294,32 @@ export class AntigravityApiService {
|
||||||
const redirectUri = `http://${host}:${AUTH_REDIRECT_PORT}`;
|
const redirectUri = `http://${host}:${AUTH_REDIRECT_PORT}`;
|
||||||
this.authClient.redirectUri = redirectUri;
|
this.authClient.redirectUri = redirectUri;
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
const authUrl = this.authClient.generateAuthUrl({
|
const authUrl = this.authClient.generateAuthUrl({
|
||||||
access_type: 'offline',
|
access_type: 'offline',
|
||||||
scope: ['https://www.googleapis.com/auth/cloud-platform']
|
scope: ['https://www.googleapis.com/auth/cloud-platform']
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('\n[Antigravity Auth] Please open this URL in your browser to authenticate:');
|
console.log('\n[Antigravity Auth] 正在自动打开浏览器进行授权...');
|
||||||
console.log(authUrl, '\n');
|
console.log('[Antigravity Auth] 授权链接:', authUrl, '\n');
|
||||||
|
|
||||||
|
// 自动打开浏览器
|
||||||
|
const showFallbackMessage = () => {
|
||||||
|
console.log('[Antigravity Auth] 无法自动打开浏览器,请手动复制上面的链接到浏览器中打开');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.config) {
|
||||||
|
try {
|
||||||
|
const childProcess = await open(authUrl);
|
||||||
|
if (childProcess) {
|
||||||
|
childProcess.on('error', () => showFallbackMessage());
|
||||||
|
}
|
||||||
|
} catch (_err) {
|
||||||
|
showFallbackMessage();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showFallbackMessage();
|
||||||
|
}
|
||||||
|
|
||||||
const server = http.createServer(async (req, res) => {
|
const server = http.createServer(async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ 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 * as readline from 'readline';
|
import * as readline from 'readline';
|
||||||
|
import open from 'open';
|
||||||
import { API_ACTIONS, formatExpiryTime } from '../common.js';
|
import { API_ACTIONS, formatExpiryTime } from '../common.js';
|
||||||
import { getProviderModels } from '../provider-models.js';
|
import { getProviderModels } from '../provider-models.js';
|
||||||
|
|
||||||
|
|
@ -257,10 +258,28 @@ export class GeminiApiService {
|
||||||
}
|
}
|
||||||
const redirectUri = `http://${host}:${AUTH_REDIRECT_PORT}`;
|
const redirectUri = `http://${host}:${AUTH_REDIRECT_PORT}`;
|
||||||
this.authClient.redirectUri = redirectUri;
|
this.authClient.redirectUri = redirectUri;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
const authUrl = this.authClient.generateAuthUrl({ access_type: 'offline', scope: ['https://www.googleapis.com/auth/cloud-platform'] });
|
const authUrl = this.authClient.generateAuthUrl({ access_type: 'offline', scope: ['https://www.googleapis.com/auth/cloud-platform'] });
|
||||||
console.log('\n[Gemini Auth] Please open this URL in your browser to authenticate:');
|
console.log('\n[Gemini Auth] 正在自动打开浏览器进行授权...');
|
||||||
console.log(authUrl, '\n');
|
|
||||||
|
// 自动打开浏览器
|
||||||
|
const showFallbackMessage = () => {
|
||||||
|
console.log('[Gemini Auth] 无法自动打开浏览器,请手动复制上面的链接到浏览器中打开');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.config) {
|
||||||
|
try {
|
||||||
|
const childProcess = await open(authUrl);
|
||||||
|
if (childProcess) {
|
||||||
|
childProcess.on('error', () => showFallbackMessage());
|
||||||
|
}
|
||||||
|
} catch (_err) {
|
||||||
|
showFallbackMessage();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showFallbackMessage();
|
||||||
|
}
|
||||||
|
console.log('[Gemini Auth] 授权链接:', authUrl, '\n');
|
||||||
const server = http.createServer(async (req, res) => {
|
const server = http.createServer(async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const url = new URL(req.url, redirectUri);
|
const url = new URL(req.url, redirectUri);
|
||||||
|
|
|
||||||
|
|
@ -1017,6 +1017,80 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset all providers health status for a specific provider type
|
||||||
|
const resetHealthMatch = pathParam.match(/^\/api\/providers\/([^\/]+)\/reset-health$/);
|
||||||
|
if (method === 'POST' && resetHealthMatch) {
|
||||||
|
const providerType = decodeURIComponent(resetHealthMatch[1]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const filePath = currentConfig.PROVIDER_POOLS_FILE_PATH || 'provider_pools.json';
|
||||||
|
let providerPools = {};
|
||||||
|
|
||||||
|
// Load existing pools
|
||||||
|
if (existsSync(filePath)) {
|
||||||
|
try {
|
||||||
|
const fileContent = readFileSync(filePath, 'utf8');
|
||||||
|
providerPools = JSON.parse(fileContent);
|
||||||
|
} catch (readError) {
|
||||||
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: { message: 'Provider pools file not found' } }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset health status for all providers of this type
|
||||||
|
const providers = providerPools[providerType] || [];
|
||||||
|
|
||||||
|
if (providers.length === 0) {
|
||||||
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: { message: 'No providers found for this type' } }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let resetCount = 0;
|
||||||
|
providers.forEach(provider => {
|
||||||
|
if (!provider.isHealthy) {
|
||||||
|
provider.isHealthy = true;
|
||||||
|
provider.errorCount = 0;
|
||||||
|
provider.lastErrorTime = null;
|
||||||
|
resetCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save to file
|
||||||
|
writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf8');
|
||||||
|
console.log(`[UI API] Reset health status for ${resetCount} providers in ${providerType}`);
|
||||||
|
|
||||||
|
// Update provider pool manager if available
|
||||||
|
if (providerPoolManager) {
|
||||||
|
providerPoolManager.providerPools = providerPools;
|
||||||
|
providerPoolManager.initializeProviderStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 广播更新事件
|
||||||
|
broadcastEvent('config_update', {
|
||||||
|
action: 'reset_health',
|
||||||
|
filePath: filePath,
|
||||||
|
providerType,
|
||||||
|
resetCount,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: `成功重置 ${resetCount} 个节点的健康状态`,
|
||||||
|
resetCount,
|
||||||
|
totalCount: providers.length
|
||||||
|
}));
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: { message: error.message } }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Generate OAuth authorization URL for providers
|
// Generate OAuth authorization URL for providers
|
||||||
const generateAuthUrlMatch = pathParam.match(/^\/api\/providers\/([^\/]+)\/generate-auth-url$/);
|
const generateAuthUrlMatch = pathParam.match(/^\/api\/providers\/([^\/]+)\/generate-auth-url$/);
|
||||||
if (method === 'POST' && generateAuthUrlMatch) {
|
if (method === 'POST' && generateAuthUrlMatch) {
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,9 @@ function showProviderManagerModal(data) {
|
||||||
<button class="btn btn-success" onclick="window.showAddProviderForm('${providerType}')">
|
<button class="btn btn-success" onclick="window.showAddProviderForm('${providerType}')">
|
||||||
<i class="fas fa-plus"></i> 添加新提供商
|
<i class="fas fa-plus"></i> 添加新提供商
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-warning" onclick="window.resetAllProvidersHealth('${providerType}')" title="将所有节点的健康状态重置为健康">
|
||||||
|
<i class="fas fa-heartbeat"></i> 重置为健康
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -996,6 +999,40 @@ async function toggleProviderStatus(uuid, event) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置所有提供商的健康状态
|
||||||
|
* @param {string} providerType - 提供商类型
|
||||||
|
*/
|
||||||
|
async function resetAllProvidersHealth(providerType) {
|
||||||
|
if (!confirm(`确定要将 ${providerType} 的所有节点重置为健康状态吗?\n\n这将清除所有节点的错误计数和错误时间。`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
showToast('正在重置健康状态...', 'info');
|
||||||
|
|
||||||
|
const response = await window.apiClient.post(
|
||||||
|
`/providers/${encodeURIComponent(providerType)}/reset-health`,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
showToast(`成功重置 ${response.resetCount} 个节点的健康状态`, 'success');
|
||||||
|
|
||||||
|
// 重新加载配置
|
||||||
|
await window.apiClient.post('/reload-config');
|
||||||
|
|
||||||
|
// 刷新提供商配置显示
|
||||||
|
await refreshProviderConfig(providerType);
|
||||||
|
} else {
|
||||||
|
showToast('重置健康状态失败', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('重置健康状态失败:', error);
|
||||||
|
showToast(`重置健康状态失败: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 渲染不支持的模型选择器(不调用API,直接使用传入的模型列表)
|
* 渲染不支持的模型选择器(不调用API,直接使用传入的模型列表)
|
||||||
* @param {string} uuid - 提供商UUID
|
* @param {string} uuid - 提供商UUID
|
||||||
|
|
@ -1045,6 +1082,7 @@ export {
|
||||||
showAddProviderForm,
|
showAddProviderForm,
|
||||||
addProvider,
|
addProvider,
|
||||||
toggleProviderStatus,
|
toggleProviderStatus,
|
||||||
|
resetAllProvidersHealth,
|
||||||
loadModelsForProviderType,
|
loadModelsForProviderType,
|
||||||
renderNotSupportedModelsSelector
|
renderNotSupportedModelsSelector
|
||||||
};
|
};
|
||||||
|
|
@ -1058,4 +1096,5 @@ window.saveProvider = saveProvider;
|
||||||
window.deleteProvider = deleteProvider;
|
window.deleteProvider = deleteProvider;
|
||||||
window.showAddProviderForm = showAddProviderForm;
|
window.showAddProviderForm = showAddProviderForm;
|
||||||
window.addProvider = addProvider;
|
window.addProvider = addProvider;
|
||||||
window.toggleProviderStatus = toggleProviderStatus;
|
window.toggleProviderStatus = toggleProviderStatus;
|
||||||
|
window.resetAllProvidersHealth = resetAllProvidersHealth;
|
||||||
|
|
@ -1357,6 +1357,7 @@ input:checked + .toggle-slider:before {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.provider-actions {
|
.provider-actions {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue