feat: add optional MindSearch skill orchestration
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
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' };
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user