fix: stop chat hang after misrouted themed article requests
Route themed page/article prompts to agent orchestration, reject them from portal direct chat, and finalize regular sessions to idle when a direct chat turn already completed. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -30,6 +30,8 @@ const OBVIOUS_AGENT_PATTERNS = [
|
|||||||
/MindSpace\/[^/\s]+\/public\/[^\s"'<>]+\.html/i,
|
/MindSpace\/[^/\s]+\/public\/[^\s"'<>]+\.html/i,
|
||||||
/(?:生成|创建|制作|做|写|设计|发布).{0,24}(?:H5|h5|HTML|html|网页|页面|活动页|宣传页|落地页|分享页)/u,
|
/(?:生成|创建|制作|做|写|设计|发布).{0,24}(?:H5|h5|HTML|html|网页|页面|活动页|宣传页|落地页|分享页)/u,
|
||||||
/(?:H5|h5|HTML|html|网页|页面).{0,24}(?:生成|创建|制作|做|写|设计|发布)/u,
|
/(?:H5|h5|HTML|html|网页|页面).{0,24}(?:生成|创建|制作|做|写|设计|发布)/u,
|
||||||
|
/(?:写|做|生成|制作|设计).{0,16}主题(?:页面|文章)/u,
|
||||||
|
/主题(?:页面|文章).{0,16}(?:写|做|生成|制作|设计)/u,
|
||||||
];
|
];
|
||||||
|
|
||||||
const OBVIOUS_DIRECT_PATTERNS = [
|
const OBVIOUS_DIRECT_PATTERNS = [
|
||||||
|
|||||||
@@ -36,6 +36,19 @@ test('classifyWithRules routes page generation to agent orchestration', () => {
|
|||||||
assert.equal(result.suggestedSkill, 'static-page-publish');
|
assert.equal(result.suggestedSkill, '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 honors forceDeepReasoning bypass', () => {
|
test('classifyWithRules honors forceDeepReasoning bypass', () => {
|
||||||
const result = classifyWithRules({
|
const result = classifyWithRules({
|
||||||
text: '你好',
|
text: '你好',
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ const TASK_EXECUTION_REQUIRED_PATTERNS = [
|
|||||||
/\b(file|page|html|docx|word|asset)\b.{0,40}\b(write|create|generate|publish|download|export|save|edit)\b/i,
|
/\b(file|page|html|docx|word|asset)\b.{0,40}\b(write|create|generate|publish|download|export|save|edit)\b/i,
|
||||||
/(?:生成|创建|制作|做|写|设计|发布|导出|下载|保存|修改|编辑).{0,24}(?:页面|网页|HTML|html|H5|h5|文件|文档|Word|word|docx|下载页|公开页|分享页|落地页|活动页)/u,
|
/(?:生成|创建|制作|做|写|设计|发布|导出|下载|保存|修改|编辑).{0,24}(?:页面|网页|HTML|html|H5|h5|文件|文档|Word|word|docx|下载页|公开页|分享页|落地页|活动页)/u,
|
||||||
/(?:页面|网页|HTML|html|H5|h5|文件|文档|Word|word|docx|下载页|公开页|分享页|落地页|活动页).{0,24}(?:生成|创建|制作|做|写|设计|发布|导出|下载|保存|修改|编辑)/u,
|
/(?:页面|网页|HTML|html|H5|h5|文件|文档|Word|word|docx|下载页|公开页|分享页|落地页|活动页).{0,24}(?:生成|创建|制作|做|写|设计|发布|导出|下载|保存|修改|编辑)/u,
|
||||||
|
/(?:写|做|生成|制作|设计).{0,16}主题(?:页面|文章)/u,
|
||||||
|
/主题(?:页面|文章).{0,16}(?:写|做|生成|制作|设计)/u,
|
||||||
/MindSpace\/[^/\s]+\/public\/[^\s"'<>]+\.html/i,
|
/MindSpace\/[^/\s]+\/public\/[^\s"'<>]+\.html/i,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -205,6 +205,30 @@ test('direct chat rejects page generation tasks so the execution backend can han
|
|||||||
}), false);
|
}), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('direct chat rejects themed article page tasks without explicit html path', () => {
|
||||||
|
const service = createDirectChatService({
|
||||||
|
enabled: true,
|
||||||
|
userAuth: {},
|
||||||
|
llmProviderService: {},
|
||||||
|
sessionSnapshotService: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(service.canHandle({
|
||||||
|
toolMode: 'chat',
|
||||||
|
userMessage: {
|
||||||
|
role: 'user',
|
||||||
|
content: [{ type: 'text', text: '帮我写个主题文章' }],
|
||||||
|
},
|
||||||
|
}), false);
|
||||||
|
assert.equal(service.explainCanHandle({
|
||||||
|
toolMode: 'chat',
|
||||||
|
userMessage: {
|
||||||
|
role: 'user',
|
||||||
|
content: [{ type: 'text', text: '帮我写个主题文章' }],
|
||||||
|
},
|
||||||
|
}).reason, 'task_execution_required');
|
||||||
|
});
|
||||||
|
|
||||||
test('direct chat allows llm-routed replies on regular agent sessions', () => {
|
test('direct chat allows llm-routed replies on regular agent sessions', () => {
|
||||||
const service = createDirectChatService({
|
const service = createDirectChatService({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
@@ -82,6 +82,18 @@ function isDirectChatSessionId(sessionId?: string | null) {
|
|||||||
return typeof sessionId === 'string' && sessionId.startsWith(DIRECT_CHAT_SESSION_PREFIX);
|
return typeof sessionId === 'string' && sessionId.startsWith(DIRECT_CHAT_SESSION_PREFIX);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sessionFinishedViaPortalDirectChat(messages: Message[], userMessage: Message) {
|
||||||
|
const lastAssistant = [...messages].reverse().find((message) => message.role === 'assistant');
|
||||||
|
if (lastAssistant?.metadata?.source !== 'portal-direct-chat') return false;
|
||||||
|
const userText = getDisplayText(userMessage).trim();
|
||||||
|
const userId = userMessage.id;
|
||||||
|
return messages.some(
|
||||||
|
(message) =>
|
||||||
|
message.role === 'user' &&
|
||||||
|
(message.id === userId || getDisplayText(message).trim() === userText),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function waitForAgentRun(runId: string): Promise<AgentRun> {
|
async function waitForAgentRun(runId: string): Promise<AgentRun> {
|
||||||
return await waitForAgentRunWithDirectChatPreview(runId, {});
|
return await waitForAgentRunWithDirectChatPreview(runId, {});
|
||||||
}
|
}
|
||||||
@@ -1344,8 +1356,33 @@ export function useTKMindChat(
|
|||||||
activeRequestId.current = null;
|
activeRequestId.current = null;
|
||||||
setChatState('idle');
|
setChatState('idle');
|
||||||
} else {
|
} else {
|
||||||
|
let finishedViaPortalDirectChat = false;
|
||||||
|
try {
|
||||||
|
const detail = await loadSessionDetail(activeSessionId);
|
||||||
|
if (submitToken === connectTokenRef.current) {
|
||||||
|
messagesRef.current = mergeConversationSnapshot(
|
||||||
|
messagesRef.current,
|
||||||
|
detail.messages,
|
||||||
|
);
|
||||||
|
messageHistoryLoadedCountRef.current = messagesRef.current.length;
|
||||||
|
setMessages(messagesRef.current);
|
||||||
|
setSession(detail.session);
|
||||||
|
finishedViaPortalDirectChat = sessionFinishedViaPortalDirectChat(
|
||||||
|
detail.messages,
|
||||||
|
userMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall through to agent streaming when the snapshot is not ready yet.
|
||||||
|
}
|
||||||
subscribeToSession(activeSessionId);
|
subscribeToSession(activeSessionId);
|
||||||
setChatState('streaming');
|
if (finishedViaPortalDirectChat) {
|
||||||
|
clearActiveRequestMissingTimer();
|
||||||
|
activeRequestId.current = null;
|
||||||
|
setChatState('idle');
|
||||||
|
} else {
|
||||||
|
setChatState('streaming');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (activeSessionId && isAmbiguousReplySubmitError(err)) {
|
if (activeSessionId && isAmbiguousReplySubmitError(err)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user