From 0816de2ba2fbe180b0cfcffed759cbbe142e0c09 Mon Sep 17 00:00:00 2001 From: hex2077 Date: Sun, 21 Dec 2025 21:09:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E6=B7=BB=E5=8A=A0=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E6=94=BE=E5=A4=A7=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E4=BB=AA=E8=A1=A8=E7=9B=98=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现二维码图片点击放大功能,重构仪表盘顶部布局将联系信息与统计卡片并排显示 添加多语言图片切换功能,根据语言显示不同的赞助和联系方式图片 优化Kiro OAuth流程,增加自动关联凭据到Pools的功能 --- src/oauth-handlers.js | 77 ++++++++++++--------- static/app/app.js | 5 ++ static/app/event-handlers.js | 74 ++++++++++++++++++++- static/app/i18n.js | 118 ++++++++++++++++++++++++++++++++- static/app/image-zoom.js | 45 +++++++++++++ static/app/provider-manager.js | 31 ++++----- static/app/styles.css | 114 +++++++++++++++++++++++++++++++ static/coffee.png | Bin 0 -> 5876 bytes static/index.html | 60 +++++++++-------- static/x.com.png | Bin 0 -> 6065 bytes 10 files changed, 448 insertions(+), 76 deletions(-) create mode 100644 static/app/image-zoom.js create mode 100644 static/coffee.png create mode 100644 static/x.com.png 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 0000000000000000000000000000000000000000..89348bc212403b6390e3ddf3cb20a865a6e290e1 GIT binary patch literal 5876 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 e)>JpD)fxlG_&D+(!H0lE1NptP9pl^6KmP|

系统概览

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

--

+

运行时间

+
-
-

--

-

运行时间

+
+ + +
+
+
+

扫码进群,注明来意

+
+ 微信二维码 +
+

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

+
+
+

系统信息

@@ -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 0000000000000000000000000000000000000000..6d9b63fefd0f6fd9a9615202dc4840beafc483fa GIT binary patch literal 6065 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