diff --git a/src/oauth-handlers.js b/src/oauth-handlers.js index 38c0495..484ac6e 100644 --- a/src/oauth-handlers.js +++ b/src/oauth-handlers.js @@ -6,6 +6,8 @@ import os from 'os'; import crypto from 'crypto'; import open from 'open'; import { broadcastEvent } from './ui-manager.js'; +import { autoLinkProviderConfigs } from './service-manager.js'; +import { CONFIG } from './config-manager.js'; /** * OAuth 提供商配置 @@ -197,6 +199,9 @@ async function createOAuthCallbackServer(config, redirectUri, authClient, credPa timestamp: new Date().toISOString() }); + // 自动关联新生成的凭据到 Pools + await autoLinkProviderConfigs(CONFIG); + res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(generateResponsePage(true, '您可以关闭此页面')); } catch (tokenError) { @@ -439,6 +444,9 @@ async function pollQwenToken(deviceCode, codeVerifier, interval = 5, expiresIn = timestamp: new Date().toISOString() }); + // 自动关联新生成的凭据到 Pools + await autoLinkProviderConfigs(CONFIG); + return data; } @@ -519,7 +527,7 @@ export async function handleQwenOAuth(currentConfig, options = {}) { } // 启动后台轮询获取令牌 - const interval = deviceAuth.interval || 5; + const interval = 5; // const expiresIn = deviceAuth.expires_in || 1800; const expiresIn = 300; @@ -630,6 +638,13 @@ async function handleKiroSocialAuth(provider, currentConfig, options = {}) { * Kiro Builder ID - Device Code Flow(类似 Qwen OAuth 模式) */ async function handleKiroBuilderIDDeviceCode(currentConfig, options = {}) { + // 停止之前的轮询任务 + for (const [existingTaskId] of activeKiroPollingTasks.entries()) { + if (existingTaskId.startsWith('kiro-')) { + stopKiroPollingTask(existingTaskId); + } + } + // 1. 注册 OIDC 客户端 const regResponse = await fetch(`${KIRO_OAUTH_CONFIG.ssoOIDCEndpoint}/client/register`, { method: 'POST', @@ -673,22 +688,16 @@ async function handleKiroBuilderIDDeviceCode(currentConfig, options = {}) { // 3. 启动后台轮询(类似 Qwen OAuth 的模式) const taskId = `kiro-${deviceAuth.deviceCode.substring(0, 8)}-${Date.now()}`; - - // 停止之前的轮询任务 - for (const [existingTaskId] of activeKiroPollingTasks.entries()) { - if (existingTaskId.startsWith('kiro-')) { - stopKiroPollingTask(existingTaskId); - } - } + // 异步轮询 pollKiroBuilderIDToken( - regData.clientId, - regData.clientSecret, - deviceAuth.deviceCode, - deviceAuth.interval || 5, - deviceAuth.expiresIn || 300, - taskId, + regData.clientId, + regData.clientSecret, + deviceAuth.deviceCode, + 5, + 300, + taskId, options ).catch(error => { console.error(`${KIRO_OAUTH_CONFIG.logPrefix} 轮询失败 [${taskId}]:`, error); @@ -761,10 +770,11 @@ async function pollKiroBuilderIDToken(clientId, clientSecret, deviceCode, interv // 保存令牌(符合现有规范) if (options.saveToConfigs) { - const targetDir = path.join(process.cwd(), 'configs', 'kiro'); - await fs.promises.mkdir(targetDir, { recursive: true }); const timestamp = Date.now(); - credPath = path.join(targetDir, `${timestamp}_oauth_creds.json`); + const folderName = `${timestamp}_kiro-auth-token`; + const targetDir = path.join(process.cwd(), 'configs', 'kiro', folderName); + await fs.promises.mkdir(targetDir, { recursive: true }); + credPath = path.join(targetDir, `${folderName}.json`); } const tokenData = { @@ -790,6 +800,9 @@ async function pollKiroBuilderIDToken(clientId, clientSecret, deviceCode, interv timestamp: new Date().toISOString() }); + // 自动关联新生成的凭据到 Pools + await autoLinkProviderConfigs(CONFIG); + return tokenData; } @@ -837,17 +850,17 @@ async function startKiroCallbackServer(codeVerifier, expectedState, options = {} const portEnd = KIRO_OAUTH_CONFIG.callbackPortEnd; for (let port = portStart; port <= portEnd; port++) { - // 关闭已存在的服务器 - await closeKiroServer(port); - - try { - const server = await createKiroHttpCallbackServer(port, codeVerifier, expectedState, options); - activeKiroServers.set(port, server); - console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 回调服务器已启动于端口 ${port}`); - return port; - } catch (err) { + // 关闭已存在的服务器 + await closeKiroServer(port); + + try { + const server = await createKiroHttpCallbackServer(port, codeVerifier, expectedState, options); + activeKiroServers.set(port, server); + console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 回调服务器已启动于端口 ${port}`); + return port; + } catch (err) { console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 端口 ${port} 被占用,尝试下一个...`); - } + } } throw new Error('所有端口都被占用'); @@ -925,10 +938,11 @@ function createKiroHttpCallbackServer(port, codeVerifier, expectedState, options let credPath = path.join(os.homedir(), KIRO_OAUTH_CONFIG.credentialsDir, KIRO_OAUTH_CONFIG.credentialsFile); if (options.saveToConfigs) { - const targetDir = path.join(process.cwd(), 'configs', 'kiro'); - await fs.promises.mkdir(targetDir, { recursive: true }); const timestamp = Date.now(); - credPath = path.join(targetDir, `${timestamp}_oauth_creds.json`); + const folderName = `${timestamp}_kiro-auth-token`; + const targetDir = path.join(process.cwd(), 'configs', 'kiro', folderName); + await fs.promises.mkdir(targetDir, { recursive: true }); + credPath = path.join(targetDir, `${folderName}.json`); } const saveData = { @@ -953,6 +967,9 @@ function createKiroHttpCallbackServer(port, codeVerifier, expectedState, options timestamp: new Date().toISOString() }); + // 自动关联新生成的凭据到 Pools + await autoLinkProviderConfigs(CONFIG); + res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(generateResponsePage(true, '授权成功!您可以关闭此页面')); diff --git a/static/app/app.js b/static/app/app.js index 68df82c..633c60e 100644 --- a/static/app/app.js +++ b/static/app/app.js @@ -71,6 +71,10 @@ import { refreshUsage } from './usage-manager.js'; +import { + initImageZoom +} from './image-zoom.js'; + /** * 加载初始数据 */ @@ -105,6 +109,7 @@ function initApp() { initRoutingExamples(); // 初始化路径路由示例功能 initUploadConfigManager(); // 初始化上传配置管理功能 initUsageManager(); // 初始化用量管理功能 + initImageZoom(); // 初始化图片放大功能 loadInitialData(); // 显示欢迎消息 diff --git a/static/app/event-handlers.js b/static/app/event-handlers.js index c49490d..5482306 100644 --- a/static/app/event-handlers.js +++ b/static/app/event-handlers.js @@ -190,6 +190,77 @@ async function handleGenerateCreds(event) { const providerType = button.getAttribute('data-provider'); const targetInputId = button.getAttribute('data-target'); + try { + // 如果是 Kiro OAuth,先显示认证方式选择对话框 + if (providerType === 'claude-kiro-oauth') { + const modal = document.createElement('div'); + modal.className = 'modal-overlay'; + modal.style.display = 'flex'; + + modal.innerHTML = ` + + `; + + document.body.appendChild(modal); + + const closeModal = () => modal.remove(); + modal.querySelector('.modal-close').onclick = closeModal; + modal.querySelector('.modal-cancel').onclick = closeModal; + + modal.querySelectorAll('.auth-method-btn').forEach(btn => { + btn.onclick = async () => { + const method = btn.dataset.method; + closeModal(); + await proceedWithAuth(providerType, targetInputId, { method }); + }; + }); + return; + } + + await proceedWithAuth(providerType, targetInputId, {}); + } catch (error) { + console.error('生成凭据失败:', error); + showToast(t('common.error'), t('modal.provider.auth.failed') + `: ${error.message}`, 'error'); + } +} + +/** + * 实际执行授权逻辑 + */ +async function proceedWithAuth(providerType, targetInputId, extraOptions = {}) { try { showToast(t('common.info'), t('modal.provider.auth.initializing'), 'info'); @@ -200,7 +271,8 @@ async function handleGenerateCreds(event) { `/providers/${encodeURIComponent(providerType)}/generate-auth-url`, { saveToConfigs: true, - providerDir: providerDir + providerDir: providerDir, + ...extraOptions } ); diff --git a/static/app/i18n.js b/static/app/i18n.js index 3b20c80..e326a7e 100644 --- a/static/app/i18n.js +++ b/static/app/i18n.js @@ -50,8 +50,12 @@ const translations = { 'dashboard.contact.title': '联系与赞助', 'dashboard.contact.wechat': '扫码进群,注明来意', 'dashboard.contact.wechatDesc': '添加微信获取更多技术支持和交流', + 'dashboard.contact.x': '关注 X.com', + 'dashboard.contact.xDesc': '在 X 上关注我们获取最新动态', 'dashboard.contact.sponsor': '扫码赞助', 'dashboard.contact.sponsorDesc': '您的赞助是项目持续发展的动力', + 'dashboard.contact.coffee': 'Buy me a coffee', + 'dashboard.contact.coffeeDesc': 'If you like this project, buy me a coffee!', // OAuth 'oauth.modal.title': 'OAuth 授权', @@ -76,6 +80,18 @@ const translations = { 'oauth.processing': '正在完成授权...', 'oauth.invalid.url': '该 URL 似乎不包含有效的授权代码', 'oauth.error.format': '无效的 URL 格式', + 'oauth.kiro.selectMethod': '选择认证方式', + 'oauth.kiro.google': 'Google 账号登录', + 'oauth.kiro.googleDesc': '使用 Google 账号进行社交登录', + 'oauth.kiro.github': 'GitHub 账号登录', + 'oauth.kiro.githubDesc': '使用 GitHub 账号进行社交登录', + 'oauth.kiro.awsBuilder': 'AWS Builder ID', + 'oauth.kiro.awsBuilderDesc': '使用 AWS Builder ID 进行设备码授权', + 'oauth.kiro.authMethodLabel': '认证方式:', + 'oauth.kiro.step1': '点击下方按钮在浏览器中打开授权链接', + 'oauth.kiro.step2': '使用您的 {method} 账号登录', + 'oauth.kiro.step3': '授权完成后页面会自动关闭', + 'oauth.kiro.step4': '刷新本页面查看凭据文件', // Config 'config.title': '配置管理', @@ -396,11 +412,15 @@ const translations = { 'dashboard.routing.nodeName.kiro': 'Claude Kiro OAuth', 'dashboard.routing.nodeName.openai': 'OpenAI Custom', 'dashboard.routing.nodeName.qwen': 'Qwen OAuth', - 'dashboard.contact.title': 'Contact & Sponsor', + 'dashboard.contact.title': 'Contact & Support', 'dashboard.contact.wechat': 'Scan to Join Group', 'dashboard.contact.wechatDesc': 'Add WeChat for more technical support and communication', - 'dashboard.contact.sponsor': 'Scan to Sponsor', - 'dashboard.contact.sponsorDesc': 'Your sponsorship is the driving force for the project\'s continuous development', + 'dashboard.contact.x': 'Follow on X.com', + 'dashboard.contact.xDesc': 'Follow us on X for latest updates', + 'dashboard.contact.sponsor': 'Scan to Support', + 'dashboard.contact.sponsorDesc': 'Your support is the driving force for the project\'s continuous development', + 'dashboard.contact.coffee': 'Buy me a coffee', + 'dashboard.contact.coffeeDesc': 'If you like this project, buy me a coffee!', // OAuth 'oauth.modal.title': 'OAuth Authorization', @@ -425,6 +445,18 @@ const translations = { 'oauth.processing': 'Completing authorization...', 'oauth.invalid.url': 'This URL does not seem to contain a valid auth code', 'oauth.error.format': 'Invalid URL format', + 'oauth.kiro.selectMethod': 'Select Authentication Method', + 'oauth.kiro.google': 'Google Account Login', + 'oauth.kiro.googleDesc': 'Login with Google account', + 'oauth.kiro.github': 'GitHub Account Login', + 'oauth.kiro.githubDesc': 'Login with GitHub account', + 'oauth.kiro.awsBuilder': 'AWS Builder ID', + 'oauth.kiro.awsBuilderDesc': 'Device code authorization via AWS Builder ID', + 'oauth.kiro.authMethodLabel': 'Auth Method:', + 'oauth.kiro.step1': 'Click the button below to open the authorization link in your browser', + 'oauth.kiro.step2': 'Log in with your {method} account', + 'oauth.kiro.step3': 'The page will close automatically after authorization', + 'oauth.kiro.step4': 'Refresh this page to view the credentials file', // Config 'config.title': 'Configuration Management', @@ -721,11 +753,89 @@ export function setLanguage(lang) { currentLanguage = lang; localStorage.setItem('language', lang); updatePageLanguage(); + // 更新图片 + updateDashboardImages(lang); // 触发语言切换事件 window.dispatchEvent(new CustomEvent('languageChanged', { detail: { language: lang } })); } } +// 更新仪表盘图片 +function updateDashboardImages(lang) { + const sponsorImg = document.getElementById('sponsor-img'); + const sponsorTitle = document.getElementById('sponsor-title'); + const sponsorDesc = document.getElementById('sponsor-desc'); + + const wechatImg = document.getElementById('wechat-img'); + const wechatIcon = document.getElementById('wechat-icon'); + const wechatTitle = document.getElementById('wechat-title'); + const wechatDesc = document.getElementById('wechat-desc'); + + if (lang === 'en-US') { + // 更新赞助图片 + if (sponsorImg) { + sponsorImg.src = 'static/coffee.png'; + sponsorImg.alt = 'Buy me a coffee'; + if (sponsorTitle) { + sponsorTitle.setAttribute('data-i18n', 'dashboard.contact.coffee'); + sponsorTitle.textContent = translations['en-US']['dashboard.contact.coffee']; + } + if (sponsorDesc) { + sponsorDesc.setAttribute('data-i18n', 'dashboard.contact.coffeeDesc'); + sponsorDesc.textContent = translations['en-US']['dashboard.contact.coffeeDesc']; + } + } + + // 更新联系方式图片 (WeChat -> X.com) + if (wechatImg) { + wechatImg.src = 'static/x.com.png'; + wechatImg.alt = 'X.com'; + if (wechatIcon) { + wechatIcon.className = 'fab fa-x-twitter'; + } + if (wechatTitle) { + wechatTitle.setAttribute('data-i18n', 'dashboard.contact.x'); + wechatTitle.textContent = translations['en-US']['dashboard.contact.x'] || 'Follow on X.com'; + } + if (wechatDesc) { + wechatDesc.setAttribute('data-i18n', 'dashboard.contact.xDesc'); + wechatDesc.textContent = translations['en-US']['dashboard.contact.xDesc'] || 'Follow us on X for latest updates'; + } + } + } else { + // 更新赞助图片 + if (sponsorImg) { + sponsorImg.src = 'static/sponsor.png'; + sponsorImg.alt = '赞助二维码'; + if (sponsorTitle) { + sponsorTitle.setAttribute('data-i18n', 'dashboard.contact.sponsor'); + sponsorTitle.textContent = translations['zh-CN']['dashboard.contact.sponsor']; + } + if (sponsorDesc) { + sponsorDesc.setAttribute('data-i18n', 'dashboard.contact.sponsorDesc'); + sponsorDesc.textContent = translations['zh-CN']['dashboard.contact.sponsorDesc']; + } + } + + // 更新联系方式图片 (X.com -> WeChat) + if (wechatImg) { + wechatImg.src = 'static/wechat.png'; + wechatImg.alt = '微信二维码'; + if (wechatIcon) { + wechatIcon.className = 'fab fa-weixin'; + } + if (wechatTitle) { + wechatTitle.setAttribute('data-i18n', 'dashboard.contact.wechat'); + wechatTitle.textContent = translations['zh-CN']['dashboard.contact.wechat']; + } + if (wechatDesc) { + wechatDesc.setAttribute('data-i18n', 'dashboard.contact.wechatDesc'); + wechatDesc.textContent = translations['zh-CN']['dashboard.contact.wechatDesc']; + } + } + } +} + // 获取当前语言 export function getCurrentLanguage() { return currentLanguage; @@ -783,6 +893,8 @@ function updatePageLanguage() { export function initI18n() { // 设置初始语言 updatePageLanguage(); + // 设置初始图片 + updateDashboardImages(currentLanguage); // 监听 DOM 变化,自动翻译新添加的元素 const observer = new MutationObserver((mutations) => { diff --git a/static/app/image-zoom.js b/static/app/image-zoom.js new file mode 100644 index 0000000..f975b8f --- /dev/null +++ b/static/app/image-zoom.js @@ -0,0 +1,45 @@ +/** + * 图像点击放大功能模块 + */ + +export function initImageZoom() { + // 创建放大图层 + const overlay = document.createElement('div'); + overlay.className = 'image-zoom-overlay'; + overlay.innerHTML = 'Zoomed Image'; + document.body.appendChild(overlay); + + const zoomedImg = overlay.querySelector('img'); + + // 监听点击事件 + document.addEventListener('click', (e) => { + const target = e.target; + + // 如果点击的是可放大的二维码 + if (target.classList.contains('clickable-qr')) { + zoomedImg.src = target.src; + overlay.style.display = 'flex'; + setTimeout(() => { + overlay.classList.add('show'); + }, 10); + } + + // 如果点击的是放大图层(或者其中的图片),则关闭 + if (overlay.classList.contains('show') && (target === overlay || target === zoomedImg)) { + overlay.classList.remove('show'); + setTimeout(() => { + overlay.style.display = 'none'; + }, 300); + } + }); + + // ESC 键关闭 + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && overlay.classList.contains('show')) { + overlay.classList.remove('show'); + setTimeout(() => { + overlay.style.display = 'none'; + }, 300); + } + }); +} diff --git a/static/app/provider-manager.js b/static/app/provider-manager.js index c98c97c..c4a3ea8 100644 --- a/static/app/provider-manager.js +++ b/static/app/provider-manager.js @@ -362,36 +362,36 @@ function showKiroAuthMethodSelector(providerType) { modal.innerHTML = ` `; @@ -502,15 +502,16 @@ function showAuthModal(authUrl, authInfo) { `; } else if (authInfo.provider === 'claude-kiro-oauth') { const methodDisplay = authInfo.authMethod === 'builder-id' ? 'AWS Builder ID' : `Social (${authInfo.socialProvider || 'Google'})`; + const methodAccount = authInfo.authMethod === 'builder-id' ? 'AWS Builder ID' : authInfo.socialProvider || 'Google'; instructionsHtml = `

${t('oauth.modal.steps')}

-

认证方式: ${methodDisplay}

+

${t('oauth.kiro.authMethodLabel')} ${methodDisplay}

    -
  1. 点击下方按钮在浏览器中打开授权链接
  2. -
  3. 使用您的 ${authInfo.authMethod === 'builder-id' ? 'AWS Builder ID' : authInfo.socialProvider || 'Google'} 账号登录
  4. -
  5. 授权完成后页面会自动关闭
  6. -
  7. 刷新本页面查看凭据文件
  8. +
  9. ${t('oauth.kiro.step1')}
  10. +
  11. ${t('oauth.kiro.step2', { method: methodAccount })}
  12. +
  13. ${t('oauth.kiro.step3')}
  14. +
  15. ${t('oauth.kiro.step4')}
`; diff --git a/static/app/styles.css b/static/app/styles.css index f6de28d..1470120 100644 --- a/static/app/styles.css +++ b/static/app/styles.css @@ -2990,6 +2990,66 @@ input:checked + .toggle-slider:before { } } +/* Dashboard Top Row Layout */ +.dashboard-top-row { + display: flex; + gap: 1.5rem; + margin-bottom: 2rem; + align-items: stretch; + justify-content: flex-start; +} + +.dashboard-top-row .stats-grid { + flex: 1; + margin-bottom: 0; + display: flex; + flex-direction: column; +} + +.dashboard-top-row .stats-grid .stat-card { + flex: 1; + height: 100%; +} + +.dashboard-top-row .dashboard-contact { + flex: 1; + margin-top: 0; + padding-top: 0; + border-top: none; +} + +.dashboard-top-row .dashboard-contact .contact-grid { + margin-top: 0; + height: 100%; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; +} + +.dashboard-top-row .dashboard-contact .contact-card { + padding: 1.25rem; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; +} + +.dashboard-top-row .dashboard-contact .qr-container { + margin: 0.75rem 0; +} + +.dashboard-top-row .dashboard-contact .qr-code { + width: 100px; + height: 100px; +} + +.dashboard-top-row .dashboard-contact .contact-card h3 { + font-size: 1rem; +} + +.dashboard-top-row .dashboard-contact .qr-description { + font-size: 0.75rem; +} + /* Contact and Sponsor Section */ .contact-grid { display: grid; @@ -3051,6 +3111,45 @@ input:checked + .toggle-slider:before { transform: scale(1.05); } +.clickable-qr { + cursor: zoom-in; +} + +/* Image Zoom Overlay */ +.image-zoom-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.85); + display: none; + justify-content: center; + align-items: center; + z-index: 2000; + cursor: zoom-out; + opacity: 0; + transition: opacity 0.3s ease; +} + +.image-zoom-overlay.show { + display: flex; + opacity: 1; +} + +.image-zoom-overlay img { + max-width: 90%; + max-height: 90%; + border-radius: 0.5rem; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); + transform: scale(0.9); + transition: transform 0.3s ease; +} + +.image-zoom-overlay.show img { + transform: scale(1); +} + .qr-description { font-size: 0.875rem; color: var(--text-secondary); @@ -3059,6 +3158,21 @@ input:checked + .toggle-slider:before { } /* 响应式调整 */ +@media (max-width: 1024px) { + .dashboard-top-row { + flex-direction: column; + } + + .dashboard-top-row .stats-grid { + flex: none; + } + + .dashboard-top-row .dashboard-contact .qr-code { + width: 160px; + height: 160px; + } +} + @media (max-width: 768px) { .contact-grid { grid-template-columns: 1fr; diff --git a/static/coffee.png b/static/coffee.png new file mode 100644 index 0000000..89348bc Binary files /dev/null and b/static/coffee.png differ diff --git a/static/index.html b/static/index.html index f14a7fe..810ded8 100644 --- a/static/index.html +++ b/static/index.html @@ -62,17 +62,40 @@

系统概览

-
-
-
- +
+
+
+
+ +
+
+

--

+

运行时间

+
-
-

--

-

运行时间

+
+ + +
+
+
+

扫码进群,注明来意

+
+ 微信二维码 +
+

添加微信获取更多技术支持和交流

+
+
+

系统信息

@@ -434,26 +457,6 @@
- -
-

联系与赞助

-
-
-

扫码进群,注明来意

-
- 微信二维码 -
-

添加微信获取更多技术支持和交流

-
-
-

扫码赞助

-
- 赞助二维码 -
-

您的赞助是项目持续发展的动力

-
-
-
@@ -633,6 +636,9 @@ + diff --git a/static/x.com.png b/static/x.com.png new file mode 100644 index 0000000..6d9b63f Binary files /dev/null and b/static/x.com.png differ