feat: add semantic WeChat session actions
This commit is contained in:
@@ -543,6 +543,23 @@ function parseRouterJson(reply) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const WECHAT_SESSION_ACTIONS = new Set(['continue', 'reset', 'ignore_previous', 'unclear']);
|
||||||
|
|
||||||
|
function buildWechatSessionActionPrompt(text) {
|
||||||
|
return [
|
||||||
|
'你是微信聊天会话动作分类器,只判断用户是否要切换上下文。',
|
||||||
|
'只输出 JSON,不要 markdown:',
|
||||||
|
'{"action":"continue|reset|ignore_previous|unclear","confidence":0.0,"reason":"一句话"}',
|
||||||
|
'reset:用户要开启全新会话、重新开始、换一个完全不同的对话。',
|
||||||
|
'ignore_previous:用户只要求不要参考之前某段内容,但不一定要创建新会话。',
|
||||||
|
'continue:用户明确要沿用当前话题或继续上文。',
|
||||||
|
'unclear:无法确定,不要擅自切换。',
|
||||||
|
'',
|
||||||
|
'[User]',
|
||||||
|
String(text ?? '').trim() || '(empty)',
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeRoute(value) {
|
function normalizeRoute(value) {
|
||||||
const normalized = String(value ?? '').trim().toLowerCase();
|
const normalized = String(value ?? '').trim().toLowerCase();
|
||||||
if (normalized === CHAT_INTENT_ROUTE.DIRECT_CHAT || normalized === ROUTER_DECISION_ROUTE.CHAT) {
|
if (normalized === CHAT_INTENT_ROUTE.DIRECT_CHAT || normalized === ROUTER_DECISION_ROUTE.CHAT) {
|
||||||
@@ -1275,10 +1292,44 @@ export function createChatIntentRouter(options = {}) {
|
|||||||
}, { source: 'fallback' }));
|
}, { source: 'fallback' }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function classifySessionAction({ text } = {}) {
|
||||||
|
// The general H5 router intentionally remains rule/skill-only. Session
|
||||||
|
// action classification is separately opt-in because it runs before every
|
||||||
|
// WeChat Agent request that contains a context hint.
|
||||||
|
if (!policy.enabled || 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: buildWechatSessionActionPrompt(text) },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
Math.min(policy.timeoutMs, 1200),
|
||||||
|
'WeChat session action classifier',
|
||||||
|
);
|
||||||
|
if (!completion?.ok) return null;
|
||||||
|
const parsed = parseRouterJson(completion.reply);
|
||||||
|
const action = String(parsed?.action ?? '').trim().toLowerCase();
|
||||||
|
if (!WECHAT_SESSION_ACTIONS.has(action)) return null;
|
||||||
|
return {
|
||||||
|
action,
|
||||||
|
confidence: Number(parsed?.confidence),
|
||||||
|
reason: String(parsed?.reason ?? '').trim(),
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
logger?.warn?.('[chat-intent-router] WeChat session action classification skipped:', err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getStatus,
|
getStatus,
|
||||||
isEnabled,
|
isEnabled,
|
||||||
classify,
|
classify,
|
||||||
|
classifySessionAction,
|
||||||
resolveAgentMemoryContext,
|
resolveAgentMemoryContext,
|
||||||
applyAgentOrchestration: applyAgentOrchestrationToUserMessage,
|
applyAgentOrchestration: applyAgentOrchestrationToUserMessage,
|
||||||
};
|
};
|
||||||
@@ -1374,6 +1425,11 @@ export function createManagedChatIntentRouter({
|
|||||||
return router.classify(input);
|
return router.classify(input);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async classifySessionAction(input = {}) {
|
||||||
|
const router = await ensureRouter();
|
||||||
|
return router.classifySessionAction(input);
|
||||||
|
},
|
||||||
|
|
||||||
async resolveAgentMemoryContext(input = {}) {
|
async resolveAgentMemoryContext(input = {}) {
|
||||||
const router = await ensureRouter();
|
const router = await ensureRouter();
|
||||||
return router.resolveAgentMemoryContext(input);
|
return router.resolveAgentMemoryContext(input);
|
||||||
|
|||||||
@@ -425,6 +425,25 @@ test('classifyWithRules bypasses llm router when user selected summarize skill',
|
|||||||
assert.equal(llmCalls.length, 0);
|
assert.equal(llmCalls.length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('chat intent router classifies ambiguous WeChat session actions semantically', async () => {
|
||||||
|
const router = createChatIntentRouter({
|
||||||
|
enabled: true,
|
||||||
|
timeoutMs: 500,
|
||||||
|
llmProviderService: {
|
||||||
|
async createChatCompletion({ messages }) {
|
||||||
|
assert.match(messages[0].content, /微信聊天会话动作分类器/);
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
reply: '{"action":"reset","confidence":0.91,"reason":"用户要求换一种对话上下文"}',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const result = await router.classifySessionAction({ text: '我们换个思路聊' });
|
||||||
|
assert.equal(result.action, 'reset');
|
||||||
|
assert.equal(result.confidence, 0.91);
|
||||||
|
});
|
||||||
|
|
||||||
test('classifyWithRules bypasses llm router for explicit web skill prompt', async () => {
|
test('classifyWithRules bypasses llm router for explicit web skill prompt', async () => {
|
||||||
const llmCalls = [];
|
const llmCalls = [];
|
||||||
const router = createChatIntentRouter({
|
const router = createChatIntentRouter({
|
||||||
|
|||||||
@@ -882,6 +882,7 @@ async function bootstrapUserAuth() {
|
|||||||
wechatScheduleLlmConfigService,
|
wechatScheduleLlmConfigService,
|
||||||
llmProviderService,
|
llmProviderService,
|
||||||
chatIntentRouter,
|
chatIntentRouter,
|
||||||
|
sessionIntentClassifier: ({ text }) => chatIntentRouter?.classifySessionAction({ text }),
|
||||||
onPageGenerated: async ({ userId, sessionId, artifacts = [] }) => {
|
onPageGenerated: async ({ userId, sessionId, artifacts = [] }) => {
|
||||||
for (const artifact of artifacts) {
|
for (const artifact of artifacts) {
|
||||||
void sendMindSpaceAnalyticsEvent({
|
void sendMindSpaceAnalyticsEvent({
|
||||||
|
|||||||
+24
-1
@@ -29,6 +29,10 @@ import {
|
|||||||
shouldHandleSyncReply,
|
shouldHandleSyncReply,
|
||||||
} from './wechat/handlers/sync-replies.mjs';
|
} from './wechat/handlers/sync-replies.mjs';
|
||||||
import { classifyWechatIntent } from './wechat/intent/classifier.mjs';
|
import { classifyWechatIntent } from './wechat/intent/classifier.mjs';
|
||||||
|
import {
|
||||||
|
isWechatSessionResetAction,
|
||||||
|
resolveWechatSessionAction,
|
||||||
|
} from './wechat/intent/session-action.mjs';
|
||||||
import { isPageGenerateText, isTopicResetText } from './wechat/intent/patterns.mjs';
|
import { isPageGenerateText, isTopicResetText } from './wechat/intent/patterns.mjs';
|
||||||
import { isPageDataIntent } from './chat-skills.mjs';
|
import { isPageDataIntent } from './chat-skills.mjs';
|
||||||
import { resolvePageGenerateOutcome } from './wechat/handlers/page-generate.mjs';
|
import { resolvePageGenerateOutcome } from './wechat/handlers/page-generate.mjs';
|
||||||
@@ -1086,6 +1090,7 @@ function isTopicResetIntent(text) {
|
|||||||
export function shouldForceNewWechatAgentSession(wechatIntent, resetCandidate) {
|
export function shouldForceNewWechatAgentSession(wechatIntent, resetCandidate) {
|
||||||
return (
|
return (
|
||||||
wechatIntent?.kind === 'session.reset' ||
|
wechatIntent?.kind === 'session.reset' ||
|
||||||
|
isWechatSessionResetAction(wechatIntent?.sessionActionResult) ||
|
||||||
isTopicResetIntent(resetCandidate) ||
|
isTopicResetIntent(resetCandidate) ||
|
||||||
isPageDataIntent(resetCandidate)
|
isPageDataIntent(resetCandidate)
|
||||||
);
|
);
|
||||||
@@ -1578,6 +1583,7 @@ export function createWechatMpService({
|
|||||||
onPageGenerated = null,
|
onPageGenerated = null,
|
||||||
applySessionLlmProvider = null,
|
applySessionLlmProvider = null,
|
||||||
refreshSessionSnapshot = null,
|
refreshSessionSnapshot = null,
|
||||||
|
sessionIntentClassifier = null,
|
||||||
pageDataFinishGuard = null,
|
pageDataFinishGuard = null,
|
||||||
wechatFetch = undiciFetch,
|
wechatFetch = undiciFetch,
|
||||||
linkExists = defaultPublicHtmlLinkExists,
|
linkExists = defaultPublicHtmlLinkExists,
|
||||||
@@ -2354,7 +2360,7 @@ export function createWechatMpService({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const runIntentMessage = async ({ inbound, intent, user }) => {
|
const runIntentMessage = async ({ inbound, intent, user }) => {
|
||||||
const wechatIntent = classifyWechatIntent(intent);
|
const wechatIntent = await resolveWechatIntent(intent);
|
||||||
const mediaAnalysisEnabled = isWechatMediaGrayUser(user, config.mediaAnalysisGrayUsers);
|
const mediaAnalysisEnabled = isWechatMediaGrayUser(user, config.mediaAnalysisGrayUsers);
|
||||||
const reliabilityEnabled = isWechatMediaGrayUser(user, config.reliabilityGrayUsers);
|
const reliabilityEnabled = isWechatMediaGrayUser(user, config.reliabilityGrayUsers);
|
||||||
const agentReplyTimeoutMs = reliabilityEnabled ? config.agentReplyTimeoutMs : 0;
|
const agentReplyTimeoutMs = reliabilityEnabled ? config.agentReplyTimeoutMs : 0;
|
||||||
@@ -2810,6 +2816,23 @@ export function createWechatMpService({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resolveWechatIntent = async (intent) => {
|
||||||
|
const wechatIntent = classifyWechatIntent(intent);
|
||||||
|
if (intent.msgType !== 'text' && intent.msgType !== 'voice') return wechatIntent;
|
||||||
|
const sessionActionResult = await resolveWechatSessionAction(intent.agentText, {
|
||||||
|
semanticClassifier: sessionIntentClassifier,
|
||||||
|
logger,
|
||||||
|
});
|
||||||
|
if (isWechatSessionResetAction(sessionActionResult)) {
|
||||||
|
intent.sessionAction = sessionActionResult.action;
|
||||||
|
return { ...wechatIntent, kind: 'session.reset', sessionActionResult };
|
||||||
|
}
|
||||||
|
if (sessionActionResult.action === 'ignore_previous') {
|
||||||
|
intent.sessionAction = sessionActionResult.action;
|
||||||
|
}
|
||||||
|
return { ...wechatIntent, sessionActionResult };
|
||||||
|
};
|
||||||
|
|
||||||
const handleInboundMessage = async (bodyText, query = {}) => {
|
const handleInboundMessage = async (bodyText, query = {}) => {
|
||||||
if (!verifyRequest(query)) {
|
if (!verifyRequest(query)) {
|
||||||
return { ok: false, status: 403, body: 'invalid signature' };
|
return { ok: false, status: 403, body: 'invalid signature' };
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
isTopicResetText,
|
isTopicResetText,
|
||||||
wantsDocxDownload,
|
wantsDocxDownload,
|
||||||
} from './patterns.mjs';
|
} from './patterns.mjs';
|
||||||
|
import { WECHAT_SESSION_ACTION } from './session-action.mjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {object} WechatPageGenerateIntent
|
* @typedef {object} WechatPageGenerateIntent
|
||||||
@@ -15,6 +16,7 @@ import {
|
|||||||
*
|
*
|
||||||
* @typedef {object} WechatSessionResetIntent
|
* @typedef {object} WechatSessionResetIntent
|
||||||
* @property {'session.reset'} kind
|
* @property {'session.reset'} kind
|
||||||
|
* @property {string} sessionAction
|
||||||
*
|
*
|
||||||
* @typedef {object} WechatStatusProbeIntent
|
* @typedef {object} WechatStatusProbeIntent
|
||||||
* @property {'status.probe'} kind
|
* @property {'status.probe'} kind
|
||||||
@@ -46,7 +48,9 @@ export function classifyWechatIntent(intent) {
|
|||||||
const text = String(intent?.agentText ?? intent?.displayText ?? '').trim();
|
const text = String(intent?.agentText ?? intent?.displayText ?? '').trim();
|
||||||
|
|
||||||
if (msgType === 'text' || msgType === 'voice') {
|
if (msgType === 'text' || msgType === 'voice') {
|
||||||
if (isTopicResetText(text)) return { kind: 'session.reset' };
|
if (isTopicResetText(text)) {
|
||||||
|
return { kind: 'session.reset', sessionAction: WECHAT_SESSION_ACTION.RESET };
|
||||||
|
}
|
||||||
if (STATUS_PROBE_PATTERN.test(text)) return { kind: 'status.probe' };
|
if (STATUS_PROBE_PATTERN.test(text)) return { kind: 'status.probe' };
|
||||||
if (GREETING_PATTERN.test(text.replace(/[!!。.\s]+$/g, ''))) return { kind: 'greeting' };
|
if (GREETING_PATTERN.test(text.replace(/[!!。.\s]+$/g, ''))) return { kind: 'greeting' };
|
||||||
if (CONNECTIVITY_TEST_PATTERN.test(text.replace(/[!!。.\s]+$/g, ''))) {
|
if (CONNECTIVITY_TEST_PATTERN.test(text.replace(/[!!。.\s]+$/g, ''))) {
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
/** 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;
|
||||||
|
}
|
||||||
@@ -133,6 +133,11 @@ export function buildWechatAgentPrompt(intent, { grantedSkills = [], imagePolicy
|
|||||||
}
|
}
|
||||||
const content = String(agentText).trim();
|
const content = String(agentText).trim();
|
||||||
const lines = [currentTimeHint];
|
const lines = [currentTimeHint];
|
||||||
|
if (intent?.sessionAction === 'ignore_previous') {
|
||||||
|
lines.push(
|
||||||
|
'【上下文边界】用户要求忽略此前相关内容;本轮只根据当前消息和必要的系统信息回答,不要引用或延续被忽略的历史话题。',
|
||||||
|
);
|
||||||
|
}
|
||||||
if (docxDownloadHint) lines.push(docxDownloadHint);
|
if (docxDownloadHint) lines.push(docxDownloadHint);
|
||||||
if (pageDataCollectHint) lines.push(pageDataCollectHint);
|
if (pageDataCollectHint) lines.push(pageDataCollectHint);
|
||||||
if (pagePublishHint) lines.push(pagePublishHint);
|
if (pagePublishHint) lines.push(pagePublishHint);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import os from 'node:os';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import { classifyWechatIntent, isPageGenerateIntent } from './intent/classifier.mjs';
|
import { classifyWechatIntent, isPageGenerateIntent } from './intent/classifier.mjs';
|
||||||
|
import { resolveWechatSessionAction } from './intent/session-action.mjs';
|
||||||
import {
|
import {
|
||||||
filterSendableHtmlArtifacts,
|
filterSendableHtmlArtifacts,
|
||||||
isStubPublicHtmlContent,
|
isStubPublicHtmlContent,
|
||||||
@@ -37,6 +38,26 @@ test('classifyWechatIntent detects session.reset', () => {
|
|||||||
assert.equal(classifyWechatIntent({ msgType: 'text', agentText: '换新会话' }).kind, 'session.reset');
|
assert.equal(classifyWechatIntent({ msgType: 'text', agentText: '换新会话' }).kind, 'session.reset');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('resolveWechatSessionAction recognizes natural new-session expressions', async () => {
|
||||||
|
for (const text of ['换新会话', '新开一个会话', '我们重新开始吧', '从头来']) {
|
||||||
|
const result = await resolveWechatSessionAction(text);
|
||||||
|
assert.equal(result.action, 'reset', text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolveWechatSessionAction separates ignore-context from reset', async () => {
|
||||||
|
assert.equal((await resolveWechatSessionAction('不要参考刚才关于医联体的内容')).action, 'ignore_previous');
|
||||||
|
assert.equal((await resolveWechatSessionAction('继续刚才的话题')).action, 'continue');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolveWechatSessionAction uses an injected semantic classifier for unclear text', async () => {
|
||||||
|
const result = await resolveWechatSessionAction('我们换个思路聊', {
|
||||||
|
semanticClassifier: async () => ({ action: 'reset', confidence: 0.88, reason: '新对话意图' }),
|
||||||
|
});
|
||||||
|
assert.equal(result.action, 'reset');
|
||||||
|
assert.equal(result.source, 'semantic_model');
|
||||||
|
});
|
||||||
|
|
||||||
test('selectSendableHtmlArtifacts never returns stub html', () => {
|
test('selectSendableHtmlArtifacts never returns stub html', () => {
|
||||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'wechat-stub-'));
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'wechat-stub-'));
|
||||||
const stubPath = path.join(dir, 'tang-poem.html');
|
const stubPath = path.join(dir, 'tang-poem.html');
|
||||||
@@ -85,6 +106,15 @@ test('buildWechatAgentPrompt lives in wechat prompts package', () => {
|
|||||||
assert.match(prompt, /schedule-assistant/);
|
assert.match(prompt, /schedule-assistant/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('buildWechatAgentPrompt carries ignore-previous context boundary', () => {
|
||||||
|
const prompt = buildWechatAgentPrompt({
|
||||||
|
msgType: 'text',
|
||||||
|
agentText: '请分析这段新内容',
|
||||||
|
sessionAction: 'ignore_previous',
|
||||||
|
});
|
||||||
|
assert.match(prompt, /忽略此前相关内容/);
|
||||||
|
});
|
||||||
|
|
||||||
test('schedule confirmation guard blocks false positives', async () => {
|
test('schedule confirmation guard blocks false positives', async () => {
|
||||||
assert.equal(looksLikeScheduleConfirmation('已经帮你设置好了待办提醒'), true);
|
assert.equal(looksLikeScheduleConfirmation('已经帮你设置好了待办提醒'), true);
|
||||||
const guarded = await guardScheduleConfirmationReply({
|
const guarded = await guardScheduleConfirmationReply({
|
||||||
|
|||||||
Reference in New Issue
Block a user