const CONFIG_TABLE = 'h5_wechat_admin_config'; const CONFIG_KEY = 'intent_router'; function normalizeBoolean(value, fallback = false) { if (value == null || value === '') return fallback; if (typeof value === 'boolean') return value; const normalized = String(value).trim().toLowerCase(); if (['1', 'true', 'yes', 'on'].includes(normalized)) return true; if (['0', 'false', 'no', 'off'].includes(normalized)) return false; return fallback; } function normalizeString(value) { if (value == null) return ''; return String(value).trim(); } function normalizeNumber(value, fallback) { if (value == null || value === '') return fallback; const parsed = Number(value); return Number.isFinite(parsed) ? parsed : fallback; } function parseConfigJson(value) { if (!value) return {}; if (typeof value === 'object') return value; try { return JSON.parse(String(value)); } catch { return {}; } } function normalizeOpenidList(value) { return [...new Set(String(value ?? '') .split(/[\s,]+/u) .map((item) => item.trim()) .filter(Boolean))].slice(0, 1000); } async function ensureConfigTable(pool) { await pool.query(` CREATE TABLE IF NOT EXISTS ${CONFIG_TABLE} ( config_key VARCHAR(64) PRIMARY KEY, config_json JSON NOT NULL, updated_by CHAR(36) NULL, updated_at BIGINT NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); } export function defaultsFromEnv(env = process.env) { return { enabled: normalizeBoolean(env.MEMIND_WECHAT_INTENT_LLM_ENABLED, false), shadowMode: normalizeBoolean(env.MEMIND_WECHAT_INTENT_LLM_SHADOW, true), modelProviderKeyId: normalizeString(env.MEMIND_WECHAT_INTENT_MODEL_PROVIDER_KEY_ID), model: normalizeString(env.MEMIND_WECHAT_INTENT_MODEL), minConfidence: normalizeNumber(env.MEMIND_WECHAT_INTENT_MIN_CONFIDENCE, 0.65), timeoutMs: normalizeNumber(env.MEMIND_WECHAT_INTENT_TIMEOUT_MS, 4000), canaryOpenids: normalizeOpenidList(env.MEMIND_WECHAT_INTENT_CANARY_OPENIDS), }; } export function createWechatIntentRouterConfigService(pool, { env = process.env } = {}) { let ensurePromise = null; async function ensureReady() { if (!ensurePromise) ensurePromise = ensureConfigTable(pool); await ensurePromise; } async function readRow() { await ensureReady(); const [rows] = await pool.query( `SELECT config_json, updated_by, updated_at FROM ${CONFIG_TABLE} WHERE config_key = ? LIMIT 1`, [CONFIG_KEY], ); return rows[0] ?? null; } function mergeConfig(row) { const defaults = defaultsFromEnv(env); const stored = parseConfigJson(row?.config_json); return { enabled: normalizeBoolean(stored.enabled, defaults.enabled), shadowMode: normalizeBoolean(stored.shadowMode, defaults.shadowMode), modelProviderKeyId: normalizeString(stored.modelProviderKeyId) || defaults.modelProviderKeyId || null, model: normalizeString(stored.model) || defaults.model || null, minConfidence: normalizeNumber(stored.minConfidence, defaults.minConfidence), timeoutMs: normalizeNumber(stored.timeoutMs, defaults.timeoutMs), canaryOpenids: stored.canaryOpenids != null ? normalizeOpenidList(stored.canaryOpenids) : defaults.canaryOpenids, }; } return { async getConfig() { const row = await readRow(); return { ...mergeConfig(row), updatedAt: row?.updated_at ? Number(row.updated_at) : null, updatedBy: row?.updated_by ?? null, }; }, async getRuntimeState() { const config = await this.getConfig(); const overrides = { MEMIND_WECHAT_INTENT_LLM_ENABLED: config.enabled ? '1' : '0', MEMIND_WECHAT_INTENT_LLM_SHADOW: config.shadowMode ? '1' : '0', MEMIND_WECHAT_INTENT_MODEL_PROVIDER_KEY_ID: config.modelProviderKeyId ?? '', MEMIND_WECHAT_INTENT_MODEL: config.model ?? '', MEMIND_WECHAT_INTENT_MIN_CONFIDENCE: String(config.minConfidence), MEMIND_WECHAT_INTENT_TIMEOUT_MS: String(config.timeoutMs), MEMIND_WECHAT_INTENT_CANARY_OPENIDS: config.canaryOpenids.join(','), }; return { source: 'admin-db', updatedAt: config.updatedAt, updatedBy: config.updatedBy, fingerprint: JSON.stringify(config), overrides, config, }; }, async updateConfig(payload = {}, { updatedBy = null } = {}) { const current = await this.getConfig(); const next = { enabled: payload.enabled === undefined ? current.enabled : normalizeBoolean(payload.enabled, current.enabled), shadowMode: payload.shadowMode === undefined ? current.shadowMode : normalizeBoolean(payload.shadowMode, current.shadowMode), modelProviderKeyId: payload.modelProviderKeyId === undefined ? (current.modelProviderKeyId ?? '') : normalizeString(payload.modelProviderKeyId), model: payload.model === undefined ? (current.model ?? '') : normalizeString(payload.model), minConfidence: payload.minConfidence === undefined ? current.minConfidence : normalizeNumber(payload.minConfidence, current.minConfidence), timeoutMs: payload.timeoutMs === undefined ? current.timeoutMs : normalizeNumber(payload.timeoutMs, current.timeoutMs), canaryOpenids: payload.canaryOpenids === undefined ? current.canaryOpenids : normalizeOpenidList(payload.canaryOpenids), }; const now = Date.now(); await pool.query( `INSERT INTO ${CONFIG_TABLE} (config_key, config_json, updated_by, updated_at) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE config_json = VALUES(config_json), updated_by = VALUES(updated_by), updated_at = VALUES(updated_at)`, [CONFIG_KEY, JSON.stringify(next), updatedBy, now], ); return this.getConfig(); }, }; } export const wechatIntentRouterConfigInternals = { normalizeBoolean, normalizeOpenidList, defaultsFromEnv, };