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:
john
2026-07-05 07:30:37 +08:00
parent a29158d589
commit 4781bbc156
5 changed files with 79 additions and 1 deletions
+2
View File
@@ -30,6 +30,8 @@ const OBVIOUS_AGENT_PATTERNS = [
/MindSpace\/[^/\s]+\/public\/[^\s"'<>]+\.html/i,
/(?:生成|创建|制作|做|写|设计|发布).{0,24}(?:H5|h5|HTML|html|网页|页面|活动页|宣传页|落地页|分享页)/u,
/(?:H5|h5|HTML|html|网页|页面).{0,24}(?:生成|创建|制作|做|写|设计|发布)/u,
/(?:写|做|生成|制作|设计).{0,16}主题(?:页面|文章)/u,
/主题(?:页面|文章).{0,16}(?:写|做|生成|制作|设计)/u,
];
const OBVIOUS_DIRECT_PATTERNS = [
+13
View File
@@ -36,6 +36,19 @@ test('classifyWithRules routes page generation to agent orchestration', () => {
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', () => {
const result = classifyWithRules({
text: '你好',
+2
View File
@@ -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,
/(?:生成|创建|制作|做|写|设计|发布|导出|下载|保存|修改|编辑).{0,24}(?:页面|网页|HTML|html|H5|h5|文件|文档|Word|word|docx|下载页|公开页|分享页|落地页|活动页)/u,
/(?:页面|网页|HTML|html|H5|h5|文件|文档|Word|word|docx|下载页|公开页|分享页|落地页|活动页).{0,24}(?:生成|创建|制作|做|写|设计|发布|导出|下载|保存|修改|编辑)/u,
/(?:写|做|生成|制作|设计).{0,16}主题(?:页面|文章)/u,
/主题(?:页面|文章).{0,16}(?:写|做|生成|制作|设计)/u,
/MindSpace\/[^/\s]+\/public\/[^\s"'<>]+\.html/i,
];
+24
View File
@@ -205,6 +205,30 @@ test('direct chat rejects page generation tasks so the execution backend can han
}), 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', () => {
const service = createDirectChatService({
enabled: true,
+38 -1
View File
@@ -82,6 +82,18 @@ function isDirectChatSessionId(sessionId?: string | null) {
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> {
return await waitForAgentRunWithDirectChatPreview(runId, {});
}
@@ -1344,8 +1356,33 @@ export function useTKMindChat(
activeRequestId.current = null;
setChatState('idle');
} 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);
setChatState('streaming');
if (finishedViaPortalDirectChat) {
clearActiveRequestMissingTimer();
activeRequestId.current = null;
setChatState('idle');
} else {
setChatState('streaming');
}
}
} catch (err) {
if (activeSessionId && isAmbiguousReplySubmitError(err)) {