Files
TalkingQ_Dashboard/scripts/settings.js
“yuanmengy” a225272d1e data&device ready
2026-06-05 17:11:56 +08:00

407 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* TalkingQ Platform - Settings Page
*/
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 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 `
<div class="notification-rule-row" data-rule-type="${item.type}">
<div class="notification-rule-main">
<div class="notification-rule-head">
<div>
<div class="notification-rule-title">${item.name}</div>
<div class="notification-rule-desc">${item.trigger}</div>
</div>
<span class="notification-rule-severity ${item.severity}">${getNotificationSeverityLabel(item.severity)}</span>
</div>
<div class="notification-rule-meta">
<span>规则:${item.ruleSummary || thresholdText}</span>
<span>窗口:${item.window || '实时'}</span>
<span>冷却:${item.cooldown || '无'}</span>
</div>
<div class="notification-rule-content">
通知内容:${item.contentFields.join('、')}
</div>
</div>
<div class="notification-rule-controls">
<label class="notification-switch">
<input type="checkbox" id="notif_${key}_enabled" ${rule.enabled !== false ? 'checked' : ''}>
<span>启用</span>
</label>
${hasThreshold ? `
<label class="notification-threshold">
<span>阈值</span>
<input type="number" class="form-control" id="notif_${key}_threshold" value="${rule.threshold || item.defaultThreshold}" min="1" style="width:84px">
<em>${item.thresholdUnit || ''}</em>
</label>
` : ''}
${!hasThreshold ? `
<div class="notification-threshold readonly">
<span>阈值</span>
<strong>${thresholdText}</strong>
</div>
` : ''}
<div class="notification-channels">
<label><input type="checkbox" id="notif_${key}_channel_in_app" checked disabled> 站内</label>
<label><input type="checkbox" id="notif_${key}_channel_email" ${channels.has('email') ? 'checked' : ''}> 邮件</label>
<label><input type="checkbox" id="notif_${key}_channel_webhook" ${channels.has('webhook') ? 'checked' : ''}> Webhook</label>
</div>
</div>
</div>
`;
}).join('');
}
registerPage('settings', function(container) {
// Load saved settings
const settings = loadSettings();
const notificationRules = getNotificationRules(settings);
container.innerHTML = `
<div class="page-header">
<div>
<div class="page-title">系统设置</div>
<div class="page-desc">配置平台参数、API 密钥和通知设置</div>
</div>
</div>
<div style="max-width:800px">
<!-- API Key Section -->
<div class="card" style="margin-bottom:16px">
<div class="card-header">
<div>
<div class="card-title">API 密钥配置</div>
<div class="card-subtitle">配置 AI 模型服务的 API 密钥</div>
</div>
</div>
<div style="padding:16px">
<div class="form-group">
<label class="form-label">OpenAI API Key</label>
<div class="input-wrapper">
<input type="password" class="form-control" id="apiKeyOpenai" value="${settings.apiKeys?.openai || ''}" placeholder="sk-...">
<button type="button" class="password-toggle" onclick="toggleApiKeyVisibility('apiKeyOpenai', this)">
<i data-lucide="eye-off"></i>
</button>
</div>
<div style="font-size:12px;color:var(--text-muted);margin-top:4px">用于 GPT-4o、GPT-4o mini 等模型调用</div>
</div>
<div class="form-group" style="margin-top:16px">
<label class="form-label">阿里云百炼 API Key</label>
<div class="input-wrapper">
<input type="password" class="form-control" id="apiKeyAli" value="${settings.apiKeys?.ali || ''}" placeholder="sk-...">
<button type="button" class="password-toggle" onclick="toggleApiKeyVisibility('apiKeyAli', this)">
<i data-lucide="eye-off"></i>
</button>
</div>
<div style="font-size:12px;color:var(--text-muted);margin-top:4px">用于通义千问等阿里云大模型服务</div>
</div>
<div class="form-group" style="margin-top:16px">
<label class="form-label">Google Gemini API Key</label>
<div class="input-wrapper">
<input type="password" class="form-control" id="apiKeyGemini" value="${settings.apiKeys?.gemini || ''}" placeholder="AIza...">
<button type="button" class="password-toggle" onclick="toggleApiKeyVisibility('apiKeyGemini', this)">
<i data-lucide="eye-off"></i>
</button>
</div>
<div style="font-size:12px;color:var(--text-muted);margin-top:4px">用于 Gemini 1.5 Flash 等模型</div>
</div>
<div style="display:flex;gap:12px;margin-top:20px;padding-top:16px;border-top:1px solid var(--border)">
<button class="btn btn-primary" onclick="saveApiKeys()">
<i data-lucide="save"></i> 保存密钥
</button>
<button class="btn btn-outline" onclick="testApiConnection(this)">
<i data-lucide="check-circle"></i> 测试连接
</button>
</div>
</div>
</div>
<!-- Platform Settings -->
<div class="card" style="margin-bottom:16px">
<div class="card-header">
<div>
<div class="card-title">平台配置</div>
<div class="card-subtitle">配置系统级参数</div>
</div>
</div>
<div style="padding:16px">
<div class="form-row">
<div class="form-group" style="flex:1">
<label class="form-label">平台名称</label>
<input type="text" class="form-control" id="platformName" value="${settings.platformName || 'TalkingQ'}" placeholder="平台名称">
</div>
<div class="form-group" style="flex:1">
<label class="form-label">数据刷新间隔</label>
<select class="form-control" id="refreshInterval">
<option value="30" ${settings.refreshInterval == 30 ? 'selected' : ''}>30 秒</option>
<option value="60" ${settings.refreshInterval == 60 ? 'selected' : ''}>1 分钟</option>
<option value="300" ${settings.refreshInterval == 300 || !settings.refreshInterval ? 'selected' : ''}>5 分钟</option>
<option value="600" ${settings.refreshInterval == 600 ? 'selected' : ''}>10 分钟</option>
</select>
</div>
</div>
<div class="form-group" style="margin-top:16px">
<label class="form-label">默认 AI 模型</label>
<select class="form-control" id="defaultModel">
<option value="gpt-4o-mini" ${settings.defaultModel == 'gpt-4o-mini' ? 'selected' : ''}>GPT-4o mini推荐 · 快速)</option>
<option value="gpt-4o" ${settings.defaultModel == 'gpt-4o' ? 'selected' : ''}>GPT-4o高能力</option>
<option value="qwen-turbo" ${settings.defaultModel == 'qwen-turbo' ? 'selected' : ''}>通义千问-Turbo</option>
<option value="gemini-1.5-flash" ${settings.defaultModel == 'gemini-1.5-flash' ? 'selected' : ''}>Gemini 1.5 Flash</option>
<option value="claude-3-haiku" ${settings.defaultModel == 'claude-3-haiku' ? 'selected' : ''}>Claude 3 Haiku</option>
</select>
</div>
<div class="form-group" style="margin-top:16px">
<label class="checkbox-label">
<input type="checkbox" id="enableDebug" ${settings.enableDebug ? 'checked' : ''}>
<span class="checkmark"></span>
<span>开启调试模式(显示详细日志)</span>
</label>
</div>
<div style="margin-top:20px;padding-top:16px;border-top:1px solid var(--border)">
<button class="btn btn-primary" onclick="savePlatformSettings()">
<i data-lucide="save"></i> 保存配置
</button>
</div>
</div>
</div>
<!-- Notification Settings -->
<div class="card">
<div class="card-header">
<div>
<div class="card-title">通知设置</div>
<div class="card-subtitle">只通知需要关注或处理的事件,包含异常、异步任务和关键生命周期操作</div>
</div>
</div>
<div style="padding:16px">
<div class="notification-principles">
<div>
<strong>异常告警</strong>
<span>设备、AI 安全、OTA 失败等需要处理的异常。</span>
</div>
<div>
<strong>异步结果</strong>
<span>应用发布、知识库索引等耗时任务的失败或完成结果。</span>
</div>
<div>
<strong>安全权限</strong>
<span>异常登录、管理员变更、API Key 创建/撤销、权限异常变更。</span>
</div>
</div>
<div class="notification-rule-list">
${renderNotificationRuleRows(notificationRules)}
</div>
<div style="margin-top:20px;padding-top:16px;border-top:1px solid var(--border)">
<button class="btn btn-primary" onclick="saveNotificationSettings()">
<i data-lucide="save"></i> 保存设置
</button>
</div>
</div>
</div>
</div>
`;
lucide.createIcons({ elements: [container] });
});
// Load settings from in-memory demo state
function loadSettings() {
if (window.__talkingqSettings) {
return JSON.parse(JSON.stringify(window.__talkingqSettings));
}
try {
localStorage.removeItem('talkingq_settings');
} catch (e) {
console.warn('Failed to clear legacy settings cache', e);
}
return {};
}
// Save settings to in-memory demo state
function saveSettings(settings) {
window.__talkingqSettings = JSON.parse(JSON.stringify(settings || {}));
}
// Toggle API key visibility
function toggleApiKeyVisibility(inputId, btn) {
const input = document.getElementById(inputId);
if (input) {
const isPassword = input.type === 'password';
input.type = isPassword ? 'text' : 'password';
btn.innerHTML = `<i data-lucide="${isPassword ? 'eye' : 'eye-off'}"></i>`;
btn.setAttribute('title', isPassword ? '隐藏密钥' : '显示密钥');
lucide.createIcons({ elements: [btn] });
}
}
async function testApiConnection(btn) {
const keys = ['apiKeyOpenai', 'apiKeyAli', 'apiKeyGemini']
.map(id => document.getElementById(id)?.value.trim())
.filter(Boolean);
if (keys.length === 0) {
Toast.warning('请先填写至少一个 API Key');
return;
}
const original = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<i data-lucide="loader-2" class="spin"></i> 测试中...';
lucide.createIcons({ elements: [btn] });
await new Promise(resolve => setTimeout(resolve, 800));
btn.disabled = false;
btn.innerHTML = original;
lucide.createIcons({ elements: [btn] });
Toast.success('API 连接测试通过');
}
// Save API Keys
function saveApiKeys() {
const settings = loadSettings();
settings.apiKeys = {
openai: document.getElementById('apiKeyOpenai')?.value || '',
ali: document.getElementById('apiKeyAli')?.value || '',
gemini: document.getElementById('apiKeyGemini')?.value || ''
};
saveSettings(settings);
Toast.success('API 密钥已保存');
}
// Save Platform Settings
function savePlatformSettings() {
const settings = loadSettings();
settings.platformName = document.getElementById('platformName')?.value || 'TalkingQ';
settings.refreshInterval = parseInt(document.getElementById('refreshInterval')?.value || '300');
settings.defaultModel = document.getElementById('defaultModel')?.value || 'gpt-4o-mini';
settings.enableDebug = document.getElementById('enableDebug')?.checked || false;
saveSettings(settings);
Toast.success('平台配置已保存');
}
// Save Notification Settings
async function saveNotificationSettings() {
const settings = loadSettings();
settings.notificationRules = {};
getNotificationCatalog().forEach(item => {
const key = getNotificationInputKey(item.type);
const channels = ['in_app'];
if (document.getElementById(`notif_${key}_channel_email`)?.checked) channels.push('email');
if (document.getElementById(`notif_${key}_channel_webhook`)?.checked) channels.push('webhook');
settings.notificationRules[item.type] = {
enabled: document.getElementById(`notif_${key}_enabled`)?.checked || false,
threshold: item.defaultThreshold !== undefined
? parseInt(document.getElementById(`notif_${key}_threshold`)?.value || item.defaultThreshold, 10)
: undefined,
thresholdUnit: item.thresholdUnit || undefined,
thresholdLabel: item.thresholdLabel || undefined,
window: item.window || undefined,
cooldown: item.cooldown || undefined,
channels,
};
});
settings.notifyBadcase = settings.notificationRules['ai.badcase_rate_exceeded']?.enabled !== false;
settings.badcaseThreshold = settings.notificationRules['ai.badcase_rate_exceeded']?.threshold || 5;
settings.notifyOffline = settings.notificationRules['device.offline_rate_exceeded']?.enabled !== false;
settings.notifyOTA = settings.notificationRules['ota.task_failed']?.enabled !== false;
saveSettings(settings);
try {
await API.settings.updateNotifications({
rules: settings.notificationRules,
tenantScope: window.__getTenantScopeParams ? window.__getTenantScopeParams() : {},
});
} catch (error) {
Toast.warning('通知设置已保存在前端,后端接口接入后会同步保存');
return;
}
window.__refreshNotifications?.();
Toast.success('通知设置已保存');
}
// Get API Key helper
function getApiKey(provider) {
const settings = loadSettings();
return settings.apiKeys?.[provider] || '';
}