/**
* 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('、')}
`;
}).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 = `
${settings.apiKeyRecords.length ? `
| 密钥名称 |
Provider |
API Key |
状态 |
创建时间 |
操作 |
${settings.apiKeyRecords.map(record => {
const status = getApiKeyStatusMeta(record.status);
return `
|
${escapeSettingsHtml(record.name)}
|
${escapeSettingsHtml(record.provider)} |
${escapeSettingsHtml(maskApiKey(record.value))} |
${status.label} |
${escapeSettingsHtml(record.createdAt || '-')} |
|
`;
}).join('')}
` : `
暂无已保存的 API Key
创建并保存后,密钥会直接显示在这里。
`}
密钥列表仅展示脱敏信息。建议按环境分别创建,并定期测试和轮换。
异常告警设备、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();
});