764981c086
# Conflicts: # agent-run-gateway.test.mjs # capabilities.mjs # package.json # server.mjs
1372 lines
45 KiB
JavaScript
1372 lines
45 KiB
JavaScript
import assert from 'node:assert/strict';
|
||
import test from 'node:test';
|
||
import {
|
||
applyAgentOrchestrationToUserMessage,
|
||
buildRouterContext,
|
||
buildAgentOrchestrationAgentText,
|
||
buildRouterNormalizedDecision,
|
||
CHAT_INTENT_ROUTE,
|
||
classifyWithRules,
|
||
coercePageDataSkill,
|
||
coercePageGenerationSkill,
|
||
createChatIntentRouter,
|
||
createManagedChatIntentRouter,
|
||
isNormalizedRouterDecisionEnabled,
|
||
isNormalizedRouterDecisionShadow,
|
||
resolveGatewayAgentSessionId,
|
||
resolveLegacyRouteFromClassification,
|
||
resolveNormalizedRouterDecisionMode,
|
||
logRouterDecisionShadow,
|
||
ROUTER_DECISION_ROUTE,
|
||
ROUTER_DECISION_MODE,
|
||
ROUTER_DECISION_SESSION_HINT,
|
||
isRealtimeInfoQuestion,
|
||
isRichChannelPageIntent,
|
||
isRichPublishableContentIntent,
|
||
IMAGE_GENERATION_MODE,
|
||
resolveImageGenerationDecision,
|
||
coerceRealtimeWebSkill,
|
||
resolveChatIntentRouterPolicy,
|
||
} from './chat-intent-router.mjs';
|
||
|
||
test('image generation intent requires a real image for explicit background requests', () => {
|
||
const decision = resolveImageGenerationDecision({
|
||
text: '帮我生成一首唐诗页面,背景是大唐盛景图片,页面要精美',
|
||
});
|
||
assert.equal(decision.mode, IMAGE_GENERATION_MODE.REQUIRED);
|
||
assert.equal(decision.source, 'intent');
|
||
});
|
||
|
||
test('image generation user override can force or disable image generation', () => {
|
||
assert.equal(resolveImageGenerationDecision({
|
||
text: '做一个纯文字页面',
|
||
requestedMode: 'required',
|
||
}).mode, IMAGE_GENERATION_MODE.REQUIRED);
|
||
assert.equal(resolveImageGenerationDecision({
|
||
text: '生成一个带背景图的页面',
|
||
requestedMode: 'disabled',
|
||
}).mode, IMAGE_GENERATION_MODE.DISABLED);
|
||
});
|
||
|
||
test('visual page hints keep image generation best-effort unless the user explicitly requests an image', () => {
|
||
const decision = resolveImageGenerationDecision({
|
||
text: '帮我写一首诗词,做个页面吧',
|
||
requestedMode: 'auto',
|
||
});
|
||
assert.equal(decision.mode, IMAGE_GENERATION_MODE.AUTO);
|
||
assert.equal(decision.source, 'intent');
|
||
});
|
||
|
||
test('router injects a fail-closed generate_image contract for required visual pages', async () => {
|
||
const router = createChatIntentRouter();
|
||
const text = '帮我生成一个精美唐诗页面,背景是大唐盛景图片';
|
||
const userMessage = {
|
||
role: 'user',
|
||
content: [{ type: 'text', text }],
|
||
metadata: {
|
||
displayText: text,
|
||
memindRun: { imageGenerationMode: 'auto' },
|
||
},
|
||
};
|
||
const classification = await router.classify({
|
||
userMessage,
|
||
grantedSkills: ['static-page-publish'],
|
||
});
|
||
assert.equal(classification.imageGeneration.mode, IMAGE_GENERATION_MODE.REQUIRED);
|
||
const enriched = router.applyAgentOrchestration(userMessage, classification, {
|
||
grantedSkills: ['static-page-publish', 'image-generation'],
|
||
});
|
||
assert.match(enriched.content[0].text, /load_skill → image-generation/);
|
||
assert.match(enriched.content[0].text, /不要反问用户工具参数/);
|
||
assert.match(enriched.content[0].text, /sandbox-fs__generate_image/);
|
||
assert.match(enriched.content[0].text, /asset\.htmlSrc/);
|
||
assert.match(enriched.content[0].text, /workspaceRelativePath 只用于工作区文件操作/);
|
||
assert.match(enriched.content[0].text, /当前这一次执行中发起新的 generate_image/);
|
||
assert.match(enriched.content[0].text, /历史对话中的成功或失败都不能作为本轮结果/);
|
||
assert.match(enriched.content[0].text, /hero 成功后必须复用该资产/);
|
||
assert.match(enriched.content[0].text, /禁止再调用 card_cover 或 feed_cover/);
|
||
assert.match(enriched.content[0].text, /不得用 SVG/);
|
||
});
|
||
|
||
test('classifyWithRules routes greetings to direct chat', () => {
|
||
const result = classifyWithRules({
|
||
text: '你好',
|
||
userMessage: {
|
||
role: 'user',
|
||
content: [{ type: 'text', text: '你好' }],
|
||
metadata: { displayText: '你好' },
|
||
},
|
||
});
|
||
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
|
||
assert.equal(result.source, 'rule');
|
||
});
|
||
|
||
test('classifyWithRules routes page generation to agent orchestration', () => {
|
||
const result = classifyWithRules({
|
||
text: '帮我做一个秋夜诗的 H5 页面',
|
||
userMessage: {
|
||
role: 'user',
|
||
content: [{ type: 'text', text: '帮我做一个秋夜诗的 H5 页面' }],
|
||
metadata: { displayText: '帮我做一个秋夜诗的 H5 页面' },
|
||
},
|
||
});
|
||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||
assert.equal(result.suggestedSkill, 'static-page-publish');
|
||
});
|
||
|
||
test('classifyWithRules routes page data collection to page-data-collect', () => {
|
||
const text = '帮我在页面增加调查问卷,密码 888 后台查看每个用户提交记录,要用 sqlite';
|
||
const result = classifyWithRules({
|
||
text,
|
||
userMessage: {
|
||
role: 'user',
|
||
content: [{ type: 'text', text }],
|
||
metadata: { displayText: text },
|
||
},
|
||
});
|
||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||
assert.equal(result.suggestedSkill, 'page-data-collect');
|
||
});
|
||
|
||
test('classifyWithRules routes storefront ordering with product admin to page-data-collect', () => {
|
||
const text = '我要做一个页面,里面可以下单,后台可以上架产品';
|
||
const result = classifyWithRules({
|
||
text,
|
||
userMessage: {
|
||
role: 'user',
|
||
content: [{ type: 'text', text }],
|
||
metadata: { displayText: text },
|
||
},
|
||
});
|
||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||
assert.equal(result.suggestedSkill, 'page-data-collect');
|
||
});
|
||
|
||
test('classifyWithRules routes ordinary household ledger language to page-data-collect', () => {
|
||
const text = '帮我做一个家庭记账页面,我每天可以记录日期、收入、支出、分类和备注,日期默认当天。再做一个管理页面,可以查看所有记录和收支汇总。';
|
||
const result = classifyWithRules({
|
||
text,
|
||
userMessage: {
|
||
role: 'user',
|
||
content: [{ type: 'text', text }],
|
||
metadata: { displayText: text },
|
||
},
|
||
});
|
||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||
assert.equal(result.suggestedSkill, 'page-data-collect');
|
||
});
|
||
|
||
test('classifyWithRules routes implicit sticky-note timeline apps to page-data-collect', () => {
|
||
const text = '帮我设计一个便签提醒,可以写便签提交,时间轴来显示';
|
||
const result = classifyWithRules({
|
||
text,
|
||
userMessage: {
|
||
role: 'user',
|
||
content: [{ type: 'text', text }],
|
||
metadata: { displayText: text },
|
||
},
|
||
});
|
||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||
assert.equal(result.suggestedSkill, 'page-data-collect');
|
||
});
|
||
|
||
test('coercePageDataSkill overrides static-page-publish when data intent is present', () => {
|
||
const text = '帮我在这个页面增加调查问卷,密码 888 查看提交记录';
|
||
const coerced = coercePageDataSkill(
|
||
{
|
||
route: CHAT_INTENT_ROUTE.AGENT,
|
||
suggestedSkill: 'static-page-publish',
|
||
reason: '明确需要生成或发布页面/文件',
|
||
agentBrief: '生成页面',
|
||
},
|
||
text,
|
||
{ grantedSkills: ['page-data-collect', 'static-page-publish'] },
|
||
);
|
||
assert.equal(coerced.suggestedSkill, 'page-data-collect');
|
||
assert.match(coerced.reason, /页面数据交互意图/);
|
||
});
|
||
|
||
test('coercePageGenerationSkill skips when page data intent is present', () => {
|
||
const text = '帮我在页面增加调查问卷和后台入口';
|
||
const coerced = coercePageGenerationSkill(
|
||
{
|
||
route: CHAT_INTENT_ROUTE.AGENT,
|
||
suggestedSkill: null,
|
||
reason: '任务编排',
|
||
agentBrief: '',
|
||
},
|
||
text,
|
||
{ grantedSkills: ['static-page-publish', 'page-data-collect'] },
|
||
);
|
||
assert.notEqual(coerced.suggestedSkill, 'static-page-publish');
|
||
});
|
||
|
||
test('classifyWithRules routes implicit travel guide page requests to static-page-publish', () => {
|
||
const result = classifyWithRules({
|
||
text: '苏州攻略页面',
|
||
userMessage: {
|
||
role: 'user',
|
||
content: [{ type: 'text', text: '苏州攻略页面' }],
|
||
metadata: { displayText: '苏州攻略页面' },
|
||
},
|
||
});
|
||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||
assert.equal(result.suggestedSkill, 'static-page-publish');
|
||
assert.match(result.agentBrief, /不要停在追问/);
|
||
});
|
||
|
||
test('coercePageGenerationSkill replaces product-campaign-page for content pages', () => {
|
||
const coerced = coercePageGenerationSkill(
|
||
{
|
||
route: CHAT_INTENT_ROUTE.AGENT,
|
||
suggestedSkill: 'product-campaign-page',
|
||
reason: '模型路由判定',
|
||
agentBrief: '',
|
||
confidence: 0.9,
|
||
source: 'llm',
|
||
},
|
||
'苏州攻略页面',
|
||
{ grantedSkills: ['static-page-publish', 'product-campaign-page'] },
|
||
);
|
||
assert.equal(coerced.suggestedSkill, 'static-page-publish');
|
||
assert.match(coerced.reason, /static-page-publish/);
|
||
});
|
||
|
||
test('classifyWithRules routes themed article requests to agent orchestration', () => {
|
||
const result = classifyWithRules({
|
||
text: '帮我写个主题文章',
|
||
userMessage: {
|
||
role: 'user',
|
||
content: [{ type: 'text', text: '帮我写个主题文章' }],
|
||
metadata: { displayText: '帮我写个主题文章' },
|
||
},
|
||
});
|
||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||
assert.equal(result.suggestedSkill, 'static-page-publish');
|
||
});
|
||
|
||
test('classifyWithRules routes rich service account content to static-page-publish', () => {
|
||
const text = `以下是今天(2026年7月7日,星期二)国际上的主要热点新闻:
|
||
|
||
🌍 地缘政治与安全
|
||
- 霍尔木兹海峡油轮遇袭。
|
||
- 伊朗最高领袖哈梅内伊葬礼。
|
||
- 中国核潜艇在南太平洋试射远程导弹。
|
||
- 俄罗斯空袭基辅。
|
||
|
||
🇺🇸 美国
|
||
- Charlie Kirk 遇刺案预审听证。
|
||
- 特朗普财务披露。
|
||
- 最高法院允许德州实施应用商店年龄验证法。
|
||
|
||
⚽ 体育
|
||
- 2026世界杯,比利时4-1淘汰美国,西班牙1-0绝杀葡萄牙。
|
||
|
||
🎤 娱乐
|
||
- Taylor Swift 与 Travis Kelce 在麦迪逊广场花园举行婚礼。
|
||
|
||
📉 财经
|
||
- 道指首次突破53,000点,AI 芯片热潮推动股市新高。
|
||
- Samsung Q2 利润飙升1,800%。
|
||
|
||
🌡️ 其他
|
||
- 新泽西高温致至少25人死亡。
|
||
- 古巴全国大停电。
|
||
|
||
请整理给服务号用户阅读。`;
|
||
const result = classifyWithRules({
|
||
text,
|
||
userMessage: {
|
||
role: 'user',
|
||
content: [{ type: 'text', text }],
|
||
metadata: { displayText: text },
|
||
},
|
||
});
|
||
assert.equal(isRichChannelPageIntent(text), true);
|
||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||
assert.equal(result.suggestedSkill, 'static-page-publish');
|
||
assert.match(result.reason, /结构化长内容/);
|
||
});
|
||
|
||
test('isRichChannelPageIntent does not route short service account copy to page', () => {
|
||
assert.equal(isRichChannelPageIntent('请整理给服务号用户阅读:今天 AI 新闻有哪些?'), false);
|
||
});
|
||
|
||
test('classifyWithRules prefers pages for rich structured content meant for readers', () => {
|
||
const text = `下面是活动资料,请整理成适合用户阅读和转发的内容:
|
||
|
||
活动概览
|
||
- 名称:城市夜跑季
|
||
- 时间:7月12日到7月20日
|
||
- 地点:滨江步道、中央公园、城市广场
|
||
|
||
亮点介绍
|
||
- 三条不同难度路线,适合亲子、入门和进阶跑者
|
||
- 每晚有补给站、音乐打卡点和城市灯光秀
|
||
- 完赛可领取电子证书和联名纪念徽章
|
||
|
||
报名信息
|
||
- 早鸟票 39 元
|
||
- 标准票 59 元
|
||
- 团队票 5 人起享 8 折
|
||
|
||
注意事项
|
||
- 需提前 30 分钟签到
|
||
- 建议穿反光装备
|
||
- 遇强降雨活动顺延
|
||
|
||
传播重点
|
||
- 强调轻松参与、城市夜景和朋友一起完成
|
||
- 内容需要清晰、有层次,适合用户直接阅读。`;
|
||
const result = classifyWithRules({
|
||
text,
|
||
userMessage: {
|
||
role: 'user',
|
||
content: [{ type: 'text', text }],
|
||
metadata: { displayText: text },
|
||
},
|
||
});
|
||
assert.equal(isRichPublishableContentIntent(text), true);
|
||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||
assert.equal(result.suggestedSkill, 'static-page-publish');
|
||
});
|
||
|
||
test('isRichPublishableContentIntent keeps plain long summaries out of page fast path', () => {
|
||
const text = `请总结以下会议纪要:
|
||
|
||
产品进展
|
||
- 首页完成第一版视觉调整
|
||
- 登录链路还需要补充错误提示
|
||
- 分享页需要统一按钮文案
|
||
|
||
技术进展
|
||
- 后端接口完成基础联调
|
||
- 前端状态管理仍有重复请求
|
||
- 缓存策略需要继续评估
|
||
|
||
风险
|
||
- 设计稿还没有最终确认
|
||
- 部分接口缺少压测数据
|
||
- 发布窗口需要和运营确认
|
||
|
||
下一步
|
||
- 明天上午确认视觉改动
|
||
- 下午完成联调复测
|
||
- 周五前整理发布清单`;
|
||
assert.equal(isRichPublishableContentIntent(text), false);
|
||
});
|
||
|
||
test('classifyWithRules honors forceDeepReasoning bypass', () => {
|
||
const result = classifyWithRules({
|
||
text: '你好',
|
||
forceDeepReasoning: true,
|
||
userMessage: {
|
||
role: 'user',
|
||
content: [{ type: 'text', text: '你好' }],
|
||
},
|
||
});
|
||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||
assert.match(result.reason, /深度推理/);
|
||
});
|
||
|
||
test('classifyWithRules keeps agent session continuation even when llm router flag is true', () => {
|
||
const continued = classifyWithRules({
|
||
text: '可以',
|
||
sessionId: '20260708_64',
|
||
sessionMessageCount: 4,
|
||
llmRouterEnabled: true,
|
||
});
|
||
assert.equal(continued.route, CHAT_INTENT_ROUTE.AGENT);
|
||
assert.match(continued.reason, /Agent 会话确认|延续已有 Agent 会话/);
|
||
});
|
||
|
||
test('classifyWithRules keeps empty pre-created sessions eligible for llm routing', () => {
|
||
const continued = classifyWithRules({
|
||
text: '继续聊聊',
|
||
sessionId: '20260704_2',
|
||
sessionMessageCount: 2,
|
||
});
|
||
assert.equal(continued.route, CHAT_INTENT_ROUTE.AGENT);
|
||
assert.equal(continued.reason, '延续已有 Agent 会话');
|
||
|
||
const fresh = classifyWithRules({
|
||
text: '我是 John,你对我的记忆知道多少?',
|
||
sessionId: '20260704_3',
|
||
sessionMessageCount: 0,
|
||
});
|
||
assert.equal(fresh.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
|
||
assert.match(fresh.reason, /记忆/);
|
||
});
|
||
|
||
test('classifyWithRules keeps memory recall on agent when session already active', () => {
|
||
const result = classifyWithRules({
|
||
text: '你记得我说想去哪儿吗',
|
||
sessionId: '20260704_9',
|
||
sessionMessageCount: 4,
|
||
userMessage: {
|
||
role: 'user',
|
||
content: [{ type: 'text', text: '你记得我说想去哪儿吗' }],
|
||
metadata: { displayText: '你记得我说想去哪儿吗' },
|
||
},
|
||
});
|
||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||
assert.match(result.reason, /延续已有 Agent 会话/);
|
||
});
|
||
|
||
test('classifyWithRules bypasses llm router when user selected summarize skill', async () => {
|
||
const llmCalls = [];
|
||
const router = createChatIntentRouter({
|
||
enabled: true,
|
||
llmProviderService: {
|
||
async createChatCompletion() {
|
||
llmCalls.push(1);
|
||
return { ok: true, reply: '{"route":"direct_chat","confidence":0.9,"reason":"x"}' };
|
||
},
|
||
},
|
||
memoryV2: {
|
||
async resolve() {
|
||
assert.fail('memory resolve should not run for explicit skill selection');
|
||
},
|
||
},
|
||
});
|
||
|
||
const prompt = '请总结以下内容,提炼核心结论、重点信息和可执行建议(条理清晰、中文输出):';
|
||
const result = await router.classify({
|
||
userMessage: {
|
||
role: 'user',
|
||
content: [{ type: 'text', text: `${prompt}这是待总结的正文` }],
|
||
metadata: {
|
||
displayText: `${prompt}这是待总结的正文`,
|
||
memindRun: { selectedChatSkill: 'summarize' },
|
||
},
|
||
},
|
||
});
|
||
|
||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||
assert.equal(result.source, 'rule');
|
||
assert.equal(result.reason, '用户已选择 skill');
|
||
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 () => {
|
||
const llmCalls = [];
|
||
const router = createChatIntentRouter({
|
||
enabled: true,
|
||
llmProviderService: {
|
||
async createChatCompletion() {
|
||
llmCalls.push(1);
|
||
return { ok: true, reply: '{"route":"direct_chat","confidence":0.9,"reason":"x"}' };
|
||
},
|
||
},
|
||
});
|
||
|
||
const result = await router.classify({
|
||
userMessage: {
|
||
role: 'user',
|
||
content: [{
|
||
type: 'text',
|
||
text: '请使用 web 技能:帮我搜索并查阅相关资料(优先官方文档),并给出中文摘要与来源链接。我的问题是:今天 AI 新闻',
|
||
}],
|
||
metadata: { displayText: '今天 AI 新闻' },
|
||
},
|
||
grantedSkills: ['web'],
|
||
});
|
||
|
||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||
assert.equal(result.suggestedSkill, 'web');
|
||
assert.equal(llmCalls.length, 0);
|
||
});
|
||
|
||
test('createChatIntentRouter keeps continued sessions on agent without llm router', async () => {
|
||
const llmCalls = [];
|
||
const router = createChatIntentRouter({
|
||
enabled: true,
|
||
modelProviderKeyId: 'key-router',
|
||
model: 'deepseek-chat',
|
||
llmProviderService: {
|
||
async createChatCompletion(payload) {
|
||
llmCalls.push(payload);
|
||
return { ok: false, message: 'router disabled' };
|
||
},
|
||
},
|
||
memoryV2: {
|
||
async resolve() {
|
||
return { source: 'legacy-conversation-memory', memories: [{ label: 'goal', text: '用户想去日本旅游' }] };
|
||
},
|
||
},
|
||
});
|
||
|
||
const result = await router.classify({
|
||
userId: 'user-john',
|
||
sessionId: '20260704_10',
|
||
sessionMessageCount: 2,
|
||
userMessage: {
|
||
role: 'user',
|
||
content: [{ type: 'text', text: '我想去日本玩' }],
|
||
metadata: { displayText: '我想去日本玩' },
|
||
},
|
||
});
|
||
|
||
assert.equal(llmCalls.length, 0);
|
||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||
assert.equal(result.source, 'rule');
|
||
assert.match(result.reason, /延续已有 Agent 会话/);
|
||
});
|
||
|
||
test('createChatIntentRouter uses agent fallback for general conversational analysis (skill-only)', async () => {
|
||
const llmCalls = [];
|
||
const resolveCalls = [];
|
||
const router = createChatIntentRouter({
|
||
enabled: true,
|
||
modelProviderKeyId: 'key-router',
|
||
model: 'deepseek-chat',
|
||
llmProviderService: {
|
||
async createChatCompletion({ messages, providerKeyId, model, temperature }) {
|
||
llmCalls.push({ messages, providerKeyId, model, temperature });
|
||
return { ok: false, message: 'router disabled' };
|
||
},
|
||
},
|
||
memoryV2: {
|
||
async resolve(input) {
|
||
resolveCalls.push(input);
|
||
return {
|
||
source: 'pgvector',
|
||
memories: [{ label: 'preference', text: '用户偏好简洁回答,不喜欢冗长铺垫' }],
|
||
activeGoals: ['完善春季促销活动页'],
|
||
};
|
||
},
|
||
},
|
||
});
|
||
|
||
const result = await router.classify({
|
||
userId: 'user-1',
|
||
userMessage: {
|
||
role: 'user',
|
||
content: [{ type: 'text', text: 'React 19 有哪些新特性?' }],
|
||
metadata: { displayText: 'React 19 有哪些新特性?' },
|
||
},
|
||
grantedSkills: ['web'],
|
||
});
|
||
|
||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||
assert.equal(result.source, 'fallback');
|
||
assert.equal(resolveCalls.length, 0);
|
||
assert.equal(llmCalls.length, 0);
|
||
});
|
||
|
||
test('createChatIntentRouter routes memory recall through rules on fresh session', async () => {
|
||
const resolveCalls = [];
|
||
const router = createChatIntentRouter({
|
||
enabled: true,
|
||
llmProviderService: {
|
||
async createChatCompletion() {
|
||
return { ok: false, message: 'router disabled' };
|
||
},
|
||
},
|
||
memoryV2: {
|
||
async resolve(input) {
|
||
resolveCalls.push(input);
|
||
return {
|
||
source: 'pgvector',
|
||
memories: [{ label: 'goal', text: '用户想去日本旅游' }],
|
||
};
|
||
},
|
||
},
|
||
});
|
||
|
||
const result = await router.classify({
|
||
userId: 'user-john',
|
||
userMessage: {
|
||
role: 'user',
|
||
content: [{ type: 'text', text: '你记得我说想去哪儿吗?' }],
|
||
metadata: { displayText: '你记得我说想去哪儿吗?' },
|
||
},
|
||
});
|
||
|
||
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
|
||
assert.equal(result.source, 'rule');
|
||
assert.equal(resolveCalls.length, 0);
|
||
});
|
||
|
||
test('buildRouterContext trims rich Memory V2 resolve payload for routing', () => {
|
||
const context = buildRouterContext({
|
||
source: 'letta',
|
||
memories: [
|
||
{ label: 'preference', text: '用户偏好简洁回答,不喜欢冗长铺垫,尤其在工程排障时希望先给结论。' },
|
||
{ label: 'habit', text: '用户经常要求生成 H5 活动页并发布到 MindSpace。' },
|
||
],
|
||
semanticMemories: ['上次正在修改活动页标题样式'],
|
||
behaviorSummary: '用户倾向先本地验证,再决定是否发布生产。额外句子不应进入轻量摘要。',
|
||
activeGoals: ['完善春季促销活动页'],
|
||
profile: { ignored: true },
|
||
});
|
||
|
||
assert.equal(context.source, 'letta');
|
||
assert.equal(context.itemsUsed, 5);
|
||
assert.match(context.content, /相关记忆/);
|
||
assert.match(context.content, /行为摘要/);
|
||
assert.match(context.content, /进行中目标/);
|
||
assert.doesNotMatch(context.content, /ignored/);
|
||
});
|
||
|
||
test('createChatIntentRouter routes memory recall through rules when memory resolve fails', async () => {
|
||
const router = createChatIntentRouter({
|
||
enabled: true,
|
||
llmProviderService: {
|
||
async createChatCompletion() {
|
||
return { ok: false, message: 'router disabled' };
|
||
},
|
||
},
|
||
memoryV2: {
|
||
async resolve() {
|
||
throw new Error('memory down');
|
||
},
|
||
},
|
||
logger: { warn() {} },
|
||
});
|
||
|
||
const result = await router.classify({
|
||
userId: 'user-1',
|
||
userMessage: {
|
||
role: 'user',
|
||
content: [{ type: 'text', text: '你记得我之前说过什么吗?' }],
|
||
metadata: { displayText: '你记得我之前说过什么吗?' },
|
||
},
|
||
grantedSkills: ['web'],
|
||
});
|
||
|
||
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
|
||
assert.equal(result.source, 'rule');
|
||
});
|
||
|
||
test('createManagedChatIntentRouter hot-loads admin config and stays skill-only', async () => {
|
||
const states = [
|
||
{
|
||
fingerprint: 'off',
|
||
overrides: {
|
||
MEMIND_CHAT_LLM_ROUTER_ENABLED: '0',
|
||
},
|
||
},
|
||
{
|
||
fingerprint: 'on',
|
||
overrides: {
|
||
MEMIND_CHAT_LLM_ROUTER_ENABLED: '1',
|
||
MEMIND_CHAT_ROUTER_MODEL_PROVIDER_KEY_ID: 'key-router',
|
||
MEMIND_CHAT_ROUTER_MODEL: 'deepseek-chat',
|
||
},
|
||
},
|
||
];
|
||
const llmCalls = [];
|
||
const router = createManagedChatIntentRouter({
|
||
configService: {
|
||
async getRuntimeState() {
|
||
return states[0];
|
||
},
|
||
},
|
||
llmProviderService: {
|
||
async createChatCompletion(input) {
|
||
llmCalls.push(input);
|
||
return {
|
||
ok: true,
|
||
providerKeyId: input.providerKeyId,
|
||
model: input.model,
|
||
reply: JSON.stringify({
|
||
route: 'direct_chat',
|
||
confidence: 0.9,
|
||
reason: '纯文字问答',
|
||
suggested_skill: null,
|
||
agent_brief: '',
|
||
}),
|
||
};
|
||
},
|
||
},
|
||
});
|
||
|
||
assert.equal(await router.isEnabled(), true);
|
||
states.shift();
|
||
assert.equal(await router.isEnabled(), true);
|
||
const result = await router.classify({
|
||
userMessage: {
|
||
role: 'user',
|
||
content: [{ type: 'text', text: '帮我概括一下这段材料的要点' }],
|
||
metadata: { displayText: '帮我概括一下这段材料的要点' },
|
||
},
|
||
});
|
||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||
assert.equal(result.source, 'fallback');
|
||
assert.equal(llmCalls.length, 0);
|
||
});
|
||
|
||
test('createChatIntentRouter uses agent fallback when rules miss (skill-only)', async () => {
|
||
const llmCalls = [];
|
||
const router = createChatIntentRouter({
|
||
enabled: true,
|
||
minConfidence: 0.8,
|
||
llmProviderService: {
|
||
async createChatCompletion() {
|
||
llmCalls.push(1);
|
||
return {
|
||
ok: true,
|
||
reply: JSON.stringify({
|
||
route: 'direct_chat',
|
||
confidence: 0.42,
|
||
reason: '可能是简单问答',
|
||
suggested_skill: null,
|
||
agent_brief: '',
|
||
}),
|
||
};
|
||
},
|
||
},
|
||
});
|
||
|
||
const result = await router.classify({
|
||
userMessage: {
|
||
role: 'user',
|
||
content: [{ type: 'text', text: '帮我整理一下这段材料' }],
|
||
metadata: { displayText: '帮我整理一下这段材料' },
|
||
},
|
||
});
|
||
|
||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||
assert.equal(result.source, 'fallback');
|
||
assert.equal(llmCalls.length, 0);
|
||
});
|
||
|
||
test('createChatIntentRouter falls back to agent when rules miss (skill-only)', async () => {
|
||
const llmCalls = [];
|
||
const router = createChatIntentRouter({
|
||
enabled: true,
|
||
llmProviderService: {
|
||
async createChatCompletion() {
|
||
llmCalls.push(1);
|
||
return { ok: false, message: 'router model unavailable' };
|
||
},
|
||
},
|
||
});
|
||
|
||
const result = await router.classify({
|
||
userMessage: {
|
||
role: 'user',
|
||
content: [{ type: 'text', text: '帮我概括一下这段材料的要点' }],
|
||
metadata: { displayText: '帮我概括一下这段材料的要点' },
|
||
},
|
||
grantedSkills: ['web'],
|
||
});
|
||
|
||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||
assert.equal(result.source, 'fallback');
|
||
assert.equal(llmCalls.length, 0);
|
||
});
|
||
|
||
test('classifyWithRules routes memory recall to direct chat on fresh session', () => {
|
||
const result = classifyWithRules({
|
||
text: '你记得我说想去哪儿吗',
|
||
sessionId: '20260704_3',
|
||
sessionMessageCount: 0,
|
||
});
|
||
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
|
||
assert.match(result.reason, /记忆/);
|
||
|
||
const lastConversation = classifyWithRules({
|
||
text: '上次我们聊了什么?',
|
||
sessionId: '20260704_4',
|
||
sessionMessageCount: 0,
|
||
});
|
||
assert.equal(lastConversation.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
|
||
});
|
||
|
||
test('createChatIntentRouter fast-paths news lookup to agent without LLM', async () => {
|
||
let llmCalls = 0;
|
||
const router = createChatIntentRouter({
|
||
enabled: true,
|
||
llmProviderService: {
|
||
async createChatCompletion() {
|
||
llmCalls += 1;
|
||
return { ok: false, message: 'router model unavailable' };
|
||
},
|
||
},
|
||
});
|
||
|
||
const result = await router.classify({
|
||
userMessage: {
|
||
role: 'user',
|
||
content: [{ type: 'text', text: '帮我查一下今天的 AI 新闻' }],
|
||
metadata: { displayText: '帮我查一下今天的 AI 新闻' },
|
||
},
|
||
grantedSkills: ['web'],
|
||
});
|
||
|
||
assert.equal(llmCalls, 0);
|
||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||
assert.equal(result.source, 'rule');
|
||
});
|
||
|
||
test('applyAgentOrchestrationToUserMessage preserves displayText and adds task envelope', () => {
|
||
const enriched = applyAgentOrchestrationToUserMessage(
|
||
{
|
||
role: 'user',
|
||
content: [{ type: 'text', text: '帮我做一个页面' }],
|
||
metadata: { displayText: '帮我做一个页面', userVisible: true },
|
||
},
|
||
{
|
||
route: CHAT_INTENT_ROUTE.AGENT,
|
||
confidence: 0.9,
|
||
reason: '需要生成页面',
|
||
suggestedSkill: 'static-page-publish',
|
||
agentBrief: '生成 HTML 并发布',
|
||
source: 'llm',
|
||
},
|
||
{ grantedSkills: ['static-page-publish'] },
|
||
);
|
||
|
||
assert.equal(enriched.metadata.displayText, '帮我做一个页面');
|
||
assert.match(enriched.content[0].text, /Memind 任务编排/);
|
||
assert.match(enriched.content[0].text, /static-page-publish/);
|
||
assert.match(enriched.content[0].text, /帮我做一个页面/);
|
||
});
|
||
|
||
test('buildAgentOrchestrationAgentText includes execution brief', () => {
|
||
const text = buildAgentOrchestrationAgentText({
|
||
displayText: '帮我搜索今天的新闻',
|
||
classification: {
|
||
reason: '需要实时搜索',
|
||
suggestedSkill: 'web',
|
||
agentBrief: '使用 web skill 搜索并整理来源',
|
||
},
|
||
skillPrompt: '请使用 web 技能:',
|
||
});
|
||
assert.match(text, /需要实时搜索/);
|
||
assert.match(text, /帮我搜索今天的新闻/);
|
||
assert.match(text, /请使用 web 技能/);
|
||
});
|
||
|
||
test('classifyWithRules attaches normalized decision on every rule result', () => {
|
||
const result = classifyWithRules({
|
||
text: '你好',
|
||
userMessage: {
|
||
role: 'user',
|
||
content: [{ type: 'text', text: '你好' }],
|
||
metadata: { displayText: '你好' },
|
||
},
|
||
});
|
||
assert.ok(result.decision);
|
||
assert.equal(result.decision.route, ROUTER_DECISION_ROUTE.CHAT);
|
||
assert.equal(result.decision.mode, ROUTER_DECISION_MODE.SSE);
|
||
assert.equal(result.decision.session_hint, ROUTER_DECISION_SESSION_HINT.NEW);
|
||
});
|
||
|
||
test('buildRouterNormalizedDecision maps legacy routes and session hints', () => {
|
||
const continued = buildRouterNormalizedDecision(
|
||
{ route: CHAT_INTENT_ROUTE.AGENT, suggestedSkill: 'web' },
|
||
{
|
||
text: '继续',
|
||
sessionId: '20260704_10',
|
||
sessionMessageCount: 2,
|
||
toolMode: 'chat',
|
||
},
|
||
);
|
||
assert.equal(continued.route, ROUTER_DECISION_ROUTE.AGENT);
|
||
assert.equal(continued.session_hint, ROUTER_DECISION_SESSION_HINT.REUSE);
|
||
assert.ok(continued.flags.includes('selected_skill'));
|
||
|
||
const freshAgentSession = buildRouterNormalizedDecision(
|
||
{ route: CHAT_INTENT_ROUTE.AGENT },
|
||
{
|
||
text: '你好',
|
||
sessionId: '20260704_11',
|
||
sessionMessageCount: 0,
|
||
},
|
||
);
|
||
assert.equal(freshAgentSession.session_hint, ROUTER_DECISION_SESSION_HINT.NEW);
|
||
|
||
const memoryRecall = buildRouterNormalizedDecision(
|
||
{ route: CHAT_INTENT_ROUTE.DIRECT_CHAT },
|
||
{ text: '你记得我说过什么吗' },
|
||
);
|
||
assert.equal(memoryRecall.route, ROUTER_DECISION_ROUTE.CHAT);
|
||
assert.ok(memoryRecall.flags.includes('memory_recall'));
|
||
});
|
||
|
||
test('resolveLegacyRouteFromClassification prefers decision when flag enabled', () => {
|
||
const classification = {
|
||
route: CHAT_INTENT_ROUTE.DIRECT_CHAT,
|
||
decision: { route: ROUTER_DECISION_ROUTE.AGENT },
|
||
};
|
||
assert.equal(
|
||
resolveLegacyRouteFromClassification(classification),
|
||
CHAT_INTENT_ROUTE.DIRECT_CHAT,
|
||
);
|
||
const previous = process.env.MEMIND_ROUTER_NORMALIZED_DECISION;
|
||
process.env.MEMIND_ROUTER_NORMALIZED_DECISION = '1';
|
||
try {
|
||
assert.equal(
|
||
resolveLegacyRouteFromClassification(classification),
|
||
CHAT_INTENT_ROUTE.AGENT,
|
||
);
|
||
assert.equal(isNormalizedRouterDecisionEnabled(), true);
|
||
} finally {
|
||
if (previous == null) delete process.env.MEMIND_ROUTER_NORMALIZED_DECISION;
|
||
else process.env.MEMIND_ROUTER_NORMALIZED_DECISION = previous;
|
||
}
|
||
});
|
||
|
||
test('resolveGatewayAgentSessionId honors session_hint without breaking hard rules', () => {
|
||
const previous = process.env.MEMIND_ROUTER_NORMALIZED_DECISION;
|
||
process.env.MEMIND_ROUTER_NORMALIZED_DECISION = '1';
|
||
try {
|
||
assert.equal(
|
||
resolveGatewayAgentSessionId({
|
||
agentSessionId: 'sess-1',
|
||
classification: {
|
||
decision: { session_hint: ROUTER_DECISION_SESSION_HINT.REUSE },
|
||
},
|
||
}),
|
||
'sess-1',
|
||
);
|
||
assert.equal(
|
||
resolveGatewayAgentSessionId({
|
||
agentSessionId: 'sess-1',
|
||
classification: {
|
||
decision: { session_hint: ROUTER_DECISION_SESSION_HINT.NEW },
|
||
},
|
||
}),
|
||
null,
|
||
);
|
||
assert.equal(
|
||
resolveGatewayAgentSessionId({
|
||
agentSessionId: 'h5direct_abc',
|
||
classification: {
|
||
decision: { session_hint: ROUTER_DECISION_SESSION_HINT.NEW },
|
||
},
|
||
}),
|
||
'h5direct_abc',
|
||
);
|
||
assert.equal(
|
||
resolveGatewayAgentSessionId({
|
||
agentSessionId: 'sess-1',
|
||
forceDeepReasoning: true,
|
||
classification: {
|
||
decision: { session_hint: ROUTER_DECISION_SESSION_HINT.NEW },
|
||
},
|
||
}),
|
||
'sess-1',
|
||
);
|
||
} finally {
|
||
if (previous == null) delete process.env.MEMIND_ROUTER_NORMALIZED_DECISION;
|
||
else process.env.MEMIND_ROUTER_NORMALIZED_DECISION = previous;
|
||
}
|
||
});
|
||
|
||
test('resolveNormalizedRouterDecisionMode supports off, shadow, and on', () => {
|
||
const previous = process.env.MEMIND_ROUTER_NORMALIZED_DECISION;
|
||
try {
|
||
delete process.env.MEMIND_ROUTER_NORMALIZED_DECISION;
|
||
assert.equal(resolveNormalizedRouterDecisionMode(), 'off');
|
||
process.env.MEMIND_ROUTER_NORMALIZED_DECISION = 'shadow';
|
||
assert.equal(resolveNormalizedRouterDecisionMode(), 'shadow');
|
||
assert.equal(isNormalizedRouterDecisionShadow(), true);
|
||
assert.equal(isNormalizedRouterDecisionEnabled(), false);
|
||
process.env.MEMIND_ROUTER_NORMALIZED_DECISION = '1';
|
||
assert.equal(resolveNormalizedRouterDecisionMode(), 'on');
|
||
assert.equal(isNormalizedRouterDecisionEnabled(), true);
|
||
} finally {
|
||
if (previous == null) delete process.env.MEMIND_ROUTER_NORMALIZED_DECISION;
|
||
else process.env.MEMIND_ROUTER_NORMALIZED_DECISION = previous;
|
||
}
|
||
});
|
||
|
||
test('shadow mode keeps legacy route for gateway decisions', () => {
|
||
const classification = {
|
||
route: CHAT_INTENT_ROUTE.DIRECT_CHAT,
|
||
decision: { route: ROUTER_DECISION_ROUTE.AGENT },
|
||
};
|
||
const previous = process.env.MEMIND_ROUTER_NORMALIZED_DECISION;
|
||
process.env.MEMIND_ROUTER_NORMALIZED_DECISION = 'shadow';
|
||
try {
|
||
assert.equal(resolveLegacyRouteFromClassification(classification), CHAT_INTENT_ROUTE.DIRECT_CHAT);
|
||
assert.equal(isNormalizedRouterDecisionEnabled(), false);
|
||
} finally {
|
||
if (previous == null) delete process.env.MEMIND_ROUTER_NORMALIZED_DECISION;
|
||
else process.env.MEMIND_ROUTER_NORMALIZED_DECISION = previous;
|
||
}
|
||
});
|
||
|
||
test('classifyWithRules fast-paths bedtime story on fresh session', () => {
|
||
const result = classifyWithRules({
|
||
text: '讲一个睡前故事吧',
|
||
llmRouterEnabled: true,
|
||
sessionId: '20260706_2',
|
||
sessionMessageCount: 0,
|
||
userMessage: {
|
||
role: 'user',
|
||
content: [{ type: 'text', text: '讲一个睡前故事吧' }],
|
||
metadata: { displayText: '讲一个睡前故事吧' },
|
||
},
|
||
});
|
||
assert.equal(result?.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
|
||
assert.equal(result?.source, 'rule');
|
||
assert.equal(result?.decision?.route, ROUTER_DECISION_ROUTE.CHAT);
|
||
});
|
||
|
||
test('classifyWithRules fast-paths world cup standings to agent orchestration', () => {
|
||
const text = '世界杯现在赛况如何';
|
||
assert.equal(isRealtimeInfoQuestion(text), true);
|
||
const result = classifyWithRules({
|
||
text,
|
||
llmRouterEnabled: true,
|
||
userMessage: {
|
||
role: 'user',
|
||
content: [{ type: 'text', text }],
|
||
metadata: { displayText: text },
|
||
},
|
||
});
|
||
assert.equal(result?.route, CHAT_INTENT_ROUTE.AGENT);
|
||
assert.equal(result?.source, 'rule');
|
||
assert.equal(result?.suggestedSkill, 'web');
|
||
assert.match(result?.reason, /实时/);
|
||
});
|
||
|
||
test('createChatIntentRouter fast-paths world cup without calling router LLM', async () => {
|
||
let llmCalls = 0;
|
||
const router = createChatIntentRouter({
|
||
enabled: true,
|
||
llmProviderService: {
|
||
async createChatCompletion() {
|
||
llmCalls += 1;
|
||
throw new Error('router should not be called');
|
||
},
|
||
},
|
||
});
|
||
|
||
const result = await router.classify({
|
||
userMessage: {
|
||
role: 'user',
|
||
content: [{ type: 'text', text: '世界杯现在赛况如何' }],
|
||
metadata: { displayText: '世界杯现在赛况如何' },
|
||
},
|
||
grantedSkills: ['web'],
|
||
});
|
||
|
||
assert.equal(llmCalls, 0);
|
||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||
assert.equal(result.source, 'rule');
|
||
});
|
||
|
||
test('resolveChatIntentRouterPolicy coerces direct_chat fallback to agent', () => {
|
||
const policy = resolveChatIntentRouterPolicy({
|
||
env: {
|
||
MEMIND_CHAT_ROUTER_FALLBACK_ROUTE: 'direct_chat',
|
||
},
|
||
});
|
||
assert.equal(policy.fallbackRoute, CHAT_INTENT_ROUTE.AGENT);
|
||
});
|
||
|
||
test('coerceRealtimeWebSkill replaces search with web for world cup query', () => {
|
||
const coerced = coerceRealtimeWebSkill(
|
||
{
|
||
route: CHAT_INTENT_ROUTE.AGENT,
|
||
confidence: 0.9,
|
||
reason: '用户询问实时赛况,需要搜索最新信息',
|
||
suggestedSkill: 'search',
|
||
source: 'llm',
|
||
},
|
||
'世界杯现在赛况如何',
|
||
{ grantedSkills: ['web', 'search'] },
|
||
);
|
||
assert.equal(coerced.suggestedSkill, 'web');
|
||
assert.match(coerced.agentBrief, /web_search/);
|
||
});
|
||
|
||
test('applyAgentOrchestrationToUserMessage uses web news prompt for realtime query', () => {
|
||
const enriched = applyAgentOrchestrationToUserMessage(
|
||
{
|
||
role: 'user',
|
||
content: [{ type: 'text', text: '世界杯现在赛况如何' }],
|
||
metadata: { displayText: '世界杯现在赛况如何', userVisible: true },
|
||
},
|
||
coerceRealtimeWebSkill(
|
||
{
|
||
route: CHAT_INTENT_ROUTE.AGENT,
|
||
confidence: 0.9,
|
||
reason: '需要搜索实时资料',
|
||
suggestedSkill: 'web',
|
||
source: 'rule',
|
||
},
|
||
'世界杯现在赛况如何',
|
||
{ grantedSkills: ['web'] },
|
||
),
|
||
{ grantedSkills: ['web'] },
|
||
);
|
||
assert.match(enriched.content[0].text, /请使用 web 技能:先搜索今天\/最新相关的新闻与热点/);
|
||
assert.doesNotMatch(enriched.content[0].text, /search 技能:帮我在工作区中查找代码或文件/);
|
||
assert.match(enriched.content[0].text, /web_search/);
|
||
});
|
||
|
||
test('createChatIntentRouter coerces LLM search route to web for realtime query', async () => {
|
||
const router = createChatIntentRouter({
|
||
enabled: true,
|
||
llmProviderService: {
|
||
async createChatCompletion() {
|
||
return {
|
||
ok: true,
|
||
reply: JSON.stringify({
|
||
route: 'agent_orchestration',
|
||
confidence: 0.92,
|
||
reason: '用户询问实时赛况,需要搜索最新信息',
|
||
suggested_skill: 'search',
|
||
agent_brief: '搜索赛况',
|
||
}),
|
||
};
|
||
},
|
||
},
|
||
});
|
||
|
||
const result = await router.classify({
|
||
userMessage: {
|
||
role: 'user',
|
||
content: [{ type: 'text', text: '世界杯现在赛况如何' }],
|
||
metadata: { displayText: '世界杯现在赛况如何' },
|
||
},
|
||
grantedSkills: ['web', 'search'],
|
||
});
|
||
|
||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||
assert.equal(result.suggestedSkill, 'web');
|
||
assert.match(result.agentBrief, /web_search/);
|
||
});
|
||
|
||
test('createChatIntentRouter timeout fallback prefers agent even when policy requests direct_chat', async () => {
|
||
const router = createChatIntentRouter({
|
||
enabled: true,
|
||
timeoutMs: 5,
|
||
fallbackRoute: CHAT_INTENT_ROUTE.DIRECT_CHAT,
|
||
llmProviderService: {
|
||
async createChatCompletion() {
|
||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||
return { ok: true, reply: '{"route":"direct_chat","confidence":0.9,"reason":"x","suggested_skill":null,"agent_brief":""}' };
|
||
},
|
||
},
|
||
});
|
||
|
||
const result = await router.classify({
|
||
userMessage: {
|
||
role: 'user',
|
||
content: [{ type: 'text', text: '帮我整理一下这段材料' }],
|
||
metadata: { displayText: '帮我整理一下这段材料' },
|
||
},
|
||
});
|
||
|
||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||
assert.equal(result.source, 'fallback');
|
||
});
|
||
|
||
test('agent memory shadow resolve is independently gated and never marked for injection', async () => {
|
||
const calls = [];
|
||
const router = createChatIntentRouter({
|
||
env: {
|
||
MEMORY_AGENT_RESOLVE_ENABLED: '1',
|
||
MEMORY_AGENT_INJECTION_MODE: 'shadow',
|
||
MEMORY_AGENT_RESOLVE_LIMIT: '3',
|
||
MEMORY_AGENT_RESOLVE_TIMEOUT_MS: '100',
|
||
},
|
||
memoryV2: {
|
||
async resolve(input) {
|
||
calls.push(input);
|
||
return { source: 'legacy-conversation-memory', memories: [{ label: 'preference', text: '喜欢完整方案' }] };
|
||
},
|
||
},
|
||
});
|
||
|
||
const result = await router.resolveAgentMemoryContext({
|
||
userId: 'u1',
|
||
sessionId: 's1',
|
||
text: '帮我设计一个商城',
|
||
});
|
||
|
||
assert.equal(result.mode, 'shadow');
|
||
assert.equal(result.injectionEnabled, false);
|
||
assert.equal(result.skipped, false);
|
||
assert.equal(result.memories.length, 1);
|
||
assert.deepEqual(calls[0], { userId: 'u1', sessionId: 's1', query: '帮我设计一个商城', limit: 3 });
|
||
});
|
||
|
||
test('agent memory canary injects only for configured user ids', async () => {
|
||
const router = createChatIntentRouter({
|
||
env: {
|
||
MEMORY_AGENT_RESOLVE_ENABLED: '1',
|
||
MEMORY_AGENT_INJECTION_MODE: 'canary',
|
||
MEMORY_AGENT_CANARY_USER_IDS: 'user-canary, user-other',
|
||
},
|
||
memoryV2: {
|
||
async resolve() {
|
||
return { memories: [{ label: 'goal', text: '当前项目是 TKMind' }] };
|
||
},
|
||
},
|
||
});
|
||
|
||
const canary = await router.resolveAgentMemoryContext({ userId: 'user-canary', text: '继续项目' });
|
||
const control = await router.resolveAgentMemoryContext({ userId: 'user-control', text: '继续项目' });
|
||
assert.equal(canary.injectionEnabled, true);
|
||
assert.equal(control.injectionEnabled, false);
|
||
assert.equal(canary.memories.length, 1);
|
||
assert.equal(control.memories.length, 1);
|
||
});
|
||
|
||
test('agent memory recall prioritizes historical session evidence over personal memory', async () => {
|
||
const episodicCalls = [];
|
||
const router = createChatIntentRouter({
|
||
env: {
|
||
MEMORY_AGENT_RESOLVE_ENABLED: '1',
|
||
MEMORY_AGENT_INJECTION_MODE: 'active',
|
||
MEMORY_AGENT_RESOLVE_LIMIT: '3',
|
||
MEMORY_AGENT_RESOLVE_TIMEOUT_MS: '200',
|
||
},
|
||
memoryV2: {
|
||
async resolve() {
|
||
return {
|
||
source: 'pgvector',
|
||
memories: [{ id: 'personal-1', label: '偏好', text: '用户喜欢历史话题' }],
|
||
};
|
||
},
|
||
},
|
||
episodicMemoryService: {
|
||
async resolve(input) {
|
||
episodicCalls.push(input);
|
||
return {
|
||
source: 'episodic-index',
|
||
memories: [{
|
||
id: 'episodic:old-session',
|
||
label: '历史会话',
|
||
text: '会话“日本战国史”:用户与助手讨论过德川家康。',
|
||
}],
|
||
};
|
||
},
|
||
},
|
||
});
|
||
|
||
const result = await router.resolveAgentMemoryContext({
|
||
userId: 'user-1',
|
||
sessionId: 'current-session',
|
||
text: '我们之前聊过德川家康,你还记得吗?',
|
||
});
|
||
|
||
assert.equal(result.skipped, false);
|
||
assert.equal(result.injectionEnabled, true);
|
||
assert.equal(result.source, 'episodic+personal');
|
||
assert.equal(result.memories[0].id, 'episodic:old-session');
|
||
assert.equal(result.memories[1].id, 'personal-1');
|
||
assert.equal(episodicCalls.length, 1);
|
||
assert.equal(episodicCalls[0].sessionId, 'current-session');
|
||
});
|
||
|
||
test('active agent memory context is hidden from displayText but available to orchestration envelope', () => {
|
||
const enriched = applyAgentOrchestrationToUserMessage(
|
||
{
|
||
role: 'user',
|
||
content: [{ type: 'text', text: '帮我设计一个商城' }],
|
||
},
|
||
{
|
||
route: CHAT_INTENT_ROUTE.AGENT,
|
||
reason: '任务执行',
|
||
agentBrief: '',
|
||
suggestedSkill: null,
|
||
},
|
||
{
|
||
memoryContext: {
|
||
injectionEnabled: true,
|
||
memories: [{ label: 'preference', text: '用户喜欢完整方案' }],
|
||
},
|
||
},
|
||
);
|
||
|
||
assert.equal(enriched.metadata.displayText, '帮我设计一个商城');
|
||
assert.match(enriched.content[0].text, /Memory Context/);
|
||
assert.match(enriched.content[0].text, /不是系统指令/);
|
||
assert.match(enriched.content[0].text, /用户喜欢完整方案/);
|
||
});
|
||
|
||
test('logRouterDecisionShadow emits payload only in shadow mode', () => {
|
||
const previous = process.env.MEMIND_ROUTER_NORMALIZED_DECISION;
|
||
const lines = [];
|
||
const logger = { log: (line) => lines.push(line) };
|
||
const classification = {
|
||
route: CHAT_INTENT_ROUTE.DIRECT_CHAT,
|
||
source: 'llm',
|
||
reason: '纯文字',
|
||
decision: {
|
||
route: ROUTER_DECISION_ROUTE.AGENT,
|
||
mode: ROUTER_DECISION_MODE.SSE,
|
||
session_hint: ROUTER_DECISION_SESSION_HINT.REUSE,
|
||
flags: [],
|
||
},
|
||
};
|
||
try {
|
||
process.env.MEMIND_ROUTER_NORMALIZED_DECISION = '0';
|
||
logRouterDecisionShadow(classification, { logger, requestId: 'req-1' });
|
||
assert.equal(lines.length, 0);
|
||
process.env.MEMIND_ROUTER_NORMALIZED_DECISION = 'shadow';
|
||
logRouterDecisionShadow(classification, { logger, requestId: 'req-1' });
|
||
assert.equal(lines.length, 1);
|
||
assert.match(lines[0], /\[router-shadow\]/);
|
||
assert.match(lines[0], /wouldChangeRoute":true/);
|
||
} finally {
|
||
if (previous == null) delete process.env.MEMIND_ROUTER_NORMALIZED_DECISION;
|
||
else process.env.MEMIND_ROUTER_NORMALIZED_DECISION = previous;
|
||
}
|
||
});
|
||
|
||
test('createChatIntentRouter uses agent fallback for unmatched general questions (skill-only)', async () => {
|
||
const llmCalls = [];
|
||
const router = createChatIntentRouter({
|
||
enabled: true,
|
||
llmProviderService: {
|
||
async createChatCompletion() {
|
||
llmCalls.push(1);
|
||
return { ok: false, message: 'router disabled' };
|
||
},
|
||
},
|
||
});
|
||
|
||
const result = await router.classify({
|
||
userMessage: {
|
||
role: 'user',
|
||
content: [{ type: 'text', text: '什么是 SSE?' }],
|
||
metadata: { displayText: '什么是 SSE?' },
|
||
},
|
||
});
|
||
|
||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||
assert.equal(result.source, 'fallback');
|
||
assert.equal(llmCalls.length, 0);
|
||
});
|
||
|
||
test('classifyWithRules routes page data dev repair before new page-data collect', () => {
|
||
const result = classifyWithRules({
|
||
text: '修复问卷 insert 403,columns_not_allowed',
|
||
sessionId: null,
|
||
sessionMessageCount: 0,
|
||
});
|
||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||
assert.match(result.reason, /Page Data 开发修复/);
|
||
assert.equal(result.suggested_skill, undefined);
|
||
});
|