'use strict'; // 局域网访问口令:服务端开启 accessToken 时,API 返回 401 会引导输入一次口令, // 写入 cookie 后所有请求(fetch / 图片 / CSV 链接)自动携带,无需逐处改代码。 (function () { let tokenAsked = false; const origFetch = window.fetch.bind(window); window.fetch = async function (input, init) { const res = await origFetch(input, init); const url = typeof input === 'string' ? input : (input && input.url) || String(input || ''); if (res.status === 401 && /\/(api|files)\b/.test(url) && !tokenAsked) { tokenAsked = true; const token = window.prompt('此服务已开启访问口令(accessToken),请输入:'); if (token) { document.cookie = 'accessToken=' + encodeURIComponent(token) + '; path=/; max-age=31536000; SameSite=Lax'; tokenAsked = false; return origFetch(input, init); } } return res; }; })(); (function () { const views = { crawl: document.getElementById('viewCrawl'), gen: document.getElementById('viewGen'), tasks: document.getElementById('viewTasks'), history: document.getElementById('viewHistory'), reports: document.getElementById('viewReports'), analysis: document.getElementById('viewAnalysis'), settings: document.getElementById('viewSettings'), }; const navItems = document.querySelectorAll('.nav-item[data-view]'); let settingsLoaded = false; let logTimer = null; let lastLogSignature = ''; function showSettingsStatus(message, type = 'info') { const status = document.getElementById('settingsStatus'); status.textContent = message; status.className = `badge ${type === 'success' ? 'badge-ok' : type === 'error' ? 'badge-fail' : 'badge-info'}`; } async function loadModels(selectedModel) { const select = document.getElementById('settingsModel'); const response = await fetch('/api/models'); if (!response.ok) throw new Error('模型列表加载失败'); const data = await response.json(); select.innerHTML = ''; for (const model of data.models || []) { const option = document.createElement('option'); option.value = typeof model === 'string' ? model : model.id; option.textContent = typeof model === 'string' ? model : (model.label || model.name || model.id); option.selected = option.value === selectedModel; select.appendChild(option); } } async function loadSettings() { showSettingsStatus('加载中…'); const response = await fetch('/api/settings'); if (!response.ok) throw new Error('设置加载失败'); const settings = await response.json(); document.getElementById('settingsApiKey').value = settings.apiKey || ''; document.getElementById('settingsApiBase').value = settings.apiBase || ''; document.getElementById('settingsRootDir').value = settings.rootDir || ''; document.getElementById('pageSizeInput').value = settings.pageSize || 50; document.getElementById('delayInput').value = settings.delayMs ?? 300; const cookieInput = document.getElementById('settingsCookie'); cookieInput.value = ''; cookieInput.placeholder = settings.cookieConfigured ? '已配置,输入新 Cookie 可更换' : '登录 melody.shop.ele.me 后从 DevTools 复制'; document.getElementById('settingsChatModel').value = settings.chatModel || 'deepseek-v4.1-flash'; document.getElementById('settingsChatApiBase').value = settings.chatApiBase || ''; const chatKeyInput = document.getElementById('settingsChatApiKey'); chatKeyInput.value = settings.chatApiKey || ''; chatKeyInput.placeholder = settings.chatKeyFallback ? '留空复用上方 API Key' : (settings.chatApiKey ? '已配置,输入新 Key 可更换' : '留空复用上方 API Key;否则填对话模型专用 Key'); const tokenInput = document.getElementById('settingsAccessToken'); tokenInput.value = ''; tokenInput.placeholder = settings.accessTokenSet ? '已设置,留空保持不变(输入 - 清除)' : '未设置'; await loadModels(settings.model); showSettingsStatus('已同步'); } async function saveSettings() { const button = document.getElementById('btnSaveSettings'); button.disabled = true; showSettingsStatus('保存中…'); try { const response = await fetch('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiKey: document.getElementById('settingsApiKey').value.trim(), model: document.getElementById('settingsModel').value, rootDir: document.getElementById('settingsRootDir').value.trim(), pageSize: Number(document.getElementById('pageSizeInput').value), delayMs: Number(document.getElementById('delayInput').value), chatModel: document.getElementById('settingsChatModel').value.trim(), ...(document.getElementById('settingsChatApiKey').value.trim() && !document.getElementById('settingsChatApiKey').value.includes('••••') ? { chatApiKey: document.getElementById('settingsChatApiKey').value.trim() } : {}), ...(document.getElementById('settingsCookie').value.trim() ? { cookie: document.getElementById('settingsCookie').value.trim() } : {}), ...(document.getElementById('settingsAccessToken').value.trim() ? { accessToken: document.getElementById('settingsAccessToken').value.trim() } : {}), }), }); const data = await response.json(); if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`); await loadSettings(); showSettingsStatus('已保存', 'success'); } catch (error) { showSettingsStatus(error.message, 'error'); } finally { button.disabled = false; } } async function testImageApi() { const status = document.getElementById('imageTestStatus'); const button = document.getElementById('btnTestImage'); button.disabled = true; status.textContent = '测试中…'; status.className = 'badge badge-info'; try { const response = await fetch('/api/image/test', { method: 'POST' }); const data = await response.json(); if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`); if (data.statusCode === 0) throw new Error(data.error || '网络不可达'); const model = data.model || ''; if (data.statusCode === 200 && data.modelFound) { status.textContent = '连通正常,模型在列表中'; status.className = 'badge badge-ok'; } else if (data.statusCode === 200 && Array.isArray(data.models)) { status.textContent = `服务可达(${data.models.length} 个模型);列表中未找到 ${model}`; status.className = 'badge badge-warn'; } else if (data.statusCode === 200) { status.textContent = '服务可达;该网关不提供模型列表,无法校验模型名'; status.className = 'badge badge-warn'; } else if (data.statusCode === 401 || data.statusCode === 403) { status.textContent = `服务返回 ${data.statusCode}:Key 可能无效`; status.className = 'badge badge-fail'; } else { status.textContent = `服务返回 ${data.statusCode}`; status.className = 'badge badge-fail'; } } catch (error) { status.textContent = '失败:' + error.message; status.className = 'badge badge-fail'; } finally { button.disabled = false; } } async function testChatModel() { const status = document.getElementById('chatTestStatus'); const button = document.getElementById('btnTestChat'); button.disabled = true; status.textContent = '测试中…'; status.className = 'badge badge-info'; try { const response = await fetch('/api/chat/test', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ chatModel: document.getElementById('settingsChatModel').value.trim(), chatApiKey: document.getElementById('settingsChatApiKey').value.trim(), }), }); const data = await response.json(); if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`); status.textContent = `连通正常:${data.reply || 'OK'}`; status.className = 'badge badge-ok'; } catch (error) { status.textContent = '失败:' + error.message; status.className = 'badge badge-fail'; } finally { button.disabled = false; } } function bindSettingsTabs() { const tabs = document.getElementById('settingsApiTabs'); if (!tabs) return; tabs.addEventListener('click', (event) => { const btn = event.target.closest('button[data-tab]'); if (!btn || btn.classList.contains('active')) return; tabs.querySelectorAll('button').forEach((b) => b.classList.toggle('active', b === btn)); document.getElementById('settingsTabImage').hidden = btn.dataset.tab !== 'image'; document.getElementById('settingsTabChat').hidden = btn.dataset.tab !== 'chat'; }); } async function initSettings() { if (settingsLoaded) return; bindSettingsTabs(); if (new URLSearchParams(location.search).get('tab') === 'chat') { document.querySelector('#settingsApiTabs button[data-tab="chat"]')?.click(); } document.getElementById('btnSaveSettings').addEventListener('click', saveSettings); document.getElementById('btnTestImage').addEventListener('click', testImageApi); document.getElementById('btnTestChat').addEventListener('click', testChatModel); document.getElementById('btnClearLog').addEventListener('click', () => { clearLogs().catch(() => {}); }); try { await loadSettings(); settingsLoaded = true; } catch (error) { showSettingsStatus(error.message, 'error'); } } function escapeLogText(value) { const div = document.createElement('div'); div.textContent = String(value ?? ''); return div.innerHTML; } function renderLogs(logs) { const panel = document.getElementById('logPanel'); if (!panel) return; const sourceMap = { task: '任务', system: '系统' }; const levelMap = { info: 'INFO', success: 'OK', warn: 'WARN', error: 'FAIL' }; panel.innerHTML = logs.map((log) => { const level = levelMap[log.level] || 'INFO'; const levelClass = log.level === 'success' ? 'ok' : log.level || 'info'; return `