From 412e5f149b941edf4deebea8f2277bad43ef6223 Mon Sep 17 00:00:00 2001 From: john Date: Fri, 24 Jul 2026 17:37:05 +0800 Subject: [PATCH] fix: allow page clarification turns without delivery error --- agent-run-gateway.mjs | 21 +++++++++++-- agent-run-gateway.test.mjs | 30 +++++++++++++++++++ chat-skills.mjs | 20 +++++++++++++ chat-skills.test.mjs | 9 ++++++ .../mindspace-publish-and-chat-finish.md | 25 ++++++++++++++++ 5 files changed, 103 insertions(+), 2 deletions(-) diff --git a/agent-run-gateway.mjs b/agent-run-gateway.mjs index cfc44c8..9f0136a 100644 --- a/agent-run-gateway.mjs +++ b/agent-run-gateway.mjs @@ -16,7 +16,11 @@ import { SESSION_FINISHED_STALE_GRACE_MS, tryRecoverRunFromDeliverables, } from './agent-run-deliverable-check.mjs'; -import { isPageDataIntent, isPageGenerationIntent } from './chat-skills.mjs'; +import { + isGenericPageGenerationRequest, + isPageDataIntent, + isPageGenerationIntent, +} from './chat-skills.mjs'; const DEFAULT_RUN_RETRY_DELAYS_MS = [1500, 5000, 15000]; const TERMINAL_STATUSES = new Set(['succeeded', 'failed']); @@ -66,6 +70,14 @@ function extractRunMessageText(row) { .join('\n'); } +function extractRunDisplayText(row) { + const message = parseDbJsonColumn(row?.user_message_json, {}) ?? {}; + const displayText = message?.metadata?.displayText; + return typeof displayText === 'string' && displayText.trim() + ? displayText.trim() + : extractRunMessageText(row); +} + function resolveRequiredImageGeneration(row, routing) { const message = parseDbJsonColumn(row?.user_message_json, {}) ?? {}; const metadata = message.metadata ?? {}; @@ -894,11 +906,16 @@ export function createAgentRunGateway({ throw error; } const runMessageText = extractRunMessageText(row); + const runDisplayText = extractRunDisplayText(row); const pageDataIntent = isPageDataIntent(runMessageText) || routing?.suggestedSkill === 'page-data-collect'; const pageGenerationIntent = isPageGenerationIntent(runMessageText) || routing?.suggestedSkill === 'static-page-publish'; - const requiresPageDeliverable = pageDataIntent || pageGenerationIntent; + // A bare “generate a page” request has no subject to render. The assistant's + // clarification is a successful conversational turn, not a failed delivery. + // Once the user supplies a subject, the existing fail-closed guard still applies. + const requiresPageDeliverable = pageDataIntent + || (pageGenerationIntent && !isGenericPageGenerationRequest(runDisplayText)); let deliverables = null; if (requiresPageDeliverable || typeof validateRunDeliverables === 'function') { const latest = await getRunById(runId); diff --git a/agent-run-gateway.test.mjs b/agent-run-gateway.test.mjs index d115eee..958fcde 100644 --- a/agent-run-gateway.test.mjs +++ b/agent-run-gateway.test.mjs @@ -536,6 +536,36 @@ test('static page run fails closed when Finish arrives without public HTML', asy assert.match(pool.runs.get(run.id).error_message, /public HTML 交付物/); }); +test('generic page request succeeds when the assistant finishes a clarification turn', async () => { + const pool = createFakePool(); + const gateway = createAgentRunGateway({ + pool, + userAuth: {}, + tkmindProxy: { + async startSessionForUser() { + return { id: 'session-public-page-clarification' }; + }, + async submitSessionReplyAndAwaitFinishForUser() { + return { ok: true, finishEvent: { type: 'Finish' } }; + }, + }, + syncUserPagesOnSuccess: async () => ({}), + retryDelaysMs: [], + }); + + const run = await gateway.createRun('user-1', { + requestId: 'req-public-page-clarification', + userMessage: { + role: 'user', + content: [{ type: 'text', text: '【TKMind 路由提示】使用 static-page-publish\n帮我生成一个页面吧' }], + metadata: { displayText: '帮我生成一个页面吧' }, + }, + }); + + await waitFor(() => pool.runs.get(run.id)?.status === 'succeeded'); + assert.equal(pool.runs.get(run.id).error_message, null); +}); + test('Page Data run succeeds only after a generated session page is detected', async () => { const pool = createFakePool({ sessionDeliverables: { diff --git a/chat-skills.mjs b/chat-skills.mjs index 2f031cd..3c64f67 100644 --- a/chat-skills.mjs +++ b/chat-skills.mjs @@ -108,6 +108,26 @@ export function isPageGenerationIntent(text) { return PAGE_GENERATION_INTENT_PATTERNS.some((pattern) => pattern.test(normalized)); } +const GENERIC_PAGE_REQUEST_FILLER_RE = + /(?:请|麻烦|能否|可以|可不可以|帮我|给我|我想要?|想要|生成|做|制作|创建|设计|写|出|一个|一份|个|页面|网页|H5|HTML|活动页|宣传页|落地页|分享页|吧|呢|好吗|谢谢)/giu; +const GENERIC_PAGE_REQUEST_ENGLISH_FILLER_RE = + /\b(?:please|can|could|would|you|help|me|i|want|to|publish|create|generate|make|build|design|a|an|one|html|web|page|landing|h5|thanks?)\b/giu; + +/** + * A bare request such as “帮我生成一个页面吧” names no subject or content. + * It is a valid page intent, but the current turn can only clarify requirements; + * the delivery guard must not require public HTML until the user supplies details. + */ +export function isGenericPageGenerationRequest(text) { + const normalized = String(text ?? '').trim(); + if (!normalized || !isPageGenerationIntent(normalized)) return false; + const remainder = normalized + .replace(GENERIC_PAGE_REQUEST_FILLER_RE, '') + .replace(GENERIC_PAGE_REQUEST_ENGLISH_FILLER_RE, '') + .replace(/[\s,。!?、,.!?;;::'"“”‘’()()[\]{}_-]+/gu, ''); + return remainder.length === 0; +} + export function isProductCampaignIntent(text) { const normalized = String(text ?? '').trim(); if (!normalized) return false; diff --git a/chat-skills.test.mjs b/chat-skills.test.mjs index 53e55a2..6a7c741 100644 --- a/chat-skills.test.mjs +++ b/chat-skills.test.mjs @@ -9,6 +9,7 @@ import { isPageDataDevIntent, isPageDataIntent, isPageGenerationIntent, + isGenericPageGenerationRequest, } from './chat-skills.mjs'; test('filterChatSkills shows summarize and analyze without granted skills', () => { @@ -125,6 +126,14 @@ test('isPageGenerationIntent matches implicit travel guide page requests', () => assert.equal(isPageGenerationIntent('你好'), false); }); +test('isGenericPageGenerationRequest only matches page requests without a subject', () => { + assert.equal(isGenericPageGenerationRequest('帮我生成一个页面吧'), true); + assert.equal(isGenericPageGenerationRequest('Please create a web page'), true); + assert.equal(isGenericPageGenerationRequest('帮我做一个秋夜诗的 H5 页面'), false); + assert.equal(isGenericPageGenerationRequest('苏州攻略页面'), false); + assert.equal(isGenericPageGenerationRequest('你好'), false); +}); + test('buildAutoChatSkillPrefix prefers page-data-collect for survey requests', () => { const text = '帮我在这个页面增加一个调查问卷,出三个问题,密码 888 查看提交记录'; const prefix = buildAutoChatSkillPrefix(text, ['page-data-collect', 'static-page-publish']); diff --git a/docs/regression-guards/mindspace-publish-and-chat-finish.md b/docs/regression-guards/mindspace-publish-and-chat-finish.md index 1bd3a36..6dadc94 100644 --- a/docs/regression-guards/mindspace-publish-and-chat-finish.md +++ b/docs/regression-guards/mindspace-publish-and-chat-finish.md @@ -50,6 +50,31 @@ --- +## 3. 页面需求澄清:禁止误报“未生成 public HTML” + +### 症状 + +- 用户只说「帮我生成一个页面吧」,没有给出主题或内容 +- 助手正常追问页面类型、主题或素材 +- Finish 后却显示红色错误条:「页面任务未生成 public HTML 交付物,不能标记成功」 + +### 根因与必须保留 + +`agent-run-gateway.mjs` 的页面交付守卫曾把所有页面意图都视为本轮必须交付 HTML, +没有区分“信息足够、可执行的页面任务”和“只能先澄清的泛化请求”。 + +- 必须使用用户消息 `metadata.displayText` 判断请求本身,不能让内部 routing / skill 前缀影响判定 +- 「帮我生成一个页面吧」这类没有主题的泛化请求允许以澄清回复正常结束 +- 「帮我做一个秋夜诗 H5 页面」等已有主题的任务仍须 fail closed:没有本轮 `public/*.html` 就不能标记成功 +- Page Data 任务的交付守卫不受此例外影响 + +### 守卫 + +- 单测:`chat-skills.test.mjs`、`agent-run-gateway.test.mjs` +- 综合验证:`npm run verify:h5-session-patches` + +--- + ## 发版 / CI 必跑命令 ```bash