Files
memind/wechat-intent-router-config.mjs
T
john 91e140d402
Memind CI / Test, build, and release guards (push) Failing after 8s
feat(wechat): add admin-pluggable LLM intent router separate from H5.
Give WeChat MP its own chat.general→page.generate LLM refinement layer with memindadm toggles, shadow mode, and canary openids so service account routing stays independent of the H5 chatIntentRouter.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-01 20:57:41 +08:00

176 lines
6.0 KiB
JavaScript

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,
};