(() => { 'use strict' const base = (window.__PLUGIN_BASE_PATH__ || '').replace(/\/$/, '') const $ = (selector) => document.querySelector(selector) let csrf = '' let pendingToken = '' let configPluginId = '' let installedPlugins = new Map() const notice = (message, error = false) => { const el = $('#notice'); el.textContent = message || ''; el.className = error ? 'notice error' : 'notice' } const api = async (path, options = {}) => { const headers = new Headers(options.headers || {}) headers.set('Accept', 'application/json') if (options.body && !(options.body instanceof FormData)) headers.set('Content-Type', 'application/json') if (csrf && options.method && options.method !== 'GET') headers.set('X-CSRF-Token', csrf) const response = await fetch(`${base}${path}`, { ...options, headers, credentials: 'same-origin' }) const data = await response.json().catch(() => ({})) if (!response.ok) throw new Error(data.error || `请求失败 (${response.status})`) return data } const setLoggedIn = (user) => { $('#login-panel').hidden = !!user $('#app-panel').hidden = !user $('#logout').hidden = !user $('#operator').textContent = user ? (user.email || user.username || '管理员') : '' } const login = async (event) => { event.preventDefault(); $('#login-error').textContent = '' const body = Object.fromEntries(new FormData(event.currentTarget).entries()) try { const result = await api('/login', { method: 'POST', body: JSON.stringify(body) }) if (result.requires_2fa) { pendingToken = result.pending_token; $('#login-form').hidden = true; $('#twofa-form').hidden = false; return } csrf = result.csrf_token; setLoggedIn(result.user); await loadAll() } catch (error) { $('#login-error').textContent = error.message } } const login2fa = async (event) => { event.preventDefault(); $('#login-error').textContent = '' const body = Object.fromEntries(new FormData(event.currentTarget).entries()); body.pending_token = pendingToken try { const result = await api('/login/2fa', { method: 'POST', body: JSON.stringify(body) }); csrf = result.csrf_token; setLoggedIn(result.user); await loadAll() } catch (error) { $('#login-error').textContent = error.message } } const logout = async () => { try { await api('/logout', { method: 'POST' }) } catch (_) {} csrf = ''; setLoggedIn(null); $('#login-form').hidden = false; $('#twofa-form').hidden = true } const badge = (state) => `${escapeHtml(state || 'unknown')}` const escapeHtml = (value) => String(value == null ? '' : value).replace(/[&<>"']/g, (char) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char])) const marketplaceInstalled = (item) => { const installed = installedPlugins.get(String(item.plugin_id || '')) return !!(item.installed || installed) } const marketplaceStatus = (item) => marketplaceInstalled(item) ? `已入库 · ${escapeHtml(item.installed_status === 'installed' ? '运行中' : item.installed_status === 'stopped' ? '已停用' : item.installed_state === 'error' ? '启用失败' : item.installed_state === 'incompatible' ? 'Core 不兼容' : '待启用')}` : '可安装' const loadMarketplace = async () => { const data = await api('/api/marketplace'); const root = $('#marketplace'); root.textContent = '' const items = Array.isArray(data.items) ? data.items : [] if (!items.length) { root.innerHTML = '
${escapeHtml(pluginId)} · v${escapeHtml(version)}
${escapeHtml(item.description || '由受控插件目录提供的 Business Plugin')}
` root.appendChild(card) } } const loadPlugins = async () => { const data = await api('/api/plugins'); const root = $('#plugins'); root.textContent = '' installedPlugins = new Map((data.items || []).map((plugin) => [String(plugin.plugin_id || ''), plugin])) if (!data.items || !data.items.length) { root.innerHTML = '${escapeHtml(plugin.plugin_id)} · v${escapeHtml(plugin.version)}
${escapeHtml(plugin.last_error || '')}
` root.appendChild(card) } } const loadAudit = async () => { const data = await api('/api/audit'); const root = $('#audit'); root.textContent = ''; for (const item of (data.items || []).slice().reverse()) { const row = document.createElement('div'); row.className = 'audit-row'; row.innerHTML = `${escapeHtml(item.action)}${escapeHtml(item.plugin_id || '-')}${escapeHtml(item.result)}`; root.appendChild(row) } } const loadAll = async () => { try { await loadPlugins(); await Promise.all([loadMarketplace(), loadAudit()]); notice('') } catch (error) { notice(error.message, true) } } const mutate = async (action, id) => { const key = `${action}-${id}-${Date.now()}`; try { const result = await api(`/api/plugins/${encodeURIComponent(id)}/${action}`, { method: 'POST', headers: { 'Idempotency-Key': key } }); notice(`操作已提交:${result.operation_id || result.state}${result.warning ? `;${result.warning}` : ''}`); await loadAll() } catch (error) { notice(error.message, true) } } const openConfig = async (id) => { configPluginId = id; $('#config-plugin').textContent = id; $('#config-error').textContent = ''; const form = $('#config-form'); form.elements.service_url.value = ''; form.elements.public_url.value = ''; try { const data = await api(`/api/plugins/${encodeURIComponent(id)}/config`); form.elements.service_url.value = data.endpoint || ''; if (data.config && typeof data.config.public_url === 'string') form.elements.public_url.value = data.config.public_url; $('#config-dialog').showModal() } catch (error) { notice(error.message, true) } } const saveConfig = async (event) => { event.preventDefault(); const form = event.currentTarget; const body = { service_url: form.elements.service_url.value.trim(), public_url: form.elements.public_url.value.trim() }; try { const result = await api(`/api/plugins/${encodeURIComponent(configPluginId)}/config`, { method: 'PUT', headers: { 'Idempotency-Key': `config-${configPluginId}-${Date.now()}` }, body: JSON.stringify(body) }); $('#config-dialog').close(); notice(`配置已保存:${result.operation_id || result.state}`); await loadAll() } catch (error) { $('#config-error').textContent = error.message } } const upload = async (file) => { const form = new FormData(); form.append('package', file); try { const result = await api('/api/plugins/install', { method: 'POST', headers: { 'Idempotency-Key': `install-${Date.now()}` }, body: form }); notice(`插件已入库,待手动启用:${result.operation_id || result.state}`); await loadAll() } catch (error) { notice(error.message, true) } } const uploadUpgrade = async (id, file) => { const form = new FormData(); form.append('package', file); try { const result = await api(`/api/plugins/${encodeURIComponent(id)}/upgrade`, { method: 'POST', headers: { 'Idempotency-Key': `upgrade-${id}-${Date.now()}` }, body: form }); notice(`升级已提交:${result.operation_id || result.state}`); await loadAll() } catch (error) { notice(error.message, true) } } const installFromMarketplace = async (button) => { const pluginId = button.dataset.id; const version = button.dataset.version if (!pluginId || !version || button.disabled) return button.disabled = true try { const result = await api('/api/marketplace/install', { method: 'POST', headers: { 'Idempotency-Key': `marketplace-install-${pluginId}-${version}-${Date.now()}` }, body: JSON.stringify({ plugin_id: pluginId, version }) }); notice(`插件已入库,待手动启用:${result.operation_id || result.state}`); await loadAll() } catch (error) { button.disabled = false; notice(error.message, true) } } $('#login-form').addEventListener('submit', login); $('#twofa-form').addEventListener('submit', login2fa); $('#logout').addEventListener('click', logout); $('#refresh').addEventListener('click', loadAll); $('#marketplace-refresh').addEventListener('click', loadAll); $('#audit-refresh').addEventListener('click', loadAudit); $('#config-form').addEventListener('submit', saveConfig); $('#config-close').addEventListener('click', () => $('#config-dialog').close()); $('#config-cancel').addEventListener('click', () => $('#config-dialog').close()) $('#package-file').addEventListener('change', (event) => { const file = event.target.files[0]; if (file) upload(file); event.target.value = '' }) $('#plugins').addEventListener('change', (event) => { const input = event.target.closest('[data-action="upgrade-file"]'); if (!input) return; const file = input.files[0]; if (file) uploadUpgrade(input.dataset.id, file); input.value = '' }) $('#plugins').addEventListener('click', (event) => { const button = event.target.closest('[data-action]'); if (!button || button.disabled) return; const action = button.dataset.action; const id = button.dataset.id; if (action === 'config') { openConfig(id); return } if (action === 'uninstall' && !window.confirm('删除后将移除插件文件,Core 数据不会删除。继续吗?')) return; mutate(action, id) }) $('#marketplace').addEventListener('click', (event) => { const button = event.target.closest('[data-market-action="install"]'); if (button) installFromMarketplace(button) }) api('/api/me').then((data) => { csrf = data.csrf_token; setLoggedIn(data.user); loadAll() }).catch(() => setLoggedIn(null)) })()