'use strict'; const database = require('../config/database'); const { getResilientSecret, saveResilientSecret, } = require('./resilientSecretStore'); const CONFIG_KEY = 'yyd_notification_center_config'; const SECRET_KEY = 'yyd_notification_center_project_secret'; const DEFAULT_BASE_URL = 'https://sc.jiansun.vip/notification-center'; const DEFAULT_TIMEOUT_MS = 15000; let cachedConfig = null; let cachedAt = 0; function normalizeText(value, maxLength = 4000) { return String(value ?? '').trim().slice(0, maxLength); } function normalizeTimeout(value) { const number = Number(value); return Number.isFinite(number) ? Math.min(Math.max(Math.trunc(number), 3000), 120000) : DEFAULT_TIMEOUT_MS; } function safeJsonParse(value, fallback = {}) { if (value && typeof value === 'object') return value; try { return JSON.parse(value || '{}'); } catch (_error) { return fallback; } } function stripSecretFields(value) { const raw = value && typeof value === 'object' && !Array.isArray(value) ? { ...value } : {}; delete raw.projectSecret; delete raw.project_secret; delete raw.secret; return raw; } function maskSecret(value) { const text = normalizeText(value); if (!text) return ''; if (text.length <= 8) return `${text.slice(0, 2)}******`; return `${text.slice(0, 4)}${'*'.repeat(Math.min(10, text.length - 8))}${text.slice(-4)}`; } function getEnvironmentConfig() { return { baseUrl: normalizeText(process.env.YYD_NOTIFICATION_CENTER_URL || DEFAULT_BASE_URL, 1000), projectCode: normalizeText( process.env.YYD_NOTIFICATION_PROJECT_CODE || process.env.YYD_NOTIFICATION_CENTER_PROJECT_CODE, 120 ), projectSecret: normalizeText( process.env.YYD_NOTIFICATION_PROJECT_SECRET || process.env.YYD_NOTIFICATION_CENTER_PROJECT_SECRET, 1000 ), recipientAlias: normalizeText( process.env.ALIBABA_1688_IMAGE_SEARCH_EXPIRY_RECIPIENT_ALIAS || '苏适', 50 ), timeoutMs: normalizeTimeout(process.env.YYD_NOTIFICATION_TIMEOUT_MS), source: 'environment', }; } async function upsertNotificationCenterConfig(config) { const rows = await database.local.query( 'SELECT id FROM system_config WHERE config_key = ? LIMIT 1', [CONFIG_KEY] ); const value = JSON.stringify(config); if (rows[0]) { await database.local.query( 'UPDATE system_config SET config_value = ?, config_type = ? WHERE config_key = ?', [value, 'json', CONFIG_KEY] ); } else { await database.local.insert('system_config', { config_key: CONFIG_KEY, config_value: value, config_type: 'json', }); } } async function getYydNotificationCenterRuntimeConfig({ forceRefresh = false } = {}) { if (!forceRefresh && cachedConfig && Date.now() - cachedAt < 30000) return { ...cachedConfig }; const fallback = getEnvironmentConfig(); let stored = {}; let storedReadable = false; try { const rows = await database.local.query( 'SELECT config_value AS configValue FROM system_config WHERE config_key = ? LIMIT 1', [CONFIG_KEY] ); stored = safeJsonParse(rows[0]?.configValue, {}); storedReadable = true; } catch (error) { console.warn('读取统一通知中心配置失败,已回退环境变量:', error.message); } const legacySecret = normalizeText(stored.projectSecret || stored.project_secret || stored.secret); const secret = await getResilientSecret(SECRET_KEY, { legacyValues: [legacySecret], fallbackValues: [fallback.projectSecret], }); const sanitizedStored = stripSecretFields(stored); cachedConfig = { baseUrl: normalizeText(sanitizedStored.baseUrl || sanitizedStored.base_url || fallback.baseUrl, 1000).replace(/\/+$/, ''), projectCode: normalizeText(sanitizedStored.projectCode || sanitizedStored.project_code || fallback.projectCode, 120), projectSecret: secret.value, recipientAlias: normalizeText(sanitizedStored.recipientAlias || sanitizedStored.recipient_alias || fallback.recipientAlias, 50), timeoutMs: normalizeTimeout(sanitizedStored.timeoutMs || sanitizedStored.timeout_ms || fallback.timeoutMs), source: Object.keys(sanitizedStored).length > 0 ? 'database' : fallback.source, secretSource: secret.source, }; if (storedReadable && legacySecret && secret.value) { try { await upsertNotificationCenterConfig({ baseUrl: cachedConfig.baseUrl, projectCode: cachedConfig.projectCode, recipientAlias: cachedConfig.recipientAlias, timeoutMs: cachedConfig.timeoutMs, }); } catch (error) { console.warn('清理通知中心旧密钥字段失败:', error.message); } } cachedAt = Date.now(); return { ...cachedConfig }; } function validateNotificationCenterConfig(config) { if (!config.baseUrl || !config.projectCode || !config.projectSecret || !config.recipientAlias) { throw Object.assign(new Error('通知中心地址、项目代码、项目密钥和提醒人不能为空'), { status: 400 }); } let parsed; try { parsed = new URL(config.baseUrl); } catch (_error) { throw Object.assign(new Error('通知中心地址格式无效'), { status: 400 }); } if (!['http:', 'https:'].includes(parsed.protocol)) { throw Object.assign(new Error('通知中心地址必须使用 HTTP 或 HTTPS'), { status: 400 }); } } async function saveYydNotificationCenterConfig(input = {}) { const current = await getYydNotificationCenterRuntimeConfig({ forceRefresh: true }); const nextSecret = normalizeText(input.projectSecret || input.project_secret || input.secret); const config = { baseUrl: normalizeText(input.baseUrl || input.base_url || current.baseUrl, 1000).replace(/\/+$/, ''), projectCode: normalizeText(input.projectCode || input.project_code || current.projectCode, 120), projectSecret: nextSecret || current.projectSecret, recipientAlias: normalizeText(input.recipientAlias || input.recipient_alias || current.recipientAlias, 50), timeoutMs: normalizeTimeout(input.timeoutMs || input.timeout_ms || current.timeoutMs), }; validateNotificationCenterConfig(config); if (nextSecret) await saveResilientSecret(SECRET_KEY, nextSecret); await upsertNotificationCenterConfig({ baseUrl: config.baseUrl, projectCode: config.projectCode, recipientAlias: config.recipientAlias, timeoutMs: config.timeoutMs, }); cachedConfig = { ...config, source: 'database', secretSource: 'database' }; cachedAt = Date.now(); return toAdminConfig(cachedConfig); } function toAdminConfig(config = {}) { return { configured: Boolean(config.baseUrl && config.projectCode && config.projectSecret && config.recipientAlias), baseUrl: config.baseUrl || '', projectCode: config.projectCode || '', projectSecretConfigured: Boolean(config.projectSecret), projectSecretPreview: maskSecret(config.projectSecret), recipientAlias: config.recipientAlias || '苏适', timeoutMs: normalizeTimeout(config.timeoutMs), source: config.source || 'environment', reminderDays: [2, 1, 0], }; } async function getYydNotificationCenterAdminConfig() { return toAdminConfig(await getYydNotificationCenterRuntimeConfig({ forceRefresh: true })); } module.exports = { CONFIG_KEY, DEFAULT_BASE_URL, SECRET_KEY, getEnvironmentConfig, getYydNotificationCenterAdminConfig, getYydNotificationCenterRuntimeConfig, maskSecret, saveYydNotificationCenterConfig, stripSecretFields, toAdminConfig, validateNotificationCenterConfig, };