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

123 lines
5.7 KiB
TypeScript

// src/lib/monitor/settings-manager.ts
import { getDb } from '@/lib/db'
import crypto from 'crypto'
import type { MonitorConfig } 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_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, 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: { webhook_url: '' },
}
export function getMonitorConfig(): MonitorConfig {
const config = JSON.parse(JSON.stringify(DEFAULT_CONFIG))
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.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.webhook_url': (v) => { config.wechat.webhook_url = v },
}
const db = getDb()
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 = '••••••••'
if (config.wechat.webhook_url) {
const keyMatch = config.wechat.webhook_url.match(/key=([0-9a-f-]+)/)
if (keyMatch) config.wechat.webhook_url = config.wechat.webhook_url.replace(keyMatch[1], '••••••••')
}
return config
}