fix: allow page clarification turns without delivery error
This commit is contained in:
+19
-2
@@ -16,7 +16,11 @@ import {
|
|||||||
SESSION_FINISHED_STALE_GRACE_MS,
|
SESSION_FINISHED_STALE_GRACE_MS,
|
||||||
tryRecoverRunFromDeliverables,
|
tryRecoverRunFromDeliverables,
|
||||||
} from './agent-run-deliverable-check.mjs';
|
} 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 DEFAULT_RUN_RETRY_DELAYS_MS = [1500, 5000, 15000];
|
||||||
const TERMINAL_STATUSES = new Set(['succeeded', 'failed']);
|
const TERMINAL_STATUSES = new Set(['succeeded', 'failed']);
|
||||||
@@ -66,6 +70,14 @@ function extractRunMessageText(row) {
|
|||||||
.join('\n');
|
.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) {
|
function resolveRequiredImageGeneration(row, routing) {
|
||||||
const message = parseDbJsonColumn(row?.user_message_json, {}) ?? {};
|
const message = parseDbJsonColumn(row?.user_message_json, {}) ?? {};
|
||||||
const metadata = message.metadata ?? {};
|
const metadata = message.metadata ?? {};
|
||||||
@@ -894,11 +906,16 @@ export function createAgentRunGateway({
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
const runMessageText = extractRunMessageText(row);
|
const runMessageText = extractRunMessageText(row);
|
||||||
|
const runDisplayText = extractRunDisplayText(row);
|
||||||
const pageDataIntent = isPageDataIntent(runMessageText)
|
const pageDataIntent = isPageDataIntent(runMessageText)
|
||||||
|| routing?.suggestedSkill === 'page-data-collect';
|
|| routing?.suggestedSkill === 'page-data-collect';
|
||||||
const pageGenerationIntent = isPageGenerationIntent(runMessageText)
|
const pageGenerationIntent = isPageGenerationIntent(runMessageText)
|
||||||
|| routing?.suggestedSkill === 'static-page-publish';
|
|| 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;
|
let deliverables = null;
|
||||||
if (requiresPageDeliverable || typeof validateRunDeliverables === 'function') {
|
if (requiresPageDeliverable || typeof validateRunDeliverables === 'function') {
|
||||||
const latest = await getRunById(runId);
|
const latest = await getRunById(runId);
|
||||||
|
|||||||
@@ -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 交付物/);
|
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 () => {
|
test('Page Data run succeeds only after a generated session page is detected', async () => {
|
||||||
const pool = createFakePool({
|
const pool = createFakePool({
|
||||||
sessionDeliverables: {
|
sessionDeliverables: {
|
||||||
|
|||||||
@@ -108,6 +108,26 @@ export function isPageGenerationIntent(text) {
|
|||||||
return PAGE_GENERATION_INTENT_PATTERNS.some((pattern) => pattern.test(normalized));
|
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) {
|
export function isProductCampaignIntent(text) {
|
||||||
const normalized = String(text ?? '').trim();
|
const normalized = String(text ?? '').trim();
|
||||||
if (!normalized) return false;
|
if (!normalized) return false;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
isPageDataDevIntent,
|
isPageDataDevIntent,
|
||||||
isPageDataIntent,
|
isPageDataIntent,
|
||||||
isPageGenerationIntent,
|
isPageGenerationIntent,
|
||||||
|
isGenericPageGenerationRequest,
|
||||||
} from './chat-skills.mjs';
|
} from './chat-skills.mjs';
|
||||||
|
|
||||||
test('filterChatSkills shows summarize and analyze without granted skills', () => {
|
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);
|
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', () => {
|
test('buildAutoChatSkillPrefix prefers page-data-collect for survey requests', () => {
|
||||||
const text = '帮我在这个页面增加一个调查问卷,出三个问题,密码 888 查看提交记录';
|
const text = '帮我在这个页面增加一个调查问卷,出三个问题,密码 888 查看提交记录';
|
||||||
const prefix = buildAutoChatSkillPrefix(text, ['page-data-collect', 'static-page-publish']);
|
const prefix = buildAutoChatSkillPrefix(text, ['page-data-collect', 'static-page-publish']);
|
||||||
|
|||||||
@@ -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 必跑命令
|
## 发版 / CI 必跑命令
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
Reference in New Issue
Block a user