Add page data delivery and publication guards
This commit is contained in:
+110
-106
@@ -3,14 +3,17 @@ import {
|
||||
buildWebNewsSkillPrompt,
|
||||
extractSelectedChatSkillName,
|
||||
hasExplicitChatSkillPrompt,
|
||||
isPageDataIntent,
|
||||
isPageGenerationIntent,
|
||||
isProductCampaignIntent,
|
||||
PAGE_DATA_COLLECT_SKILL_NAME,
|
||||
} from './chat-skills.mjs';
|
||||
import { isDirectChatSessionId } from './direct-chat-service.mjs';
|
||||
import {
|
||||
memoryLimitForIntervention,
|
||||
resolveMemoryInterventionMode,
|
||||
} from './memory-intervention.mjs';
|
||||
import { filterMemoriesByQuery } from './memory-legacy-fallback.mjs';
|
||||
|
||||
export const CHAT_INTENT_ROUTE = {
|
||||
DIRECT_CHAT: 'direct_chat',
|
||||
@@ -46,6 +49,7 @@ const SKILL_PROMPT_KEYS = {
|
||||
web: 'web',
|
||||
search: 'search',
|
||||
'static-page-publish': 'generate-page',
|
||||
'page-data-collect': 'page-data-collect',
|
||||
'form-builder': 'form-builder',
|
||||
'table-viewer': 'table-viewer',
|
||||
'product-campaign-page': 'product-campaign-page',
|
||||
@@ -66,6 +70,12 @@ const OBVIOUS_DIRECT_PATTERNS = [
|
||||
/^[??]+$/u,
|
||||
];
|
||||
|
||||
/** Short confirmations in an ongoing agent session must stay on Agent (not direct chat). */
|
||||
const AGENT_SESSION_CONTINUE_PATTERNS = [
|
||||
/^(?:可以|好的|好|行|确认|没问题|是的|对|嗯|OK|ok)[!!。.\s]*$/iu,
|
||||
/^(?:开始吧|按默认做|默认方案|继续|就这样|就这样吧)[!!。.\s]*$/iu,
|
||||
];
|
||||
|
||||
/** Pure text chat/creative prompts — fast-path even when LLM router is enabled. */
|
||||
const OBVIOUS_DIRECT_CHAT_PATTERNS = [
|
||||
/(?:讲|说|来|编).{0,10}(?:个|一段|一首|一个)?(?:睡前故事|故事|笑话|段子)/u,
|
||||
@@ -97,6 +107,8 @@ const MEMORY_RECALL_PATTERNS = [
|
||||
/(?:我|之前).{0,16}(?:说过|提到|聊过|告诉)/u,
|
||||
/(?:我的|之前的)(?:记忆|偏好|计划|目标|想法)/u,
|
||||
/之前(?:说|提|聊)(?:过|的)/u,
|
||||
/之前.{0,24}记住/u,
|
||||
/记住.{0,16}(?:是什么|叫什么|多少|哪个)/u,
|
||||
];
|
||||
|
||||
export function isMemoryRecallQuestion(text) {
|
||||
@@ -105,6 +117,12 @@ export function isMemoryRecallQuestion(text) {
|
||||
return MEMORY_RECALL_PATTERNS.some((pattern) => pattern.test(normalized));
|
||||
}
|
||||
|
||||
export function isAgentSessionContinueText(text) {
|
||||
const normalized = String(text ?? '').trim();
|
||||
if (!normalized) return false;
|
||||
return AGENT_SESSION_CONTINUE_PATTERNS.some((pattern) => pattern.test(normalized));
|
||||
}
|
||||
|
||||
export function isRealtimeInfoQuestion(text) {
|
||||
const normalized = String(text ?? '').trim();
|
||||
if (!normalized) return false;
|
||||
@@ -135,8 +153,25 @@ function buildRealtimeInfoClassification() {
|
||||
}, { source: 'rule' });
|
||||
}
|
||||
|
||||
export function coercePageDataSkill(classification, text, { grantedSkills = [] } = {}) {
|
||||
if (!classification || classification.route !== CHAT_INTENT_ROUTE.AGENT) return classification;
|
||||
if (!isPageDataIntent(text)) return classification;
|
||||
if (grantedSkills.length > 0 && !grantedSkills.includes(PAGE_DATA_COLLECT_SKILL_NAME)) {
|
||||
return classification;
|
||||
}
|
||||
|
||||
return {
|
||||
...classification,
|
||||
suggestedSkill: PAGE_DATA_COLLECT_SKILL_NAME,
|
||||
agentBrief:
|
||||
'在 MindSpace 页面实现数据收集与持久化:建表、注册 dataset、配置 page policy、HTML 使用 page-data-client.js;禁止自建后端服务。',
|
||||
reason: `${classification.reason}(页面数据交互意图)`,
|
||||
};
|
||||
}
|
||||
|
||||
export function coercePageGenerationSkill(classification, text, { grantedSkills = [] } = {}) {
|
||||
if (!classification || classification.route !== CHAT_INTENT_ROUTE.AGENT) return classification;
|
||||
if (isPageDataIntent(text)) return classification;
|
||||
if (!isPageGenerationIntent(text) || isProductCampaignIntent(text)) return classification;
|
||||
if (grantedSkills.length > 0 && !grantedSkills.includes('static-page-publish')) return classification;
|
||||
|
||||
@@ -321,7 +356,8 @@ function buildRouterSystemPrompt(grantedSkills = []) {
|
||||
: 'suggested_skill 通常填 null。',
|
||||
'',
|
||||
'skill 选择补充:',
|
||||
'- static-page-publish:攻略/游记/主题内容页、城市介绍、活动介绍、任何「做页面/生成链接」且无商品购买跳转需求。',
|
||||
'- page-data-collect:页面需要问卷/表单/报名/提交记录/后台查看/数据交互/SQLite 持久化;必须用 Page Data API,禁止自建 Express 或独立端口。',
|
||||
'- static-page-publish:攻略/游记/主题内容页、城市介绍、活动介绍、任何「做页面/生成链接」且无数据持久化需求。',
|
||||
'- product-campaign-page:仅当用户明确提供商品链接或要求购买按钮/电商转化时使用,不要用于旅游攻略或纯内容页。',
|
||||
'',
|
||||
'只输出 JSON,不要 markdown,不要解释:',
|
||||
@@ -740,10 +776,10 @@ export function classifyWithRules({
|
||||
}, { source: 'rule' }), decisionContext);
|
||||
}
|
||||
if (
|
||||
!llmRouterEnabled &&
|
||||
includeIntentPatterns &&
|
||||
normalized &&
|
||||
isMemoryRecallQuestion(normalized)
|
||||
isMemoryRecallQuestion(normalized) &&
|
||||
!hasPriorAgentConversation(sessionId, sessionMessageCount)
|
||||
) {
|
||||
return finalizeRouterClassification(normalizeClassification({
|
||||
route: CHAT_INTENT_ROUTE.DIRECT_CHAT,
|
||||
@@ -751,11 +787,28 @@ export function classifyWithRules({
|
||||
reason: '用户在询问个人记忆或历史对话',
|
||||
}, { source: 'rule' }), decisionContext);
|
||||
}
|
||||
if (!llmRouterEnabled && hasPriorAgentConversation(sessionId, sessionMessageCount)) {
|
||||
if (hasPriorAgentConversation(sessionId, sessionMessageCount)) {
|
||||
const reason = isAgentSessionContinueText(normalized)
|
||||
? 'Agent 会话确认/续聊'
|
||||
: '延续已有 Agent 会话';
|
||||
return finalizeRouterClassification(normalizeClassification({
|
||||
route: CHAT_INTENT_ROUTE.AGENT,
|
||||
confidence: 1,
|
||||
reason: '延续已有 Agent 会话',
|
||||
reason,
|
||||
}, { source: 'rule' }), decisionContext);
|
||||
}
|
||||
if (
|
||||
includeIntentPatterns &&
|
||||
normalized &&
|
||||
isPageDataIntent(normalized)
|
||||
) {
|
||||
return finalizeRouterClassification(normalizeClassification({
|
||||
route: CHAT_INTENT_ROUTE.AGENT,
|
||||
confidence: 0.96,
|
||||
reason: '页面需要数据交互与持久化',
|
||||
suggested_skill: PAGE_DATA_COLLECT_SKILL_NAME,
|
||||
agent_brief:
|
||||
'使用 Page Data API 实现页面表单提交与后台查看;建表、注册 dataset、配置 policy,HTML 引入 page-data-client.js。',
|
||||
}, { source: 'rule' }), decisionContext);
|
||||
}
|
||||
if (
|
||||
@@ -813,6 +866,7 @@ export function createChatIntentRouter(options = {}) {
|
||||
const {
|
||||
llmProviderService,
|
||||
memoryV2 = null,
|
||||
conversationMemoryService = null,
|
||||
env = process.env,
|
||||
logger = console,
|
||||
} = options;
|
||||
@@ -849,7 +903,8 @@ export function createChatIntentRouter(options = {}) {
|
||||
}
|
||||
|
||||
function isEnabled() {
|
||||
return Boolean(policy.enabled && llmProviderService?.createChatCompletion);
|
||||
// Skill + rule routing only; LLM intent router is intentionally disabled.
|
||||
return false;
|
||||
}
|
||||
|
||||
async function resolveRouterContext({ userId, sessionId, text, forceDeepReasoning = false }) {
|
||||
@@ -861,32 +916,50 @@ export function createChatIntentRouter(options = {}) {
|
||||
if (
|
||||
limit <= 0 ||
|
||||
!policy.memoryResolveEnabled ||
|
||||
!memoryV2?.resolve ||
|
||||
!userId
|
||||
!userId ||
|
||||
(!memoryV2?.resolve && !conversationMemoryService?.listMemories)
|
||||
) {
|
||||
return buildRouterContext(null);
|
||||
}
|
||||
try {
|
||||
const resolved = await withTimeout(
|
||||
memoryV2.resolve({
|
||||
userId,
|
||||
sessionId,
|
||||
query: text,
|
||||
limit,
|
||||
}),
|
||||
policy.timeoutMs,
|
||||
'Memory V2 router resolve',
|
||||
);
|
||||
return buildRouterContext(resolved);
|
||||
} catch (err) {
|
||||
logger?.warn?.(
|
||||
`[chat-intent-router] memory resolve skipped: ${err instanceof Error ? err.message : err}`,
|
||||
);
|
||||
let primaryFailed = false;
|
||||
let memories = [];
|
||||
const recallQuestion = isMemoryRecallQuestion(text);
|
||||
if (recallQuestion && conversationMemoryService?.listMemories) {
|
||||
const legacy = await conversationMemoryService.listMemories(userId, { limit }).catch(() => []);
|
||||
memories = filterMemoriesByQuery(legacy, text);
|
||||
}
|
||||
if (!memories.length && memoryV2?.resolve) {
|
||||
try {
|
||||
const resolved = await withTimeout(
|
||||
memoryV2.resolve({
|
||||
userId,
|
||||
sessionId,
|
||||
query: text,
|
||||
limit,
|
||||
}),
|
||||
policy.timeoutMs,
|
||||
'Memory V2 router resolve',
|
||||
);
|
||||
memories = Array.isArray(resolved?.memories) ? resolved.memories : [];
|
||||
} catch (err) {
|
||||
primaryFailed = true;
|
||||
logger?.warn?.(
|
||||
`[chat-intent-router] memory resolve skipped: ${err instanceof Error ? err.message : err}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!memories.length && conversationMemoryService?.listMemories) {
|
||||
const legacy = await conversationMemoryService.listMemories(userId, { limit }).catch(() => []);
|
||||
memories = filterMemoriesByQuery(legacy, text);
|
||||
if (memories.length) primaryFailed = false;
|
||||
}
|
||||
if (primaryFailed && !memories.length) {
|
||||
return buildRouterContext({
|
||||
degraded: true,
|
||||
reason: 'memory_resolve_failed',
|
||||
});
|
||||
}
|
||||
return buildRouterContext({ memories, source: 'router-resolve' });
|
||||
}
|
||||
|
||||
async function classify({
|
||||
@@ -912,7 +985,11 @@ export function createChatIntentRouter(options = {}) {
|
||||
delete base.decision;
|
||||
return finalizeRouterClassification(
|
||||
coercePageGenerationSkill(
|
||||
coerceRealtimeWebSkill(base, text, { grantedSkills }),
|
||||
coercePageDataSkill(
|
||||
coerceRealtimeWebSkill(base, text, { grantedSkills }),
|
||||
text,
|
||||
{ grantedSkills },
|
||||
),
|
||||
text,
|
||||
{ grantedSkills },
|
||||
),
|
||||
@@ -927,89 +1004,14 @@ export function createChatIntentRouter(options = {}) {
|
||||
sessionMessageCount,
|
||||
userMessage,
|
||||
includeIntentPatterns: true,
|
||||
llmRouterEnabled: Boolean(policy.enabled),
|
||||
llmRouterEnabled: false,
|
||||
});
|
||||
if (ruleResult) return finalizeWithCoercion(ruleResult);
|
||||
if (!isEnabled()) {
|
||||
return finalizeWithCoercion(normalizeClassification({
|
||||
route: policy.fallbackRoute,
|
||||
confidence: 0.5,
|
||||
reason: '意图路由未启用,走默认通道',
|
||||
}, { source: 'fallback' }));
|
||||
}
|
||||
|
||||
const routerContext = await resolveRouterContext({
|
||||
userId,
|
||||
sessionId,
|
||||
text,
|
||||
forceDeepReasoning,
|
||||
});
|
||||
let completion = null;
|
||||
try {
|
||||
completion = await withTimeout(
|
||||
llmProviderService.createChatCompletion({
|
||||
providerKeyId: policy.modelProviderKeyId || undefined,
|
||||
model: policy.model || undefined,
|
||||
modelApiType: policy.modelApiType || undefined,
|
||||
temperature: policy.temperature,
|
||||
messages: [
|
||||
{ role: 'system', content: buildRouterSystemPrompt(grantedSkills) },
|
||||
{
|
||||
role: 'user',
|
||||
content: buildRouterUserPrompt({
|
||||
text,
|
||||
routerContext: routerContext.content,
|
||||
}),
|
||||
},
|
||||
],
|
||||
}),
|
||||
policy.timeoutMs,
|
||||
'Chat intent router',
|
||||
);
|
||||
} catch (err) {
|
||||
return finalizeWithCoercion(normalizeClassification({
|
||||
route: policy.fallbackRoute,
|
||||
confidence: 0,
|
||||
reason: err instanceof Error ? err.message : '意图路由失败,走默认通道',
|
||||
memory: routerContext,
|
||||
}, { source: 'fallback', fallbackRoute: policy.fallbackRoute }));
|
||||
}
|
||||
if (!completion?.ok) {
|
||||
return finalizeWithCoercion(normalizeClassification({
|
||||
route: policy.fallbackRoute,
|
||||
confidence: 0,
|
||||
reason: completion?.message ?? '意图路由失败,走默认通道',
|
||||
memory: routerContext,
|
||||
}, { source: 'fallback', fallbackRoute: policy.fallbackRoute }));
|
||||
}
|
||||
|
||||
const parsed = parseRouterJson(completion.reply);
|
||||
if (!parsed) {
|
||||
return finalizeWithCoercion(normalizeClassification({
|
||||
route: policy.fallbackRoute,
|
||||
confidence: 0,
|
||||
reason: '意图路由响应无法解析,走默认通道',
|
||||
memory: routerContext,
|
||||
}, { source: 'fallback', fallbackRoute: policy.fallbackRoute }));
|
||||
}
|
||||
|
||||
const classification = finalizeWithCoercion({
|
||||
...normalizeClassification(parsed, { source: 'llm', fallbackRoute: policy.fallbackRoute }),
|
||||
providerKeyId: completion.providerKeyId ?? policy.modelProviderKeyId ?? null,
|
||||
model: completion.model ?? policy.model ?? null,
|
||||
memory: routerContext,
|
||||
});
|
||||
if (
|
||||
classification.route === CHAT_INTENT_ROUTE.DIRECT_CHAT &&
|
||||
classification.confidence < policy.minConfidence
|
||||
) {
|
||||
return finalizeWithCoercion(normalizeClassification({
|
||||
...classification,
|
||||
route: policy.fallbackRoute,
|
||||
reason: `${classification.reason}(置信度 ${classification.confidence} 低于阈值,走默认通道)`,
|
||||
}, { source: 'threshold', fallbackRoute: policy.fallbackRoute }));
|
||||
}
|
||||
return classification;
|
||||
return finalizeWithCoercion(normalizeClassification({
|
||||
route: policy.fallbackRoute,
|
||||
confidence: 0.5,
|
||||
reason: '规则未命中,走 skill 默认 Agent 通道',
|
||||
}, { source: 'fallback' }));
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -1023,6 +1025,7 @@ export function createChatIntentRouter(options = {}) {
|
||||
export function createManagedChatIntentRouter({
|
||||
llmProviderService,
|
||||
memoryV2 = null,
|
||||
conversationMemoryService = null,
|
||||
configService = null,
|
||||
env = process.env,
|
||||
logger = console,
|
||||
@@ -1076,6 +1079,7 @@ export function createManagedChatIntentRouter({
|
||||
activeRouter = createChatIntentRouter({
|
||||
llmProviderService,
|
||||
memoryV2,
|
||||
conversationMemoryService,
|
||||
env: state.effectiveEnv,
|
||||
logger,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user