feat(mindspace): enforce PostgreSQL user data delivery

This commit is contained in:
john
2026-07-13 15:29:29 +08:00
parent b33c943b69
commit a6620fb719
57 changed files with 2779 additions and 164 deletions
+58 -4
View File
@@ -14,6 +14,7 @@ import {
} from './auth.mjs';
import { createDbPool, initSchema, isDatabaseConfigured } from './db.mjs';
import { createWorkspacePageDeliverService } from './mindspace-workspace-page-deliver.mjs';
import { createUserDataSpaceService } from './user-data-space-service.mjs';
import { createToolGateway } from './tool-gateway.mjs';
import { createAgentRunGateway } from './agent-run-gateway.mjs';
import {
@@ -164,6 +165,7 @@ import {
registerChatDocxArtifactForConversation,
} from './mindspace-chat-docx-package.mjs';
import { scanContent } from './mindspace-content-scan.mjs';
import { scanWorkspaceFilesForProhibitedBrowserStorage } from './mindspace-browser-storage-policy.mjs';
import { renderImageAssetViewerHtml, wantsInlineImageViewer } from './mindspace-asset-preview.mjs';
import { DEFAULT_IMAGE_UPLOAD_MAX_BYTES } from './user-image-normalize.mjs';
import { createRechargeService } from './billing-recharge.mjs';
@@ -535,6 +537,10 @@ async function bootstrapUserAuth() {
h5Root: __dirname,
defaultSignupBalanceCents: Number(process.env.H5_SIGNUP_BALANCE_CENTS ?? 500),
subscriptionService,
provisionUserDataSpace: async ({ userId, workspaceRoot }) => {
const service = createUserDataSpaceService({ workspaceRoot, userId, query: pool });
return service.ensureReady();
},
});
sessionAccess = createSessionAccess({ userAuth, enabled: isSessionBrokerEnabled() });
if (sessionAccess.enabled) {
@@ -622,6 +628,12 @@ async function bootstrapUserAuth() {
expireStaleUploadsIntervalMs: 5 * 60 * 1000,
});
await userAuth.ensureAdminUser();
const userDataSpaceBackfill = await userAuth.ensureAllUserDataSpaces();
if (userDataSpaceBackfill.errors.length > 0) {
console.warn(
`[PageData] PG space backfill incomplete: ${userDataSpaceBackfill.errors.length} user(s) failed`,
);
}
llmProviderService = createLlmProviderService(pool, {
apiTarget: API_TARGET,
apiTargets: API_TARGETS,
@@ -717,8 +729,24 @@ async function bootstrapUserAuth() {
messages: [userMessage],
});
},
syncUserPagesOnSuccess: async ({ userId }) => syncUserGeneratedPages(userId),
syncUserPagesOnSuccess: async ({ userId, sessionId, runStartedAtMs }) =>
syncUserGeneratedPages(userId, { sessionId, sinceMs: runStartedAtMs }),
isSessionExternallyBusy: ({ sessionId }) => Number(sessionPageDeliveryLocks.get(sessionId) ?? 0) > 0,
validateRunDeliverables: async ({ userId, deliverables }) => {
const publishDir = resolveMindSpaceUserPublishDir(__dirname, { id: userId });
const violations = scanWorkspaceFilesForProhibitedBrowserStorage({
publishDir,
relativePaths: (deliverables?.pages ?? [])
.map((page) => page.workspaceRelativePath)
.filter(Boolean),
});
return {
errors: violations.map((violation) => ({
code: 'browser_storage_forbidden',
message: `${violation.relativePath} 使用 ${violation.apis.join(', ')}`,
})),
};
},
autoDispatch: ['1', 'true', 'yes', 'on'].includes(
String(process.env.MEMIND_AGENT_RUN_AUTODISPATCH ?? '1').trim().toLowerCase(),
),
@@ -3231,10 +3259,36 @@ async function resolveExistingSavedPage(userId, { sessionId, messageId, relative
const SAVE_TARGET_CATEGORIES = new Set(['draft', 'oa', 'public']);
// REGRESSION GUARD: mindspace-page-sync-thumbnail — remote 也经 pageSyncService RPC 同步 public HTML
async function syncUserGeneratedPages(userId) {
async function listSessionPublicHtmlRelativePaths(userId, sessionId, { sinceMs = null } = {}) {
if (!authPool || !userId || !sessionId) return [];
const sinceClause = sinceMs == null ? '' : 'AND ca.created_at >= ?';
const params = sinceMs == null ? [userId, sessionId] : [userId, sessionId, sinceMs];
const [rows] = await authPool.query(
`SELECT ca.display_name
FROM h5_conversation_artifacts ca
JOIN h5_conversation_packages cp ON cp.id = ca.package_id
WHERE cp.user_id = ?
AND cp.session_id = ?
AND ca.artifact_kind = 'public_html'
${sinceClause}
ORDER BY ca.sort_order ASC, ca.created_at ASC`,
params,
);
return [...new Set((rows ?? [])
.map((row) => normalizeWorkspaceRelativePath(`public/${String(row.display_name ?? '').trim()}`))
.filter((relativePath) => relativePath?.startsWith('public/') && relativePath.toLowerCase().endsWith('.html')))];
}
async function syncUserGeneratedPages(userId, { sessionId = null, sinceMs = null } = {}) {
if (!userId) return;
// Agent-run/Finish delivery must stay scoped to the current conversation.
// A stale Page Data page elsewhere in the user's workspace must not turn a
// successfully completed current task into a failed run.
const pageDataRelativePaths = sessionId
? await listSessionPublicHtmlRelativePaths(userId, sessionId, { sinceMs })
: null;
if (workspacePageDeliver?.syncAndDeliver) {
return await workspacePageDeliver.syncAndDeliver(userId);
return await workspacePageDeliver.syncAndDeliver(userId, { pageDataRelativePaths });
}
if (!mindSpacePageSync) return;
return await mindSpacePageSync.syncUserGeneratedPages(userId);
@@ -5099,7 +5153,7 @@ api.get('/sessions/:sessionId/events', async (req, res, next) => {
.filter(Boolean)
.join('\n')
: '';
await syncUserGeneratedPages(uid);
await syncUserGeneratedPages(uid, { sessionId: sid });
await maybeRepairPageDataAfterFinish({
sessionId: sid,
userId: uid,