feat(mindspace): enforce PostgreSQL user data delivery
This commit is contained in:
+58
-4
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user