app.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. 'use strict';
  2. (function () {
  3. const views = {
  4. crawl: document.getElementById('viewCrawl'),
  5. gen: document.getElementById('viewGen'),
  6. tasks: document.getElementById('viewTasks'),
  7. history: document.getElementById('viewHistory'),
  8. reports: document.getElementById('viewReports'),
  9. analysis: document.getElementById('viewAnalysis'),
  10. settings: document.getElementById('viewSettings'),
  11. };
  12. const navItems = document.querySelectorAll('.nav-item[data-view]');
  13. let settingsLoaded = false;
  14. let logTimer = null;
  15. let lastLogSignature = '';
  16. function showSettingsStatus(message, type = 'info') {
  17. const status = document.getElementById('settingsStatus');
  18. status.textContent = message;
  19. status.className = `badge ${type === 'success' ? 'badge-ok' : type === 'error' ? 'badge-fail' : 'badge-info'}`;
  20. }
  21. async function loadModels(selectedModel) {
  22. const select = document.getElementById('settingsModel');
  23. const response = await fetch('/api/models');
  24. if (!response.ok) throw new Error('模型列表加载失败');
  25. const data = await response.json();
  26. select.innerHTML = '';
  27. for (const model of data.models || []) {
  28. const option = document.createElement('option');
  29. option.value = typeof model === 'string' ? model : model.id;
  30. option.textContent = typeof model === 'string' ? model : (model.label || model.name || model.id);
  31. option.selected = option.value === selectedModel;
  32. select.appendChild(option);
  33. }
  34. }
  35. async function loadSettings() {
  36. showSettingsStatus('加载中…');
  37. const response = await fetch('/api/settings');
  38. if (!response.ok) throw new Error('设置加载失败');
  39. const settings = await response.json();
  40. document.getElementById('settingsApiKey').value = settings.apiKey || '';
  41. document.getElementById('settingsApiBase').value = settings.apiBase || '';
  42. document.getElementById('settingsRootDir').value = settings.rootDir || '';
  43. document.getElementById('pageSizeInput').value = settings.pageSize || 50;
  44. document.getElementById('delayInput').value = settings.delayMs ?? 300;
  45. const cookieInput = document.getElementById('settingsCookie');
  46. cookieInput.value = '';
  47. cookieInput.placeholder = settings.cookieConfigured
  48. ? '已配置,输入新 Cookie 可更换'
  49. : '登录 melody.shop.ele.me 后从 DevTools 复制';
  50. document.getElementById('settingsChatModel').value = settings.chatModel || 'deepseek-v4.1-flash';
  51. document.getElementById('settingsChatApiBase').value = settings.chatApiBase || '';
  52. const chatKeyInput = document.getElementById('settingsChatApiKey');
  53. chatKeyInput.value = settings.chatApiKey || '';
  54. chatKeyInput.placeholder = settings.chatKeyFallback
  55. ? '留空复用上方 API Key'
  56. : (settings.chatApiKey ? '已配置,输入新 Key 可更换' : '留空复用上方 API Key;否则填对话模型专用 Key');
  57. await loadModels(settings.model);
  58. showSettingsStatus('已同步');
  59. }
  60. async function saveSettings() {
  61. const button = document.getElementById('btnSaveSettings');
  62. button.disabled = true;
  63. showSettingsStatus('保存中…');
  64. try {
  65. const response = await fetch('/api/settings', {
  66. method: 'POST',
  67. headers: { 'Content-Type': 'application/json' },
  68. body: JSON.stringify({
  69. apiKey: document.getElementById('settingsApiKey').value.trim(),
  70. model: document.getElementById('settingsModel').value,
  71. rootDir: document.getElementById('settingsRootDir').value.trim(),
  72. pageSize: Number(document.getElementById('pageSizeInput').value),
  73. delayMs: Number(document.getElementById('delayInput').value),
  74. chatModel: document.getElementById('settingsChatModel').value.trim(),
  75. ...(document.getElementById('settingsChatApiKey').value.trim() && !document.getElementById('settingsChatApiKey').value.includes('••••')
  76. ? { chatApiKey: document.getElementById('settingsChatApiKey').value.trim() }
  77. : {}),
  78. ...(document.getElementById('settingsCookie').value.trim()
  79. ? { cookie: document.getElementById('settingsCookie').value.trim() }
  80. : {}),
  81. }),
  82. });
  83. const data = await response.json();
  84. if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`);
  85. await loadSettings();
  86. showSettingsStatus('已保存', 'success');
  87. } catch (error) {
  88. showSettingsStatus(error.message, 'error');
  89. } finally {
  90. button.disabled = false;
  91. }
  92. }
  93. async function testImageApi() {
  94. const status = document.getElementById('imageTestStatus');
  95. const button = document.getElementById('btnTestImage');
  96. button.disabled = true;
  97. status.textContent = '测试中…';
  98. status.className = 'badge badge-info';
  99. try {
  100. const response = await fetch('/api/image/test', { method: 'POST' });
  101. const data = await response.json();
  102. if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`);
  103. if (data.statusCode === 0) throw new Error(data.error || '网络不可达');
  104. const model = data.model || '';
  105. if (data.statusCode === 200 && data.modelFound) {
  106. status.textContent = '连通正常,模型在列表中';
  107. status.className = 'badge badge-ok';
  108. } else if (data.statusCode === 200 && Array.isArray(data.models)) {
  109. status.textContent = `服务可达(${data.models.length} 个模型);列表中未找到 ${model}`;
  110. status.className = 'badge badge-warn';
  111. } else if (data.statusCode === 200) {
  112. status.textContent = '服务可达;该网关不提供模型列表,无法校验模型名';
  113. status.className = 'badge badge-warn';
  114. } else if (data.statusCode === 401 || data.statusCode === 403) {
  115. status.textContent = `服务返回 ${data.statusCode}:Key 可能无效`;
  116. status.className = 'badge badge-fail';
  117. } else {
  118. status.textContent = `服务返回 ${data.statusCode}`;
  119. status.className = 'badge badge-fail';
  120. }
  121. } catch (error) {
  122. status.textContent = '失败:' + error.message;
  123. status.className = 'badge badge-fail';
  124. } finally {
  125. button.disabled = false;
  126. }
  127. }
  128. async function testChatModel() {
  129. const status = document.getElementById('chatTestStatus');
  130. const button = document.getElementById('btnTestChat');
  131. button.disabled = true;
  132. status.textContent = '测试中…';
  133. status.className = 'badge badge-info';
  134. try {
  135. const response = await fetch('/api/chat/test', {
  136. method: 'POST',
  137. headers: { 'Content-Type': 'application/json' },
  138. body: JSON.stringify({
  139. chatModel: document.getElementById('settingsChatModel').value.trim(),
  140. chatApiKey: document.getElementById('settingsChatApiKey').value.trim(),
  141. }),
  142. });
  143. const data = await response.json();
  144. if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`);
  145. status.textContent = `连通正常:${data.reply || 'OK'}`;
  146. status.className = 'badge badge-ok';
  147. } catch (error) {
  148. status.textContent = '失败:' + error.message;
  149. status.className = 'badge badge-fail';
  150. } finally {
  151. button.disabled = false;
  152. }
  153. }
  154. function bindSettingsTabs() {
  155. const tabs = document.getElementById('settingsApiTabs');
  156. if (!tabs) return;
  157. tabs.addEventListener('click', (event) => {
  158. const btn = event.target.closest('button[data-tab]');
  159. if (!btn || btn.classList.contains('active')) return;
  160. tabs.querySelectorAll('button').forEach((b) => b.classList.toggle('active', b === btn));
  161. document.getElementById('settingsTabImage').hidden = btn.dataset.tab !== 'image';
  162. document.getElementById('settingsTabChat').hidden = btn.dataset.tab !== 'chat';
  163. });
  164. }
  165. async function initSettings() {
  166. if (settingsLoaded) return;
  167. bindSettingsTabs();
  168. if (new URLSearchParams(location.search).get('tab') === 'chat') {
  169. document.querySelector('#settingsApiTabs button[data-tab="chat"]')?.click();
  170. }
  171. document.getElementById('btnSaveSettings').addEventListener('click', saveSettings);
  172. document.getElementById('btnTestImage').addEventListener('click', testImageApi);
  173. document.getElementById('btnTestChat').addEventListener('click', testChatModel);
  174. document.getElementById('btnClearLog').addEventListener('click', () => {
  175. clearLogs().catch(() => {});
  176. });
  177. try {
  178. await loadSettings();
  179. settingsLoaded = true;
  180. } catch (error) {
  181. showSettingsStatus(error.message, 'error');
  182. }
  183. }
  184. function escapeLogText(value) {
  185. const div = document.createElement('div');
  186. div.textContent = String(value ?? '');
  187. return div.innerHTML;
  188. }
  189. function renderLogs(logs) {
  190. const panel = document.getElementById('logPanel');
  191. if (!panel) return;
  192. const sourceMap = { task: '任务', system: '系统' };
  193. const levelMap = { info: 'INFO', success: 'OK', warn: 'WARN', error: 'FAIL' };
  194. panel.innerHTML = logs.map((log) => {
  195. const level = levelMap[log.level] || 'INFO';
  196. const levelClass = log.level === 'success' ? 'ok' : log.level || 'info';
  197. return `
  198. <div class="log-line">
  199. <span class="log-ts">${escapeLogText(log.time)}</span>
  200. <span class="log-source ${log.source === 'system' ? 'system' : 'task'}">${sourceMap[log.source] || log.source}</span>
  201. <span class="log-lvl ${levelClass}">${level}</span>
  202. <span class="log-msg">${escapeLogText(log.message)}</span>
  203. </div>`;
  204. }).join('');
  205. }
  206. async function refreshLogs() {
  207. const response = await fetch('/api/logs');
  208. if (!response.ok) return;
  209. const data = await response.json();
  210. const logs = (data.logs || []).sort((a, b) => b.seq - a.seq);
  211. const signature = logs.map((log) => `${log.seq}:${log.level}:${log.message}`).join('|');
  212. if (signature === lastLogSignature) return;
  213. lastLogSignature = signature;
  214. renderLogs(logs);
  215. }
  216. function startLogPolling() {
  217. if (logTimer) return;
  218. refreshLogs();
  219. logTimer = setInterval(refreshLogs, 1500);
  220. }
  221. async function clearLogs() {
  222. await fetch('/api/logs/clear', { method: 'POST' });
  223. lastLogSignature = '';
  224. await refreshLogs();
  225. }
  226. function switchView(name) {
  227. for (const [key, el] of Object.entries(views)) {
  228. el.classList.toggle('active', key === name);
  229. }
  230. navItems.forEach((n) => n.classList.toggle('active', n.dataset.view === name));
  231. if (name === 'crawl' && window.shanCrawlInit) window.shanCrawlInit();
  232. if (name === 'gen' && window.shanGenInit) window.shanGenInit();
  233. if (name === 'tasks' && window.shanTasksInit) window.shanTasksInit();
  234. if (name === 'history' && window.shanHistoryInit) window.shanHistoryInit();
  235. if (name === 'reports' && window.shanReportsInit) window.shanReportsInit();
  236. if (name === 'analysis' && window.shanAnalysisActivate) window.shanAnalysisActivate();
  237. if (name === 'settings') initSettings();
  238. // 智能问数机器人:仅经营分析相关页面显示
  239. if (window.shanSetAnalysisVisible) window.shanSetAnalysisVisible(name === 'reports' || name === 'analysis');
  240. }
  241. navItems.forEach((n) => n.addEventListener('click', () => switchView(n.dataset.view)));
  242. const requestedView = new URLSearchParams(location.search).get('view');
  243. switchView(views[requestedView] ? requestedView : 'crawl');
  244. startLogPolling();
  245. const splash = document.getElementById('splash');
  246. if (splash) {
  247. setTimeout(() => splash.classList.add('hide'), 1800);
  248. setTimeout(() => splash.remove(), 2400);
  249. }
  250. })();