diff --git a/mindspace-page-data-finish-guard.integration.test.mjs b/mindspace-page-data-finish-guard.integration.test.mjs index 598f40c..c27e575 100644 --- a/mindspace-page-data-finish-guard.integration.test.mjs +++ b/mindspace-page-data-finish-guard.integration.test.mjs @@ -326,7 +326,7 @@ test('integration: H5 finish guard triggers repair prompt for invalid survey htm } }); -test('integration: finish guard repairs historical unbound Page Data html without a current write', async () => { +test('integration: finish guard does not repair historical Page Data html without a current write', async () => { const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-historical-skip-')); try { await setupSurveyWorkspace(workspaceRoot); @@ -342,9 +342,9 @@ test('integration: finish guard repairs historical unbound Page Data html withou userText: '确认发布', tkmindProxy: { async submitSessionReplyForUser() { throw new Error('must not repair history'); } }, }); - assert.equal(result.structuralPageData, true); - assert.equal(result.relevantFiles.length, 1); - assert.equal(result.needsRepair, true); + assert.equal(result.structuralPageData, false); + assert.equal(result.relevantFiles.length, 0); + assert.equal(result.needsRepair, false); assert.equal(result.triggered, undefined); } finally { fs.rmSync(workspaceRoot, { recursive: true, force: true }); diff --git a/mindspace-page-data-finish-guard.mjs b/mindspace-page-data-finish-guard.mjs index 83292ec..edd78f3 100644 --- a/mindspace-page-data-finish-guard.mjs +++ b/mindspace-page-data-finish-guard.mjs @@ -229,7 +229,8 @@ export function extractRecentPageDataHtmlWrites(messages = [], { sinceMs = 0 } = if (item?.type !== 'toolRequest') continue; const toolCall = item.toolCall?.value; const name = String(toolCall?.name ?? '').trim(); - if (!['write_file', 'edit_file', 'write', 'edit'].includes(name)) continue; + const normalizedName = name.split('__').at(-1); + if (!['write_file', 'edit_file', 'write', 'edit'].includes(normalizedName)) continue; const args = toolCall?.arguments ?? {}; const candidate = String(args.path ?? args.file_path ?? '').trim().replace(/\\/g, '/'); if (!candidate.toLowerCase().endsWith('.html')) continue; @@ -240,6 +241,26 @@ export function extractRecentPageDataHtmlWrites(messages = [], { sinceMs = 0 } = return [...targets]; } +export function extractRecentPageDataBindTargets(messages = [], { sinceMs = 0 } = {}) { + const targets = new Set(); + for (const message of Array.isArray(messages) ? messages : []) { + const createdAt = Number(message?.created ?? message?.createdAt ?? 0); + if (sinceMs > 0 && createdAt > 0 && createdAt < sinceMs) continue; + for (const item of message?.content ?? []) { + if (item?.type !== 'toolRequest') continue; + const toolCall = item.toolCall?.value; + const name = String(toolCall?.name ?? '').trim().split('__').at(-1); + if (name !== 'private_data_bind_workspace_page') continue; + const candidate = String(toolCall?.arguments?.relativePath ?? '') + .trim() + .replace(/\\/g, '/'); + if (!candidate.toLowerCase().endsWith('.html')) continue; + targets.add(candidate.startsWith('public/') ? candidate : `public/${path.posix.basename(candidate)}`); + } + } + return [...targets]; +} + function isStructuralPageDataHtmlFile(file) { return Boolean(file?.evaluation?.usage?.size > 0 || file?.evaluation?.usesPageDataApi); } @@ -272,10 +293,14 @@ export function evaluatePageDataFinishGuard({ const pageDataIntent = isPageDataIntent(resolvedAgentText); const pageDataFiles = collectPageDataPublicHtmlFiles(publishDir); const recentWrites = extractRecentPageDataHtmlWrites(messages, { sinceMs: requestStartedAt }); - const structuralFiles = pageDataFiles.filter(isStructuralPageDataHtmlFile); - const relevantFiles = pageDataFiles.filter( - (file) => isStructuralPageDataHtmlFile(file) || recentWrites.includes(file.relativePath), - ); + const recentBinds = extractRecentPageDataBindTargets(messages, { sinceMs: requestStartedAt }); + const relevantPaths = new Set([...recentWrites, ...recentBinds]); + // A reused session/workspace can contain many historical Page Data pages. + // Only files touched or explicitly bound in this request belong to this + // delivery. Scanning the whole workspace lets one stale localStorage page + // incorrectly block every later, correctly bound survey. + const relevantFiles = pageDataFiles.filter((file) => relevantPaths.has(file.relativePath)); + const structuralFiles = relevantFiles.filter(isStructuralPageDataHtmlFile); const htmlIssues = relevantFiles.flatMap((file) => file.evaluation.issues.map((issue) => ({ @@ -295,11 +320,10 @@ export function evaluatePageDataFinishGuard({ ); const needsRepair = - structuralFiles.length > 0 && + pageDataIntent && (htmlIssues.length > 0 || unboundFiles.length > 0 || - (pageDataIntent && - relevantFiles.length === 0 && + (relevantFiles.length === 0 && extractRecentPageDataHtmlWrites(messages, { sinceMs: requestStartedAt }).length === 0 && usedPageDataCollectSkill(messages))); diff --git a/mindspace-page-data-finish-guard.test.mjs b/mindspace-page-data-finish-guard.test.mjs index f7a28e9..335b3ee 100644 --- a/mindspace-page-data-finish-guard.test.mjs +++ b/mindspace-page-data-finish-guard.test.mjs @@ -9,12 +9,15 @@ import { collectPageDataDeliveryArtifacts, evaluatePageDataFinishGuard, evaluatePageDataHtmlContent, + extractRecentPageDataBindTargets, + extractRecentPageDataHtmlWrites, inferPageDataBindAccessMode, maybeAutoBindPageDataHtmlPages, rewritePageDataDeliveryLinks, shouldRetryPageDataCollectReply, } from './mindspace-page-data-finish-guard.mjs'; import { writePageAccessPolicy } from './page-data-policy-store.mjs'; +import { createUserDataSpaceService } from './user-data-space-service.mjs'; const SURVEY_HTML = `