issue-ai/src/lib/monitor/settings-manager.ts

169 lines
7.4 KiB
TypeScript
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.

// 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<typeof getDb>): 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<string, (v: string) => 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<string, unknown>): 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
}