(() => { const API_BASE = '/api/language_behavior_lock' const DEFAULT_USERNAME = 'admin' const $ = id => document.getElementById(id) const now = () => Date.now() const genId = () => `${Date.now()}_${Math.random().toString(36).slice(2, 8)}` const state = { auth: { currentUser: '' }, db: { records: [], keywords: [] } } const defaultKeywords = () => ([ { id: genId(), text: '你怎么这么', type: 'speech', level: 'high', extraMinutes: 20, tip: '你触发了高风险表达,建议延长冷静期。', enabled: true, createdAt: now() }, { id: genId(), text: '早就说了', type: 'speech', level: 'medium', extraMinutes: 15, tip: '这类表达容易升级冲突,建议继续冷静。', enabled: true, createdAt: now() }, { id: genId(), text: '你一直都', type: 'speech', level: 'high', extraMinutes: 20, tip: '绝对化表达风险较高,建议先改写再决定。', enabled: true, createdAt: now() }, { id: genId(), text: '拿回家', type: 'behavior', level: 'high', extraMinutes: 25, tip: '涉及边界风险行为,建议延长冷静并复核后果。', enabled: true, createdAt: now() } ]) const normalizeKeyword = row => { const levelRaw = String(row && row.level || 'medium') const level = ['low', 'medium', 'high'].includes(levelRaw) ? levelRaw : 'medium' const typeRaw = String(row && row.type || 'all') const type = ['all', 'speech', 'behavior', 'securities'].includes(typeRaw) ? typeRaw : 'all' return { id: String(row && row.id || genId()), text: String(row && row.text || '').trim(), type, level, extraMinutes: Math.max(0, Number(row && row.extraMinutes) || 0), tip: String(row && row.tip || '').trim(), enabled: row && row.enabled !== false, createdAt: Number(row && row.createdAt) || now() } } const normalizeDB = raw => { const db = raw && typeof raw === 'object' ? raw : {} const records = Array.isArray(db.records) ? db.records : [] const keywordsRaw = Array.isArray(db.keywords) ? db.keywords : [] const keywords = keywordsRaw.map(normalizeKeyword).filter(k => k.text) return { records, keywords: keywords.length ? keywords : defaultKeywords() } } const setAuthStatus = text => { $('auth-status').textContent = text || '' } const setFormStatus = text => { $('form-status').textContent = text || '' } const openModal = ({ title, message, okText = '知道了', cancelText = '', tone = 'normal' }) => { return new Promise(resolve => { const mask = $('app-modal') const titleBox = $('modal-title') const msgBox = $('modal-message') const actions = $('modal-actions') actions.innerHTML = '' titleBox.textContent = title || '' msgBox.textContent = message || '' const close = val => { mask.style.display = 'none' resolve(val) } if (cancelText) { const cancelBtn = document.createElement('button') cancelBtn.className = 'btn' cancelBtn.type = 'button' cancelBtn.textContent = cancelText cancelBtn.addEventListener('click', () => close(false)) actions.appendChild(cancelBtn) } const okBtn = document.createElement('button') okBtn.className = tone === 'primary' ? 'btn btn-primary' : 'btn' okBtn.type = 'button' okBtn.textContent = okText okBtn.addEventListener('click', () => close(true)) actions.appendChild(okBtn) mask.style.display = '' }) } const modalAlert = (title, message) => openModal({ title, message, okText: '我知道了' }) const modalConfirm = (title, message, okText = '继续', cancelText = '取消') => openModal({ title, message, okText, cancelText, tone: 'primary' }) const apiRequest = async (path, options = {}) => { const res = await fetch(`${API_BASE}${path}`, { method: options.method || 'GET', headers: { 'Content-Type': 'application/json', ...(options.headers || {}) }, body: options.body === undefined ? undefined : JSON.stringify(options.body), credentials: 'include' }) let payload = null try { payload = await res.json() } catch { payload = null } if (!res.ok) { const msg = payload && payload.error ? String(payload.error) : `http_${res.status}` throw new Error(msg) } return payload || {} } const loadDB = async () => { const payload = await apiRequest('/data') state.db = normalizeDB(payload.db) } const saveDB = async () => { await apiRequest('/data', { method: 'PUT', body: { db: state.db, draft: null } }) } const levelText = level => ({ low: '低风险', medium: '中风险', high: '高风险' })[level] || '中风险' const renderAuth = () => { const user = state.auth.currentUser $('auth-login-box').style.display = user ? 'none' : '' $('auth-logged-box').style.display = user ? '' : 'none' $('app-content').style.display = user ? '' : 'none' $('auth-current-user').textContent = user ? `当前账号:${user}` : '' } const renderKeywordRules = () => { const box = $('keyword-list') box.innerHTML = '' if (!state.db.keywords.length) { box.textContent = '暂无关键词规则' return } state.db.keywords.slice().sort((a, b) => b.createdAt - a.createdAt).forEach(rule => { const row = document.createElement('div') row.className = 'row' const typeLabel = rule.type === 'speech' ? '发言' : rule.type === 'behavior' ? '行为' : rule.type === 'securities' ? '证券' : '全部' row.innerHTML = `
${rule.text} ${levelText(rule.level)}
适用:${typeLabel} | 建议加时:${rule.extraMinutes} 分钟 | 状态:${rule.enabled ? '启用' : '停用'}
${rule.tip || ''}
` box.appendChild(row) }) } const renderAll = () => { renderAuth() renderKeywordRules() } const bindKeywordAdd = () => { $('btn-keyword-add').addEventListener('click', async () => { if (!state.auth.currentUser) { await modalAlert('请先登录', '请先输入账号和密码登录后再操作。') return } const text = $('kw-text').value.trim() if (!text) { await modalAlert('无法新增', '请先输入关键词') return } const exists = state.db.keywords.some(k => k.text.toLowerCase() === text.toLowerCase()) if (exists) { await modalAlert('无法新增', '该关键词已存在') return } const rule = normalizeKeyword({ id: genId(), text, type: $('kw-type').value || 'all', level: $('kw-level').value || 'medium', extraMinutes: Number($('kw-extra-minutes').value || 0), tip: $('kw-tip').value.trim(), enabled: true, createdAt: now() }) state.db.keywords.unshift(rule) await saveDB() $('kw-text').value = '' $('kw-tip').value = '' renderKeywordRules() setFormStatus('关键词规则已新增') await modalAlert('规则已新增', `关键词“${rule.text}”已启用,后续命中会自动提醒。`) }) } const bindActions = () => { document.addEventListener('click', async e => { const btn = e.target.closest('button[data-act]') if (!btn) return if (!state.auth.currentUser) { await modalAlert('请先登录', '请先输入账号和密码登录后再操作。') return } const id = btn.getAttribute('data-id') const act = btn.getAttribute('data-act') if (!id || !act) return if (act === 'toggle-rule') { const idx = state.db.keywords.findIndex(k => k.id === id) if (idx < 0) return state.db.keywords[idx].enabled = !state.db.keywords[idx].enabled await saveDB() renderKeywordRules() setFormStatus(`关键词已${state.db.keywords[idx].enabled ? '启用' : '停用'}`) await modalAlert('状态已更新', `关键词已${state.db.keywords[idx].enabled ? '启用' : '停用'}。`) return } if (act === 'delete-rule') { const rule = state.db.keywords.find(k => k.id === id) if (!rule) return const ok = await modalConfirm('确认删除', `确认删除关键词“${rule.text}”吗?`, '确认删除', '取消') if (!ok) return state.db.keywords = state.db.keywords.filter(k => k.id !== id) await saveDB() renderKeywordRules() setFormStatus('关键词已删除') await modalAlert('删除成功', `关键词“${rule.text}”已删除。`) } }) } const bindAuth = () => { const tryLogin = async () => { const username = String($('login-username').value || '').trim() const password = String($('login-password').value || '') if (!username || !password) { setAuthStatus('请输入账号和密码') return } try { await apiRequest('/auth/login', { method: 'POST', body: { username, password } }) } catch { setAuthStatus('账号或密码不正确,请重试') await modalAlert('登录失败', '账号或密码不正确,请重新输入。') return } state.auth.currentUser = username $('login-password').value = '' await loadDB() renderAll() setAuthStatus(`登录成功,欢迎你:${username}`) setFormStatus('可以开始维护关键词规则') } $('btn-login').addEventListener('click', tryLogin) $('login-password').addEventListener('keydown', async e => { if (e.key !== 'Enter') return e.preventDefault() await tryLogin() }) $('btn-logout').addEventListener('click', async () => { try { await apiRequest('/auth/logout', { method: 'POST' }) } catch {} state.auth.currentUser = '' state.db = { records: [], keywords: defaultKeywords() } setFormStatus('') setAuthStatus('已退出登录') renderAll() }) } const bindNav = () => { $('btn-back-main').addEventListener('click', () => { window.location.href = '/tools/language_behavior_lock/index.html' }) } const init = async () => { bindAuth() bindKeywordAdd() bindActions() bindNav() try { const me = await apiRequest('/auth/me') state.auth.currentUser = String(me.username || DEFAULT_USERNAME) await loadDB() setAuthStatus(`已登录:${state.auth.currentUser}`) setFormStatus('可以开始维护关键词规则') } catch { state.auth.currentUser = '' state.db = { records: [], keywords: defaultKeywords() } setAuthStatus('请先登录') } renderAll() } init() })()