feat(wechat): add admin-pluggable LLM intent router separate from H5.
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:
john
2026-08-01 20:57:41 +08:00
parent cdc265d7e0
commit 91e140d402
14 changed files with 1145 additions and 4 deletions
+345
View File
@@ -0,0 +1,345 @@
import { wantsDocxDownload } from './wechat/intent/patterns.mjs';
import { defaultsFromEnv } from './wechat-intent-router-config.mjs';
export const WECHAT_LLM_REFINABLE_KINDS = new Set(['chat.general']);
const DEFAULT_MIN_CONFIDENCE = 0.65;
const DEFAULT_TIMEOUT_MS = 4000;
function envFlag(value, fallback = false) {
const raw = String(value ?? '').trim().toLowerCase();
if (!raw) return fallback;
return ['1', 'true', 'yes', 'on'].includes(raw);
}
function boundedNumber(value, fallback, { min = 0, max = Number.POSITIVE_INFINITY } = {}) {
if (value == null || value === '') return fallback;
const parsed = Number(value);
if (!Number.isFinite(parsed)) return fallback;
return Math.min(max, Math.max(min, parsed));
}
function normalizeOpenidList(value) {
return [...new Set(String(value ?? '')
.split(/[\s,]+/u)
.map((item) => item.trim())
.filter(Boolean))].slice(0, 1000);
}
export function resolveWechatIntentRouterPolicy(env = process.env, overrides = {}) {
const merged = { ...env, ...overrides };
return {
enabled: envFlag(merged.MEMIND_WECHAT_INTENT_LLM_ENABLED, false),
shadowMode: envFlag(merged.MEMIND_WECHAT_INTENT_LLM_SHADOW, true),
modelProviderKeyId: String(merged.MEMIND_WECHAT_INTENT_MODEL_PROVIDER_KEY_ID ?? '').trim() || null,
model: String(merged.MEMIND_WECHAT_INTENT_MODEL ?? '').trim() || null,
minConfidence: boundedNumber(
merged.MEMIND_WECHAT_INTENT_MIN_CONFIDENCE,
DEFAULT_MIN_CONFIDENCE,
{ min: 0, max: 1 },
),
timeoutMs: Math.round(boundedNumber(
merged.MEMIND_WECHAT_INTENT_TIMEOUT_MS,
DEFAULT_TIMEOUT_MS,
{ min: 500, max: 30_000 },
)),
canaryOpenids: normalizeOpenidList(merged.MEMIND_WECHAT_INTENT_CANARY_OPENIDS),
};
}
export function isWechatIntentLlmEligible({ policy, openid = null } = {}) {
if (!policy?.enabled) return false;
if (!Array.isArray(policy.canaryOpenids) || policy.canaryOpenids.length === 0) return true;
const normalized = String(openid ?? '').trim();
return normalized.length > 0 && policy.canaryOpenids.includes(normalized);
}
export function shouldRefineWechatIntent(ruleIntent, text = '') {
const kind = String(ruleIntent?.kind ?? '').trim();
if (!WECHAT_LLM_REFINABLE_KINDS.has(kind)) return false;
return String(text ?? '').trim().length >= 2;
}
function buildWechatIntentSystemPrompt() {
return [
'你是微信服务号意图分类器,只判断用户消息更适合哪种处理通道。',
'只输出 JSON,不要 markdown',
'{"kind":"page.generate|chat.general","confidence":0.0,"reason":"一句话"}',
'page.generate:用户希望生成/整理/排版 HTML 页面、图文、推文、专题、报告页,或明确要把内容做成可发布/可转发的页面。',
'chat.general:普通问答、闲聊、解释、建议、查询,不需要产出 HTML 页面。',
'如果用户只是口头提到“页面/文章”但没有生成或交付页面意图,仍判 chat.general。',
].join('\n');
}
function buildWechatIntentUserPrompt(text) {
return ['[User]', String(text ?? '').trim() || '(empty)'].join('\n');
}
export function parseWechatIntentJson(reply) {
const text = String(reply ?? '').trim();
if (!text) return null;
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
const candidate = (fenced?.[1] ?? text).trim();
try {
return JSON.parse(candidate);
} catch {
const start = candidate.indexOf('{');
const end = candidate.lastIndexOf('}');
if (start < 0 || end <= start) return null;
try {
return JSON.parse(candidate.slice(start, end + 1));
} catch {
return null;
}
}
}
export function normalizeWechatLlmIntent(raw) {
const kindRaw = String(raw?.kind ?? '').trim().toLowerCase();
const kind = kindRaw === 'page.generate' ? 'page.generate' : 'chat.general';
const confidenceRaw = Number(raw?.confidence);
const confidence = Number.isFinite(confidenceRaw)
? Math.min(1, Math.max(0, confidenceRaw))
: 0.7;
const reason = String(raw?.reason ?? '').trim() || '微信 LLM 意图判定';
return { kind, confidence, reason, source: 'llm' };
}
async function withTimeout(promise, timeoutMs, label) {
if (!timeoutMs || timeoutMs <= 0) return promise;
let timer = null;
try {
return await Promise.race([
promise,
new Promise((_, reject) => {
timer = setTimeout(() => {
const err = new Error(`${label} timed out after ${timeoutMs}ms`);
err.code = 'WECHAT_INTENT_ROUTER_TIMEOUT';
reject(err);
}, timeoutMs);
timer.unref?.();
}),
]);
} finally {
if (timer) clearTimeout(timer);
}
}
export function createWechatIntentRouter({
llmProviderService,
env = process.env,
logger = console,
policy: policyOverride = null,
} = {}) {
const policy = policyOverride ?? resolveWechatIntentRouterPolicy(env);
async function classifyWithLlm(text) {
if (typeof llmProviderService?.createChatCompletion !== 'function') return null;
try {
const completion = await withTimeout(
llmProviderService.createChatCompletion({
providerKeyId: policy.modelProviderKeyId || undefined,
model: policy.model || undefined,
temperature: 0,
messages: [
{ role: 'system', content: buildWechatIntentSystemPrompt() },
{ role: 'user', content: buildWechatIntentUserPrompt(text) },
],
}),
policy.timeoutMs,
'WeChat intent router',
);
if (!completion?.ok) return null;
const parsed = parseWechatIntentJson(completion.reply);
if (!parsed) return null;
return normalizeWechatLlmIntent({
...parsed,
model: completion.model ?? policy.model,
providerKeyId: completion.providerKeyId ?? policy.modelProviderKeyId,
});
} catch (err) {
logger?.warn?.(
`[wechat-intent-router] LLM classify skipped: ${err instanceof Error ? err.message : err}`,
);
return null;
}
}
function getStatus() {
return {
enabled: policy.enabled,
shadowMode: policy.shadowMode,
llmRoutingEnabled: Boolean(policy.enabled && !policy.shadowMode),
llmRoutingShadow: Boolean(policy.enabled && policy.shadowMode),
modelProviderKeyId: policy.modelProviderKeyId,
model: policy.model,
minConfidence: policy.minConfidence,
timeoutMs: policy.timeoutMs,
canaryOpenidCount: policy.canaryOpenids.length,
};
}
async function refineIntent({
ruleIntent,
text = '',
openid = null,
} = {}) {
const baseline = ruleIntent ?? { kind: 'chat.general' };
if (!shouldRefineWechatIntent(baseline, text)) {
return { ...baseline, intentSource: 'rules' };
}
if (!isWechatIntentLlmEligible({ policy, openid })) {
return { ...baseline, intentSource: 'rules' };
}
const llmResult = await classifyWithLlm(text);
const shadowPayload = llmResult
? {
kind: llmResult.kind,
confidence: llmResult.confidence,
reason: llmResult.reason,
wouldChangeKind: llmResult.kind !== baseline.kind,
}
: {
skipped: true,
reason: 'llm_unavailable_or_low_signal',
wouldChangeKind: false,
};
if (policy.shadowMode) {
logger?.info?.('[wechat-intent-router-shadow]', {
openid: openid ?? null,
baselineKind: baseline.kind,
...shadowPayload,
});
return {
...baseline,
intentSource: 'rules',
llmShadow: shadowPayload,
};
}
if (!llmResult || llmResult.confidence < policy.minConfidence) {
return {
...baseline,
intentSource: 'rules',
llmSuggestion: llmResult,
};
}
if (llmResult.kind === baseline.kind) {
return {
...baseline,
intentSource: 'rules',
llmSuggestion: llmResult,
};
}
if (llmResult.kind === 'page.generate') {
return {
kind: 'page.generate',
topic: String(text ?? '').trim(),
wantsDocx: wantsDocxDownload(text),
intentSource: 'llm',
llmReason: llmResult.reason,
llmConfidence: llmResult.confidence,
sessionActionResult: baseline.sessionActionResult,
};
}
return {
kind: 'chat.general',
text: String(text ?? '').trim(),
intentSource: 'llm',
llmReason: llmResult.reason,
llmConfidence: llmResult.confidence,
sessionActionResult: baseline.sessionActionResult,
};
}
return {
getStatus,
refineIntent,
};
}
export function createManagedWechatIntentRouter({
llmProviderService,
configService = null,
env = process.env,
logger = console,
} = {}) {
let activeRouter = null;
let activeFingerprint = null;
let activeMeta = { source: 'env', updatedAt: null, updatedBy: null, configError: null };
async function loadRouterState() {
if (!configService?.getRuntimeState) {
return {
source: 'env',
updatedAt: null,
updatedBy: null,
fingerprint: 'env-only',
effectiveEnv: env,
configError: null,
};
}
try {
const state = await configService.getRuntimeState();
return {
source: state?.source ?? 'admin-db',
updatedAt: state?.updatedAt ?? null,
updatedBy: state?.updatedBy ?? null,
fingerprint: state?.fingerprint ?? `admin-db:${Date.now()}`,
effectiveEnv: { ...env, ...(state?.overrides ?? {}) },
configError: null,
};
} catch (err) {
logger?.warn?.(
`[wechat-intent-router] admin config unavailable, using process env: ${err instanceof Error ? err.message : err}`,
);
return {
source: 'env-fallback',
updatedAt: null,
updatedBy: null,
fingerprint: 'env-fallback',
effectiveEnv: { ...env },
configError: err instanceof Error ? err.message : String(err),
};
}
}
async function ensureRouter() {
const state = await loadRouterState();
if (activeRouter && activeFingerprint === state.fingerprint) {
activeMeta = state;
return activeRouter;
}
activeRouter = createWechatIntentRouter({
llmProviderService,
env: state.effectiveEnv,
logger,
policy: resolveWechatIntentRouterPolicy(state.effectiveEnv),
});
activeFingerprint = state.fingerprint;
activeMeta = state;
return activeRouter;
}
return {
async getStatus() {
const router = await ensureRouter();
return {
...router.getStatus(),
configSource: activeMeta.source,
configUpdatedAt: activeMeta.updatedAt,
configUpdatedBy: activeMeta.updatedBy,
configError: activeMeta.configError,
};
},
async refineIntent(input = {}) {
const router = await ensureRouter();
return router.refineIntent(input);
},
};
}