/** * TalkingQ Platform - Settings Page */ const SETTINGS_API_KEY_PROVIDERS = [ { id: 'openai', name: 'OpenAI API Key', provider: 'OpenAI', placeholder: 'sk-...', description: '用于 OpenAI 文本、多模态和实时模型调用', }, { id: 'ali', name: '阿里云百炼 API Key', provider: 'Alibaba Cloud Bailian', placeholder: 'sk-...', description: '用于通义千问、百炼应用和阿里云模型服务', }, { id: 'gemini', name: 'Google Gemini API Key', provider: 'Google Gemini', placeholder: 'AIza...', description: '用于 Gemini 文本与多模态模型调用', }, ]; function getNotificationCatalog() { return window.TQ_NOTIFICATION_EVENT_CATALOG || [ { type: 'device.offline_rate_exceeded', category: '设备健康', name: '设备离线率异常', severity: 'critical', trigger: '公司设备离线率连续 10 分钟超过阈值', ruleSummary: '离线率 > 5%,且连续 10 分钟未恢复', window: '10 分钟连续检测', cooldown: '30 分钟内同公司同规则只通知 1 次', contentFields: ['公司名称', '当前离线率', '影响设备数', '最近检测时间', '建议处理动作'], defaultThreshold: 5, thresholdUnit: '%', defaultChannels: ['in_app', 'webhook'], }, ]; } function getNotificationInputKey(type) { return String(type).replace(/[^a-zA-Z0-9]/g, '_'); } function getNotificationSeverityLabel(severity) { return { critical: '高危', warning: '预警', info: '提醒', success: '完成', }[severity] || '提醒'; } function getDefaultNotificationRules(settings = {}) { const legacyBadcaseEnabled = settings.notifyBadcase !== false; const legacyOfflineEnabled = settings.notifyOffline !== false; const legacyOTAEnabled = settings.notifyOTA !== false; return getNotificationCatalog().reduce((rules, item) => { let enabled = item.defaultEnabled !== false; if (item.type === 'device.offline_rate_exceeded') enabled = legacyOfflineEnabled; if (item.type === 'ota.task_failed') enabled = legacyOTAEnabled; if (item.type === 'ota.failure_rate_exceeded') enabled = legacyOTAEnabled; if (item.type === 'ai.badcase_rate_exceeded') enabled = legacyBadcaseEnabled; rules[item.type] = { enabled, threshold: item.type === 'ai.badcase_rate_exceeded' ? (settings.badcaseThreshold || item.defaultThreshold || '') : (item.defaultThreshold || ''), channels: item.defaultChannels || ['in_app'], }; return rules; }, {}); } function getNotificationRules(settings = {}) { const defaults = getDefaultNotificationRules(settings); const saved = settings.notificationRules || {}; return Object.fromEntries(Object.entries(defaults).map(([type, rule]) => [ type, { ...rule, ...(saved[type] || {}), channels: saved[type]?.channels || rule.channels, }, ])); } function getDefaultSettings() { return { apiKeys: { openai: '', ali: 'sk-talkingq-demo-bailian-20260609', gemini: '', }, apiKeyRecords: [ { id: 'ali', name: '百炼运行密钥', providerId: 'ali', provider: 'Alibaba Cloud Bailian', value: 'sk-talkingq-demo-bailian-20260609', status: 'available', createdAt: '2026-06-09', lastTestedAt: '2026-06-09', }, ], platformName: 'TalkingQ', refreshInterval: 300, defaultModel: 'qwen-turbo', enableDebug: false, notificationDelivery: { emails: 'admin@talkingq.com', webhookUrl: 'https://hooks.talkingq.com/demo/alerts', }, notificationRules: {}, }; } function normalizeSettings(settings = {}) { const defaults = getDefaultSettings(); const legacyKeys = { ...defaults.apiKeys, ...(settings.apiKeys || {}), }; const apiKeyRecords = Array.isArray(settings.apiKeyRecords) ? settings.apiKeyRecords.map(record => ({ ...record })) : SETTINGS_API_KEY_PROVIDERS .filter(provider => String(legacyKeys[provider.id] || '').trim()) .map(provider => ({ id: provider.id, name: provider.name.replace(' API Key', ''), providerId: provider.id, provider: provider.provider, value: legacyKeys[provider.id], status: 'configured', createdAt: '2026-06-09', lastTestedAt: '', })); const apiKeys = apiKeyRecords.reduce((result, record) => { if (record.providerId && record.value) result[record.providerId] = record.value; return result; }, { openai: '', ali: '', gemini: '' }); return { ...defaults, ...settings, apiKeys, apiKeyRecords, notificationDelivery: { ...defaults.notificationDelivery, ...(settings.notificationDelivery || {}), }, notificationRules: { ...getDefaultNotificationRules(settings), ...(settings.notificationRules || {}), }, }; } function loadSettings() { if (window.__talkingqSettings) { return normalizeSettings(JSON.parse(JSON.stringify(window.__talkingqSettings))); } try { localStorage.removeItem('talkingq_settings'); } catch (error) { console.warn('Failed to clear legacy settings cache', error); } return normalizeSettings(); } function saveSettings(settings) { window.__talkingqSettings = JSON.parse(JSON.stringify(normalizeSettings(settings || {}))); } function getApiKey(provider) { const settings = loadSettings(); return settings.apiKeyRecords?.find(record => record.providerId === provider)?.value || settings.apiKeys?.[provider] || ''; } function escapeSettingsHtml(value) { return String(value ?? '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function getApiKeyStatusMeta(status) { if (status === 'available') return { label: '连接可用', badge: 'badge-success' }; if (status === 'failed') return { label: '连接失败', badge: 'badge-danger' }; if (status === 'testing') return { label: '测试中', badge: 'badge-warning' }; return { label: '已保存', badge: 'badge-info' }; } function maskApiKey(value) { const key = String(value || ''); if (!key) return '-'; if (key.length <= 8) return `${key.slice(0, 2)}****`; return `${key.slice(0, 5)}****${key.slice(-4)}`; } function getSettingsDateText() { const now = new Date(); const pad = value => String(value).padStart(2, '0'); return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`; } function isValidEmailList(value) { const emails = String(value || '') .split(/[,,;\s]+/) .map(item => item.trim()) .filter(Boolean); return emails.length > 0 && emails.every(email => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)); } function isValidWebhookUrl(value) { return /^https?:\/\/[^\s]+$/i.test(String(value || '').trim()); } async function callSettingsApi(method, ...args) { try { if (typeof API !== 'undefined' && API.settings?.[method]) { return await API.settings[method](...args); } } catch (error) { console.warn(`Mock settings API ${method} failed:`, error); throw error; } return null; } function renderNotificationRuleRows(rules) { return getNotificationCatalog().map(item => { const key = getNotificationInputKey(item.type); const rule = rules[item.type] || {}; const channels = new Set(rule.channels || item.defaultChannels || ['in_app']); const hasThreshold = item.defaultThreshold !== undefined; const thresholdText = item.thresholdLabel || (hasThreshold ? `${rule.threshold || item.defaultThreshold}${item.thresholdUnit || ''}` : '发生即通知'); return `
${escapeSettingsHtml(item.name)}
${escapeSettingsHtml(item.trigger)}
${getNotificationSeverityLabel(item.severity)}
规则:${escapeSettingsHtml(item.ruleSummary || thresholdText)} 窗口:${escapeSettingsHtml(item.window || '实时')} 冷却:${escapeSettingsHtml(item.cooldown || '无')}
通知内容:${item.contentFields.map(escapeSettingsHtml).join('、')}
${hasThreshold ? ` ` : `
阈值 ${escapeSettingsHtml(thresholdText)}
`}
`; }).join(''); } registerPage('settings', function(container) { let settings = loadSettings(); function renderPage() { const notificationRules = getNotificationRules(settings); const enabledRuleCount = Object.values(notificationRules).filter(rule => rule.enabled !== false).length; const availableKeyCount = settings.apiKeyRecords.filter(record => record.status === 'available').length; container.innerHTML = `

API 密钥配置

统一维护智能体运行时可选择的 API Key

${settings.apiKeyRecords.length ? `
${settings.apiKeyRecords.map(record => { const status = getApiKeyStatusMeta(record.status); return ` `; }).join('')}
密钥名称 Provider API Key 状态 创建时间 操作
${escapeSettingsHtml(record.name)}
${escapeSettingsHtml(record.provider)} ${escapeSettingsHtml(maskApiKey(record.value))} ${status.label} ${escapeSettingsHtml(record.createdAt || '-')}
` : `
暂无已保存的 API Key

创建并保存后,密钥会直接显示在这里。

`}
密钥列表仅展示脱敏信息。建议按环境分别创建,并定期测试和轮换。

平台配置

系统展示和默认运行参数

通知渠道

配置消息的实际接收地址

多个邮箱使用逗号分隔
用于向企业内部系统推送告警消息

通知规则

只通知需要关注或处理的异常、异步任务和关键安全操作

${enabledRuleCount} 项已启用
异常告警设备、AI 安全、OTA 失败
异步结果应用发布和知识库任务
安全权限登录、管理员和权限变更
${renderNotificationRuleRows(notificationRules)}
`; bindEvents(); syncAllNotificationRows(); lucide.createIcons({ elements: [container] }); } function bindEvents() { container.querySelector('#createApiKeyBtn')?.addEventListener('click', openCreateApiKeyModal); container.querySelector('#createApiKeyEmptyBtn')?.addEventListener('click', openCreateApiKeyModal); container.querySelectorAll('[data-settings-target]').forEach(button => { button.addEventListener('click', () => { const section = container.querySelector(`#${button.dataset.settingsTarget}`); if (!section) return; container.querySelectorAll('.settings-nav-item').forEach(item => item.classList.toggle('active', item === button)); section.scrollIntoView({ behavior: 'smooth', block: 'start' }); }); }); container.querySelectorAll('[data-api-key-action]').forEach(button => { button.addEventListener('click', () => { const keyId = button.dataset.keyId; if (button.dataset.apiKeyAction === 'copy') copySavedApiKey(keyId); if (button.dataset.apiKeyAction === 'test') testSavedApiKey(keyId, button); if (button.dataset.apiKeyAction === 'delete') deleteSavedApiKey(keyId); }); }); container.querySelector('#savePlatformBtn')?.addEventListener('click', savePlatformSettingsFromForm); container.querySelector('#resetPlatformBtn')?.addEventListener('click', resetPlatformSettings); container.querySelector('#testEmailBtn')?.addEventListener('click', event => testNotificationEmail(event.currentTarget)); container.querySelector('#testWebhookBtn')?.addEventListener('click', event => testNotificationWebhook(event.currentTarget)); container.querySelector('#saveNotificationsBtn')?.addEventListener('click', saveNotificationSettingsFromForm); container.querySelector('#resetNotificationsBtn')?.addEventListener('click', resetNotificationSettings); container.querySelectorAll('.notification-enabled-input').forEach(input => { input.addEventListener('change', () => syncNotificationRow(input.closest('.notification-rule-row'))); }); } function syncNotificationRow(row) { if (!row) return; const enabled = row.querySelector('.notification-enabled-input')?.checked !== false; row.style.opacity = enabled ? '1' : '.58'; row.querySelectorAll('.notification-threshold-input, .notification-channel-input').forEach(input => { input.disabled = !enabled; }); } function syncAllNotificationRows() { container.querySelectorAll('.notification-rule-row').forEach(syncNotificationRow); } function persistApiKeyRecords(records) { settings = normalizeSettings({ ...settings, apiKeyRecords: records }); saveSettings(settings); window.dispatchEvent(new CustomEvent('settingsChanged', { detail: { section: 'apiKeys' } })); } function openCreateApiKeyModal() { const modal = createModal({ title: '创建 API Key', content: `
保存后该密钥会直接显示在列表中,并出现在智能体运行 API Key 下拉选择里。
`, onConfirm: async () => { const name = modal.overlay.querySelector('#newApiKeyName')?.value.trim(); const providerSelection = modal.overlay.querySelector('#newApiKeyProvider')?.value || ''; const customProvider = modal.overlay.querySelector('#customApiProviderName')?.value.trim(); const value = modal.overlay.querySelector('#newApiKeyValue')?.value.trim(); if (!name || !providerSelection || !value) { Toast.warning('请填写密钥名称、Provider 和 API Key'); return false; } if (providerSelection === 'custom' && !customProvider) { Toast.warning('请填写 Provider 名称'); return false; } if (value.length < 8) { Toast.warning('API Key 长度不能少于 8 位'); return false; } if (settings.apiKeyRecords.some(record => record.name === name)) { Toast.warning('密钥名称已存在'); return false; } const providerConfig = SETTINGS_API_KEY_PROVIDERS.find(provider => provider.id === providerSelection); const record = { id: `key-${Date.now()}`, name, providerId: providerConfig?.id || `custom-${Date.now()}`, provider: providerConfig?.provider || customProvider, value, status: 'configured', createdAt: getSettingsDateText(), lastTestedAt: '', }; persistApiKeyRecords([record, ...settings.apiKeyRecords]); try { await callSettingsApi('createApiKey', { name: record.name, provider: record.provider, providerId: record.providerId, apiKey: record.value, }); } catch (error) { console.warn('Mock create API key failed:', error); } Toast.success('API Key 已创建并保存'); renderPage(); }, confirmText: '创建并保存', }); const providerSelect = modal.overlay.querySelector('#newApiKeyProvider'); const customGroup = modal.overlay.querySelector('#customApiProviderGroup'); providerSelect?.addEventListener('change', () => { if (customGroup) customGroup.style.display = providerSelect.value === 'custom' ? '' : 'none'; }); modal.overlay.querySelector('#newApiKeyVisibility')?.addEventListener('click', event => { const input = modal.overlay.querySelector('#newApiKeyValue'); if (!input) return; const show = input.type === 'password'; input.type = show ? 'text' : 'password'; event.currentTarget.innerHTML = ``; lucide.createIcons({ elements: [event.currentTarget] }); }); } async function copySavedApiKey(keyId) { const record = settings.apiKeyRecords.find(item => item.id === keyId); if (!record) return; try { await navigator.clipboard.writeText(record.value); Toast.success('API Key 已复制'); } catch (error) { Toast.warning('当前浏览器不允许自动复制'); } } async function testSavedApiKey(keyId, button) { const record = settings.apiKeyRecords.find(item => item.id === keyId); if (!record) return; button.disabled = true; button.innerHTML = ' 测试中'; lucide.createIcons({ elements: [button] }); await new Promise(resolve => setTimeout(resolve, 650)); const ok = record.value.length >= 8 && !/(invalid|error|fail)/i.test(record.value); const nextRecords = settings.apiKeyRecords.map(item => item.id === keyId ? { ...item, status: ok ? 'available' : 'failed', lastTestedAt: getSettingsDateText() } : item); persistApiKeyRecords(nextRecords); Toast[ok ? 'success' : 'error'](ok ? 'API Key 连接可用' : 'API Key 连接失败'); renderPage(); } function deleteSavedApiKey(keyId) { const record = settings.apiKeyRecords.find(item => item.id === keyId); if (!record) return; createModal({ title: '删除 API Key', content: `

确认删除「${escapeSettingsHtml(record.name)}」?

删除后智能体运行页将无法再选择该密钥。

`, onConfirm: async () => { persistApiKeyRecords(settings.apiKeyRecords.filter(item => item.id !== keyId)); try { await callSettingsApi('revokeApiKey', keyId); } catch (error) { console.warn('Mock revoke API key failed:', error); } Toast.success('API Key 已删除'); renderPage(); }, confirmText: '确认删除', }); lucide.createIcons(); } function savePlatformSettingsFromForm() { const platformName = container.querySelector('#platformName')?.value.trim(); if (!platformName) { Toast.warning('请填写平台名称'); return; } settings = normalizeSettings({ ...settings, platformName, refreshInterval: Number(container.querySelector('#refreshInterval')?.value || 300), defaultModel: container.querySelector('#defaultModel')?.value || 'qwen-turbo', enableDebug: Boolean(container.querySelector('#enableDebug')?.checked), }); saveSettings(settings); window.dispatchEvent(new CustomEvent('settingsChanged', { detail: { section: 'platform' } })); Toast.success('平台配置已保存'); renderPage(); } function resetPlatformSettings() { createModal({ title: '恢复平台默认配置', content: '

确认恢复平台名称、刷新间隔、默认模型和调试模式的默认值?

', onConfirm: () => { const defaults = getDefaultSettings(); settings = normalizeSettings({ ...settings, platformName: defaults.platformName, refreshInterval: defaults.refreshInterval, defaultModel: defaults.defaultModel, enableDebug: defaults.enableDebug, }); saveSettings(settings); Toast.success('平台配置已恢复默认'); renderPage(); }, confirmText: '确认恢复', }); } async function runLoadingButton(button, loadingText, task) { const original = button.innerHTML; button.disabled = true; button.innerHTML = ` ${loadingText}`; lucide.createIcons({ elements: [button] }); try { await task(); } finally { if (document.body.contains(button)) { button.disabled = false; button.innerHTML = original; lucide.createIcons({ elements: [button] }); } } } async function testNotificationEmail(button) { const emails = container.querySelector('#notificationEmails')?.value.trim() || ''; if (!isValidEmailList(emails)) { Toast.warning('请输入有效的通知邮箱,多个邮箱可用逗号分隔'); return; } await runLoadingButton(button, '发送中...', async () => { await new Promise(resolve => setTimeout(resolve, 500)); Toast.success('测试邮件已发送'); }); } async function testNotificationWebhook(button) { const url = container.querySelector('#notificationWebhookUrl')?.value.trim() || ''; if (!isValidWebhookUrl(url)) { Toast.warning('请输入以 http:// 或 https:// 开头的 Webhook URL'); return; } await runLoadingButton(button, '测试中...', async () => { try { await callSettingsApi('testWebhook', url); } catch (error) { Toast.warning('后端测试接口暂不可用,已完成前端格式校验'); return; } await new Promise(resolve => setTimeout(resolve, 400)); Toast.success('Webhook 测试请求已发送'); }); } function collectNotificationForm() { const rules = {}; let error = ''; getNotificationCatalog().forEach(item => { const key = getNotificationInputKey(item.type); const enabled = Boolean(container.querySelector(`#notif_${key}_enabled`)?.checked); const channels = ['in_app']; if (container.querySelector(`#notif_${key}_channel_email`)?.checked) channels.push('email'); if (container.querySelector(`#notif_${key}_channel_webhook`)?.checked) channels.push('webhook'); let threshold; if (item.defaultThreshold !== undefined) { threshold = Number(container.querySelector(`#notif_${key}_threshold`)?.value); if (enabled && (!Number.isFinite(threshold) || threshold < 1 || (item.thresholdUnit === '%' && threshold > 100))) { error = `${item.name} 的阈值设置不正确`; } } rules[item.type] = { enabled, threshold, thresholdUnit: item.thresholdUnit || undefined, thresholdLabel: item.thresholdLabel || undefined, window: item.window || undefined, cooldown: item.cooldown || undefined, channels, }; }); return { rules, error }; } async function saveNotificationSettingsFromForm() { const emails = container.querySelector('#notificationEmails')?.value.trim() || ''; const webhookUrl = container.querySelector('#notificationWebhookUrl')?.value.trim() || ''; const { rules, error } = collectNotificationForm(); if (error) { Toast.warning(error); return; } const enabledRules = Object.values(rules).filter(rule => rule.enabled); const needsEmail = enabledRules.some(rule => rule.channels.includes('email')); const needsWebhook = enabledRules.some(rule => rule.channels.includes('webhook')); if (needsEmail && !isValidEmailList(emails)) { Toast.warning('已有规则启用邮件通知,请先填写有效通知邮箱'); return; } if (needsWebhook && !isValidWebhookUrl(webhookUrl)) { Toast.warning('已有规则启用 Webhook,请先填写有效 Webhook URL'); return; } settings.notificationRules = rules; settings.notificationDelivery = { emails, webhookUrl }; settings.notifyBadcase = rules['ai.badcase_rate_exceeded']?.enabled !== false; settings.badcaseThreshold = rules['ai.badcase_rate_exceeded']?.threshold || 5; settings.notifyOffline = rules['device.offline_rate_exceeded']?.enabled !== false; settings.notifyOTA = rules['ota.task_failed']?.enabled !== false; saveSettings(settings); try { await callSettingsApi('updateNotifications', { rules, delivery: settings.notificationDelivery, tenantScope: window.__getTenantScopeParams ? window.__getTenantScopeParams() : {}, }); } catch (error) { Toast.warning('通知设置已保存在前端,后端接口接入后会自动同步'); renderPage(); return; } window.__refreshNotifications?.(); Toast.success('通知设置已保存'); renderPage(); } function resetNotificationSettings() { createModal({ title: '恢复默认通知设置', content: '

确认恢复全部通知规则阈值和默认通知渠道?通知邮箱和 Webhook 地址会保留。

', onConfirm: () => { settings.notificationRules = getDefaultNotificationRules({}); saveSettings(settings); Toast.success('通知规则已恢复默认'); renderPage(); }, confirmText: '确认恢复', }); } renderPage(); });