feat(h5): inject direct-chat history on agent escalation and defer ambiguous routing.
Phase B appends snapshot context when leaving h5direct sessions so Goose no longer starts blind. Phase C lets the 0.72 fallback return null on gated chat text so the existing LLM router can run, flags default off. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -150,6 +150,15 @@ H5_PUBLIC_BASE_URL=http://127.0.0.1:5173
|
|||||||
# MEMIND_CHAT_ROUTER_MEMORY_ENABLED=1
|
# MEMIND_CHAT_ROUTER_MEMORY_ENABLED=1
|
||||||
# MEMIND_CHAT_ROUTER_FALLBACK_ROUTE=direct_chat
|
# MEMIND_CHAT_ROUTER_FALLBACK_ROUTE=direct_chat
|
||||||
|
|
||||||
|
# ----- Chat task intent layer(docs/architecture/chat-task-intent-layer-review-20260828.md)-----
|
||||||
|
# 阶段 B:direct→agent 升级时把 snapshot 历史注入 Goose 输入(默认关,建议先 canary)
|
||||||
|
# MEMIND_DIRECT_ESCALATION_CONTEXT_ENABLED=0
|
||||||
|
# MEMIND_DIRECT_ESCALATION_CONTEXT_CANARY_USER_IDS=
|
||||||
|
# 阶段 C:0.72 兜底在歧义文本上 defer 给 LLM router(默认关;建议先 SHADOW=1 观测)
|
||||||
|
# MEMIND_CHAT_ROUTER_CHAT_SESSION_DEFER_ENABLED=0
|
||||||
|
# MEMIND_CHAT_ROUTER_CHAT_SESSION_DEFER_CANARY_USER_IDS=
|
||||||
|
# MEMIND_CHAT_ROUTER_CHAT_SESSION_DEFER_MIN_TEXT_LENGTH=12
|
||||||
|
|
||||||
# ----- Token 压缩(本地开发可选,降低 DeepSeek 上下文)-----
|
# ----- Token 压缩(本地开发可选,降低 DeepSeek 上下文)-----
|
||||||
# Agent 会话历史压缩:超过阈值后清空 Goose 会话并保留摘要(默认 180 条 / 12 万字符,偏松)
|
# Agent 会话历史压缩:超过阈值后清空 Goose 会话并保留摘要(默认 180 条 / 12 万字符,偏松)
|
||||||
# MEMIND_AGENT_SESSION_COMPACT_MESSAGE_COUNT=40
|
# MEMIND_AGENT_SESSION_COMPACT_MESSAGE_COUNT=40
|
||||||
|
|||||||
+54
-2
@@ -45,6 +45,10 @@ import {
|
|||||||
import { applyCursorFirstAgentExecution } from './cursor-page-routing.mjs';
|
import { applyCursorFirstAgentExecution } from './cursor-page-routing.mjs';
|
||||||
import { cursorDeepseekFallbackEnabled } from './cursor-agent-launch.mjs';
|
import { cursorDeepseekFallbackEnabled } from './cursor-agent-launch.mjs';
|
||||||
import { buildCursorBillingTokenState } from './cursor-agent-usage.mjs';
|
import { buildCursorBillingTokenState } from './cursor-agent-usage.mjs';
|
||||||
|
import {
|
||||||
|
isDirectEscalationContextEnabledForUser,
|
||||||
|
resolveDirectEscalationContextPolicy,
|
||||||
|
} from './chat-task-intent-config.mjs';
|
||||||
|
|
||||||
const DEFAULT_RUN_RETRY_DELAYS_MS = [1500, 5000, 15000];
|
const DEFAULT_RUN_RETRY_DELAYS_MS = [1500, 5000, 15000];
|
||||||
const TERMINAL_STATUSES = new Set(['succeeded', 'failed']);
|
const TERMINAL_STATUSES = new Set(['succeeded', 'failed']);
|
||||||
@@ -260,6 +264,26 @@ function appendFreshSessionContext(
|
|||||||
return { ...message, content };
|
return { ...message, content };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function maybeInjectDirectEscalationContext({
|
||||||
|
userMessage,
|
||||||
|
conversation,
|
||||||
|
userId,
|
||||||
|
policy,
|
||||||
|
} = {}) {
|
||||||
|
if (!isDirectEscalationContextEnabledForUser(userId, policy)) {
|
||||||
|
return { userMessage, injected: false, messageCount: 0 };
|
||||||
|
}
|
||||||
|
const context = buildFreshSessionContext(conversation);
|
||||||
|
if (!context) {
|
||||||
|
return { userMessage, injected: false, messageCount: 0 };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
userMessage: appendFreshSessionContext(userMessage, conversation),
|
||||||
|
injected: true,
|
||||||
|
messageCount: Array.isArray(conversation) ? conversation.length : 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const FRESH_SESSION_RECOVERY_CODES = new Set([
|
const FRESH_SESSION_RECOVERY_CODES = new Set([
|
||||||
'SESSION_TOOL_HISTORY_FRESH_SESSION_REQUIRED',
|
'SESSION_TOOL_HISTORY_FRESH_SESSION_REQUIRED',
|
||||||
'SESSION_REASONING_CONTENT_POISONED',
|
'SESSION_REASONING_CONTENT_POISONED',
|
||||||
@@ -871,9 +895,12 @@ export function createAgentRunGateway({
|
|||||||
),
|
),
|
||||||
goalRunService = null,
|
goalRunService = null,
|
||||||
workerIdentity = null,
|
workerIdentity = null,
|
||||||
|
directEscalationContextPolicy = null,
|
||||||
}) {
|
}) {
|
||||||
const worker = normalizeAgentRunWorkerIdentity(workerIdentity ?? {});
|
const worker = normalizeAgentRunWorkerIdentity(workerIdentity ?? {});
|
||||||
const sessionStore = resolveSessionAccess({ userAuth, sessionAccess });
|
const sessionStore = resolveSessionAccess({ userAuth, sessionAccess });
|
||||||
|
const escalationContextPolicy = directEscalationContextPolicy
|
||||||
|
?? resolveDirectEscalationContextPolicy();
|
||||||
const inFlight = new Set();
|
const inFlight = new Set();
|
||||||
const queuedDispatches = [];
|
const queuedDispatches = [];
|
||||||
const queuedDispatchSet = new Set();
|
const queuedDispatchSet = new Set();
|
||||||
@@ -2044,6 +2071,7 @@ export function createAgentRunGateway({
|
|||||||
forceDeepReasoning: runOptions.forceDeepReasoning,
|
forceDeepReasoning: runOptions.forceDeepReasoning,
|
||||||
});
|
});
|
||||||
let escalatedDirectSessionId = null;
|
let escalatedDirectSessionId = null;
|
||||||
|
let escalationContextMessages = null;
|
||||||
if (isDirectChatSessionId(sessionId)) {
|
if (isDirectChatSessionId(sessionId)) {
|
||||||
escalatedDirectSessionId = sessionId;
|
escalatedDirectSessionId = sessionId;
|
||||||
await appendEvent(runId, 'direct_session_escalated_to_deep_reasoning', {
|
await appendEvent(runId, 'direct_session_escalated_to_deep_reasoning', {
|
||||||
@@ -2069,7 +2097,7 @@ export function createAgentRunGateway({
|
|||||||
});
|
});
|
||||||
await appendRunSnapshot(runId);
|
await appendRunSnapshot(runId);
|
||||||
if (escalatedDirectSessionId) {
|
if (escalatedDirectSessionId) {
|
||||||
const priorMessages = await loadSnapshotMessages(
|
escalationContextMessages = await loadSnapshotMessages(
|
||||||
sessionSnapshotService,
|
sessionSnapshotService,
|
||||||
escalatedDirectSessionId,
|
escalatedDirectSessionId,
|
||||||
);
|
);
|
||||||
@@ -2077,7 +2105,7 @@ export function createAgentRunGateway({
|
|||||||
conversationMemoryService,
|
conversationMemoryService,
|
||||||
sessionId,
|
sessionId,
|
||||||
userId: row.user_id,
|
userId: row.user_id,
|
||||||
messages: priorMessages,
|
messages: escalationContextMessages,
|
||||||
});
|
});
|
||||||
if (persisted.saved > 0) {
|
if (persisted.saved > 0) {
|
||||||
await appendEvent(runId, 'direct_session_transcript_persisted', {
|
await appendEvent(runId, 'direct_session_transcript_persisted', {
|
||||||
@@ -2102,6 +2130,30 @@ export function createAgentRunGateway({
|
|||||||
sessionId,
|
sessionId,
|
||||||
saved: transcriptPersisted.saved,
|
saved: transcriptPersisted.saved,
|
||||||
});
|
});
|
||||||
|
if (!escalationContextMessages?.length) {
|
||||||
|
escalationContextMessages = await loadSnapshotMessages(
|
||||||
|
sessionSnapshotService,
|
||||||
|
escalatedDirectSessionId ?? sessionId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (escalationContextMessages?.length) {
|
||||||
|
const escalationContext = maybeInjectDirectEscalationContext({
|
||||||
|
userMessage,
|
||||||
|
conversation: escalationContextMessages,
|
||||||
|
userId: row.user_id,
|
||||||
|
policy: escalationContextPolicy,
|
||||||
|
});
|
||||||
|
if (escalationContext.injected) {
|
||||||
|
userMessage = escalationContext.userMessage;
|
||||||
|
await appendEvent(runId, 'direct_escalation_context_injected', {
|
||||||
|
sessionId,
|
||||||
|
previousSessionId: escalatedDirectSessionId ?? null,
|
||||||
|
messageCount: escalationContext.messageCount,
|
||||||
|
retainedContextMessages: 16,
|
||||||
|
retainedContextChars: 12_000,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (typeof tkmindProxy.compactSessionConversationForUser === 'function') {
|
if (typeof tkmindProxy.compactSessionConversationForUser === 'function') {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -2737,6 +2737,67 @@ test('agent run persists direct session transcript before escalating to goosed',
|
|||||||
assert.equal(removed[0], 'deep-session-1');
|
assert.equal(removed[0], 'deep-session-1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('agent run injects direct escalation context when feature flag is enabled', async () => {
|
||||||
|
const pool = createFakePool({
|
||||||
|
sessionDeliverables: {
|
||||||
|
'user-1:deep-session-1': [{
|
||||||
|
page_id: 'page-deep-context',
|
||||||
|
title: '深度页面',
|
||||||
|
publication_id: 'pub-deep-context',
|
||||||
|
publication_status: 'online',
|
||||||
|
public_url: 'http://127.0.0.1:5173/MindSpace/user-1/public/a.html',
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const submitted = [];
|
||||||
|
const gateway = createAgentRunGateway({
|
||||||
|
pool,
|
||||||
|
tkmindProxy: {
|
||||||
|
async startSessionForUser(userId) {
|
||||||
|
assert.equal(userId, 'user-1');
|
||||||
|
return { id: 'deep-session-1' };
|
||||||
|
},
|
||||||
|
async submitSessionReplyForUser(userId, sessionId, requestId, userMessage, options = {}) {
|
||||||
|
submitted.push({ userId, sessionId, requestId, userMessage, options });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sessionSnapshotService: {
|
||||||
|
async get(sessionId) {
|
||||||
|
if (sessionId !== 'h5direct_existing') return null;
|
||||||
|
return {
|
||||||
|
messages: [
|
||||||
|
{ role: 'user', content: [{ type: 'text', text: '中考政策' }] },
|
||||||
|
{ role: 'assistant', content: [{ type: 'text', text: '政策摘要' }] },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async remove() {},
|
||||||
|
},
|
||||||
|
conversationMemoryService: {
|
||||||
|
async saveConversationMessages(_sessionId, _userId, messages) {
|
||||||
|
return messages;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
directEscalationContextPolicy: {
|
||||||
|
enabled: true,
|
||||||
|
canaryUserIds: new Set(),
|
||||||
|
},
|
||||||
|
retryDelaysMs: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const run = await gateway.createRun('user-1', {
|
||||||
|
sessionId: 'h5direct_existing',
|
||||||
|
requestId: 'req-force-deep-context',
|
||||||
|
userMessage: { role: 'user', content: [{ type: 'text', text: '帮我生成页面 public/a.html' }] },
|
||||||
|
forceDeepReasoning: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => pool.runs.get(run.id)?.status === 'succeeded');
|
||||||
|
assert.ok(pool.events.some((event) => event.eventType === 'direct_escalation_context_injected'));
|
||||||
|
assert.match(submitted[0].userMessage.content[0].text, /会话恢复上下文/);
|
||||||
|
assert.match(submitted[0].userMessage.content[0].text, /中考政策/);
|
||||||
|
});
|
||||||
|
|
||||||
test('agent run rejects reused goosed session when broker ownership check fails', async () => {
|
test('agent run rejects reused goosed session when broker ownership check fails', async () => {
|
||||||
const pool = createFakePool();
|
const pool = createFakePool();
|
||||||
const gateway = createAgentRunGateway({
|
const gateway = createAgentRunGateway({
|
||||||
|
|||||||
@@ -22,7 +22,17 @@ import {
|
|||||||
} from './conversation-display.mjs';
|
} from './conversation-display.mjs';
|
||||||
import { isGoalRunIntent } from './goal-run-intent.mjs';
|
import { isGoalRunIntent } from './goal-run-intent.mjs';
|
||||||
import { pgvectorMemoryBackendInternals } from './memory-v2-pgvector.mjs';
|
import { pgvectorMemoryBackendInternals } from './memory-v2-pgvector.mjs';
|
||||||
|
import {
|
||||||
|
isChatSessionDeferEnabledForUser,
|
||||||
|
resolveChatSessionDeferPolicy,
|
||||||
|
shouldDeferChatSessionRoutingToLlm,
|
||||||
|
} from './chat-task-intent-config.mjs';
|
||||||
|
|
||||||
|
export { shouldDeferChatSessionRoutingToLlm } from './chat-task-intent-config.mjs';
|
||||||
|
export {
|
||||||
|
resolveChatSessionDeferPolicy,
|
||||||
|
isChatSessionDeferEnabledForUser,
|
||||||
|
} from './chat-task-intent-config.mjs';
|
||||||
export { matchDirectChatFaqRule, DIRECT_CHAT_FAQ_RULES, FAQ_EXCLUSION_PATTERNS, isExplicitTextOnlyRequest } from './chat-intent-router-rules.mjs';
|
export { matchDirectChatFaqRule, DIRECT_CHAT_FAQ_RULES, FAQ_EXCLUSION_PATTERNS, isExplicitTextOnlyRequest } from './chat-intent-router-rules.mjs';
|
||||||
|
|
||||||
export const CHAT_INTENT_ROUTE = {
|
export const CHAT_INTENT_ROUTE = {
|
||||||
@@ -1208,6 +1218,8 @@ export function classifyWithRules({
|
|||||||
includeIntentPatterns = true,
|
includeIntentPatterns = true,
|
||||||
activeTaskContext = null,
|
activeTaskContext = null,
|
||||||
grantedSkills = [],
|
grantedSkills = [],
|
||||||
|
chatSessionDeferEnabled = false,
|
||||||
|
chatSessionDeferMinTextLength = 12,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const decisionContext = {
|
const decisionContext = {
|
||||||
text,
|
text,
|
||||||
@@ -1363,6 +1375,14 @@ export function classifyWithRules({
|
|||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (shouldDeferChatSessionRoutingToLlm({
|
||||||
|
enabled: chatSessionDeferEnabled,
|
||||||
|
text: normalized,
|
||||||
|
minTextLength: chatSessionDeferMinTextLength,
|
||||||
|
isExplicitDirectChatOnlyText,
|
||||||
|
})) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return finalizeRouterClassification(normalizeClassification({
|
return finalizeRouterClassification(normalizeClassification({
|
||||||
route: CHAT_INTENT_ROUTE.DIRECT_CHAT,
|
route: CHAT_INTENT_ROUTE.DIRECT_CHAT,
|
||||||
confidence: 0.72,
|
confidence: 0.72,
|
||||||
@@ -1398,6 +1418,8 @@ export function createChatIntentRouter(options = {}) {
|
|||||||
'fallbackRoute',
|
'fallbackRoute',
|
||||||
]),
|
]),
|
||||||
});
|
});
|
||||||
|
const chatSessionDeferPolicy = options.chatSessionDeferPolicy
|
||||||
|
?? resolveChatSessionDeferPolicy(env);
|
||||||
|
|
||||||
function isLlmRouterEligibleForUser(userId) {
|
function isLlmRouterEligibleForUser(userId) {
|
||||||
return isChatLlmRouterEligible({
|
return isChatLlmRouterEligible({
|
||||||
@@ -1723,6 +1745,7 @@ export function createChatIntentRouter(options = {}) {
|
|||||||
}, decisionContext);
|
}, decisionContext);
|
||||||
};
|
};
|
||||||
const llmRouterEligible = isLlmRouterEligibleForUser(userId);
|
const llmRouterEligible = isLlmRouterEligibleForUser(userId);
|
||||||
|
const chatSessionDeferEnabled = isChatSessionDeferEnabledForUser(userId, chatSessionDeferPolicy);
|
||||||
const ruleResult = classifyWithRules({
|
const ruleResult = classifyWithRules({
|
||||||
text,
|
text,
|
||||||
forceDeepReasoning,
|
forceDeepReasoning,
|
||||||
@@ -1733,6 +1756,8 @@ export function createChatIntentRouter(options = {}) {
|
|||||||
includeIntentPatterns: true,
|
includeIntentPatterns: true,
|
||||||
activeTaskContext,
|
activeTaskContext,
|
||||||
grantedSkills,
|
grantedSkills,
|
||||||
|
chatSessionDeferEnabled,
|
||||||
|
chatSessionDeferMinTextLength: chatSessionDeferPolicy.minTextLength,
|
||||||
});
|
});
|
||||||
if (ruleResult) return finalizeWithCoercion(ruleResult);
|
if (ruleResult) return finalizeWithCoercion(ruleResult);
|
||||||
|
|
||||||
|
|||||||
@@ -1054,6 +1054,27 @@ test('classifyWithRules routes memory recall to direct chat on fresh session', (
|
|||||||
assert.equal(lastConversation.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
|
assert.equal(lastConversation.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('classifyWithRules defers ambiguous chat text to llm when chat session defer is enabled', () => {
|
||||||
|
const result = classifyWithRules({
|
||||||
|
text: '帮我比较一下英得尔和美的车载冰箱',
|
||||||
|
sessionId: 'h5direct_abc',
|
||||||
|
sessionMessageCount: 3,
|
||||||
|
chatSessionDeferEnabled: true,
|
||||||
|
chatSessionDeferMinTextLength: 12,
|
||||||
|
});
|
||||||
|
assert.equal(result, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('classifyWithRules keeps memory recall on direct chat when defer is enabled', () => {
|
||||||
|
const result = classifyWithRules({
|
||||||
|
text: '你记得我说想去哪儿吗',
|
||||||
|
sessionId: 'h5direct_abc',
|
||||||
|
sessionMessageCount: 3,
|
||||||
|
chatSessionDeferEnabled: true,
|
||||||
|
});
|
||||||
|
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
|
||||||
|
});
|
||||||
|
|
||||||
test('createChatIntentRouter fast-paths news lookup to agent without LLM', async () => {
|
test('createChatIntentRouter fast-paths news lookup to agent without LLM', async () => {
|
||||||
let llmCalls = 0;
|
let llmCalls = 0;
|
||||||
const router = createChatIntentRouter({
|
const router = createChatIntentRouter({
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
function envFlag(value, fallback = false) {
|
||||||
|
const raw = String(value ?? '').trim().toLowerCase();
|
||||||
|
if (!raw) return fallback;
|
||||||
|
return ['1', 'true', 'yes', 'on'].includes(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseUserIdSet(raw) {
|
||||||
|
return new Set(
|
||||||
|
String(raw ?? '')
|
||||||
|
.split(/[,;\s]+/)
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function boundedNumber(value, fallback, { min = 0, max = Number.MAX_SAFE_INTEGER } = {}) {
|
||||||
|
const parsed = Number(value);
|
||||||
|
if (!Number.isFinite(parsed)) return fallback;
|
||||||
|
return Math.min(max, Math.max(min, parsed));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveDirectEscalationContextPolicy(env = process.env) {
|
||||||
|
return {
|
||||||
|
enabled: envFlag(env?.MEMIND_DIRECT_ESCALATION_CONTEXT_ENABLED, false),
|
||||||
|
canaryUserIds: parseUserIdSet(env?.MEMIND_DIRECT_ESCALATION_CONTEXT_CANARY_USER_IDS),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDirectEscalationContextEnabledForUser(userId, policy) {
|
||||||
|
if (!policy?.enabled) return false;
|
||||||
|
if (!policy.canaryUserIds?.size) return true;
|
||||||
|
return policy.canaryUserIds.has(String(userId ?? '').trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveChatSessionDeferPolicy(env = process.env) {
|
||||||
|
return {
|
||||||
|
enabled: envFlag(env?.MEMIND_CHAT_ROUTER_CHAT_SESSION_DEFER_ENABLED, false),
|
||||||
|
canaryUserIds: parseUserIdSet(env?.MEMIND_CHAT_ROUTER_CHAT_SESSION_DEFER_CANARY_USER_IDS),
|
||||||
|
minTextLength: Math.round(boundedNumber(
|
||||||
|
env?.MEMIND_CHAT_ROUTER_CHAT_SESSION_DEFER_MIN_TEXT_LENGTH,
|
||||||
|
12,
|
||||||
|
{ min: 4, max: 200 },
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isChatSessionDeferEnabledForUser(userId, policy) {
|
||||||
|
if (!policy?.enabled) return false;
|
||||||
|
if (!policy.canaryUserIds?.size) return true;
|
||||||
|
return policy.canaryUserIds.has(String(userId ?? '').trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldDeferChatSessionRoutingToLlm({
|
||||||
|
enabled = false,
|
||||||
|
text = '',
|
||||||
|
minTextLength = 12,
|
||||||
|
isExplicitDirectChatOnlyText = () => false,
|
||||||
|
} = {}) {
|
||||||
|
if (!enabled) return false;
|
||||||
|
const normalized = String(text ?? '').trim();
|
||||||
|
if (!normalized || isExplicitDirectChatOnlyText(normalized)) return false;
|
||||||
|
return normalized.length >= Math.max(1, Number(minTextLength) || 12);
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import {
|
||||||
|
isChatSessionDeferEnabledForUser,
|
||||||
|
isDirectEscalationContextEnabledForUser,
|
||||||
|
resolveChatSessionDeferPolicy,
|
||||||
|
resolveDirectEscalationContextPolicy,
|
||||||
|
shouldDeferChatSessionRoutingToLlm,
|
||||||
|
} from './chat-task-intent-config.mjs';
|
||||||
|
|
||||||
|
test('resolveDirectEscalationContextPolicy defaults to disabled', () => {
|
||||||
|
const policy = resolveDirectEscalationContextPolicy({});
|
||||||
|
assert.equal(policy.enabled, false);
|
||||||
|
assert.equal(policy.canaryUserIds.size, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isDirectEscalationContextEnabledForUser respects canary list', () => {
|
||||||
|
const policy = {
|
||||||
|
enabled: true,
|
||||||
|
canaryUserIds: new Set(['user-a']),
|
||||||
|
};
|
||||||
|
assert.equal(isDirectEscalationContextEnabledForUser('user-a', policy), true);
|
||||||
|
assert.equal(isDirectEscalationContextEnabledForUser('user-b', policy), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shouldDeferChatSessionRoutingToLlm skips explicit direct chat text', () => {
|
||||||
|
assert.equal(shouldDeferChatSessionRoutingToLlm({
|
||||||
|
enabled: true,
|
||||||
|
text: '你好',
|
||||||
|
isExplicitDirectChatOnlyText: (value) => value === '你好',
|
||||||
|
}), false);
|
||||||
|
assert.equal(shouldDeferChatSessionRoutingToLlm({
|
||||||
|
enabled: true,
|
||||||
|
text: '帮我研究一下车载冰箱的参数和选购建议',
|
||||||
|
minTextLength: 12,
|
||||||
|
isExplicitDirectChatOnlyText: () => false,
|
||||||
|
}), true);
|
||||||
|
assert.equal(shouldDeferChatSessionRoutingToLlm({
|
||||||
|
enabled: false,
|
||||||
|
text: '帮我研究一下车载冰箱的参数和选购建议',
|
||||||
|
isExplicitDirectChatOnlyText: () => false,
|
||||||
|
}), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolveChatSessionDeferPolicy parses min text length', () => {
|
||||||
|
const policy = resolveChatSessionDeferPolicy({
|
||||||
|
MEMIND_CHAT_ROUTER_CHAT_SESSION_DEFER_ENABLED: '1',
|
||||||
|
MEMIND_CHAT_ROUTER_CHAT_SESSION_DEFER_MIN_TEXT_LENGTH: '20',
|
||||||
|
MEMIND_CHAT_ROUTER_CHAT_SESSION_DEFER_CANARY_USER_IDS: 'john',
|
||||||
|
});
|
||||||
|
assert.equal(policy.enabled, true);
|
||||||
|
assert.equal(policy.minTextLength, 20);
|
||||||
|
assert.deepEqual([...policy.canaryUserIds], ['john']);
|
||||||
|
assert.equal(isChatSessionDeferEnabledForUser('john', policy), true);
|
||||||
|
assert.equal(isChatSessionDeferEnabledForUser('other', policy), false);
|
||||||
|
});
|
||||||
@@ -138,7 +138,7 @@ function buildMemorySystemBlock({ memories, routingMemoryContent } = {}) {
|
|||||||
function buildModelMessages({ previousMessages, userMessage, memories, routingMemoryContent }) {
|
function buildModelMessages({ previousMessages, userMessage, memories, routingMemoryContent }) {
|
||||||
const system = [
|
const system = [
|
||||||
'你是 TKMind H5 聊天助手。',
|
'你是 TKMind H5 聊天助手。',
|
||||||
'优先直接回答用户问题;不要调用工具;涉及需要执行代码、改文件、生成页面或操作外部系统的任务时,简要说明已交由后台任务处理或请用户确认具体任务。',
|
'优先直接回答用户问题;不要调用工具。涉及需要执行代码、改文件、生成页面或操作外部系统的任务时,说明当前只能文字讨论,并提示用户开启「深度推理」或用更明确的任务描述。',
|
||||||
buildMemorySystemBlock({ memories, routingMemoryContent }),
|
buildMemorySystemBlock({ memories, routingMemoryContent }),
|
||||||
].filter(Boolean).join('\n\n');
|
].filter(Boolean).join('\n\n');
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,364 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Local acceptance for chat-task-intent-layer (docs/architecture/chat-task-intent-layer-review-20260828.md).
|
||||||
|
*
|
||||||
|
* Phase B: direct→agent escalation injects snapshot context (direct_escalation_context_injected).
|
||||||
|
* Phase C: optional shadow observation when chat session defer + LLM router shadow are enabled.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* MEMIND_DIRECT_ESCALATION_CONTEXT_ENABLED=1 node scripts/verify-chat-task-intent-layer.mjs
|
||||||
|
*
|
||||||
|
* Requires Portal on 8081, agent-run-worker, goosed, and DB from .env.
|
||||||
|
*/
|
||||||
|
import crypto from 'node:crypto';
|
||||||
|
import { loadH5Environment } from './load-env.mjs';
|
||||||
|
import { createDbPool } from '../db.mjs';
|
||||||
|
import { createUserAuth, USER_COOKIE } from '../user-auth.mjs';
|
||||||
|
import {
|
||||||
|
CHAT_INTENT_ROUTE,
|
||||||
|
classifyWithRules,
|
||||||
|
} from '../chat-intent-router.mjs';
|
||||||
|
import {
|
||||||
|
isDirectEscalationContextEnabledForUser,
|
||||||
|
resolveDirectEscalationContextPolicy,
|
||||||
|
resolveChatSessionDeferPolicy,
|
||||||
|
shouldDeferChatSessionRoutingToLlm,
|
||||||
|
} from '../chat-task-intent-config.mjs';
|
||||||
|
|
||||||
|
loadH5Environment(import.meta.dirname);
|
||||||
|
|
||||||
|
const PORTAL = `http://127.0.0.1:${process.env.H5_PORT ?? 8081}`;
|
||||||
|
const USERNAME = process.env.MEMIND_E2E_USERNAME ?? process.env.VERIFY_LLM_ROUTER_USER ?? 'john';
|
||||||
|
const PASSWORD = process.env.JOHN_PASSWORD ?? process.env.H5_ACCESS_PASSWORD ?? '888888';
|
||||||
|
const DIRECT_TURN_MESSAGE = '我想了解车载冰箱,MPV 用,宽度不超过 60cm,先聊聊选购要点';
|
||||||
|
const AGENT_TURN_MESSAGE = '帮我生成对比页面 public/fridge-compare-test.html';
|
||||||
|
const MAX_RUN_WAIT_MS = Number(process.env.E2E_RUN_WAIT_MS ?? 300_000);
|
||||||
|
const MAX_REPLY_WAIT_MS = 120_000;
|
||||||
|
|
||||||
|
const issues = [];
|
||||||
|
const checks = [];
|
||||||
|
|
||||||
|
function pass(label, detail = '') {
|
||||||
|
checks.push({ ok: true, label, detail });
|
||||||
|
console.log(`✔ ${label}${detail ? `: ${detail}` : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fail(label, detail = '') {
|
||||||
|
issues.push({ label, detail });
|
||||||
|
checks.push({ ok: false, label, detail });
|
||||||
|
console.error(`✘ ${label}${detail ? `: ${detail}` : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseEventData(raw) {
|
||||||
|
if (!raw) return null;
|
||||||
|
if (typeof raw === 'object') return raw;
|
||||||
|
try {
|
||||||
|
return JSON.parse(String(raw));
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loginViaApi() {
|
||||||
|
const response = await fetch(`${PORTAL}/auth/login`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username: USERNAME, password: PASSWORD }),
|
||||||
|
});
|
||||||
|
const body = await response.json().catch(() => ({}));
|
||||||
|
if (response.ok && body?.authenticated) {
|
||||||
|
const setCookie = response.headers.getSetCookie?.() ?? [];
|
||||||
|
const cookieLine = setCookie.find((line) => line.startsWith(`${USER_COOKIE}=`))
|
||||||
|
?? response.headers.get('set-cookie');
|
||||||
|
const match = String(cookieLine ?? '').match(new RegExp(`${USER_COOKIE}=([^;]+)`));
|
||||||
|
if (match?.[1]) {
|
||||||
|
return {
|
||||||
|
token: decodeURIComponent(match[1]),
|
||||||
|
userId: body.user?.id ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pool = await createDbPool();
|
||||||
|
const auth = createUserAuth(pool);
|
||||||
|
const result = await auth.login({ username: USERNAME, password: PASSWORD, ip: '127.0.0.1' });
|
||||||
|
await pool.end();
|
||||||
|
if (!result.ok || !result.token) {
|
||||||
|
throw new Error(`登录失败: ${result.message ?? 'unknown'}`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
token: result.token,
|
||||||
|
userId: result.user?.id ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createRun(token, { message, sessionId = null, forceDeepReasoning = false } = {}) {
|
||||||
|
const requestId = crypto.randomUUID();
|
||||||
|
const response = await fetch(`${PORTAL}/api/agent/runs`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Cookie: `${USER_COOKIE}=${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
request_id: requestId,
|
||||||
|
session_id: sessionId,
|
||||||
|
force_deep_reasoning: forceDeepReasoning,
|
||||||
|
user_message: {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
role: 'user',
|
||||||
|
content: [{ type: 'text', text: message }],
|
||||||
|
metadata: { displayText: message, userVisible: true },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const payload = await response.json().catch(() => ({}));
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`POST /api/agent/runs ${response.status}: ${JSON.stringify(payload)}`);
|
||||||
|
}
|
||||||
|
const run = payload.run ?? payload;
|
||||||
|
return {
|
||||||
|
runId: run.id,
|
||||||
|
sessionId: resolveRunSessionId(run, sessionId),
|
||||||
|
status: run.status,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRunSessionId(run, fallbackSessionId = null) {
|
||||||
|
return run?.sessionId ?? run?.agent_session_id ?? fallbackSessionId ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRun(token, runId) {
|
||||||
|
const response = await fetch(`${PORTAL}/api/agent/runs/${runId}`, {
|
||||||
|
headers: { Cookie: `${USER_COOKIE}=${token}` },
|
||||||
|
});
|
||||||
|
const payload = await response.json().catch(() => ({}));
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`GET run ${response.status}: ${JSON.stringify(payload)}`);
|
||||||
|
}
|
||||||
|
return payload.run ?? payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readRunEvents(runId) {
|
||||||
|
const pool = await createDbPool();
|
||||||
|
const [rows] = await pool.query(
|
||||||
|
`SELECT event_type, data_json, created_at
|
||||||
|
FROM h5_agent_run_events
|
||||||
|
WHERE run_id = ?
|
||||||
|
ORDER BY created_at ASC`,
|
||||||
|
[runId],
|
||||||
|
);
|
||||||
|
await pool.end();
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForRunTerminal(token, runId) {
|
||||||
|
const started = Date.now();
|
||||||
|
let last = null;
|
||||||
|
while (Date.now() - started < MAX_RUN_WAIT_MS) {
|
||||||
|
last = await getRun(token, runId);
|
||||||
|
if (['succeeded', 'failed'].includes(last.status)) return last;
|
||||||
|
await sleep(2000);
|
||||||
|
}
|
||||||
|
throw new Error(`run ${runId} 超时,最后状态 ${last?.status ?? 'unknown'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForDirectReply(token, sessionId) {
|
||||||
|
const started = Date.now();
|
||||||
|
while (Date.now() - started < MAX_REPLY_WAIT_MS) {
|
||||||
|
const response = await fetch(`${PORTAL}/api/sessions/${encodeURIComponent(sessionId)}`, {
|
||||||
|
headers: { Cookie: `${USER_COOKIE}=${token}` },
|
||||||
|
});
|
||||||
|
const payload = await response.json().catch(() => ({}));
|
||||||
|
if (response.ok) {
|
||||||
|
const messages = payload?.messages ?? payload?.conversation ?? [];
|
||||||
|
const assistant = [...messages].reverse().find((item) =>
|
||||||
|
item?.role === 'assistant' && item?.metadata?.source === 'portal-direct-chat');
|
||||||
|
if (assistant) {
|
||||||
|
const text = Array.isArray(assistant.content)
|
||||||
|
? assistant.content.map((part) => part?.text ?? '').join('')
|
||||||
|
: String(assistant.content ?? '');
|
||||||
|
if (text.trim().length > 10) {
|
||||||
|
return text.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await sleep(1500);
|
||||||
|
}
|
||||||
|
throw new Error(`direct chat 回复超时 session=${sessionId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyStaticGates(userId) {
|
||||||
|
const escalationPolicy = resolveDirectEscalationContextPolicy(process.env);
|
||||||
|
if (!escalationPolicy.enabled) {
|
||||||
|
fail('阶段 B flag', 'MEMIND_DIRECT_ESCALATION_CONTEXT_ENABLED 未开启,E2E 无法验收注入');
|
||||||
|
} else if (!isDirectEscalationContextEnabledForUser(userId, escalationPolicy)) {
|
||||||
|
fail('阶段 B canary', `用户 ${userId} 不在 MEMIND_DIRECT_ESCALATION_CONTEXT_CANARY_USER_IDS`);
|
||||||
|
} else {
|
||||||
|
pass('阶段 B flag', 'direct escalation context enabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
const deferPolicy = resolveChatSessionDeferPolicy(process.env);
|
||||||
|
const deferSample = classifyWithRules({
|
||||||
|
text: '帮我比较一下英得尔和美的车载冰箱参数',
|
||||||
|
sessionId: 'h5direct_verify',
|
||||||
|
sessionMessageCount: 2,
|
||||||
|
chatSessionDeferEnabled: deferPolicy.enabled,
|
||||||
|
chatSessionDeferMinTextLength: deferPolicy.minTextLength,
|
||||||
|
});
|
||||||
|
if (deferPolicy.enabled) {
|
||||||
|
if (deferSample !== null) {
|
||||||
|
fail('阶段 C defer 规则', '歧义文本应返回 null 交给 LLM router');
|
||||||
|
} else {
|
||||||
|
pass('阶段 C defer 规则', '歧义文本已 defer');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pass('阶段 C defer 规则', 'flag 未开,跳过(仅静态检查 defer 函数)');
|
||||||
|
if (!shouldDeferChatSessionRoutingToLlm({
|
||||||
|
enabled: true,
|
||||||
|
text: '帮我比较一下英得尔和美的车载冰箱参数',
|
||||||
|
minTextLength: 12,
|
||||||
|
isExplicitDirectChatOnlyText: () => false,
|
||||||
|
})) {
|
||||||
|
fail('阶段 C defer 函数', 'shouldDeferChatSessionRoutingToLlm 预期 true');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const recall = classifyWithRules({
|
||||||
|
text: '你记得我说想去哪儿吗',
|
||||||
|
sessionId: 'h5direct_verify',
|
||||||
|
chatSessionDeferEnabled: true,
|
||||||
|
});
|
||||||
|
if (recall?.route !== CHAT_INTENT_ROUTE.DIRECT_CHAT) {
|
||||||
|
fail('记忆召回硬规则', `期望 direct_chat,实际 ${recall?.route ?? 'null'}`);
|
||||||
|
} else {
|
||||||
|
pass('记忆召回硬规则', recall.reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const health = await fetch(`${PORTAL}/auth/status`).catch(() => null);
|
||||||
|
if (!health?.ok) {
|
||||||
|
throw new Error(`Portal 不可用: ${PORTAL}/auth/status`);
|
||||||
|
}
|
||||||
|
pass('Portal 健康', PORTAL);
|
||||||
|
|
||||||
|
const { token, userId } = await loginViaApi();
|
||||||
|
pass('登录', `${USERNAME} (${userId ?? 'unknown-id'})`);
|
||||||
|
|
||||||
|
verifyStaticGates(userId);
|
||||||
|
|
||||||
|
const runTag = crypto.randomUUID().slice(0, 8);
|
||||||
|
const directMessage = `${DIRECT_TURN_MESSAGE} [verify-${runTag}]`;
|
||||||
|
const agentMessage = `${AGENT_TURN_MESSAGE.replace('.html', `-${runTag}.html`)}`;
|
||||||
|
|
||||||
|
const directRun = await createRun(token, { message: directMessage });
|
||||||
|
pass('direct 轮次提交', `run=${directRun.runId} session=${directRun.sessionId ?? 'pending'}`);
|
||||||
|
|
||||||
|
const directTerminal = await waitForRunTerminal(token, directRun.runId);
|
||||||
|
const directEvents = await readRunEvents(directRun.runId);
|
||||||
|
const directRouted = directEvents.find((row) => row.event_type === 'intent_routed');
|
||||||
|
const routedData = parseEventData(directRouted?.data_json);
|
||||||
|
if (routedData?.route !== CHAT_INTENT_ROUTE.DIRECT_CHAT) {
|
||||||
|
fail('direct 路由', `期望 direct_chat,实际 ${routedData?.route ?? 'missing'}`);
|
||||||
|
} else {
|
||||||
|
pass('direct 路由', routedData.reason ?? 'direct_chat');
|
||||||
|
}
|
||||||
|
if (directTerminal.status !== 'succeeded') {
|
||||||
|
fail('direct run 终态', directTerminal.status);
|
||||||
|
} else {
|
||||||
|
pass('direct run 终态', 'succeeded');
|
||||||
|
}
|
||||||
|
|
||||||
|
let sessionId = resolveRunSessionId(directTerminal, directRun.sessionId);
|
||||||
|
if (!sessionId) {
|
||||||
|
const directEventsEarly = await readRunEvents(directRun.runId);
|
||||||
|
const completed = directEventsEarly.find((row) => row.event_type === 'direct_chat_completed');
|
||||||
|
sessionId = parseEventData(completed?.data_json)?.sessionId ?? null;
|
||||||
|
}
|
||||||
|
if (!sessionId) {
|
||||||
|
fail('direct session', '未获得 sessionId');
|
||||||
|
} else {
|
||||||
|
pass('direct session', sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
let directReply = '';
|
||||||
|
try {
|
||||||
|
directReply = await waitForDirectReply(token, sessionId);
|
||||||
|
pass('direct 回复', `${directReply.length} chars`);
|
||||||
|
if (/已交由后台任务处理/.test(directReply)) {
|
||||||
|
fail('direct 文案', '仍包含「已交由后台任务处理」');
|
||||||
|
} else {
|
||||||
|
pass('direct 文案', '未谎报后台执行');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
fail('direct 回复', err instanceof Error ? err.message : String(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
const agentRun = await createRun(token, {
|
||||||
|
message: agentMessage,
|
||||||
|
sessionId,
|
||||||
|
forceDeepReasoning: true,
|
||||||
|
});
|
||||||
|
pass('agent 升级提交', `run=${agentRun.runId}`);
|
||||||
|
|
||||||
|
const agentTerminal = await waitForRunTerminal(token, agentRun.runId);
|
||||||
|
const agentEvents = await readRunEvents(agentRun.runId);
|
||||||
|
if (agentTerminal.status !== 'succeeded') {
|
||||||
|
fail('agent run 终态', `${agentTerminal.status} ${agentTerminal.error_message ?? ''}`);
|
||||||
|
} else {
|
||||||
|
pass('agent run 终态', 'succeeded');
|
||||||
|
}
|
||||||
|
|
||||||
|
const escalated = agentEvents.some((row) => row.event_type === 'direct_session_escalated_to_deep_reasoning');
|
||||||
|
if (escalated) {
|
||||||
|
pass('升级事件', 'direct_session_escalated_to_deep_reasoning');
|
||||||
|
} else {
|
||||||
|
pass('升级事件', '同 session agent(非 h5direct 升级路径)');
|
||||||
|
}
|
||||||
|
|
||||||
|
const contextInjected = agentEvents.find((row) => row.event_type === 'direct_escalation_context_injected');
|
||||||
|
if (resolveDirectEscalationContextPolicy(process.env).enabled) {
|
||||||
|
if (!contextInjected) {
|
||||||
|
fail('阶段 B 注入', '缺少 direct_escalation_context_injected 事件');
|
||||||
|
} else {
|
||||||
|
const data = parseEventData(contextInjected.data_json);
|
||||||
|
pass('阶段 B 注入', `messages=${data?.messageCount ?? '?'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const transcriptPersisted = agentEvents.some((row) =>
|
||||||
|
row.event_type === 'direct_session_transcript_persisted'
|
||||||
|
|| row.event_type === 'portal_direct_transcript_persisted');
|
||||||
|
if (transcriptPersisted) {
|
||||||
|
pass('transcript 持久化', 'ok');
|
||||||
|
} else {
|
||||||
|
fail('transcript 持久化', '未观察到 transcript 事件');
|
||||||
|
}
|
||||||
|
|
||||||
|
const shadowEvent = agentEvents.find((row) => row.event_type === 'intent_routed');
|
||||||
|
const shadowData = parseEventData(shadowEvent?.data_json);
|
||||||
|
if (shadowData?.llmShadow?.wouldChangeRoute != null) {
|
||||||
|
pass('阶段 C shadow', `wouldChangeRoute=${shadowData.llmShadow.wouldChangeRoute}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n=== 汇总 ===');
|
||||||
|
console.log(`通过: ${checks.filter((item) => item.ok).length}/${checks.length}`);
|
||||||
|
if (issues.length) {
|
||||||
|
console.log('失败项:');
|
||||||
|
for (const item of issues) {
|
||||||
|
console.log(` - ${item.label}: ${item.detail}`);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log('chat-task-intent-layer 本地验收通过');
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error(err instanceof Error ? err.stack ?? err.message : err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user