(() => { 'use strict'; const $ = selector => document.querySelector(selector); const root = document.documentElement; const themeButton = $('.theme-toggle'); const themePreference = matchMedia('(prefers-color-scheme: dark)'); function updateThemeButton() { const dark = root.classList.contains('dark'); themeButton.setAttribute('aria-label', dark ? '切换至浅色主题' : '切换至深色主题'); themeButton.title = dark ? '切换至浅色主题' : '切换至深色主题'; const meta = $('meta[name="theme-color"]'); if (meta) meta.content = dark ? '#101722' : '#f5f7fc'; } themeButton.addEventListener('click', () => { const dark = root.classList.toggle('dark'); try { localStorage.setItem('newapi:theme:v1:mode', dark ? 'dark' : 'light'); } catch (_) {} updateThemeButton(); }); if (themePreference.addEventListener) themePreference.addEventListener('change', event => { let saved; try { saved = localStorage.getItem('newapi:theme:v1:mode'); } catch (_) {} if (saved !== 'dark' && saved !== 'light') { root.classList.toggle('dark', event.matches); updateThemeButton(); } }); updateThemeButton(); const menu = $('.menu-toggle'); const mobileNav = $('#mobile-nav'); function setMenu(open, restoreFocus = false) { menu.setAttribute('aria-expanded', String(open)); menu.setAttribute('aria-label', open ? '收起导航' : '展开导航'); mobileNav.hidden = !open; if (restoreFocus) menu.focus(); } menu.addEventListener('click', () => setMenu(menu.getAttribute('aria-expanded') !== 'true')); mobileNav.addEventListener('click', event => { if (event.target.closest('a')) setMenu(false); }); document.addEventListener('keydown', event => { if (event.key === 'Escape' && menu.getAttribute('aria-expanded') === 'true') setMenu(false, true); }); document.addEventListener('click', event => { if (menu.getAttribute('aria-expanded') === 'true' && !event.target.closest('.site-header')) setMenu(false); }); document.addEventListener('focusin', event => { if (menu.getAttribute('aria-expanded') === 'true' && !event.target.closest('.site-header')) setMenu(false); }); const desktop = matchMedia('(min-width: 768px)'); if (desktop.addEventListener) desktop.addEventListener('change', event => { if (event.matches) setMenu(false); }); const tabs = Array.from(document.querySelectorAll('[data-os]')); function selectOS(os, focus = false) { const windows = os === 'windows'; tabs.forEach(tab => { const selected = tab.dataset.os === os; tab.setAttribute('aria-selected', String(selected)); tab.tabIndex = selected ? 0 : -1; if (selected && focus) tab.focus(); }); $('#install-command-panel').setAttribute('aria-labelledby', windows ? 'os-windows' : 'os-unix'); $('#install-command').textContent = windows ? 'irm https://app.duiliu.uk/install.ps1 | iex' : 'curl -fsSL https://app.duiliu.uk/install.sh | bash'; $('#install-instruction').textContent = windows ? '打开 PowerShell,粘贴并运行' : '打开终端,粘贴并运行'; $('#download-script').setAttribute('href', windows ? '/install.ps1' : '/install.sh'); } tabs.forEach((tab, index) => { tab.addEventListener('click', () => selectOS(tab.dataset.os)); tab.addEventListener('keydown', event => { if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return; event.preventDefault(); const next = event.key === 'Home' ? 0 : event.key === 'End' ? tabs.length - 1 : (index + 1) % tabs.length; selectOS(tabs[next].dataset.os, true); }); }); if (/Mac|Linux|Android|iPhone|iPad/i.test(navigator.userAgent) && !/Windows/i.test(navigator.userAgent)) selectOS('unix'); const toast = $('#copy-status'); let toastTimer; function announce(text) { clearTimeout(toastTimer); toast.textContent = text; toast.classList.add('show'); toastTimer = setTimeout(() => toast.classList.remove('show'), 2600); } async function copyText(text) { if (navigator.clipboard && window.isSecureContext) { try { await navigator.clipboard.writeText(text); return true; } catch (_) {} } const input = document.createElement('textarea'); input.value = text; input.readOnly = true; input.style.cssText = 'position:fixed;left:-9999px;top:0'; document.body.append(input); const active = document.activeElement; input.select(); let copied = false; try { copied = document.execCommand('copy'); } catch (_) {} input.remove(); if (active && typeof active.focus === 'function') active.focus({ preventScroll: true }); return copied; } document.querySelectorAll('[data-copy]').forEach(button => button.addEventListener('click', async () => { const target = document.getElementById(button.dataset.copy); if (!target || button.disabled) return; button.disabled = true; button.setAttribute('aria-busy', 'true'); try { const copied = await copyText(target.textContent); announce(copied ? '已复制,可以粘贴使用。' : '暂时无法复制,请选中命令手动复制。'); } finally { button.disabled = false; button.removeAttribute('aria-busy'); } })); // One observer handles one-time entrances. The unenhanced page is fully visible. const reduced = matchMedia('(prefers-reduced-motion: reduce)'); let revealObserver; if ('IntersectionObserver' in window && !reduced.matches) { try { revealObserver = new IntersectionObserver(entries => { entries.forEach(entry => { if (!entry.isIntersecting) return; entry.target.classList.add('is-visible'); revealObserver.unobserve(entry.target); }); }, { threshold: 0.08, rootMargin: '0px 0px 36px 0px' }); const sections = Array.from(document.querySelectorAll('[data-reveal]')); sections.forEach(section => revealObserver.observe(section)); root.classList.add('js-motion'); } catch (_) { root.classList.remove('js-motion'); } } if (reduced.addEventListener) reduced.addEventListener('change', event => { if (event.matches) { root.classList.remove('js-motion'); if (revealObserver) revealObserver.disconnect(); } }); if ('IntersectionObserver' in window) { const headerObserver = new IntersectionObserver(entries => { $('.site-header').classList.toggle('is-scrolled', !entries[0].isIntersecting); }); headerObserver.observe($('#top-edge')); } // One initial read; subsequent reads require an explicit user action. const retryButton = $('#catalog-retry'); const catalogNote = $('#catalog-note'); const modelGrid = $('#model-grid'); let catalogPending = false; let retryAt = 0; let cooldownTimer; function updateRetry() { clearTimeout(cooldownTimer); const seconds = Math.max(0, Math.ceil((retryAt - Date.now()) / 1000)); retryButton.disabled = catalogPending || seconds > 0; retryButton.textContent = seconds ? '请等待 ' + seconds + ' 秒' : '重新加载'; if (seconds) cooldownTimer = setTimeout(updateRetry, 1000); } async function loadCatalog() { if (catalogPending || Date.now() < retryAt) return; catalogPending = true; updateRetry(); modelGrid.setAttribute('aria-busy', 'true'); catalogNote.textContent = '正在获取最新模型目录…'; const controller = new AbortController(); const catalogTimeout = setTimeout(() => controller.abort(), 9000); try { const response = await fetch('/api/pricing', { credentials: 'same-origin', signal: controller.signal }); if (response.status === 429 || response.status === 503) { const value = response.headers.get('Retry-After'); const seconds = value && /^\d+$/.test(value.trim()) ? Number(value.trim()) : NaN; const date = value ? Date.parse(value) : NaN; const delay = Number.isFinite(seconds) ? seconds * 1000 : Number.isFinite(date) ? Math.max(0, date - Date.now()) : 30000; retryAt = Date.now() + Math.max(1000, delay); throw new Error(response.status === 429 ? 'rate-limit' : 'unavailable'); } if (!response.ok) throw new Error('catalog'); const payload = await response.json(); if (!payload.success || !Array.isArray(payload.data)) throw new Error('catalog'); const list = [...new Map(payload.data.filter(model => typeof model.model_name === 'string' && model.model_name.length > 0).map(model => [model.model_name, model])).values()]; const preferred = ['gpt-6.1-sol', 'gpt-6-astra', 'gpt-6-luna', 'gpt-6-sol']; const rank = name => preferred.indexOf(name) < 0 ? 999 : preferred.indexOf(name); list.sort((a, b) => rank(a.model_name) - rank(b.model_name)); if (!list.length) throw new Error('empty catalog'); const fragment = document.createDocumentFragment(); list.slice(0, 4).forEach((model, index) => { const card = document.createElement('a'); card.className = 'model-card'; card.href = '/pricing'; const top = document.createElement('div'); top.className = 'model-card-top'; const icon = document.createElement('span'); icon.className = 'model-icon'; icon.setAttribute('aria-hidden', 'true'); icon.textContent = ['✳', '✧', '↯', '⌁'][index]; const sequence = document.createElement('span'); sequence.className = 'model-sequence'; sequence.textContent = 'MODEL / 0' + (index + 1); top.append(icon, sequence); const title = document.createElement('h3'); title.textContent = model.model_name; const description = document.createElement('p'); description.textContent = '查看模型详情、可用接口与当前分组费率。'; const bottom = document.createElement('span'); bottom.className = 'model-card-bottom'; const types = Array.isArray(model.supported_endpoint_types) ? model.supported_endpoint_types.filter(type => typeof type === 'string') : []; const endpoint = document.createElement('span'); endpoint.textContent = types.length ? types.map(type => type === 'openai' ? 'OpenAI 兼容' : type).join(' · ') : '查看接入详情'; const arrow = document.createElement('span'); arrow.className = 'card-arrow'; arrow.setAttribute('aria-hidden', 'true'); arrow.textContent = '↗'; bottom.append(endpoint, arrow); card.append(top, title, description, bottom); fragment.append(card); }); modelGrid.replaceChildren(fragment); catalogNote.textContent = '当前目录收录 ' + list.length + ' 个模型。实际可用模型与费率以密钥权限及分组配置为准。'; $('#example-model').textContent = JSON.stringify(list[0].model_name); retryButton.hidden = true; } catch (error) { catalogNote.textContent = error.message === 'rate-limit' ? '访问较频繁,请等待后重试。' : error.message === 'unavailable' ? '目录服务暂时繁忙,请稍后重试。' : error.name === 'AbortError' ? '加载超时,请检查网络后重试,或打开完整模型目录。' : '暂未取得最新目录,可重新加载或打开完整模型目录。'; retryButton.hidden = false; // Even failures without a server hint get a short manual retry cooldown. retryAt = Math.max(retryAt, Date.now() + 1000); } finally { clearTimeout(catalogTimeout); catalogPending = false; modelGrid.setAttribute('aria-busy', 'false'); updateRetry(); } } retryButton.addEventListener('click', loadCatalog); loadCatalog(); })();