const yzp = (() => { let analysts = [] let reports = {} let activeAnalystId = null let uiMode = 'debug' let dialogTimer = null let dialogCountdown = 0 let dialogHasError = false let queryCode = '' let hintMessage = '请通过首页九宫格进入,或使用正确的访问链接' let currentScenarioId = 'stock_invest' let scenarioConfig = null let scenariosData = [] let imageBase64 = '' let userProfile = null const fetchHint = async () => { try { const resp = await fetch('/api/yuanzhupai/auth/hint') const data = await resp.json().catch(() => null) if (data && data.ok && data.message) hintMessage = data.message } catch {} } const el = id => document.getElementById(id) const esc = s => String(s || '').replace(/[&<>"]/g, m => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[m])) const getUiMode = () => uiMode const nowStr = () => { const d = new Date() return `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}:${String(d.getSeconds()).padStart(2,'0')}` } const clearDialogTimer = () => { if (dialogTimer) { clearInterval(dialogTimer); dialogTimer = null } } const openDialog = () => { clearDialogTimer() dialogHasError = false dialogCountdown = 0 const box = el('ydp-dialog-logs') if (box) box.innerHTML = '' const status = el('ydp-dialog-status') if (status) { status.textContent = '运行中'; status.className = 'log-line info' } const dlg = el('ydp-dialog') if (dlg) dlg.classList.add('show') } const closeDialog = () => { clearDialogTimer() const dlg = el('ydp-dialog') if (dlg) dlg.classList.remove('show') } const appendLog = (line, type) => { const box = el('ydp-dialog-logs') if (!box) return const div = document.createElement('div') div.className = `log-line${type ? ` ${type}` : ''}` div.textContent = `[${nowStr()}] ${String(line || '')}` box.appendChild(div) box.scrollTop = box.scrollHeight } const finishDialog = () => { if (dialogHasError) return dialogCountdown = uiMode === 'debug' ? 3 : 2 const status = el('ydp-dialog-status') const updateStatus = () => { if (status) { status.textContent = `成功,${dialogCountdown}s 后自动关闭`; status.className = 'log-line ok' } } updateStatus() clearDialogTimer() dialogTimer = setInterval(() => { dialogCountdown -= 1 if (dialogCountdown <= 0) { clearDialogTimer(); closeDialog(); return } updateStatus() }, 1000) } const failDialog = err => { dialogHasError = true const status = el('ydp-dialog-status') if (status) { status.textContent = '发生错误,已中断'; status.className = 'log-line err' } appendLog(String(err && err.message ? err.message : err), 'err') } const copyText = async text => { if (!navigator.clipboard || !navigator.clipboard.writeText) throw new Error('当前环境不支持剪贴板写入') await navigator.clipboard.writeText(String(text || '')) } const loadConfig = async (version, scenarioId) => { await fetchHint() const ver = String(version || 'x').trim() const sid = String(scenarioId || 'stock_invest').trim() currentScenarioId = sid scenarioConfig = null let authOk = false try { const resp = await fetch('/api/yuanzhupai/analysts?version=' + ver + '&scenario=' + sid) const data = await resp.json().catch(() => null) if (data && data.ok && Array.isArray(data.analysts)) { analysts = data.analysts uiMode = data.ui_mode || 'debug' if (data.scenario) scenarioConfig = data.scenario authOk = true return } if (resp.status === 401) authOk = false } catch {} analysts = [ { id:'business', name:'张志远', title:'商业模式分析师', area:'商业模式', initials:'ZZ', color:'#3b82f6', verdict:'buy', verdict_text:'买入' }, { id:'financial', name:'陈雨薇', title:'资深财务分析师', area:'财务健康', initials:'CY', color:'#06b6d4', verdict:'buy', verdict_text:'买入' }, { id:'moat', name:'李铭哲', title:'竞争护城河专家', area:'竞争护城河', initials:'LM', color:'#8b5cf6', verdict:'buy', verdict_text:'买入' }, { id:'valuation', name:'王芷茜', title:'估值与定价分析师', area:'股票估值', initials:'WZ', color:'#f59e0b', verdict:'buy', verdict_text:'买入' }, { id:'risk', name:'刘浩宇', title:'风险管理总监', area:'风险分析', initials:'LH', color:'#ef4444', verdict:'hold', verdict_text:'持有' }, { id:'growth', name:'孙雪莹', title:'增长战略分析师', area:'增长潜力', initials:'SX', color:'#22c55e', verdict:'buy', verdict_text:'买入' }, { id:'institutional', name:'赵启航', title:'机构投资策略师', area:'机构视角', initials:'ZQ', color:'#14b8a6', verdict:'buy', verdict_text:'买入' }, { id:'bull', name:'周俊杰', title:'看涨首席分析师', area:'看涨论点', initials:'ZJ', color:'#4ade80', verdict:'buy', verdict_text:'买入' }, { id:'bear', name:'吴静波', title:'看跌风险分析师', area:'看跌论点', initials:'WJ', color:'#f87171', verdict:'sell', verdict_text:'卖出' }, { id:'chairman', name:'Yang John', title:'圆桌主席 · 首席投资官', area:'最终判决', initials:'YJ', color:'#c9a84c', verdict:'hold', verdict_text:'综合裁定' } ] if (!authOk) { const rpEmptyP = document.querySelector('.rp-empty-p') if (rpEmptyP) { rpEmptyP.innerHTML = `请通过首页九宫格进入
或使用正确的访问链接

${esc(hintMessage)}` } } } const positionSeats = () => { const wrap = el('tcWrap') if (!wrap || !analysts.length) return const cx = 258, cy = 258, r = 210 const total = analysts.length analysts.forEach((a, i) => { const angle = -Math.PI / 2 + (2 * Math.PI * i) / total const left = cx + r * Math.cos(angle) - 25 const top = cy + r * Math.sin(angle) - 25 const seat = el(`seat-${a.id}`) if (seat) { seat.style.left = `${left}px`; seat.style.top = `${top}px` } }) } const renderSeats = () => { const wrap = el('tcWrap') if (!wrap) return wrap.querySelectorAll('.seat').forEach(s => s.remove()) analysts.forEach(a => { const div = document.createElement('div') div.className = 'seat' div.id = `seat-${a.id}` div.innerHTML = `
${esc(a.initials)} ${esc((a.verdictText || a.verdict_text || '').charAt(0))}
${esc(a.name)}
${esc(a.title)}
` div.onclick = () => selectAnalyst(a.id) wrap.appendChild(div) }) positionSeats() } const renderBottomChips = () => { const container = el('botChips') if (!container) return container.innerHTML = '' analysts.forEach(a => { const chip = document.createElement('div') chip.className = `chip${activeAnalystId === a.id ? ' active' : ''}` chip.id = `chip-${a.id}` chip.innerHTML = `${esc(a.name)}` chip.onclick = () => selectAnalyst(a.id) container.appendChild(chip) }) } const renderProgress = () => { const container = el('progList') if (!container) return container.innerHTML = '' const verdicts = (scenarioConfig && scenarioConfig.verdicts) || { buy: { label: '买入', color: '#4ade80' }, hold: { label: '持有', color: '#f59e0b' }, sell: { label: '卖出', color: '#f87171' } } const verdictKeys = Object.keys(verdicts) const total = analysts.length const done = Object.keys(reports).filter(k => reports[k] && !reports[k].error).length analysts.forEach(a => { const r = reports[a.id] const isDone = r && !r.error && !r._loading const isLoading = r && r._loading const div = document.createElement('div') div.className = 'prog-item' div.style.opacity = isDone ? '1' : isLoading ? '.7' : '.35' div.innerHTML = `
${esc(a.initials)}
${esc(a.name)}
${isLoading ? '分析中…' : isDone ? '已完成' : '待开始'}
` div.onclick = () => r && selectAnalyst(a.id) container.appendChild(div) }) if (done > 0) { const conBar = el('conBar') const verdictCounts = {} verdictKeys.forEach(v => { verdictCounts[v] = Object.values(reports).filter(r => r && r.verdict === v && !r.error).length }) if (conBar) { const totalDone = Object.values(verdictCounts).reduce((s, v) => s + v, 0) || 1 conBar.innerHTML = verdictKeys.map(v => `
`).join('') } const conLbls = el('conLbls') if (conLbls) { conLbls.innerHTML = verdictKeys.map(v => `
${verdicts[v].label} ${verdictCounts[v]}/${total}
`).join('') } } } const selectAnalyst = id => { activeAnalystId = id const r = reports[id] renderSeats() renderBottomChips() const active = el(`seat-${id}`) if (active) { document.querySelectorAll('.seat.active').forEach(s => s.classList.remove('active')) active.classList.add('active') } if (r && !r._loading) { renderReport(id) } else if (!r) { showEmptyReport() } } const showEmptyReport = () => { const empty = el('rpEmpty') const content = el('rpContent') if (empty) empty.style.display = '' if (content) content.classList.remove('show') } const showLoadingReport = () => { const empty = el('rpEmpty') const content = el('rpContent') if (empty) empty.style.display = 'none' if (content) { content.classList.add('show') content.innerHTML = '
分析中…
' } } const renderReport = id => { const r = reports[id] if (!r) { showEmptyReport(); return } const a = analysts.find(x => x.id === id) || {} const empty = el('rpEmpty') const content = el('rpContent') if (empty) empty.style.display = 'none' if (!content) return content.classList.add('show') if (r.error) { const vColor = (scenarioConfig && scenarioConfig.verdicts && scenarioConfig.verdicts[a.verdict] && scenarioConfig.verdicts[a.verdict].color) || '#888' content.innerHTML = `
${esc(a.initials)}
${esc(a.name)}
${esc(a.title)}
${esc(a.verdict_text || a.verdictText || '')}
分析失败:${esc(r.error)}
` return } if (r._loading) { const vColor2 = (scenarioConfig && scenarioConfig.verdicts && scenarioConfig.verdicts[a.verdict] && scenarioConfig.verdicts[a.verdict].color) || '#888' content.innerHTML = `
${esc(a.initials)}
${esc(a.name)}
${esc(a.title)}
${esc(a.verdict_text || a.verdictText || '')}
分析报告中…
` return } const tokenInfo = r.tokens ? `${r.tokens.total} tokens · ${r.request_ms}ms` : '' const vColor3 = (scenarioConfig && scenarioConfig.verdicts && scenarioConfig.verdicts[a.verdict] && scenarioConfig.verdicts[a.verdict].color) || '#888' content.innerHTML = `
${esc(a.initials)}
${esc(a.name)}
${esc(a.title)}
${esc(a.verdict_text || a.verdictText || '')} ${tokenInfo ? `${esc(tokenInfo)}` : ''}
${esc(r.report)}
` } const copyReport = async id => { const r = reports[id] if (!r || !r.report) return try { await copyText(r.report) if (uiMode === 'normal') { showToast('已复制到剪贴板', 'ok') } } catch { if (uiMode === 'normal') { showToast('复制失败', 'err') } } } let toastTimer = null const showToast = (msg, type) => { if (toastTimer) clearTimeout(toastTimer) let el = document.getElementById('yzp-toast') if (!el) { el = document.createElement('div') el.id = 'yzp-toast' el.style.cssText = 'position:fixed;top:20px;left:50%;transform:translateX(-50%);z-index:9999;padding:8px 20px;border-radius:20px;font-size:12px;font-weight:600;pointer-events:none;transition:opacity .3s;' document.body.appendChild(el) } if (type === 'ok') { el.style.background = '#166534'; el.style.color = '#86efac' } else if (type === 'err') { el.style.background = '#991b1b'; el.style.color = '#fca5a5' } else { el.style.background = '#1e3a5f'; el.style.color = '#93c5fd' } el.textContent = msg el.style.opacity = '1' toastTimer = setTimeout(() => { el.style.opacity = '0' }, 2000) } const updateChairmanInfo = () => { const ceoName = el('ceo-name') const ceoTitle = el('ceo-title') const chairman = analysts.find(a => a.id === 'chairman' || a.id === 'cio' || a.id === 'final_verdict') if (ceoName && chairman) ceoName.textContent = chairman.initials || '主' if (ceoTitle && chairman) ceoTitle.textContent = `${chairman.name} · ${chairman.title}` } const updateDate = () => { const d = new Date() const pad = n => String(n).padStart(2, '0') const el2 = document.getElementById('dateEl') if (el2) el2.textContent = `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}` } const setButtonLoading = (btn, loading) => { if (!btn) return if (loading) { btn._origText = btn.textContent btn.disabled = true btn.textContent = '分析中…' } else { btn.disabled = false btn.textContent = btn._origText || '开始分析' } } const updateCenterTable = (code, label) => { const codeEl = el('ts-code') const nameEl = el('ts-name') const priceEl = el('ts-price') const chgEl = el('ts-chg') if (codeEl) codeEl.textContent = code || '输入议题' if (nameEl) nameEl.textContent = label || code || '待分析议题' if (priceEl) priceEl.textContent = '--' if (chgEl) { chgEl.textContent = label ? '分析完成' : '等待分析'; chgEl.className = label ? 'ts-chg' : 'ts-chg' } } const updateSidebar = (code, resultSummary) => { const companyEl = el('sb-company') const sectorEl = el('sb-sector') const codeEl = el('sb-code') const doneEl = el('sb-done') const sbTitle = (scenarioConfig && scenarioConfig.sidebar && scenarioConfig.sidebar.consensus_title) || '共识评级' const consensusLabel = el('consensus-label') if (consensusLabel) consensusLabel.textContent = sbTitle if (companyEl) companyEl.textContent = code || '待分析标的' if (sectorEl) sectorEl.textContent = resultSummary ? '会议已完成' : ((scenarioConfig && scenarioConfig.input && scenarioConfig.input.placeholder) || '输入议题') if (codeEl) codeEl.textContent = code || '--' if (doneEl) doneEl.textContent = resultSummary ? `${resultSummary.success || 0} / ${resultSummary.total || analysts.length}` : '0' const sbCount = el('sb-count') if (sbCount) sbCount.textContent = analysts.length + '人' const badge = el('live-badge') if (badge) badge.innerHTML = resultSummary ? '会议已结束' : '会议待启' } const startAnalysis = async () => { const input = el('hd-search-input') if (!input) return const code = String(input.value || '').trim() if (!code) return queryCode = code const searchBtn = el('hd-search-btn') setButtonLoading(searchBtn, true) reports = {} analysts.forEach(a => { reports[a.id] = { _loading: true, verdict: a.verdict, verdictText: a.verdictText || a.verdict_text || '' } }) activeAnalystId = null renderSeats() renderBottomChips() renderProgress() updateCenterTable(queryCode) updateSidebar(queryCode, null) showEmptyReport() updateDate() if (uiMode === 'debug') openDialog() appendLog(`开始圆桌分析,标的:${code}`) appendLog(`分析师人数:${analysts.length}`, 'info') try { const resp = await fetch('/api/yuanzhupai/analyze', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code, version: el('hd-version') ? el('hd-version').value : 'x', scenario: currentScenarioId, imageBase64, userProfile }) }) if (resp.status === 401) throw new Error(`未登录:${hintMessage}`) const data = await resp.json().catch(() => null) if (!resp.ok || !data || !data.ok) throw new Error((data && data.error) || `请求失败: ${resp.status}`) const result = data.result const apiLogs = Array.isArray(data.logs) ? data.logs : [] apiLogs.forEach(l => appendLog(l)) reports = {} ;(result.analysts || []).forEach(r => { reports[r.id] = { report: r.report || '', error: r.error || null, tokens: r.tokens || { prompt: 0, completion: 0, total: 0 }, request_ms: r.request_ms || 0, verdict: r.verdict, verdictText: r.verdictText, _loading: false } const seat = el(`seat-${r.id}`) if (seat) seat.classList.remove('loading') }) renderSeats() renderBottomChips() renderProgress() updateCenterTable(queryCode, queryCode) updateSidebar(queryCode, result.summary) if (uiMode === 'debug') { finishDialog() } else { closeDialog() const successCount = result.summary ? result.summary.success : (result.analysts || []).filter(x => !x.error).length showToast(`${successCount}/${analysts.length} 位分析师完成报告`, 'ok') } } catch (e) { const errMsg = String(e.message || e) const isAuth = errMsg.includes('未登录') || errMsg.includes('unauthorized') appendLog(`分析失败:${errMsg}`, 'err') if (uiMode === 'debug') { failDialog(e) } else { showToast(`分析失败:${errMsg}`, 'err') } if (isAuth) { const empty = el('rpEmpty') const content = el('rpContent') if (empty && content) { empty.style.display = 'none' content.classList.add('show') content.innerHTML = `
🔐
需要身份验证
请通过首页九宫格点击 圆桌派 进入
或使用 GUID 链接直接访问
🏠 从首页九宫格进入
` } } analysts.forEach(a => { if (reports[a.id] && reports[a.id]._loading) { reports[a.id] = { error: e.message || e, _loading: false } const seat = el(`seat-${a.id}`) if (seat) seat.classList.remove('loading') } }) renderSeats() renderProgress() } finally { setButtonLoading(searchBtn, false) updateDate() } } const updateImageBar = () => { const bar = el('imageBar') if (!bar) return const inputType = (scenarioConfig && scenarioConfig.input && scenarioConfig.input.type) || 'text' if (inputType === 'image_text') { bar.style.display = '' } else { bar.style.display = 'none' resetImageUI() } } const resetImageUI = () => { imageBase64 = '' const preview = el('imagePreview') const wrap = el('imagePreviewWrap') if (preview) preview.src = '' if (wrap) wrap.style.display = 'none' const btn = el('imageUploadBtn') if (btn) btn.textContent = '📷 上传穿搭照片' const fileInput = el('imageFileInput') if (fileInput) fileInput.value = '' } const loadDefaultProfile = () => { if (scenarioConfig && scenarioConfig.input && scenarioConfig.input.user_profile) { userProfile = JSON.parse(JSON.stringify(scenarioConfig.input.user_profile)) } else { userProfile = null } updateProfileSummary() } const updateProfileSummary = () => { const el = document.getElementById('profileSummary') if (!el) return if (!userProfile) { el.textContent = ''; return } const p = userProfile const parts = [] parts.push(`年龄:${p.age || '--'}岁`) if (p.industry) parts.push(p.industry) parts.push(`${p.height || '--'}cm/${p.weight || '--'}kg`) el.textContent = parts.join(' · ') } const openProfileModal = () => { const panel = el('yp-profile-panel') if (!panel) return const p = userProfile || {} const goal = Array.isArray(p.goal) ? p.goal.join(', ') : (p.goal || '') const style = Array.isArray(p.style_preference) ? p.style_preference.join(', ') : (p.style_preference || '') const setVal = (id, val) => { const e = document.getElementById(id); if (e) e.value = val || '' } setVal('pf-age', p.age || '') setVal('pf-industry', p.industry || '') setVal('pf-height', p.height || '') setVal('pf-weight', p.weight || '') setVal('pf-goal', goal) setVal('pf-style', style) panel.classList.add('show') } const closeProfileModal = () => { const panel = el('yp-profile-panel') if (panel) panel.classList.remove('show') } const saveProfile = () => { const getVal = id => { const e = document.getElementById(id); return e ? String(e.value || '').trim() : '' } userProfile = { age: parseInt(getVal('pf-age'), 10) || 0, industry: getVal('pf-industry'), height: parseInt(getVal('pf-height'), 10) || 0, weight: parseInt(getVal('pf-weight'), 10) || 0, goal: getVal('pf-goal').split(/[,,]/).map(s => s.trim()).filter(Boolean), style_preference: getVal('pf-style').split(/[,,]/).map(s => s.trim()).filter(Boolean) } updateProfileSummary() closeProfileModal() } const populateVersionSelect = sid => { const sel = el('hd-version') if (!sel) return const scenario = scenariosData.find(s => s.id === sid) || scenariosData[0] const variants = (scenario && scenario.variants) || {} const keys = Object.keys(variants) if (!keys.length) return const currentVal = sel.value sel.innerHTML = keys.map(k => ``).join('') if (keys.includes(currentVal)) { sel.value = currentVal } } const copyAllLogs = async () => { const box = el('ydp-dialog-logs') if (!box) return const text = Array.from(box.querySelectorAll('.log-line')).map(d => d.textContent).join('\n') try { await copyText(text) } catch {} } const bindEvents = () => { const searchBtn = el('hd-search-btn') if (searchBtn) searchBtn.onclick = startAnalysis const searchInput = el('hd-search-input') if (searchInput) { searchInput.onkeydown = e => { if (e.key === 'Enter') startAnalysis() } } const scenarioSelect = el('hd-scenario') if (scenarioSelect) { scenarioSelect.onchange = async () => { reports = {} activeAnalystId = null resetImageUI() const sid = scenarioSelect.value populateVersionSelect(sid) const ver = el('hd-version') ? el('hd-version').value : 'x' await loadConfig(ver, sid) loadDefaultProfile() updateImageBar() renderSeats() renderBottomChips() renderProgress() updateChairmanInfo() updateSidebar(queryCode || '', null) showEmptyReport() const input = el('hd-search-input') if (input && scenarioConfig && scenarioConfig.input) { input.placeholder = scenarioConfig.input.placeholder || '请输入…' } } } const historyBtn = el('hd-history-btn') if (historyBtn) historyBtn.onclick = openHistory const historyClose = el('yp-history-close') if (historyClose) historyClose.onclick = closeHistory const historyBack = el('yp-history-back') if (historyBack) { historyBack.style.display = 'none' historyBack.onclick = openHistory } const historyCloseBottom = el('yp-history-close-bottom') if (historyCloseBottom) historyCloseBottom.onclick = closeHistory const versionSelect = el('hd-version') if (versionSelect) { versionSelect.onchange = () => { reports = {} activeAnalystId = null const sid = el('hd-scenario') ? el('hd-scenario').value : 'stock_invest' loadConfig(versionSelect.value, sid).then(() => { renderSeats() renderBottomChips() renderProgress() updateChairmanInfo() updateImageBar() showEmptyReport() }) } } const dialogClose = el('ydp-dialog-close') if (dialogClose) dialogClose.onclick = closeDialog const dialogCopy = el('ydp-dialog-copy') if (dialogCopy) dialogCopy.onclick = copyAllLogs const dialogCloseBtn = el('ydp-dialog-close-bottom') if (dialogCloseBtn) dialogCloseBtn.onclick = closeDialog const imageUploadBtn = el('imageUploadBtn') const imageFileInput = el('imageFileInput') if (imageUploadBtn && imageFileInput) { imageUploadBtn.onclick = () => imageFileInput.click() imageFileInput.onchange = () => { const file = imageFileInput.files && imageFileInput.files[0] if (!file) return const reader = new FileReader() reader.onload = () => { imageBase64 = reader.result const preview = el('imagePreview') const wrap = el('imagePreviewWrap') if (preview) preview.src = imageBase64 if (wrap) wrap.style.display = '' if (imageUploadBtn) imageUploadBtn.textContent = '📷 已上传照片' } reader.readAsDataURL(file) } } const imageRemoveBtn = el('imageRemoveBtn') if (imageRemoveBtn) { imageRemoveBtn.onclick = () => { imageBase64 = '' const preview = el('imagePreview') const wrap = el('imagePreviewWrap') if (preview) preview.src = '' if (wrap) wrap.style.display = 'none' if (imageUploadBtn) imageUploadBtn.textContent = '📷 上传穿搭照片' const fileInput = el('imageFileInput') if (fileInput) fileInput.value = '' } } const profileEditBtn = el('profileEditBtn') if (profileEditBtn) profileEditBtn.onclick = openProfileModal const profileClose = el('yp-profile-close') if (profileClose) profileClose.onclick = closeProfileModal const profileSave = el('yp-profile-save') if (profileSave) profileSave.onclick = saveProfile } const init = async () => { await fetchHint() const scenarioSelect = el('hd-scenario') const versionSelect = el('hd-version') const sid = scenarioSelect ? scenarioSelect.value : 'stock_invest' if (scenarioSelect) { try { const resp = await fetch('/api/yuanzhupai/scenarios') const data = await resp.json().catch(() => null) if (data && data.ok && Array.isArray(data.scenarios) && data.scenarios.length) { scenariosData = data.scenarios scenarioSelect.innerHTML = data.scenarios.map(s => ``).join('') populateVersionSelect(sid) } } catch {} } const ver = versionSelect ? versionSelect.value : 'x' await loadConfig(ver, sid) loadDefaultProfile() updateImageBar() if (scenarioConfig && scenarioConfig.input) { const input = el('hd-search-input') if (input) input.placeholder = scenarioConfig.input.placeholder || '请输入…' } renderSeats() renderBottomChips() renderProgress() updateChairmanInfo() updateDate() bindEvents() setInterval(updateDate, 60000) } document.addEventListener('DOMContentLoaded', init) const openHistory = async () => { const panel = el('yp-history-panel') const list = el('yp-history-list') const backBtn = el('yp-history-back') if (backBtn) backBtn.style.display = 'none' if (panel) panel.classList.add('show') if (list) list.innerHTML = '
加载中…
' try { const resp = await fetch('/api/yuanzhupai/sessions?limit=20') const data = await resp.json().catch(() => null) if (!data || !data.ok || !Array.isArray(data.sessions)) throw new Error('获取失败') if (!data.sessions.length) { if (list) list.innerHTML = '
暂无历史会议记录
'; return } if (list) { list.innerHTML = data.sessions.map(s => { const d = new Date(s.created_at) const ts = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}` return `
${esc(s.query_code)}
${ts} · ${esc(s.version || 'x')}版本
→
` }).join('') } } catch (e) { if (list) list.innerHTML = `
加载失败:${esc(String(e.message || e))}
` } } const loadHistorySession = async sessionId => { const list = el('yp-history-list') const backBtn = el('yp-history-back') if (!list) return list.innerHTML = '
加载会议记录…
' try { const resp = await fetch('/api/yuanzhupai/sessions/' + sessionId) const data = await resp.json().catch(() => null) if (!data || !data.ok || !data.session) throw new Error('session_not_found') const s = data.session const reports = Array.isArray(s.reports) ? s.reports : [] if (backBtn) backBtn.style.display = '' list.innerHTML = `
${esc(s.query_code)}
${esc(s.created_at)} · ${esc(s.version || 'x')}版本 · ${reports.length}位分析师
` reports.forEach(r => { const div = document.createElement('div') div.style.cssText = 'padding:12px 16px;border-bottom:1px solid var(--border-sub)' div.innerHTML = `
${esc(r.analyst_name)} ${esc(r.analyst_title)} ${r.tokens_used || 0}t · ${r.request_ms || 0}ms
${esc(r.report_text)}
` list.appendChild(div) }) } catch (e) { list.innerHTML = `
加载失败:${esc(String(e.message || e))}
` } } const closeHistory = () => { const panel = el('yp-history-panel') if (panel) panel.classList.remove('show') } return { copyReport, openHistory, loadHistorySession, closeHistory } })()