mirror of
https://github.com/adminlove520/AI-Account-Toolkit.git
synced 2026-05-16 09:26:46 +08:00
503 lines
22 KiB
JavaScript
503 lines
22 KiB
JavaScript
/* ===== Auth ===== */
|
||
const TOKEN = localStorage.getItem('adminToken');
|
||
const api = (url, opts = {}) => {
|
||
opts.headers = { ...opts.headers, 'Authorization': 'Bearer ' + TOKEN, 'Content-Type': 'application/json' };
|
||
return fetch(url, opts).then(r => { if (r.status === 401) { logout(); throw new Error('unauthorized'); } return r.json(); });
|
||
};
|
||
|
||
function checkAuth() {
|
||
if (!TOKEN) { location.href = '/static/login.html'; return; }
|
||
api('/admin/status').catch(() => logout());
|
||
}
|
||
|
||
function logout() {
|
||
localStorage.removeItem('adminToken');
|
||
location.href = '/static/login.html';
|
||
}
|
||
|
||
/* ===== Toast ===== */
|
||
function showToast(msg, type = 'info') {
|
||
const colors = { success: 'bg-green-600', error: 'bg-red-600', info: 'bg-gray-900' };
|
||
const el = document.createElement('div');
|
||
el.className = `fixed bottom-4 right-4 ${colors[type] || colors.info} text-white px-4 py-2.5 rounded-lg shadow-lg text-sm font-medium z-[200] animate-slide-up`;
|
||
el.textContent = msg;
|
||
document.body.appendChild(el);
|
||
setTimeout(() => { el.style.opacity = '0'; el.style.transition = 'opacity .3s'; setTimeout(() => el.remove(), 300); }, 2000);
|
||
}
|
||
|
||
/* ===== Tabs ===== */
|
||
function switchTab(tab) {
|
||
['accounts', 'settings', 'chat'].forEach(t => {
|
||
document.getElementById('panel' + t.charAt(0).toUpperCase() + t.slice(1)).classList.toggle('hidden', t !== tab);
|
||
const btn = document.getElementById('tab' + t.charAt(0).toUpperCase() + t.slice(1));
|
||
btn.classList.toggle('border-primary', t === tab);
|
||
btn.classList.toggle('border-transparent', t !== tab);
|
||
btn.classList.toggle('text-muted-foreground', t !== tab);
|
||
});
|
||
if (tab === 'settings') loadSettings();
|
||
if (tab === 'accounts') loadAccounts();
|
||
if (tab === 'chat') loadChatModels();
|
||
}
|
||
|
||
/* ===== Accounts ===== */
|
||
let _accounts = [];
|
||
|
||
function loadAccounts() {
|
||
api('/admin/accounts').then(d => {
|
||
_accounts = d.accounts || [];
|
||
const s = d.stats || {};
|
||
document.getElementById('statTotal').textContent = s.total ?? _accounts.length;
|
||
document.getElementById('statActive').textContent = s.active ?? '-';
|
||
document.getElementById('statCost').textContent = '$' + (s.cost ?? 0).toFixed(2);
|
||
document.getElementById('statRequests').textContent = s.requests ?? '-';
|
||
renderAccounts();
|
||
}).catch(() => showToast('加载账号失败', 'error'));
|
||
}
|
||
|
||
let _pageSize = 20;
|
||
let _currentPage = 1;
|
||
|
||
function renderAccounts() {
|
||
const box = document.getElementById('accountList');
|
||
const filter = document.getElementById('accountFilter').value;
|
||
const filtered = _accounts.map((a, i) => ({ ...a, _idx: i })).filter(a => {
|
||
if (filter === 'active') return a.active === true;
|
||
if (filter === 'expired') return a.active !== true;
|
||
return true;
|
||
});
|
||
const totalPages = Math.max(1, Math.ceil(filtered.length / _pageSize));
|
||
if (_currentPage > totalPages) _currentPage = totalPages;
|
||
const start = (_currentPage - 1) * _pageSize;
|
||
const paged = filtered.slice(start, start + _pageSize);
|
||
if (!filtered.length) {
|
||
box.innerHTML = `<tr><td colspan="6" class="text-center text-muted-foreground py-6 text-sm">${_accounts.length ? '无匹配账号' : '暂无账号,点击右上角添加'}</td></tr>`;
|
||
} else {
|
||
box.innerHTML = paged.map(a => {
|
||
const active = a.active === true;
|
||
const atTag = a.at_mask ? `<span class="text-green-600 font-mono text-xs">${a.at_mask}</span>` : '<span class="text-red-500">无</span>';
|
||
const rtTag = a.rt_mask ? `<span class="text-green-600 font-mono text-xs">${a.rt_mask}</span>` : '<span class="text-red-500">无</span>';
|
||
const statusHtml = active
|
||
? `<span class="inline-flex items-center gap-1.5 text-green-600"><span class="w-1.5 h-1.5 rounded-full bg-green-500"></span><span data-expires="${a.expires_at}"></span></span>`
|
||
: `<span class="inline-flex items-center gap-1.5 text-muted-foreground"><span class="w-1.5 h-1.5 rounded-full bg-gray-400"></span>已过期</span>`;
|
||
const checked = _selected.has(a._idx) ? 'checked' : '';
|
||
return `<tr class="border-b border-border hover:bg-secondary/50 transition-colors">
|
||
<td class="px-4 py-2.5 w-8"><input type="checkbox" data-idx="${a._idx}" ${checked} onchange="toggleSelect(${a._idx})" class="rounded"></td>
|
||
<td class="px-4 py-2.5 font-medium">${a.email || '未知邮箱'}</td>
|
||
<td class="px-4 py-2.5">${atTag}</td>
|
||
<td class="px-4 py-2.5">${rtTag}</td>
|
||
<td class="px-4 py-2.5">${statusHtml}</td>
|
||
<td class="px-4 py-2.5 text-right">
|
||
<button onclick="refreshAccount(${a._idx})" class="text-xs px-2 py-1 rounded border border-border hover:bg-secondary transition-colors">刷新</button>
|
||
<button onclick="removeAccount(${a._idx})" class="text-xs px-2 py-1 rounded border border-red-200 text-red-600 hover:bg-red-50 transition-colors ml-1">删除</button>
|
||
</td>
|
||
</tr>`;
|
||
}).join('');
|
||
}
|
||
const activeCount = _accounts.filter(a => a.active === true).length;
|
||
const selCount = _selected.size;
|
||
const selText = selCount ? `已选 ${selCount} | ` : '';
|
||
// pagination
|
||
let pageHtml = '';
|
||
if (totalPages > 1) {
|
||
pageHtml = `<button onclick="changePage(-1)" class="px-2 py-0.5 rounded border border-border hover:bg-secondary text-xs" ${_currentPage <= 1 ? 'disabled' : ''}><</button>
|
||
<span class="mx-1">${_currentPage}/${totalPages}</span>
|
||
<button onclick="changePage(1)" class="px-2 py-0.5 rounded border border-border hover:bg-secondary text-xs" ${_currentPage >= totalPages ? 'disabled' : ''}>></button>`;
|
||
}
|
||
document.getElementById('accountFooter').innerHTML = `<div class="flex items-center justify-between">
|
||
<span>${selText}显示 ${filtered.length} / ${_accounts.length} 个账号(活跃 ${activeCount})</span>
|
||
<div class="flex items-center gap-2">
|
||
${pageHtml}
|
||
<select onchange="changePageSize(this.value)" class="text-xs h-6 rounded border border-input bg-background px-1">
|
||
${[10,20,50,100,200,500,1000].map(n => `<option value="${n}" ${n===_pageSize?'selected':''}>${n}条/页</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
</div>`;
|
||
document.getElementById('selectAll').checked = paged.length > 0 && paged.every(a => _selected.has(a._idx));
|
||
updateCountdowns();
|
||
}
|
||
|
||
let _selected = new Set();
|
||
function toggleSelect(idx) {
|
||
_selected.has(idx) ? _selected.delete(idx) : _selected.add(idx);
|
||
renderAccounts();
|
||
}
|
||
function toggleSelectAll() {
|
||
const all = document.getElementById('selectAll').checked;
|
||
const filter = document.getElementById('accountFilter').value;
|
||
const filtered = _accounts.map((a, i) => ({ ...a, _idx: i })).filter(a => {
|
||
if (filter === 'active') return a.active === true;
|
||
if (filter === 'expired') return a.active !== true;
|
||
return true;
|
||
});
|
||
const start = (_currentPage - 1) * _pageSize;
|
||
const paged = filtered.slice(start, start + _pageSize);
|
||
paged.forEach(a => all ? _selected.add(a._idx) : _selected.delete(a._idx));
|
||
renderAccounts();
|
||
}
|
||
function getSelectedIndices() { return [..._selected]; }
|
||
function changePageSize(v) { _pageSize = parseInt(v); _currentPage = 1; renderAccounts(); }
|
||
function changePage(delta) { _currentPage += delta; renderAccounts(); }
|
||
|
||
function updateCountdowns() {
|
||
document.querySelectorAll('[data-expires]').forEach(el => {
|
||
const exp = parseInt(el.dataset.expires);
|
||
const diff = exp - Date.now();
|
||
if (diff <= 0) { el.textContent = '已过期'; el.className = 'text-red-500'; return; }
|
||
const d = Math.floor(diff / 86400000);
|
||
const h = Math.floor((diff % 86400000) / 3600000);
|
||
const m = Math.floor((diff % 3600000) / 60000);
|
||
let text = '';
|
||
if (d > 0) text += d + '天';
|
||
text += h + 'h ' + m + 'm';
|
||
el.textContent = text;
|
||
if (d < 1) el.className = 'text-orange-500';
|
||
});
|
||
}
|
||
if (!window._countdownTimer) window._countdownTimer = setInterval(updateCountdowns, 1000);
|
||
|
||
function refreshAccount(idx) {
|
||
api(`/admin/accounts/${idx}/refresh`, { method: 'POST' }).then(d => {
|
||
d.ok ? showToast('刷新成功', 'success') : showToast(d.error || '刷新失败', 'error');
|
||
loadAccounts();
|
||
});
|
||
}
|
||
|
||
function removeAccount(idx) {
|
||
if (!confirm('确定删除该账号?')) return;
|
||
api(`/admin/accounts/${idx}`, { method: 'DELETE' }).then(() => { showToast('已删除', 'success'); loadAccounts(); });
|
||
}
|
||
|
||
function refreshAllAccounts() {
|
||
api('/admin/refresh', { method: 'POST' }).then(d => {
|
||
d.ok ? showToast('全部刷新完成', 'success') : showToast('刷新失败', 'error');
|
||
loadAccounts();
|
||
});
|
||
}
|
||
|
||
function batchDeleteAccounts() {
|
||
if (!confirm('确定删除所有账号?')) return;
|
||
const indices = _accounts.map((_, i) => i);
|
||
api('/admin/accounts/batch-delete', { method: 'POST', body: JSON.stringify({ indices }) }).then(d => {
|
||
showToast(`已删除 ${d.removed} 个账号`, 'success'); loadAccounts();
|
||
});
|
||
}
|
||
|
||
function exportAccounts() {
|
||
api('/admin/accounts/export', { method: 'POST' }).then(d => {
|
||
const blob = new Blob([JSON.stringify(d.accounts, null, 2)], { type: 'application/json' });
|
||
const a = document.createElement('a');
|
||
a.href = URL.createObjectURL(blob); a.download = 'ob1_accounts.json'; a.click();
|
||
showToast('导出成功', 'success');
|
||
});
|
||
}
|
||
|
||
function importAccounts() {
|
||
const input = document.createElement('input');
|
||
input.type = 'file'; input.accept = '.json';
|
||
input.onchange = e => {
|
||
const file = e.target.files[0]; if (!file) return;
|
||
const reader = new FileReader();
|
||
reader.onload = ev => {
|
||
try {
|
||
const accounts = JSON.parse(ev.target.result);
|
||
api('/admin/accounts/import', { method: 'POST', body: JSON.stringify({ accounts: Array.isArray(accounts) ? accounts : [accounts] }) })
|
||
.then(d => { showToast(`导入 ${d.imported} 个账号`, 'success'); loadAccounts(); });
|
||
} catch { showToast('JSON 格式错误', 'error'); }
|
||
};
|
||
reader.readAsText(file);
|
||
};
|
||
input.click();
|
||
}
|
||
|
||
/* ===== Device Auth (Add Account) ===== */
|
||
let _pollTimer = null;
|
||
|
||
function openDeviceAuth() {
|
||
document.getElementById('deviceAuthModal').classList.remove('hidden');
|
||
document.getElementById('deviceAuthContent').classList.remove('hidden');
|
||
document.getElementById('deviceAuthPending').classList.add('hidden');
|
||
}
|
||
|
||
function startDeviceAuth() {
|
||
document.getElementById('deviceAuthContent').classList.add('hidden');
|
||
document.getElementById('deviceAuthPending').classList.remove('hidden');
|
||
api('/admin/device-auth', { method: 'POST' }).then(d => {
|
||
if (d.error) {
|
||
document.getElementById('deviceAuthContent').classList.remove('hidden');
|
||
document.getElementById('deviceAuthPending').classList.add('hidden');
|
||
showToast(d.error, 'error');
|
||
return;
|
||
}
|
||
const link = document.getElementById('deviceAuthLink');
|
||
link.href = d.verification_uri_complete || d.verification_uri;
|
||
link.textContent = d.verification_uri_complete || d.verification_uri;
|
||
document.getElementById('deviceAuthCode').textContent = d.user_code || '';
|
||
pollDeviceAuth(d.device_code, d.interval || 5);
|
||
}).catch(() => {
|
||
document.getElementById('deviceAuthContent').classList.remove('hidden');
|
||
document.getElementById('deviceAuthPending').classList.add('hidden');
|
||
showToast('获取授权失败', 'error');
|
||
});
|
||
}
|
||
|
||
function pollDeviceAuth(code, interval) {
|
||
clearInterval(_pollTimer);
|
||
_pollTimer = setInterval(() => {
|
||
api('/admin/device-auth/poll', { method: 'POST', body: JSON.stringify({ device_code: code }) }).then(d => {
|
||
if (d.status === 'complete') {
|
||
clearInterval(_pollTimer);
|
||
closeDeviceAuth();
|
||
showToast(`已添加账号: ${d.email}`, 'success');
|
||
loadAccounts();
|
||
} else if (d.status === 'expired' || d.status === 'error') {
|
||
clearInterval(_pollTimer);
|
||
showToast(d.message || '授权失败', 'error');
|
||
}
|
||
});
|
||
}, interval * 1000);
|
||
}
|
||
|
||
function closeDeviceAuth() {
|
||
clearInterval(_pollTimer);
|
||
document.getElementById('deviceAuthModal').classList.add('hidden');
|
||
}
|
||
|
||
/* ===== Settings ===== */
|
||
function loadSettings() {
|
||
api('/admin/settings').then(d => {
|
||
document.getElementById('cfgUsername').value = d.username || '';
|
||
document.getElementById('cfgCurrentKey').value = d.api_key || '';
|
||
document.getElementById('cfgProxy').value = d.proxy_url || '';
|
||
selectRotation(d.rotation_mode || 'cache-first', false);
|
||
document.getElementById('cfgDebugLog').checked = (d.log_level || 'INFO') === 'DEBUG';
|
||
document.getElementById('cfgRefreshInterval').value = d.refresh_interval || 0;
|
||
});
|
||
}
|
||
|
||
function updatePassword() {
|
||
const old_password = document.getElementById('cfgOldPwd').value;
|
||
const new_password = document.getElementById('cfgNewPwd').value;
|
||
if (!old_password || !new_password) { showToast('请填写完整', 'error'); return; }
|
||
api('/admin/settings/password', { method: 'POST', body: JSON.stringify({ old_password, new_password }) }).then(d => {
|
||
d.ok ? (showToast('密码已更新', 'success'), document.getElementById('cfgOldPwd').value = '', document.getElementById('cfgNewPwd').value = '') : showToast(d.message || '更新失败', 'error');
|
||
});
|
||
}
|
||
|
||
function updateUsername() {
|
||
const username = document.getElementById('cfgUsername').value.trim();
|
||
if (!username) return;
|
||
api('/admin/settings/username', { method: 'POST', body: JSON.stringify({ username }) }).then(d => {
|
||
d.ok ? showToast('用户名已更新', 'success') : showToast('更新失败', 'error');
|
||
});
|
||
}
|
||
|
||
function updateAPIKey() {
|
||
const api_key = document.getElementById('cfgNewKey').value.trim();
|
||
if (!api_key) return;
|
||
api('/admin/settings/api-key', { method: 'POST', body: JSON.stringify({ api_key }) }).then(d => {
|
||
d.ok ? showToast('API Key 已更新', 'success') : showToast('更新失败', 'error');
|
||
});
|
||
}
|
||
|
||
function updateProxy() {
|
||
const url = document.getElementById('cfgProxy').value.trim();
|
||
api('/admin/settings/proxy', { method: 'POST', body: JSON.stringify({ url }) }).then(d => {
|
||
d.ok ? showToast('代理已更新', 'success') : showToast('更新失败', 'error');
|
||
});
|
||
}
|
||
|
||
function testProxy() {
|
||
const url = document.getElementById('cfgProxy').value.trim();
|
||
if (!url) { showToast('请先填写代理地址', 'error'); return; }
|
||
const btn = document.getElementById('btnTestProxy');
|
||
btn.disabled = true; btn.textContent = '测试中...';
|
||
api('/admin/settings/proxy-test', { method: 'POST', body: JSON.stringify({ url }) }).then(d => {
|
||
if (d.ok) showToast('代理可用,IP: ' + d.ip, 'success');
|
||
else showToast('代理不可用: ' + d.error, 'error');
|
||
}).catch(() => showToast('测试请求失败', 'error'))
|
||
.finally(() => { btn.disabled = false; btn.textContent = '测试'; });
|
||
}
|
||
|
||
function selectRotation(mode, save = true) {
|
||
document.getElementById('cfgRotationMode').value = mode;
|
||
if (save) {
|
||
api('/admin/settings/rotation-mode', { method: 'POST', body: JSON.stringify({ mode }) }).then(d => {
|
||
d.ok ? showToast('调度模式已更新', 'success') : showToast(d.message || '更新失败', 'error');
|
||
});
|
||
}
|
||
}
|
||
|
||
function toggleDebugLog() {
|
||
const level = document.getElementById('cfgDebugLog').checked ? 'DEBUG' : 'INFO';
|
||
api('/admin/settings/log-level', { method: 'POST', body: JSON.stringify({ level }) }).then(d => {
|
||
d.ok ? showToast(level === 'DEBUG' ? '调试日志已开启' : '调试日志已关闭', 'success') : showToast(d.message || '更新失败', 'error');
|
||
});
|
||
}
|
||
|
||
function updateRefreshInterval() {
|
||
const interval = parseInt(document.getElementById('cfgRefreshInterval').value) || 0;
|
||
api('/admin/settings/refresh-interval', { method: 'POST', body: JSON.stringify({ interval }) }).then(d => {
|
||
d.ok ? showToast(interval > 0 ? `自动续期检查已设置为 ${interval} 分钟` : '自动续期已关闭', 'success') : showToast(d.message || '更新失败', 'error');
|
||
});
|
||
}
|
||
|
||
/* ===== Chat ===== */
|
||
let _chatMessages = [];
|
||
|
||
const TOP_MODELS = [
|
||
'anthropic/claude-opus-4.6',
|
||
'anthropic/claude-sonnet-4.6',
|
||
'openai/gpt-5.4-pro',
|
||
'google/gemini-3.1-flash-image-preview',
|
||
'openai/gpt-5.3-codex',
|
||
'x-ai/grok-4.1-fast',
|
||
'qwen/qwen-3.5-397b',
|
||
];
|
||
|
||
function _shortName(id) { return id.includes('/') ? id.split('/').pop() : id; }
|
||
|
||
function loadChatModels() {
|
||
const sel = document.getElementById('chatModel');
|
||
const prev = sel.value;
|
||
fetch('/v1/models', { headers: { 'Authorization': 'Bearer ' + TOKEN } })
|
||
.then(r => r.json())
|
||
.then(d => {
|
||
const apiIds = (d.data || []).map(m => m.id);
|
||
_fillModelSelect(sel, apiIds, prev);
|
||
})
|
||
.catch(() => _fillModelSelect(sel, [], prev));
|
||
}
|
||
|
||
function _fillModelSelect(sel, apiIds, prev) {
|
||
const all = new Set([...TOP_MODELS, ...apiIds]);
|
||
const topSet = new Set(TOP_MODELS);
|
||
const rest = [...all].filter(id => !topSet.has(id)).sort();
|
||
let html = '<optgroup label="常用">';
|
||
html += TOP_MODELS.map(id => `<option value="${id}">${_shortName(id)}</option>`).join('');
|
||
html += '</optgroup>';
|
||
if (rest.length) {
|
||
html += '<optgroup label="全部">';
|
||
html += rest.map(id => `<option value="${id}">${_shortName(id)}</option>`).join('');
|
||
html += '</optgroup>';
|
||
}
|
||
sel.innerHTML = html;
|
||
sel.value = prev && all.has(prev) ? prev : TOP_MODELS[0];
|
||
}
|
||
|
||
function _parseAssistantMsg(msg) {
|
||
const content = msg.content;
|
||
const result = { role: 'assistant', content: '', images: [] };
|
||
if (typeof content === 'string') {
|
||
result.content = content || '';
|
||
} else if (Array.isArray(content)) {
|
||
for (const part of content) {
|
||
if (part.type === 'text') result.content += part.text || '';
|
||
else if (part.type === 'image_url') result.images.push(part.image_url?.url || '');
|
||
}
|
||
}
|
||
// OB1/OpenRouter puts images in message.images
|
||
if (Array.isArray(msg.images)) {
|
||
for (const img of msg.images) {
|
||
if (img.image_url?.url) result.images.push(img.image_url.url);
|
||
else if (typeof img === 'string') result.images.push(img);
|
||
}
|
||
}
|
||
if (!result.content && !result.images.length) result.content = 'No response';
|
||
return result;
|
||
}
|
||
|
||
function sendChat() {
|
||
const input = document.getElementById('chatInput');
|
||
const msg = input.value.trim();
|
||
if (!msg) return;
|
||
input.value = '';
|
||
_chatMessages.push({ role: 'user', content: msg });
|
||
renderChat();
|
||
|
||
const model = document.getElementById('chatModel').value;
|
||
const stream = document.getElementById('chatStream').checked;
|
||
|
||
if (stream) {
|
||
streamChat(model, msg);
|
||
} else {
|
||
api('/v1/chat/completions', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ model, messages: _chatMessages, stream: false })
|
||
}).then(d => {
|
||
const msg = d.choices?.[0]?.message || {};
|
||
const parsed = _parseAssistantMsg(msg);
|
||
_chatMessages.push(parsed);
|
||
renderChat();
|
||
}).catch(() => {
|
||
_chatMessages.push({ role: 'assistant', content: '请求失败' });
|
||
renderChat();
|
||
});
|
||
}
|
||
}
|
||
|
||
function streamChat(model, msg) {
|
||
const assistantMsg = { role: 'assistant', content: '' };
|
||
_chatMessages.push(assistantMsg);
|
||
renderChat();
|
||
|
||
fetch('/v1/chat/completions', {
|
||
method: 'POST',
|
||
headers: { 'Authorization': 'Bearer ' + TOKEN, 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ model, messages: _chatMessages.slice(0, -1), stream: true })
|
||
}).then(resp => {
|
||
const reader = resp.body.getReader();
|
||
const decoder = new TextDecoder();
|
||
let buf = '';
|
||
function read() {
|
||
reader.read().then(({ done, value }) => {
|
||
if (done) { renderChat(); return; }
|
||
buf += decoder.decode(value, { stream: true });
|
||
const lines = buf.split('\n');
|
||
buf = lines.pop();
|
||
for (const line of lines) {
|
||
if (!line.startsWith('data: ') || line === 'data: [DONE]') continue;
|
||
try {
|
||
const j = JSON.parse(line.slice(6));
|
||
const delta = j.choices?.[0]?.delta?.content;
|
||
if (delta) { assistantMsg.content += delta; renderChat(); }
|
||
} catch {}
|
||
}
|
||
read();
|
||
});
|
||
}
|
||
read();
|
||
}).catch(() => { assistantMsg.content = '流式请求失败'; renderChat(); });
|
||
}
|
||
|
||
function renderChat() {
|
||
const box = document.getElementById('chatMessages');
|
||
box.innerHTML = _chatMessages.map(m => {
|
||
const isUser = m.role === 'user';
|
||
let rendered = typeof marked !== 'undefined' ? marked.parse(m.content || '') : (m.content || '');
|
||
if (m.images && m.images.length) {
|
||
rendered += m.images.map(url => `<img src="${url}" class="mt-2 max-w-full rounded-lg cursor-pointer" style="max-height:400px" onclick="window.open(this.src,'_blank')" alt="生成图片">`).join('');
|
||
}
|
||
return `<div class="flex ${isUser ? 'justify-end' : 'justify-start'} mb-3">
|
||
<div class="chat-msg max-w-[80%] rounded-lg px-4 py-2 text-sm ${isUser ? 'bg-primary text-primary-foreground' : 'bg-secondary'}">
|
||
${rendered}
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
box.scrollTop = box.scrollHeight;
|
||
document.querySelectorAll('.chat-msg pre code').forEach(el => hljs.highlightElement(el));
|
||
}
|
||
|
||
function clearChat() {
|
||
_chatMessages = [];
|
||
document.getElementById('chatMessages').innerHTML = '';
|
||
}
|
||
|
||
/* ===== Init ===== */
|
||
window.addEventListener('DOMContentLoaded', () => {
|
||
checkAuth();
|
||
loadAccounts();
|
||
document.getElementById('chatInput').addEventListener('keydown', e => {
|
||
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendChat(); }
|
||
});
|
||
});
|