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:
@@ -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 交付链接必须用 Portal(8081),不要用 Vite UI(5173)。');
|
||||
|
||||
const passed = insert.status === 201 && admin.listStatus === 200;
|
||||
console.log(`\n${passed ? '✔ 修复完成,链路可用' : '✘ 仍有问题,请检查上方输出'}`);
|
||||
|
||||
await pool.end();
|
||||
process.exit(passed ? 0 : 1);
|
||||
Reference in New Issue
Block a user