fix(mindspace): fail closed on invalid page data delivery

This commit is contained in:
john
2026-07-14 19:40:47 +08:00
parent 192548b0b9
commit 72fd88219c
6 changed files with 114 additions and 11 deletions
+38 -4
View File
@@ -138,8 +138,11 @@ import {
} from './mindspace-public-finish-sync.mjs';
import { getPageDeliveryContract, markPageDeliveryContractReady, preparePageDeliveryContract } from './mindspace-delivery-contract.mjs';
import { maybeRepairH5HtmlAfterFinish } from './mindspace-h5-html-finish-guard.mjs';
import { maybeRepairPageDataAfterFinish } from './mindspace-page-data-finish-guard.mjs';
import { evaluatePageDataHtmlContent, maybeRepairPageDataAfterFinish } from './mindspace-page-data-finish-guard.mjs';
import { ensurePageDataHtmlPagesBound } from './page-data-workspace-ensure.mjs';
import { detectPageDataDatasetUsageFromHtml } from './page-data-html-detect.mjs';
import { readPageAccessPolicy } from './page-data-policy-store.mjs';
import { policyAllowsAction } from './page-access-policy.mjs';
import { quickPlazaFromChat, quickPlazaFromPublicHtml, getQuickPlazaFromPublicHtmlStatus } from './mindspace-chat-plaza.mjs';
import { injectPublicFileShareButton } from './mindspace-public-share-widget.mjs';
import { resolvePlazaPostPath, resolvePlazaPublicBase } from './src/utils/public-site-bases.mjs';
@@ -736,6 +739,30 @@ async function bootstrapUserAuth() {
isSessionExternallyBusy: ({ sessionId }) => Number(sessionPageDeliveryLocks.get(sessionId) ?? 0) > 0,
validateRunDeliverables: async ({ userId, deliverables }) => {
const publishDir = resolveMindSpaceUserPublishDir(__dirname, { id: userId });
const pageDataErrors = [];
for (const page of deliverables?.pages ?? []) {
const relativePath = normalizeWorkspaceRelativePath(page.workspaceRelativePath);
if (!relativePath?.startsWith('public/')) continue;
const filePath = path.resolve(publishDir, relativePath);
if (!filePath.startsWith(`${path.resolve(publishDir)}${path.sep}`) || !fs.existsSync(filePath)) continue;
const html = fs.readFileSync(filePath, 'utf8');
const evaluation = evaluatePageDataHtmlContent(html, { relativePath });
if (!evaluation.usesPageDataApi) continue;
for (const issue of evaluation.issues) {
pageDataErrors.push({ code: issue, message: `${relativePath} Page Data HTML 不可交付:${issue}` });
}
const policy = page.pageId ? readPageAccessPolicy(publishDir, page.pageId) : null;
for (const [dataset, actions] of detectPageDataDatasetUsageFromHtml(html)) {
for (const action of ['read', 'insert']) {
if (actions?.[action] && !policyAllowsAction(policy, dataset, action)) {
pageDataErrors.push({
code: 'page_data_policy_action_missing',
message: `${relativePath}${dataset}.${action} 未获最终 policy 授权或 dataset 已关闭`,
});
}
}
}
}
const violations = scanWorkspaceFilesForProhibitedBrowserStorage({
publishDir,
relativePaths: (deliverables?.pages ?? [])
@@ -743,10 +770,10 @@ async function bootstrapUserAuth() {
.filter(Boolean),
});
return {
errors: violations.map((violation) => ({
errors: [...pageDataErrors, ...violations.map((violation) => ({
code: 'browser_storage_forbidden',
message: `${violation.relativePath} 使用 ${violation.apis.join(', ')}`,
})),
}))],
};
},
autoDispatch: ['1', 'true', 'yes', 'on'].includes(
@@ -3286,9 +3313,16 @@ async function syncUserGeneratedPages(userId, { sessionId = null, sinceMs = null
// Agent-run/Finish delivery must stay scoped to the current conversation.
// A stale Page Data page elsewhere in the user's workspace must not turn a
// successfully completed current task into a failed run.
const pageDataRelativePaths = sessionId
const discoveredRelativePaths = sessionId
? await listSessionPublicHtmlRelativePaths(userId, sessionId, { sinceMs })
: null;
// An empty artifact package does not prove that this run wrote no page: a
// normal static-page-publish flow may only leave a public HTML workspace
// file. `null` means "discover current workspace output"; `[]` would tell
// the sync service to inspect nothing and make a completed page run fail.
const pageDataRelativePaths = discoveredRelativePaths?.length
? discoveredRelativePaths
: null;
if (workspacePageDeliver?.syncAndDeliver) {
return await workspacePageDeliver.syncAndDeliver(userId, { pageDataRelativePaths });
}