import { resolveMindSpaceUserPublishDir } from './mindspace-runtime-config.mjs'; import { publicationInternals } from './mindspace-publications.mjs'; import { buildEffectiveDataset, buildPageDataPolicyFromPublishInput, closePolicyDataset, normalizePageAccessPolicy, policyAllowsAction, } from './page-access-policy.mjs'; import { extractPageDataCaptchaToken, stripPageDataMetaFields, verifyPageDataCaptcha, } from './page-data-captcha.mjs'; import { buildEffectiveDatasetForRole, resolveRowAccessScope, resolveRolePermissions, resolveVisitorRole, roleAllowsAction, } from './page-access-visitor.mjs'; import { readPageAccessPolicy, writePageAccessPolicy } from './page-data-policy-store.mjs'; import { createPageDataRateLimiter, createPageDataSessionStore, hashClientMeta, } from './page-data-session-store.mjs'; import { appendPageDataLog, listPageDataLogs } from './page-data-log-store.mjs'; import { getPageDataPolicyIndex, listPageDataPolicyIndexByOwner, upsertPageDataPolicyIndex, } from './page-data-policy-index.mjs'; import { createUserDataSpaceService } from './user-data-space-service.mjs'; function mapPublicError(error) { if (error?.status && error?.code && error?.message) { return error; } const code = error?.code; if (code === 'publication_not_found') { return { status: 404, code: 'publication_not_found', message: '公开页面不存在或已下线' }; } if (code === 'policy_not_found') { return { status: 404, code: 'policy_not_found', message: '页面数据策略未配置' }; } if (code === 'dataset_not_found') { return { status: 404, code: 'dataset_not_found', message: '数据集不存在' }; } if (code === 'action_not_allowed') { return { status: 403, code: 'action_not_allowed', message: error.message }; } if (code === 'columns_not_allowed') { return { status: 403, code: 'columns_not_allowed', message: error.message }; } if (code === 'auth_required' || code === 'token_invalid' || code === 'password_invalid') { return { status: 401, code, message: error.message }; } if (code === 'rate_limited') { return { status: 429, code: 'rate_limited', message: error.message }; } if (code === 'invalid_identifier') { return { status: 403, code: 'columns_not_allowed', message: error.message }; } if (code === 'forbidden') { return { status: 403, code: 'forbidden', message: error.message }; } if (code === 'row_not_allowed' || code === 'row_policy_invalid') { return { status: 403, code: code ?? 'row_not_allowed', message: error.message }; } if (code === 'unauthorized') { return { status: 401, code: 'unauthorized', message: error.message }; } if (code === 'invalid_payload' || code === 'payload_too_large' || code === 'invalid_policy') { return { status: 400, code: code ?? 'invalid_request', message: error.message }; } if (code === 'captcha_required' || code === 'captcha_invalid') { return { status: error.status ?? 400, code, message: error.message }; } return { status: 400, code: 'page_data_failed', message: error instanceof Error ? error.message : '页面数据操作失败', }; } export function mapPageDataPublicError(error) { return mapPublicError(error); } function rethrowPublicError(error) { throw mapPublicError(error); } async function runPublicAction(fn) { try { return await fn(); } catch (error) { rethrowPublicError(error); } } function extractPageDataToken(req) { const header = String(req.headers?.['x-page-data-token'] ?? req.headers?.authorization ?? '').trim(); if (!header) return null; if (header.toLowerCase().startsWith('bearer ')) return header.slice(7).trim(); return header; } export function isPageDataPublicPath(path, method) { if (!path.startsWith('/public/pages/')) return false; if (method === 'POST' && /^\/public\/pages\/[^/]+\/data-auth$/.test(path)) return true; if (method === 'POST' && /^\/public\/pages\/[^/]+\/data\/[^/]+\/rows$/.test(path)) return true; if (method === 'PATCH' && /^\/public\/pages\/[^/]+\/data\/[^/]+\/rows\/[^/]+$/.test(path)) return true; if (method === 'DELETE' && /^\/public\/pages\/[^/]+\/data\/[^/]+\/rows\/[^/]+$/.test(path)) return true; if (method !== 'GET') return false; return ( /^\/public\/pages\/[^/]+\/data\/[^/]+$/.test(path) || /^\/public\/pages\/[^/]+\/data\/[^/]+\/schema$/.test(path) || /^\/public\/pages\/[^/]+\/data\/[^/]+\/stats$/.test(path) ); } export function createPageDataPublicService(deps = {}) { const getPool = deps.getPool ?? (() => null); const resolveH5Root = deps.resolveH5Root ?? (() => process.cwd()); const resolveWorkspaceRootForOwner = deps.resolveWorkspaceRootForOwner ?? ((ownerUserId) => resolveMindSpaceUserPublishDir(resolveH5Root(), { id: ownerUserId })); const sessionStore = deps.sessionStore ?? createPageDataSessionStore(); const rateLimiter = deps.rateLimiter ?? createPageDataRateLimiter({ windowMs: Number(process.env.PAGE_DATA_RATE_WINDOW_MS ?? 60_000), maxRequests: Number(process.env.PAGE_DATA_RATE_MAX_REQUESTS ?? 30), }); async function queryPublication(pageId) { const pool = getPool(); if (!pool) { throw Object.assign(new Error('数据库未配置'), { code: 'feature_disabled', status: 503 }); } const [rows] = await pool.query( `SELECT pr.id, pr.user_id, pr.page_id, pr.access_mode, pr.password_hash, pr.status, pr.expires_at FROM h5_publish_records pr WHERE pr.page_id = ? AND pr.status = 'online' ORDER BY pr.published_at DESC LIMIT 1`, [pageId], ); const row = rows[0]; if (!row) { throw Object.assign(new Error('公开页面不存在或已下线'), { code: 'publication_not_found' }); } if (row.expires_at && Number(row.expires_at) <= Date.now()) { throw Object.assign(new Error('公开页面不存在或已下线'), { code: 'publication_not_found' }); } return row; } function resolveWorkspaceRoot(ownerUserId) { return resolveWorkspaceRootForOwner(ownerUserId); } function createOwnerService(ownerUserId) { const pool = getPool(); return createUserDataSpaceService({ workspaceRoot: resolveWorkspaceRoot(ownerUserId), userId: ownerUserId, query: pool ?? null, }); } async function resolveEffectiveDataset(ownerUserId, policy, datasetName, rolePermissions = null) { const policyDataset = policy.datasets?.[datasetName]; if (!policyDataset) { throw Object.assign(new Error('dataset 未授权'), { code: 'action_not_allowed' }); } const ownerService = createOwnerService(ownerUserId); const registryDataset = await ownerService.getDataset(datasetName); if (!registryDataset) { throw Object.assign(new Error('dataset 不存在'), { code: 'dataset_not_found' }); } const effective = rolePermissions ? buildEffectiveDatasetForRole(registryDataset, policyDataset, rolePermissions) : buildEffectiveDataset(registryDataset, policyDataset); if (!effective) { throw Object.assign(new Error('dataset 未授权'), { code: 'action_not_allowed' }); } return { ownerService, effective, policyDataset }; } async function resolveRowScope(ownerService, effective, policyDataset, visitorContext) { if (!policyDataset?.rowPolicy || !visitorContext) return null; const tableColumns = await ownerService.listTableColumns(effective.table); return resolveRowAccessScope(policyDataset.rowPolicy, { visitorRole: visitorContext.visitorRole, visitorUserId: visitorContext.visitorUserId, ownerUserId: visitorContext.ownerUserId, tableColumns, }); } function enforceRateLimit(req, publication, action) { const ip = req.ip ?? req.socket?.remoteAddress ?? 'unknown'; rateLimiter.check(`${publication.page_id}:${action}:${ip}`); } function resolveAccessContext({ publication, policy, req, action, datasetName }) { const accessMode = publication.access_mode; if (accessMode === 'public') { if (action === 'update' || action === 'soft_delete') { throw Object.assign(new Error('当前页面未开放此数据操作'), { code: 'action_not_allowed' }); } if (action === 'read') { if (!policyAllowsAction(policy, datasetName, 'read')) { throw Object.assign(new Error('当前页面未开放此数据操作'), { code: 'action_not_allowed' }); } return { accessMode, session: null }; } if (!policyAllowsAction(policy, datasetName, action)) { throw Object.assign(new Error(`dataset 未授权 ${action}`), { code: 'action_not_allowed' }); } return { accessMode, session: null }; } if (accessMode === 'password') { const token = extractPageDataToken(req); const session = token ? sessionStore.verify(token) : null; if (!session || session.pageId !== publication.page_id) { throw Object.assign(new Error('需要有效的页面数据访问令牌'), { code: 'token_invalid', }); } if (!policyAllowsAction(policy, datasetName, action)) { throw Object.assign(new Error(`dataset 未授权 ${action}`), { code: 'action_not_allowed' }); } return { accessMode, session }; } if (accessMode === 'login_required') { const viewerId = req.currentUser?.id ?? null; if (!viewerId) { throw Object.assign(new Error('需要登录后才能访问页面数据'), { code: 'auth_required' }); } const visitorRole = resolveVisitorRole(policy, viewerId); if (!visitorRole) { throw Object.assign(new Error('当前账号未被授权访问此页面数据'), { code: 'forbidden' }); } const rolePermissions = resolveRolePermissions(policy, visitorRole); if (!policyAllowsAction(policy, datasetName, action)) { throw Object.assign(new Error(`dataset 未授权 ${action}`), { code: 'action_not_allowed' }); } if (!roleAllowsAction(rolePermissions, action)) { throw Object.assign(new Error(`当前角色未授权 ${action}`), { code: 'action_not_allowed' }); } return { accessMode, session: { sessionId: viewerId, ownerUserId: publication.user_id, visitorUserId: viewerId }, visitorRole, rolePermissions, ownerUserId: publication.user_id, }; } throw Object.assign(new Error('当前页面访问模式不支持 Page Data API'), { code: 'action_not_allowed' }); } async function loadPolicy(publication) { const workspaceRoot = resolveWorkspaceRoot(publication.user_id); const policy = readPageAccessPolicy(workspaceRoot, publication.page_id); if (!policy) { throw Object.assign(new Error('页面数据策略未配置'), { code: 'policy_not_found' }); } if (policy.ownerUserId !== publication.user_id) { throw Object.assign(new Error('页面数据策略无效'), { code: 'invalid_policy' }); } return policy; } async function authenticate(pageId, password, req) { return runPublicAction(async () => { const publication = await queryPublication(pageId); enforceRateLimit(req, publication, 'data-auth'); if (publication.access_mode !== 'password') { throw Object.assign(new Error('当前页面不需要口令认证'), { code: 'auth_not_required' }); } if (!publicationInternals.verifyPassword(password, publication.password_hash)) { throw Object.assign(new Error('页面口令不正确'), { code: 'password_invalid' }); } return sessionStore.issue({ pageId: publication.page_id, ownerUserId: publication.user_id, publicationId: publication.id, accessMode: publication.access_mode, }); }); } async function listRows(pageId, datasetName, req, query = {}) { return runPublicAction(async () => { const publication = await queryPublication(pageId); const policy = await loadPolicy(publication); const access = resolveAccessContext({ publication, policy, req, action: 'read', datasetName }); enforceRateLimit(req, publication, 'read'); const { ownerService, effective, policyDataset } = await resolveEffectiveDataset( publication.user_id, policy, datasetName, access.rolePermissions ?? null, ); const rowScope = await resolveRowScope( ownerService, effective, policyDataset, access.accessMode === 'login_required' ? { visitorRole: access.visitorRole, visitorUserId: access.session?.visitorUserId, ownerUserId: access.ownerUserId, } : null, ); return ownerService.readRowsForDataset(effective, { ...query, rowScope }); }); } async function getSchema(pageId, datasetName, req) { return runPublicAction(async () => { const publication = await queryPublication(pageId); const policy = await loadPolicy(publication); const access = resolveAccessContext({ publication, policy, req, action: 'read', datasetName }); const { ownerService, effective } = await resolveEffectiveDataset( publication.user_id, policy, datasetName, access.rolePermissions ?? null, ); return ownerService.getSchemaForDataset(effective); }); } async function getStats(pageId, datasetName, req) { return runPublicAction(async () => { const publication = await queryPublication(pageId); const policy = await loadPolicy(publication); const access = resolveAccessContext({ publication, policy, req, action: 'read', datasetName }); const { ownerService, effective, policyDataset } = await resolveEffectiveDataset( publication.user_id, policy, datasetName, access.rolePermissions ?? null, ); const rowScope = await resolveRowScope( ownerService, effective, policyDataset, access.accessMode === 'login_required' ? { visitorRole: access.visitorRole, visitorUserId: access.session?.visitorUserId, ownerUserId: access.ownerUserId, } : null, ); return ownerService.getStatsForDataset(effective, { rowScope }); }); } function recordPublicLog(workspaceRoot, publication, { datasetName, action, rowId, req, session, accessMode, visitorRole, summary, }) { appendPageDataLog(workspaceRoot, publication.page_id, { pageId: publication.page_id, dataset: datasetName, action, rowId: rowId ?? null, accessMode, actor: { type: accessMode === 'public' ? 'anonymous' : accessMode, userId: req.currentUser?.id ?? session?.visitorUserId ?? null, visitorUserId: session?.visitorUserId ?? req.currentUser?.id ?? null, role: visitorRole ?? null, sessionId: session?.sessionId ?? null, ipHash: hashClientMeta(req.ip ?? req.socket?.remoteAddress), userAgentHash: hashClientMeta(req.headers?.['user-agent']), }, summary: summary ?? null, }); } async function insertRow(pageId, datasetName, req, payloadInput) { return runPublicAction(async () => { const publication = await queryPublication(pageId); const policy = await loadPolicy(publication); const access = resolveAccessContext({ publication, policy, req, action: 'insert', datasetName, }); if (access.accessMode === 'public') { await verifyPageDataCaptcha( extractPageDataCaptchaToken(req, payloadInput), req.ip ?? req.socket?.remoteAddress, ); } const payload = stripPageDataMetaFields(payloadInput); enforceRateLimit(req, publication, 'insert'); const { ownerService, effective, policyDataset } = await resolveEffectiveDataset( publication.user_id, policy, datasetName, access.rolePermissions ?? null, ); const rowScope = await resolveRowScope( ownerService, effective, policyDataset, access.accessMode === 'login_required' ? { visitorRole: access.visitorRole, visitorUserId: access.session?.visitorUserId, ownerUserId: access.ownerUserId, } : null, ); const result = await ownerService.insertRowForDataset(effective, payload, { createdIpHash: hashClientMeta(req.ip ?? req.socket?.remoteAddress), userAgentHash: hashClientMeta(req.headers?.['user-agent']), pageDataSessionId: access.session?.sessionId ?? null, rowScope, }); recordPublicLog(resolveWorkspaceRoot(publication.user_id), publication, { datasetName, action: 'insert', rowId: result.row?.id ?? null, req, session: access.session, accessMode: access.accessMode, visitorRole: access.visitorRole, summary: { fields: Object.keys(payload ?? {}) }, }); return result; }); } async function updateRow(pageId, datasetName, rowId, req, payload) { return runPublicAction(async () => { const publication = await queryPublication(pageId); const policy = await loadPolicy(publication); const access = resolveAccessContext({ publication, policy, req, action: 'update', datasetName, }); enforceRateLimit(req, publication, 'update'); const { ownerService, effective, policyDataset } = await resolveEffectiveDataset( publication.user_id, policy, datasetName, access.rolePermissions ?? null, ); const rowScope = await resolveRowScope( ownerService, effective, policyDataset, access.accessMode === 'login_required' ? { visitorRole: access.visitorRole, visitorUserId: access.session?.visitorUserId, ownerUserId: access.ownerUserId, } : null, ); const result = await ownerService.updateRowForDataset(effective, rowId, payload, { updatedByLabel: req.body?.updated_by_label ?? access.session?.sessionId ?? 'public', rowScope, }); recordPublicLog(resolveWorkspaceRoot(publication.user_id), publication, { datasetName, action: 'update', rowId, req, session: access.session, accessMode: access.accessMode, visitorRole: access.visitorRole, summary: { fields: Object.keys(payload ?? {}) }, }); return result; }); } async function deleteRow(pageId, datasetName, rowId, req) { return runPublicAction(async () => { const publication = await queryPublication(pageId); const policy = await loadPolicy(publication); const deleteAction = policyAllowsAction(policy, datasetName, 'hard_delete') ? 'hard_delete' : 'soft_delete'; const access = resolveAccessContext({ publication, policy, req, action: deleteAction, datasetName, }); enforceRateLimit(req, publication, deleteAction); const { ownerService, effective, policyDataset } = await resolveEffectiveDataset( publication.user_id, policy, datasetName, access.rolePermissions ?? null, ); const rowScope = await resolveRowScope( ownerService, effective, policyDataset, access.accessMode === 'login_required' ? { visitorRole: access.visitorRole, visitorUserId: access.session?.visitorUserId, ownerUserId: access.ownerUserId, } : null, ); const result = deleteAction === 'hard_delete' ? await ownerService.hardDeleteRowForDataset(effective, rowId, { rowScope }) : await ownerService.softDeleteRowForDataset(effective, rowId, { deletedBy: access.session?.sessionId ?? 'public', rowScope, }); recordPublicLog(resolveWorkspaceRoot(publication.user_id), publication, { datasetName, action: deleteAction, rowId, req, session: access.session, accessMode: access.accessMode, visitorRole: access.visitorRole, summary: null, }); return result; }); } function syncPolicyIndex(policy) { const pool = getPool(); if (!pool || !policy) return null; return upsertPageDataPolicyIndex(pool, policy).catch(() => null); } async function getOwnerPolicy(user, pageId) { if (!user?.id) throw Object.assign(new Error('未授权'), { code: 'unauthorized', status: 401 }); const workspaceRoot = user.workspaceRoot ?? resolveWorkspaceRoot(user.id); const policy = readPageAccessPolicy(workspaceRoot, pageId); if (!policy) return null; if (policy.ownerUserId !== user.id) { throw Object.assign(new Error('无权访问该页面策略'), { code: 'forbidden', status: 403 }); } return policy; } async function saveOwnerPolicy(user, pageId, policyInput) { if (!user?.id) throw Object.assign(new Error('未授权'), { code: 'unauthorized', status: 401 }); const publication = await queryPublication(pageId).catch(() => null); if (!publication || publication.user_id !== user.id) { throw Object.assign(new Error('页面不存在或未发布'), { code: 'publication_not_found', status: 404 }); } const workspaceRoot = user.workspaceRoot ?? resolveWorkspaceRoot(user.id); const policy = normalizePageAccessPolicy( { ...policyInput, pageId, ownerUserId: user.id, accessMode: policyInput.accessMode ?? publication.access_mode, }, { fallbackPageId: pageId, fallbackOwnerUserId: user.id }, ); const saved = writePageAccessPolicy(workspaceRoot, policy); await syncPolicyIndex(saved); return saved; } async function saveOwnerPolicyFromPublish(user, pageId, { datasetName, capabilities = {} } = {}) { if (!user?.id) throw Object.assign(new Error('未授权'), { code: 'unauthorized', status: 401 }); const publication = await queryPublication(pageId).catch(() => null); if (!publication || publication.user_id !== user.id) { throw Object.assign(new Error('页面不存在或未发布'), { code: 'publication_not_found', status: 404 }); } const normalizedDatasetName = String(datasetName ?? '').trim(); if (!normalizedDatasetName) { throw Object.assign(new Error('缺少 dataset 名称'), { code: 'invalid_request', status: 400 }); } const ownerService = createOwnerService(user.id); const registryDataset = await ownerService.getDataset(normalizedDatasetName); if (!registryDataset) { throw Object.assign(new Error('dataset 未注册'), { code: 'dataset_not_found', status: 404 }); } const workspaceRoot = user.workspaceRoot ?? resolveWorkspaceRoot(user.id); const policy = buildPageDataPolicyFromPublishInput({ pageId, ownerUserId: user.id, accessMode: publication.access_mode, datasetName: registryDataset.name, registryDataset, capabilities, }); const saved = writePageAccessPolicy(workspaceRoot, policy); await syncPolicyIndex(saved); return saved; } async function listOwnerPolicies(user) { if (!user?.id) throw Object.assign(new Error('未授权'), { code: 'unauthorized', status: 401 }); const pool = getPool(); if (!pool) return []; return listPageDataPolicyIndexByOwner(pool, user.id); } async function getPageOpsOverview(user, pageId) { if (!user?.id) throw Object.assign(new Error('未授权'), { code: 'unauthorized', status: 401 }); const publication = await queryPublication(pageId); if (publication.user_id !== user.id) { throw Object.assign(new Error('无权访问该页面运维信息'), { code: 'forbidden', status: 403 }); } const workspaceRoot = user.workspaceRoot ?? resolveWorkspaceRoot(user.id); const policy = readPageAccessPolicy(workspaceRoot, pageId); if (!policy) { throw Object.assign(new Error('页面数据策略未配置'), { code: 'policy_not_found', status: 404 }); } const ownerService = createOwnerService(user.id); const datasets = []; for (const [name, config] of Object.entries(policy.datasets ?? {})) { let stats = null; try { if (await ownerService.getDataset(name)) { stats = await ownerService.getDatasetStats(name); } } catch { stats = null; } datasets.push({ name, read: config.read, insert: config.insert, update: config.update, softDelete: config.softDelete, closed: Boolean(config.closed), closedAt: config.closedAt ?? null, stats, }); } const logs = listPageDataLogs(workspaceRoot, pageId, { limit: 50, offset: 0 }); const recentSubmissions = logs.filter((entry) => entry.action === 'insert').slice(0, 10); const activeSessions = sessionStore.listByPageId(pageId); const index = await getPageDataPolicyIndex(getPool(), pageId); const registeredDatasets = (await ownerService.listDatasets()).map((dataset) => dataset.name); const configuredNames = new Set(Object.keys(policy.datasets ?? {})); const unconfiguredDatasets = registeredDatasets.filter((name) => !configuredNames.has(name)); return { pageId, publication: { id: publication.id, pageId: publication.page_id, accessMode: publication.access_mode, status: publication.status, }, policy, index, registeredDatasets, unconfiguredDatasets, datasets, recentLogs: logs.slice(0, 20), recentSubmissions, activeSessionCount: activeSessions.length, activeSessions, }; } async function revokePageDataTokens(user, pageId, { token = null, revokeAll = false } = {}) { if (!user?.id) throw Object.assign(new Error('未授权'), { code: 'unauthorized', status: 401 }); const publication = await queryPublication(pageId); if (publication.user_id !== user.id) { throw Object.assign(new Error('无权撤销该页面令牌'), { code: 'forbidden', status: 403 }); } let revokedCount = 0; if (revokeAll) { revokedCount = sessionStore.revokeByPageId(pageId); } else if (token) { revokedCount = sessionStore.revoke(token) ? 1 : 0; } else { throw Object.assign(new Error('请提供 token 或设置 revokeAll'), { code: 'invalid_request', status: 400 }); } const workspaceRoot = user.workspaceRoot ?? resolveWorkspaceRoot(user.id); appendPageDataLog(workspaceRoot, pageId, { pageId, action: 'revoke_tokens', dataset: null, rowId: null, accessMode: publication.access_mode, actor: { type: 'owner', userId: user.id, role: 'owner' }, summary: { revokedCount, revokeAll: Boolean(revokeAll) }, }); return { pageId, revokedCount, revokeAll: Boolean(revokeAll) }; } async function resetPageDataPassword(user, pageId, password) { if (!user?.id) throw Object.assign(new Error('未授权'), { code: 'unauthorized', status: 401 }); const publication = await queryPublication(pageId); if (publication.user_id !== user.id) { throw Object.assign(new Error('无权重置该页面口令'), { code: 'forbidden', status: 403 }); } if (publication.access_mode !== 'password') { throw Object.assign(new Error('当前页面不是口令访问模式'), { code: 'auth_not_required', status: 400 }); } const normalizedPassword = publicationInternals.normalizePassword(password, true); const passwordHash = publicationInternals.hashPassword(normalizedPassword); const pool = getPool(); if (!pool) { throw Object.assign(new Error('数据库未配置'), { code: 'feature_disabled', status: 503 }); } await pool.query( `UPDATE h5_publish_records SET password_hash = ?, updated_at = ? WHERE id = ? AND user_id = ?`, [passwordHash, Date.now(), publication.id, user.id], ); const revokedCount = sessionStore.revokeByPageId(pageId); const workspaceRoot = user.workspaceRoot ?? resolveWorkspaceRoot(user.id); appendPageDataLog(workspaceRoot, pageId, { pageId, action: 'password_reset', dataset: null, rowId: null, accessMode: publication.access_mode, actor: { type: 'owner', userId: user.id, role: 'owner' }, summary: { revokedSessions: revokedCount }, }); return { pageId, passwordReset: true, revokedSessions: revokedCount }; } async function closePageDataset(user, pageId, datasetName) { if (!user?.id) throw Object.assign(new Error('未授权'), { code: 'unauthorized', status: 401 }); const publication = await queryPublication(pageId); if (publication.user_id !== user.id) { throw Object.assign(new Error('无权关闭该页面 dataset'), { code: 'forbidden', status: 403 }); } const workspaceRoot = user.workspaceRoot ?? resolveWorkspaceRoot(user.id); const policy = readPageAccessPolicy(workspaceRoot, pageId); if (!policy) { throw Object.assign(new Error('页面数据策略未配置'), { code: 'policy_not_found', status: 404 }); } const closedPolicy = closePolicyDataset(policy, datasetName); const saved = writePageAccessPolicy(workspaceRoot, closedPolicy); await syncPolicyIndex(saved); appendPageDataLog(workspaceRoot, pageId, { pageId, action: 'close_dataset', dataset: datasetName, rowId: null, accessMode: publication.access_mode, actor: { type: 'owner', userId: user.id, role: 'owner' }, summary: null, }); return { pageId, dataset: datasetName, closed: true, policyDataset: saved.datasets[datasetName], }; } return { authenticate, listRows, getSchema, getStats, insertRow, updateRow, deleteRow, softDeleteRow: deleteRow, getOwnerPolicy, saveOwnerPolicy, saveOwnerPolicyFromPublish, listOwnerPolicies, getPageOpsOverview, revokePageDataTokens, resetPageDataPassword, closePageDataset, queryPublication, }; }