Add page data delivery and publication guards

This commit is contained in:
john
2026-07-08 21:10:47 +08:00
parent 8d629e7a4e
commit eea14c1855
66 changed files with 3035 additions and 413 deletions
+110 -106
View File
@@ -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、配置 policyHTML 引入 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,
});