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
+195
View File
@@ -0,0 +1,195 @@
#!/usr/bin/env node
/**
* 修复 john 用户「每日待办登记」Page Data 交付:
* - 前台页改为 public + insertRow
* - 后台页 password + listRows/updateRow/deleteRow
*
* Usage: node scripts/repair-daily-todo-local.mjs
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createDbPool } from '../db.mjs';
import { bindWorkspaceHtmlForPageData } from '../page-data-workspace-bind.mjs';
import { resolveMindSpaceStorageRoot } from '../mindspace-runtime-config.mjs';
import { readPageAccessPolicy, writePageAccessPolicy } from '../page-data-policy-store.mjs';
import { resolvePageDataDeliveryBaseUrl } from '../user-publish.mjs';
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
function loadEnvFile(filePath) {
if (!fs.existsSync(filePath)) return;
for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eq = trimmed.indexOf('=');
if (eq < 0) continue;
const key = trimmed.slice(0, eq).trim();
const value = trimmed.slice(eq + 1).trim();
if (!process.env[key]) process.env[key] = value;
}
}
loadEnvFile(path.join(root, '.env'));
loadEnvFile(path.join(root, '../../.env.local'));
const JOHN_USER_ID = '1c99b83b-0454-474f-a5d2-129d34506a32';
const WORKSPACE_ROOT = path.join(root, 'MindSpace', JOHN_USER_ID);
const ADMIN_PASSWORD = 'adminadmin';
const DATASET = 'daily_todo';
const LOCAL_BASE = process.env.MEMIND_PAGE_DATA_DELIVERY_BASE_URL || resolvePageDataDeliveryBaseUrl();
const FORM_POLICY = {
accessMode: 'public',
datasets: {
[DATASET]: {
insert: true,
read: false,
columns: {
insert: ['name', 'date', 'todo_items', 'priority', 'status', 'remark'],
},
},
},
};
const ADMIN_POLICY = {
accessMode: 'password',
datasets: {
[DATASET]: {
insert: false,
read: true,
update: true,
softDelete: true,
columns: {
read: ['id', 'name', 'date', 'todo_items', 'priority', 'status', 'remark', 'created_at', 'updated_at'],
update: ['name', 'date', 'todo_items', 'priority', 'status', 'remark', 'updated_at'],
soft_delete: ['id'],
},
},
},
};
async function bindPages(pool, h5Root, storageRoot) {
const pages = [
{
relativePath: 'public/daily-todo.html',
accessMode: 'public',
password: null,
policy: FORM_POLICY,
},
{
relativePath: 'public/daily-todo-admin.html',
accessMode: 'password',
password: ADMIN_PASSWORD,
policy: ADMIN_POLICY,
},
];
const bound = [];
for (const page of pages) {
const result = await bindWorkspaceHtmlForPageData({
pool,
h5Root,
storageRoot,
userId: JOHN_USER_ID,
workspaceRoot: WORKSPACE_ROOT,
relativePath: page.relativePath,
accessMode: page.accessMode,
password: page.password,
pageDataPolicy: page.policy,
});
bound.push({
relativePath: page.relativePath,
pageId: result.pageId,
workspaceUrl: result.workspaceUrl,
publicationAccessMode: result.publicationAccessMode,
});
}
return bound;
}
function patchAdminPolicySoftDelete(pageId) {
const policy = readPageAccessPolicy(WORKSPACE_ROOT, pageId);
if (!policy?.datasets?.[DATASET]) return policy;
policy.datasets[DATASET].update = true;
policy.datasets[DATASET].softDelete = true;
policy.datasets[DATASET].hardDelete = false;
policy.datasets[DATASET].columns.update = ADMIN_POLICY.datasets[DATASET].columns.update;
return writePageAccessPolicy(WORKSPACE_ROOT, policy);
}
async function smokeInsert(pageId) {
const res = await fetch(`${LOCAL_BASE}/api/public/pages/${pageId}/data/${DATASET}/rows`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({
name: '本地验证',
date: '2026-07-13',
todo_items: '测试待办',
priority: '普通',
status: '待完成',
remark: 'repair script',
}),
});
const body = await res.json().catch(() => ({}));
return { status: res.status, body };
}
async function smokeAdminList(pageId) {
const authRes = await fetch(`${LOCAL_BASE}/api/public/pages/${pageId}/data-auth`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({ password: ADMIN_PASSWORD }),
});
const authBody = await authRes.json().catch(() => ({}));
const token = authBody?.data?.token;
if (!token) return { status: authRes.status, body: authBody, listStatus: 0 };
const listRes = await fetch(`${LOCAL_BASE}/api/public/pages/${pageId}/data/${DATASET}?limit=5`, {
headers: { Accept: 'application/json', 'x-page-data-token': token },
});
const listBody = await listRes.json().catch(() => ({}));
return { status: authRes.status, listStatus: listRes.status, rows: listBody?.data?.rows?.length ?? 0 };
}
const pool = createDbPool();
const h5Root = root;
const storageRoot = resolveMindSpaceStorageRoot(h5Root);
console.log('=== 每日待办登记 · 本地修复 ===\n');
const bound = await bindPages(pool, h5Root, storageRoot);
console.log('✔ 页面已重新绑定\n');
for (const item of bound) {
console.log(` ${item.relativePath}`);
console.log(` pageId: ${item.pageId}`);
console.log(` access: ${item.publicationAccessMode}`);
console.log(` url: ${item.workspaceUrl}\n`);
}
const adminPageId = bound.find((item) => item.relativePath === 'public/daily-todo-admin.html')?.pageId;
if (adminPageId) {
patchAdminPolicySoftDelete(adminPageId);
console.log('✔ 后台策略已启用 update + softDelete\n');
}
const registerPageId = bound.find((item) => item.relativePath === 'public/daily-todo.html')?.pageId;
const insert = registerPageId ? await smokeInsert(registerPageId) : { status: 0 };
const admin = adminPageId ? await smokeAdminList(adminPageId) : { listStatus: 0 };
console.log('【insert 冒烟】', JSON.stringify(insert, null, 2));
console.log('【admin 冒烟】', JSON.stringify(admin, null, 2));
console.log('\n=== 正确访问链接(Portal 8081===');
for (const item of bound) {
console.log(` ${item.relativePath}: ${item.workspaceUrl}`);
}
console.log(` 后台管理密码: ${ADMIN_PASSWORD}`);
console.log('\n注意:Agent 交付链接必须用 Portal8081),不要用 Vite UI5173)。');
const passed = insert.status === 201 && admin.listStatus === 200;
console.log(`\n${passed ? '✔ 修复完成,链路可用' : '✘ 仍有问题,请检查上方输出'}`);
await pool.end();
process.exit(passed ? 0 : 1);