fix(page-data): harden structural delivery and Portal base URL

Reject /api/page-data/ legacy passthrough, verify live API before send,
and resolve delivery links to Portal (8081) instead of Vite (5173).
Add local repair/verify scripts for daily-register Page Data flow.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-07-13 09:35:40 +08:00
parent 249b0697cb
commit 804f13bc04
11 changed files with 712 additions and 51 deletions
+49 -27
View File
@@ -4,6 +4,7 @@ import path from 'node:path';
import { isPageDataIntent } from './chat-skills.mjs';
import {
detectPageDataDatasetUsageFromHtml,
htmlUsesForbiddenLegacyPageDataApi,
htmlUsesPageDataApi,
inferPageDataBindAccessMode,
} from './page-data-html-detect.mjs';
@@ -15,7 +16,7 @@ import {
normalizePageDataApiBase,
verifyPageDataDeliveryArtifacts,
} from './page-data-delivery-assess.mjs';
import { buildPublicUrl, resolvePublicBaseUrl } from './user-publish.mjs';
import { buildPublicUrl, resolvePageDataDeliveryBaseUrl } from './user-publish.mjs';
export { inferPageDataBindAccessMode };
@@ -50,7 +51,7 @@ function readPublicHtmlFiles(publishDir) {
export function collectPageDataDeliveryArtifacts(
publishDir,
{ publicBaseUrl = resolvePublicBaseUrl() } = {},
{ publicBaseUrl = resolvePageDataDeliveryBaseUrl() } = {},
) {
const ownerKey = path.basename(path.resolve(String(publishDir ?? '')));
if (!ownerKey) return [];
@@ -112,13 +113,17 @@ export function evaluatePageDataHtmlContent(html, { relativePath = '' } = {}) {
const usesPageDataApi =
usage.size > 0 ||
PAGE_DATA_CLIENT_SCRIPT_PATTERN.test(content) ||
/\bMindSpacePageData\b/.test(content);
/\bMindSpacePageData\b/.test(content) ||
htmlUsesForbiddenLegacyPageDataApi(content);
if (!usesPageDataApi) {
return { usesPageDataApi: false, issues: [] };
}
const issues = [];
if (htmlUsesForbiddenLegacyPageDataApi(content)) {
issues.push('forbidden_legacy_page_data_api');
}
if (!PAGE_DATA_CLIENT_SCRIPT_PATTERN.test(content)) {
issues.push('missing_page_data_client_script');
}
@@ -382,7 +387,7 @@ export function buildPageDataCollectFailureText() {
'这次问卷/数据收集页面没有完成 Page Data API 绑定,所以我先不发链接。',
'请直接重发一次完整需求(例如:调查问卷 + 后台查看),我会按 page-data-collect 技能:',
'建表 → 注册 dataset → 写含 page-data-client.js 的 HTML → private_data_bind_workspace_page 发布。',
'数据必须走平台 API 写入 SQLite,禁止 localStorage 或自建后端。',
'数据必须走平台 APIMindSpacePageData.createClient + /api/public/pages/...),禁止 /api/page-data/ 旁路、localStorage 或自建后端。',
].join('');
}
@@ -394,8 +399,9 @@ export function buildPageDataCollectRepairPrompt({
'【系统补绑请求】检测到 Page Data 问卷/数据页交付不完整。请立即按 page-data-collect 技能修复:',
'1. load_skill → page-data-collect',
'2. 确保 public/*.html 引入 /assets/page-data-client.js,且 JS 使用 MindSpacePageData.createClient({ apiBase: "/api" })',
'3. 禁止 localStorage / 浏览器本地存储 fallback',
'3. 禁止 /api/page-data/ 旁路、localStorage 或自建后端',
'4. 对每个 public/*.html 调用 private_data_bind_workspace_page(问卷页 public insert,后台页 password read,口令默认 88888888',
'5. 向用户交付的链接必须使用 Portal(本地 http://127.0.0.1:8081),不要用 Vite 5173',
];
if (htmlIssues.length) {
lines.push('', 'HTML 问题:');
@@ -507,34 +513,50 @@ export async function resolvePageDataCollectOutcomeAsync({
return { action: 'fail', failureText: buildPageDataCollectFailureText(), reason: 'invalid_html', evaluation };
}
if (evaluation.unboundFiles.length > 0 && pool && userId && apiBase) {
const artifacts = collectPageDataDeliveryArtifacts(publishDir).filter((artifact) =>
evaluation.relevantFiles.some((file) => file.relativePath === artifact.relativePath),
);
if (artifacts.length > 0) {
const failures = await verifyPageDataDeliveryArtifacts({
artifacts,
const deliveryArtifacts =
evaluation.relevantFiles.length > 0
? collectPageDataDeliveryArtifacts(publishDir).filter((artifact) =>
evaluation.relevantFiles.some((file) => file.relativePath === artifact.relativePath),
)
: [];
if (pool && userId && apiBase && deliveryArtifacts.length > 0) {
const deliveryFailures = await verifyPageDataDeliveryArtifacts({
artifacts: deliveryArtifacts,
publishDir,
apiBase,
pool,
userId,
findPageByRelativePath,
fetchImpl,
});
if (deliveryFailures.length === 0) {
const reassessment = await evaluatePageDataFinishGuardAsync({
publishDir,
apiBase,
agentText,
messages: reply?.messages ?? [],
requestStartedAt,
pool,
userId,
findPageByRelativePath,
fetchImpl,
});
if (failures.length === 0) {
const reassessment = await evaluatePageDataFinishGuardAsync({
publishDir,
agentText,
messages: reply?.messages ?? [],
requestStartedAt,
pool,
userId,
findPageByRelativePath,
});
if (reassessment.unboundFiles.length === 0 && reassessment.htmlIssues.length === 0) {
return { action: 'send', reason: 'verified_by_live_api', evaluation: reassessment };
}
if (reassessment.unboundFiles.length === 0 && reassessment.htmlIssues.length === 0) {
return { action: 'send', reason: 'verified_by_live_api', evaluation: reassessment };
}
} else if (evaluation.unboundFiles.length > 0) {
return {
action: 'retry',
reason: 'missing_bind',
evaluation,
deliveryFailures,
};
} else {
return {
action: 'retry',
reason: 'delivery_smoke_failed',
evaluation,
deliveryFailures,
};
}
}