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