/* ═══════════════════════════════════════════════════════════
* 光湖 · 微调模型内测 · 前端逻辑
* 守护: 铸渊 · ICE-GL-ZY001
* 版权: 国作登字-2026-A-00037559
* ═══════════════════════════════════════════════════════════ */
(function () {
'use strict';
// ─── 配置 ───
var API_BASE = (function () {
if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
// 本地开发: 后端默认 PORT=3000 (src/index.js)
return 'http://' + location.hostname + ':3000';
}
// 生产: 同域走 Nginx 反代
return location.protocol + '//' + location.host;
})();
var TOKEN_KEY = 'ftchat_token';
var EMAIL_KEY = 'ftchat_email';
var USER_HASH_KEY = 'ftchat_user_hash';
var SLOT_KEY = 'ftchat_slot';
// ─── 状态 ───
var token = null;
var email = null;
var userHash = null;
var slotIndex = null;
var currentSessionId = null;
var sessions = []; // 服务端列表(左侧栏)
var localSessions = {}; // localStorage: sessionId → {messages, title}
var isStreaming = false;
// ─── DOM ───
var $ = function (id) { return document.getElementById(id); };
var loginScreen = $('loginScreen');
var chatScreen = $('chatScreen');
var emailInput = $('emailInput');
var codeInput = $('codeInput');
var sendCodeBtn = $('sendCodeBtn');
var loginBtn = $('loginBtn');
var loginForm = $('loginForm');
var loginMsg = $('loginMsg');
var slotsBanner = $('slotsBanner');
var slotsText = $('slotsText');
var sessionsList = $('sessionsList');
var newChatBtn = $('newChatBtn');
var sidebar = $('sidebar');
var sidebarOverlay = $('sidebarOverlay');
var sidebarToggle = $('sidebarToggle');
var sidebarClose = $('sidebarClose');
var chatBody = $('chatBody');
var msgInput = $('msgInput');
var sendBtn = $('sendBtn');
var composer = $('composer');
var logoutBtn = $('logoutBtn');
var userBadge = $('userBadge');
var slotBadgeEl = $('slotIndex');
var metaBar = $('metaBar');
// ─── 工具 ───
function loadStorage() {
try {
token = localStorage.getItem(TOKEN_KEY);
email = localStorage.getItem(EMAIL_KEY);
userHash = localStorage.getItem(USER_HASH_KEY);
slotIndex = localStorage.getItem(SLOT_KEY);
} catch (_e) { /* ignore */ }
}
function persistSession() {
try {
localStorage.setItem(TOKEN_KEY, token);
localStorage.setItem(EMAIL_KEY, email);
localStorage.setItem(USER_HASH_KEY, userHash);
localStorage.setItem(SLOT_KEY, String(slotIndex));
} catch (_e) { /* ignore */ }
}
function clearSession() {
token = email = userHash = slotIndex = null;
try {
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(EMAIL_KEY);
localStorage.removeItem(USER_HASH_KEY);
localStorage.removeItem(SLOT_KEY);
} catch (_e) { /* ignore */ }
}
function localKey(suffix) {
return 'ftchat_' + (userHash || 'anon') + '_' + suffix;
}
function loadLocalSessions() {
try {
var raw = localStorage.getItem(localKey('sessions'));
localSessions = raw ? JSON.parse(raw) : {};
} catch (_e) { localSessions = {}; }
}
function saveLocalSessions() {
try {
localStorage.setItem(localKey('sessions'), JSON.stringify(localSessions));
} catch (_e) { /* quota or private mode */ }
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, function (c) {
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
});
}
// 使用 Web Crypto 生成随机字符 (避免 Math.random 在 ID 生成中的 CodeQL 警告)
function cryptoRandomId(len) {
var alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789';
var out = '';
if (window.crypto && window.crypto.getRandomValues) {
var arr = new Uint32Array(len);
window.crypto.getRandomValues(arr);
for (var i = 0; i < len; i++) out += alphabet[arr[i] % alphabet.length];
return out;
}
// 兜底 (不应在现代浏览器触发)
for (var j = 0; j < len; j++) out += alphabet[Math.floor(Math.random() * alphabet.length)]; // lgtm[js/insecure-randomness]
return out;
}
function renderMarkdown(text) {
if (typeof marked === 'undefined' || typeof DOMPurify === 'undefined') {
// 降级: 转义 + 换行
return escapeHtml(text).replace(/\n/g, '
');
}
try {
marked.setOptions({ breaks: true, gfm: true });
var html = marked.parse(text);
return DOMPurify.sanitize(html, {
FORBID_TAGS: ['style', 'script', 'iframe', 'object', 'embed'],
FORBID_ATTR: ['onclick', 'onerror', 'onload', 'style']
});
} catch (_e) {
return escapeHtml(text).replace(/\n/g, '
');
}
}
function showLoginMsg(text, isSuccess) {
loginMsg.textContent = text;
if (isSuccess) loginMsg.classList.add('success');
else loginMsg.classList.remove('success');
}
function setMetaBar(meta) {
if (!meta) { metaBar.textContent = ''; return; }
var parts = [];
if (meta.time_anchor) parts.push('🕒 ' + meta.time_anchor);
if (meta.prompt_source) parts.push('📖 ' + meta.prompt_source);
if (meta.model_variant) parts.push('🎯 ' + (meta.model_variant === 'naipping' ? '情感线' : '系统线'));
metaBar.textContent = parts.join(' · ');
}
// 字节级直连百炼后, 服务端不再下发 meta 帧 ——
// 这条 meta 完全由前端本地生成, 仅用于 UI 展示, 不进入对话上下文。
function localMeta(variant) {
var now = new Date();
var bj = now.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false });
return {
time_anchor: bj,
prompt_source: '直连百炼',
model_variant: variant === 'naipping' ? 'naipping' : 'system'
};
}
// ─── API 调用 ───
function api(path, opts) {
opts = opts || {};
var headers = Object.assign({ 'Content-Type': 'application/json' }, opts.headers || {});
if (token) headers['Authorization'] = 'Bearer ' + token;
return fetch(API_BASE + path, {
method: opts.method || 'GET',
headers: headers,
body: opts.body ? JSON.stringify(opts.body) : undefined
}).then(function (res) {
return res.json().then(function (data) {
if (!res.ok) {
var err = new Error(data.message || 'HTTP ' + res.status);
err.status = res.status;
err.data = data;
throw err;
}
return data;
});
});
}
// ─── 槽位查询 ───
function refreshSlots() {
fetch(API_BASE + '/hli/ftchat/status').then(function (r) { return r.json(); }).then(function (s) {
if (typeof s.slots_taken === 'number') {
slotsText.textContent = '当前席位: ' + s.slots_taken + ' / ' + s.slots_total +
(s.slots_remaining === 0 ? ' (已满,请等待成员退出)' : ' (剩余 ' + s.slots_remaining + ' 席)');
}
}).catch(function () {
slotsText.textContent = '席位状态查询失败';
});
}
// ─── 登录流程 ───
sendCodeBtn.addEventListener('click', function () {
var em = (emailInput.value || '').trim();
if (!em) { showLoginMsg('请填写 QQ 邮箱'); return; }
sendCodeBtn.disabled = true;
sendCodeBtn.textContent = '发送中…';
showLoginMsg('');
api('/hli/ftchat/login', { method: 'POST', body: { email: em } })
.then(function (resp) {
showLoginMsg(resp.message || '验证码已发送', true);
var seconds = 60;
var timer = setInterval(function () {
if (seconds <= 0) {
clearInterval(timer);
sendCodeBtn.disabled = false;
sendCodeBtn.textContent = '获取验证码';
} else {
sendCodeBtn.textContent = seconds + 's 后重发';
seconds--;
}
}, 1000);
})
.catch(function (err) {
sendCodeBtn.disabled = false;
sendCodeBtn.textContent = '获取验证码';
showLoginMsg(err.message || '发送失败');
refreshSlots();
});
});
loginForm.addEventListener('submit', function (ev) {
ev.preventDefault();
var em = (emailInput.value || '').trim();
var code = (codeInput.value || '').trim();
if (!em || !code) { showLoginMsg('请填写邮箱和验证码'); return; }
loginBtn.disabled = true;
loginBtn.textContent = '验证中…';
api('/hli/ftchat/verify', { method: 'POST', body: { email: em, code: code } })
.then(function (resp) {
if (!resp.success) throw new Error(resp.message || '验证失败');
token = resp.token;
email = em;
userHash = resp.user_hash;
slotIndex = resp.slot_index;
persistSession();
showLoginMsg('登录成功,正在进入…', true);
setTimeout(enterChat, 400);
})
.catch(function (err) {
loginBtn.disabled = false;
loginBtn.textContent = '进入对话';
showLoginMsg(err.message || '验证失败');
});
});
logoutBtn.addEventListener('click', function () {
clearSession();
location.reload();
});
// ─── 聊天进入 ───
function enterChat() {
loginScreen.hidden = true;
chatScreen.hidden = false;
// 防御性兜底: 直接用 inline style 切换显示, 避免 CDN/WebView 缓存
// 让 [hidden] 的 CSS 修复未到达客户端时, 卡在登录页的退化
try {
loginScreen.style.display = 'none';
chatScreen.style.display = '';
} catch (_e) { /* ignore */ }
userBadge.textContent = email || '匿名';
userBadge.title = email || '';
slotBadgeEl.textContent = '席位 ' + slotIndex + '/10';
loadLocalSessions();
refreshServerSessions();
// 选最新的一个 session 或创建新会话
var sessionIds = Object.keys(localSessions);
if (sessionIds.length > 0) {
// 选最新
sessionIds.sort(function (a, b) {
return (localSessions[b].updated_at || 0) - (localSessions[a].updated_at || 0);
});
switchSession(sessionIds[0]);
} else {
createNewSession(false);
}
}
function refreshServerSessions() {
api('/hli/ftchat/sessions').then(function (data) {
sessions = data.sessions || [];
renderSidebar();
}).catch(function () { /* ignore */ });
}
function renderSidebar() {
sessionsList.innerHTML = '';
// 合并本地 sessions 与服务端 sessions(按 session_id)
var ids = Object.keys(localSessions);
var serverIds = sessions.map(function (s) { return s.session_id; });
serverIds.forEach(function (id) { if (ids.indexOf(id) === -1) ids.push(id); });
// 按 updated_at 排序
ids.sort(function (a, b) {
var ta = (localSessions[a] && localSessions[a].updated_at) || 0;
var tb = (localSessions[b] && localSessions[b].updated_at) || 0;
var serverA = sessions.find(function (s) { return s.session_id === a; });
var serverB = sessions.find(function (s) { return s.session_id === b; });
if (serverA) ta = Math.max(ta, new Date(serverA.updated_at).getTime() || 0);
if (serverB) tb = Math.max(tb, new Date(serverB.updated_at).getTime() || 0);
return tb - ta;
});
if (ids.length === 0) {
sessionsList.innerHTML = '