fix(mindspace): 修复删除时无法解析 MySQL JSON 导致页面复活

source_snapshot_json 在 mysql2 中已是对象,JSON.parse 失败会使 relative_path 丢失、工作区 HTML 未清理;同步后页面重新出现。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-06-30 01:39:47 +08:00
parent e93fb6a75f
commit 7906f3c36d
3 changed files with 39 additions and 10 deletions
+11 -6
View File
@@ -52,6 +52,16 @@ async function findPageByWorkspaceRelativePath(pool, userId, relativePath) {
return { id: row.id, updatedAt: Number(row.updated_at ?? 0) }; return { id: row.id, updatedAt: Number(row.updated_at ?? 0) };
} }
function parseJsonColumn(value, fallback = {}) {
if (value == null || value === '') return { ...fallback };
if (typeof value === 'object' && !Buffer.isBuffer(value)) return value;
try {
return JSON.parse(String(value));
} catch {
return { ...fallback };
}
}
async function loadIndexedWorkspacePages(pool, userId) { async function loadIndexedWorkspacePages(pool, userId) {
const [rows] = await pool.query( const [rows] = await pool.query(
`SELECT p.id, p.updated_at, p.source_asset_id, pv.source_snapshot_json `SELECT p.id, p.updated_at, p.source_asset_id, pv.source_snapshot_json
@@ -62,12 +72,7 @@ async function loadIndexedWorkspacePages(pool, userId) {
); );
const byPath = new Map(); const byPath = new Map();
for (const row of rows) { for (const row of rows) {
let snapshot = {}; const snapshot = parseJsonColumn(row.source_snapshot_json);
try {
snapshot = JSON.parse(row.source_snapshot_json ?? '{}');
} catch {
snapshot = {};
}
const relativePath = normalizePublicHtmlPath(snapshot.relative_path); const relativePath = normalizePublicHtmlPath(snapshot.relative_path);
if (relativePath) { if (relativePath) {
byPath.set(relativePath, { byPath.set(relativePath, {
+18 -4
View File
@@ -29,6 +29,16 @@ function asNumber(value) {
return Number(value ?? 0); return Number(value ?? 0);
} }
export function parseJsonColumn(value, fallback = {}) {
if (value == null || value === '') return { ...fallback };
if (typeof value === 'object' && !Buffer.isBuffer(value)) return value;
try {
return JSON.parse(String(value));
} catch {
return { ...fallback };
}
}
function pageError(message, code, details) { function pageError(message, code, details) {
return Object.assign(new Error(message), { code, details }); return Object.assign(new Error(message), { code, details });
} }
@@ -730,7 +740,7 @@ export function createPageService(pool, options = {}) {
); );
let snapshot = {}; let snapshot = {};
try { try {
snapshot = JSON.parse(rows[0]?.source_snapshot_json ?? '{}'); snapshot = parseJsonColumn(rows[0]?.source_snapshot_json);
} catch { } catch {
snapshot = {}; snapshot = {};
} }
@@ -1084,7 +1094,7 @@ export function createPageService(pool, options = {}) {
LIMIT 1`, LIMIT 1`,
[pageId, userId], [pageId, userId],
); );
const snapshot = JSON.parse(rows[0]?.source_snapshot_json ?? '{}'); const snapshot = parseJsonColumn(rows[0]?.source_snapshot_json);
workspaceHtmlRelativePath = snapshot.relative_path ?? null; workspaceHtmlRelativePath = snapshot.relative_path ?? null;
} catch { } catch {
workspaceHtmlRelativePath = null; workspaceHtmlRelativePath = null;
@@ -1166,7 +1176,10 @@ export function createPageService(pool, options = {}) {
publishDir: workspacePublishDir, publishDir: workspacePublishDir,
htmlRelativePath: workspaceHtmlRelativePath, htmlRelativePath: workspaceHtmlRelativePath,
html: page.content ?? '', html: page.content ?? '',
}).catch(() => ({ removed: [], skipped: [] })); }).catch((error) => {
console.warn('[MindSpace] workspace purge failed:', error?.message ?? error);
return { removed: [], skipped: [] };
});
return { ...result, workspaceFilesRemoved: purgeResult.removed ?? [] }; return { ...result, workspaceFilesRemoved: purgeResult.removed ?? [] };
}; };
@@ -1190,7 +1203,7 @@ export function createPageService(pool, options = {}) {
const content = await fs.readFile(storagePath, 'utf8'); const content = await fs.readFile(storagePath, 'utf8');
let snapshot = {}; let snapshot = {};
try { try {
snapshot = JSON.parse(row.source_snapshot_json ?? '{}'); snapshot = parseJsonColumn(row.source_snapshot_json);
} catch { } catch {
snapshot = {}; snapshot = {};
} }
@@ -1375,6 +1388,7 @@ export const pageInternals = {
escapeHtml, escapeHtml,
normalizePageInput, normalizePageInput,
normalizeListPageFilters, normalizeListPageFilters,
parseJsonColumn,
renderContent, renderContent,
renderPreviewHtml, renderPreviewHtml,
renderHtmlPreview, renderHtmlPreview,
+10
View File
@@ -2,6 +2,16 @@ import assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import { pageInternals } from './mindspace-pages.mjs'; import { pageInternals } from './mindspace-pages.mjs';
test('parseJsonColumn accepts mysql json objects and strings', () => {
const objectValue = { relative_path: 'public/demo.html', auto_synced: true };
assert.deepEqual(pageInternals.parseJsonColumn(objectValue), objectValue);
assert.deepEqual(
pageInternals.parseJsonColumn(JSON.stringify(objectValue)),
objectValue,
);
assert.deepEqual(pageInternals.parseJsonColumn(null, { ok: true }), { ok: true });
});
test('normalizePageInput trims values and constrains templates', () => { test('normalizePageInput trims values and constrains templates', () => {
const result = pageInternals.normalizePageInput({ const result = pageInternals.normalizePageInput({
title: ' 页面标题 ', title: ' 页面标题 ',