import { SKILL_ROUTER_V2_ENV } from './chat-skills.mjs'; import { buildPublicSkillRuntimeConfig, buildSkillRuntimeCatalogSummary, } from './skill-runtime-policy.mjs'; const CONFIG_TABLE = 'h5_skill_runtime_config'; const CONFIG_SCOPE = 'global'; function defaultConfigShape() { return { router: { v2Enabled: false, manifestRoutingEnabled: false, }, }; } 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 cloneConfig(config = null) { return structuredClone?.(config ?? defaultConfigShape()) ?? JSON.parse(JSON.stringify(config ?? defaultConfigShape())); } function parseJsonLike(value, fallback) { if (value == null || value === '') return fallback; if (typeof value === 'string') { try { return JSON.parse(value); } catch { return fallback; } } if (typeof value === 'object') return value; return fallback; } function envRouterEnabled(env = process.env) { return /^(1|true|yes)$/i.test(String(env?.[SKILL_ROUTER_V2_ENV] ?? '')); } function applyEnv(config, env = process.env) { const next = cloneConfig(config); if (next.router.v2Enabled === false && envRouterEnabled(env)) { next.router.v2Enabled = true; } if (next.router.manifestRoutingEnabled === false && next.router.v2Enabled) { next.router.manifestRoutingEnabled = envRouterEnabled(env); } return next; } function mergePatch(currentConfig, patch = {}) { const next = cloneConfig(currentConfig); if (patch?.router && 'v2Enabled' in patch.router) { next.router.v2Enabled = normalizeBoolean(patch.router.v2Enabled, false); } if (patch?.router && 'manifestRoutingEnabled' in patch.router) { next.router.manifestRoutingEnabled = normalizeBoolean(patch.router.manifestRoutingEnabled, false); } if (!next.router.v2Enabled) { next.router.manifestRoutingEnabled = false; } return next; } async function ensureConfigTable(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 `); } async function loadStoredState(pool) { await ensureConfigTable(pool); const [rows] = await pool.query( `SELECT config_json, updated_by, updated_at FROM ${CONFIG_TABLE} WHERE config_scope = ? LIMIT 1`, [CONFIG_SCOPE], ); const row = rows[0]; if (!row) return null; return { config: { ...defaultConfigShape(), ...parseJsonLike(row.config_json, {}) }, updatedAt: Number(row.updated_at ?? 0) || null, updatedBy: row.updated_by ?? null, }; } export function createSkillRuntimeAdminConfigService(pool, { env = process.env, h5Root = process.cwd() } = {}) { async function loadEffectiveConfig() { const stored = await loadStoredState(pool); if (!stored) { const config = applyEnv(defaultConfigShape(), env); return { config, updatedAt: null, updatedBy: null, source: envRouterEnabled(env) ? 'env' : 'default', }; } return { config: cloneConfig(stored.config), updatedAt: stored.updatedAt, updatedBy: stored.updatedBy, source: 'admin-db', }; } return { async getAdminConfig() { const state = await loadEffectiveConfig(); return { config: state.config, updatedAt: state.updatedAt, updatedBy: state.updatedBy, source: state.source, }; }, async updateAdminConfig(patch = {}, { updatedBy = null } = {}) { const stored = await loadStoredState(pool); const base = stored?.config ?? defaultConfigShape(); const nextConfig = mergePatch(base, patch.config ?? patch); await ensureConfigTable(pool); 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(nextConfig), updatedBy, now], ); return this.getAdminConfig(); }, async getRuntimeState() { const state = await loadEffectiveConfig(); return { source: state.source, updatedAt: state.updatedAt, updatedBy: state.updatedBy, config: state.config, }; }, async getPublicRuntimeConfig() { const state = await loadEffectiveConfig(); return buildPublicSkillRuntimeConfig(state.config, h5Root); }, listCatalogSummary() { return buildSkillRuntimeCatalogSummary(h5Root); }, }; } export const skillRuntimeAdminConfigInternals = { CONFIG_SCOPE, CONFIG_TABLE, defaultConfigShape, applyEnv, mergePatch, };