const CONFIG_TABLE = 'h5_mindsearch_config'; const CONFIG_SCOPE = 'global'; export const MINDSEARCH_DEFAULT_CONFIG = Object.freeze({ enabled: false, mode: 'off', providers: { searxng: false, github: false, reader: false }, settings: { searxngEndpoint: '', maxResults: 10, timeoutMs: 8000, readerMaxChars: 12000 }, }); const clone = (value) => JSON.parse(JSON.stringify(value)); const bool = (value, fallback = false) => { if (typeof value === 'boolean') return value; if (value == null || value === '') return fallback; return /^(1|true|yes|on)$/i.test(String(value)); }; export function normalizeMindSearchConfig(input = {}) { const mode = ['off', 'shadow', 'assist'].includes(input.mode) ? input.mode : 'off'; const config = { enabled: bool(input.enabled), mode, providers: { searxng: bool(input.providers?.searxng), github: bool(input.providers?.github), reader: bool(input.providers?.reader), }, settings: { searxngEndpoint: String(input.settings?.searxngEndpoint ?? '').trim(), maxResults: Math.max(1, Math.min(20, Number(input.settings?.maxResults ?? 10) || 10)), timeoutMs: Math.max(1000, Math.min(30000, Number(input.settings?.timeoutMs ?? 8000) || 8000)), readerMaxChars: Math.max(1000, Math.min(50000, Number(input.settings?.readerMaxChars ?? 12000) || 12000)), }, }; if (!config.enabled || config.mode === 'off') { config.enabled = false; config.mode = 'off'; } return config; } function envConfig(env = process.env) { const enabled = bool(env.TKMIND_SEARCH_ENABLED); return normalizeMindSearchConfig({ enabled, mode: env.TKMIND_SEARCH_MODE || (enabled ? 'assist' : 'off'), providers: { searxng: env.TKMIND_SEARCH_PROVIDER_SEARXNG_ENABLED, github: env.TKMIND_SEARCH_PROVIDER_GITHUB_ENABLED, reader: env.TKMIND_SEARCH_READER_ENABLED, }, }); } export async function ensureMindSearchConfigSchema(pool) { await pool.query(`CREATE TABLE IF NOT EXISTS ${CONFIG_TABLE} ( config_scope VARCHAR(32) 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 createMindSearchConfigService(pool, { env = process.env } = {}) { let ready = null; const ensure = async () => { if (!pool) return; ready ??= ensureMindSearchConfigSchema(pool); await ready; }; const load = async () => { const fallback = envConfig(env); if (!pool) return { config: fallback, source: 'env' }; await ensure(); const [rows] = await pool.query(`SELECT config_json, updated_by, updated_at FROM ${CONFIG_TABLE} WHERE config_scope = ? LIMIT 1`, [CONFIG_SCOPE]); if (!rows?.[0]) return { config: fallback, source: 'env' }; let stored = rows[0].config_json; if (typeof stored === 'string') { try { stored = JSON.parse(stored); } catch { stored = {}; } } return { config: normalizeMindSearchConfig(stored), source: 'admin', updatedBy: rows[0].updated_by ?? null, updatedAt: Number(rows[0].updated_at ?? 0) || null }; }; return { ensureSchema: ensure, getEffectiveConfig: async () => (await load()).config, getAdminConfig: async () => load(), updateAdminConfig: async (patch, { updatedBy = null } = {}) => { const current = await load(); const next = normalizeMindSearchConfig({ ...current.config, ...patch, providers: { ...current.config.providers, ...(patch?.providers ?? {}) }, settings: { ...current.config.settings, ...(patch?.settings ?? {}) } }); if (!pool) return { config: next, source: 'env' }; await ensure(); const now = Date.now(); await pool.query(`INSERT INTO ${CONFIG_TABLE} (config_scope, 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_SCOPE, JSON.stringify(next), updatedBy, now]); return { config: next, source: 'admin', updatedBy, updatedAt: now }; }, getRuntimeState: async () => { const config = await (async () => (await load()).config)(); return { enabled: config.enabled, mode: config.mode, providers: config.providers, effective: config.enabled && config.mode !== 'off' }; }, }; }