// src/lib/monitor/settings-manager.ts import { getDb } from '@/lib/db' import crypto from 'crypto' import type { MonitorConfig, WebhookConfig } from './types' const ALGORITHM = 'aes-256-cbc' function getEncryptionKey(): string { const db = getDb() const row = db.prepare("SELECT value FROM settings WHERE key = 'monitor.encryption_key'").get() as { value: string } | undefined if (row) return row.value // 首次启动:生成密钥 const newKey = crypto.randomBytes(32).toString('hex') db.prepare("INSERT OR IGNORE INTO settings (key, value, category) VALUES ('monitor.encryption_key', ?, 'security')").run(newKey) return newKey } export function encryptValue(plaintext: string): string { if (!plaintext) return '' const key = Buffer.from(getEncryptionKey(), 'hex') const iv = crypto.randomBytes(16) const cipher = crypto.createCipheriv(ALGORITHM, key, iv) let encrypted = cipher.update(plaintext, 'utf8', 'hex') encrypted += cipher.final('hex') return iv.toString('hex') + ':' + encrypted } export function decryptValue(ciphertext: string): string { if (!ciphertext) return '' try { const key = Buffer.from(getEncryptionKey(), 'hex') const [ivHex, encrypted] = ciphertext.split(':') const iv = Buffer.from(ivHex, 'hex') const decipher = crypto.createDecipheriv(ALGORITHM, key, iv) let decrypted = decipher.update(encrypted, 'hex', 'utf8') decrypted += decipher.final('utf8') return decrypted } catch { console.error('[Settings] 密码解密失败,可能密钥不匹配') return '' } } export function getSetting(key: string): string | null { const db = getDb() const row = db.prepare('SELECT value FROM settings WHERE key = ?').get(key) as { value: string } | undefined return row?.value ?? null } export function setSetting(key: string, value: string, category = 'general'): void { const db = getDb() db.prepare( "INSERT INTO settings (key, value, category, updated_at) VALUES (?, ?, ?, datetime('now', '+8 hours')) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at" ).run(key, value, category) } const DEFAULT_WEBHOOK: WebhookConfig = { id: 'default', name: '默认推送', url: '', enabled: true, params: { interval_seconds: 60, push_delay_ms: 2000, silent_start_hour: 0, silent_end_hour: 7 }, } const DEFAULT_CONFIG: MonitorConfig = { monitor: { enabled: false, interval_seconds: 60, push_delay_ms: 2000, silent_start_hour: 0, silent_end_hour: 7 }, mail: { address: 'gxp@qx002575.com', imap_server: 'imaphz.qiye.163.com', imap_port: 993, pop3_server: 'pophz.qiye.163.com', pop3_port: 995, smtp_server: 'smtphz.qiye.163.com', smtp_port: 465, password: '' }, filter: { subject_keywords: ['服务器故障单'], sender_email: 'tencent_dcops@tencent.com', oem_repair_keywords: { base_keyword: '服务器故障单', type_keyword: 'OEM维修', exclude_keyword: 'OEM诊断' }, oem_diag_keywords: { base_keyword: '服务器故障单', type_keyword: 'OEM诊断', exclude_keyword: 'OEM维修' }, }, wechat: { webhooks: [JSON.parse(JSON.stringify(DEFAULT_WEBHOOK))] }, } export function generateWebhookId(): string { return 'wh_' + crypto.randomBytes(8).toString('hex') } // 将旧的 wechat.webhook_url 迁移为 wechat.webhooks 数组 function migrateWebhookSettings(db: ReturnType): void { const legacyUrl = db.prepare("SELECT value FROM settings WHERE key = 'wechat.webhook_url'").get() as { value: string } | undefined if (legacyUrl?.value) { const webhook: WebhookConfig = { id: generateWebhookId(), name: '默认推送', url: legacyUrl.value, enabled: true, params: { interval_seconds: 60, push_delay_ms: 2000, silent_start_hour: 0, silent_end_hour: 7 }, } setSetting('wechat.webhooks', JSON.stringify([webhook]), 'wechat') db.prepare("DELETE FROM settings WHERE key = 'wechat.webhook_url'").run() } } export function getMonitorConfig(): MonitorConfig { const config = JSON.parse(JSON.stringify(DEFAULT_CONFIG)) const db = getDb() // 迁移旧格式 migrateWebhookSettings(db) const mappings: Record void> = { 'monitor.enabled': (v) => { config.monitor.enabled = v === 'true' }, 'monitor.interval_seconds': (v) => { config.monitor.interval_seconds = parseInt(v) || 60 }, 'monitor.push_delay_ms': (v) => { config.monitor.push_delay_ms = parseInt(v) || 2000 }, 'monitor.silent_start_hour': (v) => { config.monitor.silent_start_hour = parseInt(v) || 0 }, 'monitor.silent_end_hour': (v) => { config.monitor.silent_end_hour = parseInt(v) || 7 }, 'mail.address': (v) => { config.mail.address = v }, 'mail.imap_server': (v) => { config.mail.imap_server = v }, 'mail.imap_port': (v) => { config.mail.imap_port = parseInt(v) || 993 }, 'mail.pop3_server': (v) => { config.mail.pop3_server = v }, 'mail.pop3_port': (v) => { config.mail.pop3_port = parseInt(v) || 995 }, 'mail.smtp_server': (v) => { config.mail.smtp_server = v }, 'mail.smtp_port': (v) => { config.mail.smtp_port = parseInt(v) || 465 }, 'mail.password': (v) => { config.mail.password = v ? decryptValue(v) : '' }, 'filter.subject_keywords': (v) => { try { config.filter.subject_keywords = JSON.parse(v) } catch {} }, 'filter.sender_email': (v) => { config.filter.sender_email = v }, 'filter.oem_repair_keywords': (v) => { try { config.filter.oem_repair_keywords = JSON.parse(v) } catch {} }, 'filter.oem_diag_keywords': (v) => { try { config.filter.oem_diag_keywords = JSON.parse(v) } catch {} }, 'wechat.webhooks': (v) => { try { const parsed = JSON.parse(v) if (Array.isArray(parsed) && parsed.length > 0) config.wechat.webhooks = parsed } catch {} }, } const rows = db.prepare("SELECT key, value FROM settings WHERE key LIKE 'monitor.%' OR key LIKE 'mail.%' OR key LIKE 'filter.%' OR key LIKE 'wechat.%'").all() as { key: string; value: string }[] for (const row of rows) { mappings[row.key]?.(row.value) } return config } export function updateMonitorConfig(updates: Record): void { const encryptedFields = new Set(['mail.password']) for (const [key, value] of Object.entries(updates)) { if (key === 'monitor.encryption_key') continue // 不允许修改密钥 let stringValue: string if (typeof value === 'boolean') stringValue = String(value) else if (typeof value === 'number') stringValue = String(value) else if (typeof value === 'object') stringValue = JSON.stringify(value) else stringValue = String(value ?? '') if (encryptedFields.has(key) && stringValue && !stringValue.match(/^[0-9a-f]{32}:/)) { stringValue = encryptValue(stringValue) } const category = key.split('.')[0] setSetting(key, stringValue, category) } } export function getMaskedConfig(): MonitorConfig { const config = getMonitorConfig() if (config.mail.password) config.mail.password = '••••••••' // 深拷贝 webhooks 数组,避免 mask 操作污染 getMonitorConfig() 返回的原对象 // (前端拿到 masked URL 后若不重新输入就测试,会用 •••••••• 调企业微信导致 93000 invalid url) config.wechat.webhooks = config.wechat.webhooks.map(wh => { if (wh.url) { const keyMatch = wh.url.match(/key=([0-9a-f-]+)/) if (keyMatch) { return { ...wh, url: wh.url.replace(keyMatch[1], '••••••••') } } } return wh }) return config }