const AUTH_STORAGE_KEY = 'cyberstrike-auth'; let authToken = null; let authTokenExpiry = null; let authUser = null; let authRoles = []; let authPermissions = new Set(); let authScope = ''; let authPromise = null; let authPromiseResolvers = []; let isAppInitialized = false; let robotBindingCountdownTimer = null; let robotBindingExpiresAt = 0; let robotBindingLifetimeMs = 5 * 60 * 1000; let activeRobotBindingCode = ''; function isTokenValid() { return !!authToken && authTokenExpiry instanceof Date && authTokenExpiry.getTime() > Date.now(); } function saveAuth(token, expiresAt, meta = {}) { const expiry = expiresAt instanceof Date ? expiresAt : new Date(expiresAt); authToken = token; authTokenExpiry = expiry; authUser = meta.user || null; authRoles = Array.isArray(meta.roles) ? meta.roles : []; authPermissions = new Set(Array.isArray(meta.permissions) ? meta.permissions : []); authScope = meta.scope || ''; try { localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify({ token, expiresAt: expiry.toISOString(), user: authUser, roles: authRoles, permissions: Array.from(authPermissions), scope: authScope, })); } catch (error) { console.warn('无法持久化认证信息:', error); } renderUserMenuProfile(); } function clearAuthStorage() { authToken = null; authTokenExpiry = null; authUser = null; authRoles = []; authPermissions = new Set(); authScope = ''; try { localStorage.removeItem(AUTH_STORAGE_KEY); } catch (error) { console.warn('无法清除认证信息:', error); } renderUserMenuProfile(); } function loadAuthFromStorage() { try { const raw = localStorage.getItem(AUTH_STORAGE_KEY); if (!raw) { return false; } const stored = JSON.parse(raw); if (!stored.token || !stored.expiresAt) { clearAuthStorage(); return false; } const expiry = new Date(stored.expiresAt); if (Number.isNaN(expiry.getTime())) { clearAuthStorage(); return false; } authToken = stored.token; authTokenExpiry = expiry; authUser = stored.user || null; authRoles = Array.isArray(stored.roles) ? stored.roles : []; authPermissions = new Set(Array.isArray(stored.permissions) ? stored.permissions : []); authScope = stored.scope || ''; return isTokenValid(); } catch (error) { console.error('读取认证信息失败:', error); clearAuthStorage(); return false; } } function resolveAuthPromises(success) { authPromiseResolvers.forEach(resolve => resolve(success)); authPromiseResolvers = []; authPromise = null; } function showLoginOverlay(message = '') { const overlay = document.getElementById('login-overlay'); const errorBox = document.getElementById('login-error'); const usernameInput = document.getElementById('login-username'); const passwordInput = document.getElementById('login-password'); if (!overlay) { return; } openAppModal('login-overlay', { focus: false }); if (errorBox) { if (message) { errorBox.textContent = message; errorBox.style.display = 'block'; } else { errorBox.textContent = ''; errorBox.style.display = 'none'; } } setTimeout(function () { if (usernameInput && !usernameInput.value) { usernameInput.focus(); } else if (passwordInput) { passwordInput.focus(); } }, 100); } function hideLoginOverlay() { const overlay = document.getElementById('login-overlay'); const errorBox = document.getElementById('login-error'); const usernameInput = document.getElementById('login-username'); const passwordInput = document.getElementById('login-password'); closeAppModal('login-overlay'); if (errorBox) { errorBox.textContent = ''; errorBox.style.display = 'none'; } if (passwordInput) { passwordInput.value = ''; } if (usernameInput && !authUser) { usernameInput.value = ''; } } function ensureAuthPromise() { if (!authPromise) { authPromise = new Promise(resolve => { authPromiseResolvers.push(resolve); }); } return authPromise; } async function ensureAuthenticated() { if (isTokenValid()) { return true; } showLoginOverlay(); await ensureAuthPromise(); return true; } function handleUnauthorized({ message = null, silent = false } = {}) { clearAuthStorage(); authPromise = null; authPromiseResolvers = []; let finalMessage = message; if (!finalMessage) { if (typeof window !== 'undefined' && typeof window.t === 'function') { finalMessage = window.t('auth.sessionExpired'); } else { finalMessage = '认证已过期,请重新登录'; } } if (!silent) { showLoginOverlay(finalMessage); } else { showLoginOverlay(); } return false; } async function apiFetch(url, options = {}) { await ensureAuthenticated(); const opts = { ...options }; const headers = new Headers(options && options.headers ? options.headers : undefined); if (authToken && !headers.has('Authorization')) { headers.set('Authorization', `Bearer ${authToken}`); } opts.headers = headers; const response = await fetch(url, opts); if (response.status === 401) { handleUnauthorized(); const msg = (typeof window !== 'undefined' && typeof window.t === 'function') ? window.t('auth.unauthorized') : '未授权访问'; throw new Error(msg); } // 403 属于可预期的 RBAC 拒绝,返回 Response 供调用方通过 res.ok / ensureApiOk 处理。 return response; } /** * multipart POST with XMLHttpRequest so upload progress is available (fetch 无法可靠上报进度). * 返回与 fetch 类似的对象:ok、status、json()、text() */ async function apiUploadWithProgress(url, formData, options = {}) { await ensureAuthenticated(); const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null; return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('POST', url); if (authToken) { xhr.setRequestHeader('Authorization', `Bearer ${authToken}`); } xhr.upload.onprogress = (e) => { if (!onProgress || !e.lengthComputable) return; const percent = e.total > 0 ? Math.round((e.loaded / e.total) * 100) : 0; onProgress({ loaded: e.loaded, total: e.total, percent }); }; xhr.onerror = () => { reject(new Error('Network error')); }; xhr.onload = () => { if (xhr.status === 401) { handleUnauthorized(); const msg = (typeof window !== 'undefined' && typeof window.t === 'function') ? window.t('auth.unauthorized') : '未授权访问'; reject(new Error(msg)); return; } const responseText = xhr.responseText || ''; resolve({ ok: xhr.status >= 200 && xhr.status < 300, status: xhr.status, text: async () => responseText, json: async () => { try { return responseText ? JSON.parse(responseText) : {}; } catch (err) { throw err; } }, }); }; xhr.send(formData); }); } async function submitLogin(event) { event.preventDefault(); const usernameInput = document.getElementById('login-username'); const passwordInput = document.getElementById('login-password'); const errorBox = document.getElementById('login-error'); const submitBtn = document.querySelector('.login-submit'); if (!passwordInput) { return; } const username = usernameInput ? usernameInput.value.trim() : ''; const password = passwordInput.value.trim(); if (!password) { if (errorBox) { const msgEmpty = (typeof window !== 'undefined' && typeof window.t === 'function') ? window.t('auth.enterPassword') : '请输入密码'; errorBox.textContent = msgEmpty; errorBox.style.display = 'block'; } return; } if (submitBtn) { submitBtn.disabled = true; } try { const response = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ username, password }), }); const result = await response.json().catch(() => ({})); if (!response.ok || !result.token) { if (errorBox) { const fallback = (typeof window !== 'undefined' && typeof window.t === 'function') ? window.t('auth.loginFailedCheck') : '登录失败,请检查密码'; errorBox.textContent = result.error || fallback; errorBox.style.display = 'block'; } return; } saveAuth(result.token, result.expires_at, { user: result.user, roles: result.roles, permissions: result.permissions, scope: result.scope, }); hideLoginOverlay(); applyRBACToUI(); resolveAuthPromises(true); if (!isAppInitialized) { await bootstrapApp(); } else { await refreshAppData(); } } catch (error) { console.error('登录失败:', error); if (errorBox) { const fallback = (typeof window !== 'undefined' && typeof window.t === 'function') ? window.t('auth.loginFailedRetry') : '登录失败,请稍后重试'; errorBox.textContent = fallback; errorBox.style.display = 'block'; } } finally { if (submitBtn) { submitBtn.disabled = false; } } } async function refreshAppData(showTaskErrors = false) { if (typeof initChatAgentModeFromConfig === 'function') { try { await initChatAgentModeFromConfig(); } catch (error) { console.warn('刷新对话模式配置失败:', error); } } await Promise.allSettled([ loadConversations(), loadActiveTasks(showTaskErrors), ]); } async function bootstrapApp() { if (!isAppInitialized) { // 等待 i18n 首包加载完成后再插系统就绪消息,避免清除缓存后语言显示 English 气泡仍是中文 try { if (window.i18nReady && typeof window.i18nReady.then === 'function') { await window.i18nReady; } } catch (e) { console.warn('等待 i18n 就绪失败,继续初始化聊天', e); } initializeChatUI(); isAppInitialized = true; } applyRBACToUI(); if (typeof installWriteHandlerGuards === 'function') { installWriteHandlerGuards(); } await refreshAppData(); } const PAGE_PERMISSION_MAP = { dashboard: 'dashboard:read', chat: 'chat:read', hitl: 'hitl:read', 'info-collect': 'fofa:execute', tasks: 'tasks:read', workflows: 'workflow:read', projects: 'project:read', vulnerabilities: 'vulnerability:read', 'chat-files': 'files:read', webshell: 'webshell:read', c2: 'c2:read', 'c2-listeners': 'c2:read', 'c2-sessions': 'c2:read', 'c2-tasks': 'c2:read', 'c2-payloads': 'c2:read', 'c2-events': 'c2:read', 'c2-profiles': 'c2:read', mcp: 'mcp:read', 'mcp-monitor': 'monitor:read', 'mcp-management': 'mcp:read', knowledge: 'knowledge:read', 'knowledge-retrieval-logs': 'knowledge:read', 'knowledge-management': 'knowledge:read', skills: 'skills:read', 'skills-monitor': 'skills:read', 'skills-management': 'skills:read', agents: 'agents:read', 'agents-management': 'agents:read', roles: 'roles:read', 'roles-management': 'roles:read', 'platform-rbac': 'rbac:read', settings: 'config:read', }; function hasPermission(permission) { return !permission || authPermissions.has(permission); } function hasAnyPermission(permissions) { if (!Array.isArray(permissions) || !permissions.length) return true; return permissions.some((permission) => hasPermission(permission)); } async function readApiError(response, fallback) { if (!response) { return fallback || authT('auth.requestFailed', '请求失败'); } try { const body = await response.clone().json(); return body.error || body.message || fallback || authT('auth.requestFailed', '请求失败'); } catch (error) { return fallback || authT('auth.requestFailed', '请求失败'); } } function notifyApiError(message, type = 'error') { const text = (message || '').trim() || authT('auth.requestFailed', '请求失败'); if (typeof showNotification === 'function') { showNotification(text, type); return; } if (typeof showToast === 'function') { showToast(text, type); return; } alert(text); } async function notifyApiResponseError(response, fallback) { notifyApiError(await readApiError(response, fallback)); } async function ensureApiOk(response, fallback) { if (response && response.ok) return true; await notifyApiResponseError(response, fallback); return false; } function requirePermission(permission, customMessage) { const allowed = Array.isArray(permission) ? hasAnyPermission(permission) : hasPermission(permission); if (allowed) return true; notifyApiError(customMessage || authT('auth.forbidden', '权限不足')); return false; } function permissionAllowedForElement(el) { if (!el) return true; const anyOf = el.getAttribute('data-require-permission-any'); const permission = el.getAttribute('data-require-permission'); if (anyOf) { return hasAnyPermission(anyOf.split(/[\s,|]+/).map((item) => item.trim()).filter(Boolean)); } if (permission) { return hasPermission(permission); } return true; } function applyPermissionElement(el) { const anyOf = el.getAttribute('data-require-permission-any'); const permission = el.getAttribute('data-require-permission'); if (!anyOf && !permission) return; const allowed = permissionAllowedForElement(el); el.hidden = !allowed; el.classList.toggle('rbac-permission-denied', !allowed); if ('disabled' in el) { el.disabled = !allowed; } el.setAttribute('aria-hidden', allowed ? 'false' : 'true'); el.setAttribute('aria-disabled', allowed ? 'false' : 'true'); } let permissionClickGuardInstalled = false; function installPermissionClickGuard() { if (permissionClickGuardInstalled) return; permissionClickGuardInstalled = true; document.addEventListener('click', (event) => { const target = event.target instanceof Element ? event.target.closest('[data-require-permission], [data-require-permission-any]') : null; if (!target || permissionAllowedForElement(target)) return; event.preventDefault(); event.stopPropagation(); if (typeof event.stopImmediatePropagation === 'function') { event.stopImmediatePropagation(); } notifyApiError(authT('auth.forbidden', '权限不足')); }, true); } function applyRBACToUI(root) { installPermissionClickGuard(); document.querySelectorAll('[data-page]').forEach((el) => { const page = el.getAttribute('data-page'); const permission = PAGE_PERMISSION_MAP[page]; if (!permission) return; const allowed = hasPermission(permission); el.hidden = !allowed; el.setAttribute('aria-hidden', allowed ? 'false' : 'true'); }); const permissionRoot = root instanceof Element ? root : document; permissionRoot.querySelectorAll('[data-require-permission], [data-require-permission-any]').forEach(applyPermissionElement); if (permissionRoot instanceof Element && permissionRoot.matches('[data-require-permission], [data-require-permission-any]')) { applyPermissionElement(permissionRoot); } const userAvatar = document.querySelector('.user-avatar-btn'); if (userAvatar && authUser && authUser.username) { const displayName = getAuthDisplayName(); userAvatar.setAttribute('title', displayName); userAvatar.setAttribute('aria-label', authT('header.userMenuFor', '用户菜单:{{name}}', { name: displayName })); } renderUserMenuProfile(); } function authT(key, fallback, opts = {}) { if (typeof window !== 'undefined' && typeof window.t === 'function') { const translated = window.t(key, opts); if (translated && translated !== key) { return translated; } } return fallback.replace(/\{\{\s*(\w+)\s*\}\}/g, function (_, name) { return Object.prototype.hasOwnProperty.call(opts, name) ? String(opts[name]) : ''; }); } function getAuthDisplayName() { if (!authUser) { return authT('header.unknownUser', '未知用户'); } return String(authUser.display_name || authUser.displayName || authUser.username || '').trim() || authT('header.unknownUser', '未知用户'); } function getAuthUsername() { if (!authUser) { return '-'; } const username = String(authUser.username || '').trim(); return username ? `@${username}` : '-'; } function getScopeLabel(scope) { const normalized = String(scope || '').trim().toLowerCase(); const keyMap = { all: 'header.scopeAll', assigned: 'header.scopeAssigned', own: 'header.scopeOwn', }; const fallbackMap = { all: '全部资源', assigned: '指定资源', own: '自己的资源', }; const key = keyMap[normalized] || 'header.scopeUnknown'; const fallback = fallbackMap[normalized] || '资源范围未知'; return authT(key, fallback); } function renderUserMenuProfile() { const displayNameEl = document.getElementById('user-menu-display-name'); const usernameEl = document.getElementById('user-menu-username'); const scopeEl = document.getElementById('user-menu-scope'); const rolesEl = document.getElementById('user-menu-roles'); const permissionsEl = document.getElementById('user-menu-permissions'); const avatarBtn = document.getElementById('user-avatar-btn') || document.querySelector('.user-avatar-btn'); const displayName = getAuthDisplayName(); const roleCount = Array.isArray(authRoles) ? authRoles.length : 0; const permissionCount = authPermissions instanceof Set ? authPermissions.size : 0; if (displayNameEl) displayNameEl.textContent = displayName; if (usernameEl) usernameEl.textContent = getAuthUsername(); if (scopeEl) scopeEl.textContent = getScopeLabel(authScope); if (rolesEl) rolesEl.textContent = authT('header.rolesCount', '{{count}} 个角色', { count: roleCount }); if (permissionsEl) permissionsEl.textContent = authT('header.permissionsCount', '{{count}} 项权限', { count: permissionCount }); if (avatarBtn && authUser) { avatarBtn.setAttribute('title', displayName); avatarBtn.setAttribute('aria-label', authT('header.userMenuFor', '用户菜单:{{name}}', { name: displayName })); } else if (avatarBtn) { avatarBtn.setAttribute('aria-label', authT('header.userMenu', '用户菜单')); } } function setUserMenuOpen(open) { const dropdown = document.getElementById('user-menu-dropdown'); const avatarBtn = document.getElementById('user-avatar-btn') || document.querySelector('.user-avatar-btn'); if (!dropdown) return; dropdown.style.display = open ? 'block' : 'none'; if (avatarBtn) { avatarBtn.classList.toggle('active', open); avatarBtn.setAttribute('aria-expanded', open ? 'true' : 'false'); } if (open) { renderUserMenuProfile(); } } // 通用工具函数 function getStatusText(status) { const s = (status && String(status).toLowerCase()) || ''; if (typeof window.t !== 'function') { const fallback = { pending: '等待中', running: '执行中', completed: '已完成', failed: '失败', cancelled: '已终止' }; return fallback[s] || status; } const keyMap = { pending: 'mcpDetailModal.statusPending', running: 'mcpDetailModal.statusRunning', completed: 'mcpDetailModal.statusCompleted', failed: 'mcpDetailModal.statusFailed', cancelled: 'mcpDetailModal.statusCancelled' }; const key = keyMap[s]; return key ? window.t(key) : status; } function formatDuration(ms) { const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); if (hours > 0) { return `${hours}小时${minutes % 60}分钟`; } else if (minutes > 0) { return `${minutes}分钟${seconds % 60}秒`; } else { return `${seconds}秒`; } } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** @param {string} text @param {{ profile?: 'chat'|'timeline' }} [options] */ function formatMarkdown(text, options) { if (typeof window.csMarkdownSanitize !== 'undefined') { return window.csMarkdownSanitize.formatMarkdownToHtml(text, options); } const raw = text == null ? '' : String(text); return escapeHtml(raw).replace(/\n/g, '
'); } function setupLoginUI() { installPermissionClickGuard(); const loginForm = document.getElementById('login-form'); if (loginForm) { loginForm.addEventListener('submit', submitLogin); } } async function initializeApp() { setupLoginUI(); const hasStoredAuth = loadAuthFromStorage(); if (hasStoredAuth && isTokenValid()) { try { const response = await apiFetch('/api/auth/validate', { method: 'GET', }); if (response.ok) { const result = await response.json().catch(() => ({})); saveAuth(result.token || authToken, result.expires_at || authTokenExpiry, { user: result.user || authUser, roles: result.roles || authRoles, permissions: result.permissions || Array.from(authPermissions), scope: result.scope || authScope, }); hideLoginOverlay(); applyRBACToUI(); resolveAuthPromises(true); await bootstrapApp(); return; } } catch (error) { console.warn('本地会话已失效,需重新登录'); } } clearAuthStorage(); showLoginOverlay(); } // 用户菜单控制 function toggleUserMenu() { const dropdown = document.getElementById('user-menu-dropdown'); if (!dropdown) return; const isVisible = dropdown.style.display !== 'none'; setUserMenuOpen(!isVisible); } // 点击页面其他地方时关闭下拉菜单 document.addEventListener('click', function(event) { const dropdown = document.getElementById('user-menu-dropdown'); const avatarBtn = document.querySelector('.user-avatar-btn'); if (dropdown && avatarBtn && !dropdown.contains(event.target) && !avatarBtn.contains(event.target)) { setUserMenuOpen(false); } }); document.addEventListener('languagechange', function () { renderUserMenuProfile(); }); async function openRobotAccountBinding() { setUserMenuOpen(false); openAppModal('robot-account-binding-modal'); if (robotBindingExpiresAt) updateRobotBindingCountdown(); await loadRobotAccountBindings(); } function closeRobotAccountBinding() { closeAppModal('robot-account-binding-modal'); } async function generateRobotBindingCode() { const generateBtn = document.getElementById('robot-binding-generate-btn'); if (generateBtn) { generateBtn.disabled = true; generateBtn.textContent = '正在生成…'; } let response; let data; try { response = await apiFetch('/api/auth/robot-binding-code', { method: 'POST' }); data = await response.json().catch(() => ({})); } catch (error) { if (typeof showNotification === 'function') showNotification('生成绑定码失败,请检查网络连接', 'error'); resetRobotBindingGenerateButton(); return; } if (!response.ok || !data.code) { if (typeof showNotification === 'function') showNotification(data.error || '生成绑定码失败', 'error'); resetRobotBindingGenerateButton(); return; } const codeEl = document.getElementById('robot-binding-code'); const copyBtn = document.getElementById('robot-binding-copy-btn'); const card = document.getElementById('robot-binding-code-card'); const timer = document.getElementById('robot-binding-timer'); const state = document.getElementById('robot-binding-code-state'); activeRobotBindingCode = data.code; robotBindingLifetimeMs = Math.max(1000, Number(data.expires_in_seconds || 300) * 1000); // Use the server-provided duration instead of comparing wall clocks, so a // client machine with clock skew still gets an accurate countdown. robotBindingExpiresAt = Date.now() + robotBindingLifetimeMs; if (codeEl) codeEl.textContent = data.code; if (copyBtn) copyBtn.disabled = false; if (card) card.className = 'robot-binding-code-card is-active'; if (timer) timer.hidden = false; if (state) state.textContent = '等待绑定'; if (generateBtn) { generateBtn.disabled = false; generateBtn.textContent = '重新生成'; } startRobotBindingCountdown(); } function resetRobotBindingGenerateButton() { const generateBtn = document.getElementById('robot-binding-generate-btn'); if (generateBtn) { generateBtn.disabled = false; generateBtn.textContent = activeRobotBindingCode ? '重新生成' : '生成绑定码'; } } function startRobotBindingCountdown() { if (robotBindingCountdownTimer) clearInterval(robotBindingCountdownTimer); updateRobotBindingCountdown(); robotBindingCountdownTimer = setInterval(updateRobotBindingCountdown, 250); } function updateRobotBindingCountdown() { if (!robotBindingExpiresAt) return; const remaining = Math.max(0, robotBindingExpiresAt - Date.now()); const seconds = Math.ceil(remaining / 1000); const minutesPart = String(Math.floor(seconds / 60)).padStart(2, '0'); const secondsPart = String(seconds % 60).padStart(2, '0'); const countdown = document.getElementById('robot-binding-countdown'); const progress = document.getElementById('robot-binding-progress-bar'); const expiry = document.getElementById('robot-binding-expiry'); if (countdown) countdown.textContent = `${minutesPart}:${secondsPart}`; if (progress) progress.style.width = `${Math.max(0, Math.min(100, remaining / robotBindingLifetimeMs * 100))}%`; if (expiry && activeRobotBindingCode) expiry.textContent = `请发送:绑定 ${activeRobotBindingCode}`; if (remaining <= 0) expireRobotBindingCode(); } function expireRobotBindingCode() { if (robotBindingCountdownTimer) clearInterval(robotBindingCountdownTimer); robotBindingCountdownTimer = null; robotBindingExpiresAt = 0; activeRobotBindingCode = ''; const card = document.getElementById('robot-binding-code-card'); const codeEl = document.getElementById('robot-binding-code'); const state = document.getElementById('robot-binding-code-state'); const expiry = document.getElementById('robot-binding-expiry'); const countdown = document.getElementById('robot-binding-countdown'); const progress = document.getElementById('robot-binding-progress-bar'); const copyBtn = document.getElementById('robot-binding-copy-btn'); if (card) card.className = 'robot-binding-code-card is-expired'; if (codeEl) codeEl.textContent = '已失效'; if (state) state.textContent = '已过期'; if (expiry) expiry.textContent = '绑定码已过期,请重新生成'; if (countdown) countdown.textContent = '00:00'; if (progress) progress.style.width = '0%'; if (copyBtn) copyBtn.disabled = true; resetRobotBindingGenerateButton(); const generateBtn = document.getElementById('robot-binding-generate-btn'); if (generateBtn) generateBtn.textContent = '重新生成'; loadRobotAccountBindings(); } async function copyRobotBindingCode() { if (!activeRobotBindingCode || !robotBindingExpiresAt || robotBindingExpiresAt <= Date.now()) return; const command = `绑定 ${activeRobotBindingCode}`; try { await navigator.clipboard.writeText(command); } catch (_) { const textarea = document.createElement('textarea'); textarea.value = command; textarea.style.position = 'fixed'; textarea.style.opacity = '0'; document.body.appendChild(textarea); textarea.select(); document.execCommand('copy'); textarea.remove(); } const copyBtn = document.getElementById('robot-binding-copy-btn'); const label = copyBtn?.querySelector('span'); if (label) label.textContent = '已复制'; setTimeout(() => { if (label) label.textContent = '复制命令'; }, 1400); if (typeof showNotification === 'function') showNotification('绑定命令已复制', 'success'); } async function loadRobotAccountBindings() { const list = document.getElementById('robot-account-binding-list'); if (!list) return; const response = await apiFetch('/api/auth/robot-bindings'); const data = await response.json().catch(() => ({})); if (!response.ok) { list.textContent = data.error || '获取绑定失败'; return; } const bindings = Array.isArray(data.bindings) ? data.bindings : []; if (!bindings.length) { list.innerHTML = `
暂无绑定账号

生成绑定码并在机器人中发送,即可完成首次绑定。

`; return; } const platformLabels = { wechat: '微信', wecom: '企业微信', dingtalk: '钉钉', lark: '飞书', telegram: 'Telegram', slack: 'Slack', discord: 'Discord', qq: 'QQ' }; list.innerHTML = bindings.map(binding => `
${escapeHtml((platformLabels[binding.platform] || binding.platform || '?').slice(0, 1).toUpperCase())}
账号标识 ${escapeHtml(binding.external_user_hint || '-')} · 更新于 ${escapeHtml(formatRobotBindingTime(binding.updated_at))}
`).join(''); } function formatRobotBindingTime(value) { const date = new Date(value || ''); if (Number.isNaN(date.getTime())) return '未知时间'; return date.toLocaleString([], { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); } async function deleteRobotAccountBinding(id) { if (!id || !window.confirm('确定解除该机器人账号绑定吗?')) return; const response = await apiFetch(`/api/auth/robot-bindings/${encodeURIComponent(id)}`, { method: 'DELETE' }); if (!response.ok) { const data = await response.json().catch(() => ({})); if (typeof showNotification === 'function') showNotification(data.error || '解绑失败', 'error'); return; } await loadRobotAccountBindings(); } // 退出登录 async function logout() { // 关闭下拉菜单 setUserMenuOpen(false); try { // 先尝试调用退出API(如果token有效) if (authToken) { const headers = new Headers(); headers.set('Authorization', `Bearer ${authToken}`); await fetch('/api/auth/logout', { method: 'POST', headers: headers, }).catch(() => { // 忽略错误,继续清除本地认证信息 }); } } catch (error) { console.error('退出登录API调用失败:', error); } finally { // 无论如何都清除本地认证信息 clearAuthStorage(); hideLoginOverlay(); showLoginOverlay(typeof window.t === 'function' ? window.t('auth.loggedOut') : '已退出登录'); } } // 导出函数供HTML使用 window.toggleUserMenu = toggleUserMenu; window.openRobotAccountBinding = openRobotAccountBinding; window.closeRobotAccountBinding = closeRobotAccountBinding; window.generateRobotBindingCode = generateRobotBindingCode; window.copyRobotBindingCode = copyRobotBindingCode; window.deleteRobotAccountBinding = deleteRobotAccountBinding; window.logout = logout; window.hasPermission = hasPermission; window.hasAnyPermission = hasAnyPermission; window.readApiError = readApiError; window.notifyApiError = notifyApiError; window.notifyApiResponseError = notifyApiResponseError; window.ensureApiOk = ensureApiOk; window.requirePermission = requirePermission; window.permissionAllowedForElement = permissionAllowedForElement; window.applyRBACToUI = applyRBACToUI; function rbacAfterDynamicRender(root) { applyRBACToUI(root); } window.rbacAfterDynamicRender = rbacAfterDynamicRender; document.addEventListener('DOMContentLoaded', initializeApp);