feat(wechat): add admin-pluggable LLM intent router separate from H5.
Memind CI / Test, build, and release guards (push) Failing after 8s
Memind CI / Test, build, and release guards (push) Failing after 8s
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>
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
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,
|
||||
};
|
||||
Reference in New Issue
Block a user