/** Session actions are broader than one fixed reset phrase. */ export const WECHAT_SESSION_ACTION = Object.freeze({ CONTINUE: 'continue', RESET: 'reset', IGNORE_PREVIOUS: 'ignore_previous', UNCLEAR: 'unclear', }); const RESET_VERBS = /(?:换|开|新建|开启|开始|另起|重新|从头|清空|作废)/u; const RESET_TARGETS = /(?:新?会话|对话|聊天|上下文|话题|主题)/u; const CONTEXT_REFERENCES = /(?:之前|刚才|前面|上面|当前|这个|刚刚)/u; const IGNORE_VERBS = /(?:忽略|不要管|别管|不参考|不要参考|不用参考|忘掉|忘记)/u; const CONTINUE_VERBS = /(?:继续|接着|还是|回到|沿着)/u; const SESSION_SEMANTIC_HINT = /(?:换个思路|另一个话题|不同话题|重置|清除|忘记刚才|刚才.*不要|从现在开始)/u; function normalizeText(text) { return String(text ?? '') .trim() .replace(/[\u200b-\u200d\ufeff]/g, '') .replace(/[\s,。!?、;:“”‘’()()【】\[\]{}]+/gu, ''); } function classifyByMeaning(text) { const normalized = normalizeText(text); if (!normalized) return { action: WECHAT_SESSION_ACTION.UNCLEAR, confidence: 0 }; const hasTarget = RESET_TARGETS.test(normalized); const hasContext = CONTEXT_REFERENCES.test(normalized); const hasResetVerb = RESET_VERBS.test(normalized); const hasIgnoreVerb = IGNORE_VERBS.test(normalized); const hasContinueVerb = CONTINUE_VERBS.test(normalized); if (hasContinueVerb && hasTarget && !hasResetVerb) { return { action: WECHAT_SESSION_ACTION.CONTINUE, confidence: 0.94, source: 'semantic_rules' }; } if (hasIgnoreVerb && (hasContext || hasTarget) && !/(?:新|重新|另起|开启|开始)/u.test(normalized)) { return { action: WECHAT_SESSION_ACTION.IGNORE_PREVIOUS, confidence: 0.9, source: 'semantic_rules' }; } if ((hasResetVerb && hasTarget) || /(?:重新开始|从头来|重来一次|另开一个)/u.test(normalized)) { return { action: WECHAT_SESSION_ACTION.RESET, confidence: 0.98, source: 'semantic_rules' }; } return { action: WECHAT_SESSION_ACTION.UNCLEAR, confidence: 0.35, source: 'fallback' }; } function normalizeSemanticResult(result) { if (!result) return null; const raw = String(result.action ?? result.intent ?? result.label ?? '').trim().toLowerCase(); const aliases = { 'session.reset': WECHAT_SESSION_ACTION.RESET, reset: WECHAT_SESSION_ACTION.RESET, new_session: WECHAT_SESSION_ACTION.RESET, continue: WECHAT_SESSION_ACTION.CONTINUE, reuse: WECHAT_SESSION_ACTION.CONTINUE, ignore_previous: WECHAT_SESSION_ACTION.IGNORE_PREVIOUS, ignore: WECHAT_SESSION_ACTION.IGNORE_PREVIOUS, unclear: WECHAT_SESSION_ACTION.UNCLEAR, }; const action = aliases[raw]; if (!action) return null; return { action, confidence: Number.isFinite(Number(result.confidence)) ? Number(result.confidence) : 0.7, source: 'semantic_model', reason: String(result.reason ?? '').trim() || null, }; } /** * Resolve a session action. A channel may inject a small semantic model for * ambiguous phrases; the meaning-based fallback keeps this path safe when the * model is unavailable or times out. */ export async function resolveWechatSessionAction(text, { semanticClassifier = null, logger = console } = {}) { const deterministic = classifyByMeaning(text); if (deterministic.action !== WECHAT_SESSION_ACTION.UNCLEAR) return deterministic; if (typeof semanticClassifier !== 'function') return deterministic; const normalized = normalizeText(text); if (!RESET_TARGETS.test(normalized) && !CONTEXT_REFERENCES.test(normalized) && !SESSION_SEMANTIC_HINT.test(normalized)) { return deterministic; } try { const semantic = normalizeSemanticResult(await semanticClassifier({ text: String(text ?? '').trim(), actions: Object.values(WECHAT_SESSION_ACTION), })); if (semantic) return semantic; } catch (err) { logger?.warn?.('[wechat-session-intent] semantic classifier failed:', err); } return deterministic; } export function isWechatSessionResetAction(result) { return result?.action === WECHAT_SESSION_ACTION.RESET; }