fix(page-data): isolate invalid workspace bindings

This commit is contained in:
john
2026-07-20 11:39:17 +08:00
parent 105a72c1f1
commit b6f2b40942
3 changed files with 34 additions and 16 deletions
+7 -3
View File
@@ -47,13 +47,17 @@ export function detectPageDataDatasetUsageFromHtml(html) {
// this API must therefore only require the safe, default capability.
remember(match[1], { softDelete: true });
}
for (const match of text.matchAll(/\.insertRow\(\s*([A-Za-z_$][\w$]*)/g)) {
// `${...}` inside an HTML template literal is a row-id interpolation, not a
// JavaScript identifier passed as the dataset argument. Without this guard,
// snippets such as `onclick="deleteRow(${row.id})"` are detected as a `$`
// dataset and can make the whole workspace binding pass fail.
for (const match of text.matchAll(/\.insertRow\(\s*(?!\$\{)([A-Za-z_$][\w$]*)/g)) {
remember(constants.get(match[1]) ?? match[1], { insert: true });
}
for (const match of text.matchAll(/\.listRows\(\s*([A-Za-z_$][\w$]*)/g)) {
for (const match of text.matchAll(/\.listRows\(\s*(?!\$\{)([A-Za-z_$][\w$]*)/g)) {
remember(constants.get(match[1]) ?? match[1], { read: true });
}
for (const match of text.matchAll(/\.deleteRow\(\s*([A-Za-z_$][\w$]*)/g)) {
for (const match of text.matchAll(/\.deleteRow\(\s*(?!\$\{)([A-Za-z_$][\w$]*)/g)) {
remember(constants.get(match[1]) ?? match[1], { softDelete: true });
}
+11
View File
@@ -44,6 +44,17 @@ test('detectPageDataDatasetUsageFromHtml resolves dataset constants', () => {
assert.deepEqual(usage.get('reading_survey'), { read: true });
});
test('detectPageDataDatasetUsageFromHtml ignores template-literal row id interpolation', () => {
const html = `
const DATASET = 'daily_log';
await client.listRows(DATASET, { limit: 200 });
rows.map((row) => \`<button onclick="window.deleteRow(\${row.id})">删除</button>\`);
`;
const usage = detectPageDataDatasetUsageFromHtml(html);
assert.deepEqual([...usage.entries()], [['daily_log', { read: true }]]);
assert.equal(usage.has('$'), false);
});
test('assertPolicyMatchesHtmlDatasets rejects mismatched dataset names', () => {
const html = `await c.insertRow('tkmind_exp_survey', {});`;
assert.throws(
+16 -13
View File
@@ -137,20 +137,23 @@ export async function ensurePageDataHtmlPagesBound({
continue;
}
const assessment = await assessPageDataHtmlBinding({
pool,
userId,
publishDir: workspaceRoot,
relativePath: file.relativePath,
html: file.content,
findPageByRelativePath,
});
if (assessment.bound) {
skipped.push({ relativePath: file.relativePath, reason: 'already_bound' });
continue;
}
try {
// Keep every page's assessment and repair isolated. A malformed legacy
// page must be reported in `errors`, not reject the entire MindSpace page
// listing that invokes this best-effort maintenance pass.
const assessment = await assessPageDataHtmlBinding({
pool,
userId,
publishDir: workspaceRoot,
relativePath: file.relativePath,
html: file.content,
findPageByRelativePath,
});
if (assessment.bound) {
skipped.push({ relativePath: file.relativePath, reason: 'already_bound' });
continue;
}
for (const datasetName of usage.keys()) {
await ensureRegisteredDatasetFromHtml({
workspaceRoot,