feat(page-data): complete Phase 4-5, ops UI, and publish integration
Add visitor roles, row-level scope, owner ops APIs, MySQL policy index, Turnstile captcha, browser client SDK, publish-panel dataset binding, acceptance tests, and usage documentation. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const DEFAULT_MAX_LOG_BYTES = 2 * 1024 * 1024;
|
||||
|
||||
export function resolvePageDataLogDir(workspaceRoot) {
|
||||
return path.join(workspaceRoot, '.mindspace', 'page-data-logs');
|
||||
}
|
||||
|
||||
export function resolvePageDataLogPath(workspaceRoot, pageId) {
|
||||
const safePageId = String(pageId ?? '').trim();
|
||||
if (!safePageId || /[\\/]/.test(safePageId)) {
|
||||
throw Object.assign(new Error('pageId 无效'), { code: 'invalid_page_id' });
|
||||
}
|
||||
return path.join(resolvePageDataLogDir(workspaceRoot), `${safePageId}.jsonl`);
|
||||
}
|
||||
|
||||
export function appendPageDataLog(workspaceRoot, pageId, entry, { maxBytes = DEFAULT_MAX_LOG_BYTES } = {}) {
|
||||
const logPath = resolvePageDataLogPath(workspaceRoot, pageId);
|
||||
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
||||
const line = `${JSON.stringify({ ...entry, at: entry.at ?? new Date().toISOString() })}\n`;
|
||||
fs.appendFileSync(logPath, line, 'utf8');
|
||||
if (maxBytes > 0 && fs.existsSync(logPath) && fs.statSync(logPath).size > maxBytes) {
|
||||
const raw = fs.readFileSync(logPath, 'utf8').trim().split('\n');
|
||||
const trimmed = raw.slice(-Math.max(50, Math.floor(raw.length / 2)));
|
||||
fs.writeFileSync(logPath, `${trimmed.join('\n')}\n`, 'utf8');
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
export function listPageDataLogs(workspaceRoot, pageId, { limit = 100, offset = 0 } = {}) {
|
||||
const logPath = resolvePageDataLogPath(workspaceRoot, pageId);
|
||||
if (!fs.existsSync(logPath)) return [];
|
||||
const lines = fs.readFileSync(logPath, 'utf8').trim().split('\n').filter(Boolean);
|
||||
const parsed = lines
|
||||
.map((line) => {
|
||||
try {
|
||||
return JSON.parse(line);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Boolean);
|
||||
const safeOffset = Math.max(0, Number(offset ?? 0));
|
||||
const safeLimit = Math.min(Math.max(1, Number(limit ?? 100)), 500);
|
||||
return parsed.slice(safeOffset, safeOffset + safeLimit);
|
||||
}
|
||||
Reference in New Issue
Block a user