merge: server architecture modularization
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { createAgentRunGateway } from '../agent-run-gateway.mjs';
|
||||
import { scanWorkspaceFilesForProhibitedBrowserStorage } from '../mindspace-browser-storage-policy.mjs';
|
||||
import { evaluatePageDataHtmlContent } from '../mindspace-page-data-finish-guard.mjs';
|
||||
import { normalizeWorkspaceRelativePath } from '../mindspace-pages.mjs';
|
||||
import { resolveMindSpaceUserPublishDir } from '../mindspace-runtime-config.mjs';
|
||||
import { policyAllowsAction } from '../page-access-policy.mjs';
|
||||
import { detectPageDataDatasetUsageFromHtml } from '../page-data-html-detect.mjs';
|
||||
import { readPageAccessPolicy } from '../page-data-policy-store.mjs';
|
||||
import { createTkmindProxy } from '../tkmind-proxy.mjs';
|
||||
import { createToolGateway } from '../tool-gateway.mjs';
|
||||
|
||||
function isEnabledFlag(value, fallback = '') {
|
||||
return ['1', 'true', 'yes', 'on'].includes(
|
||||
String(value ?? fallback).trim().toLowerCase(),
|
||||
);
|
||||
}
|
||||
|
||||
export function createPortalRunDeliverablesValidator({
|
||||
h5Root,
|
||||
resolveMindSpaceUserPublishDirFn =
|
||||
resolveMindSpaceUserPublishDir,
|
||||
normalizeWorkspaceRelativePathFn =
|
||||
normalizeWorkspaceRelativePath,
|
||||
evaluatePageDataHtmlContentFn =
|
||||
evaluatePageDataHtmlContent,
|
||||
readPageAccessPolicyFn = readPageAccessPolicy,
|
||||
detectPageDataDatasetUsageFromHtmlFn =
|
||||
detectPageDataDatasetUsageFromHtml,
|
||||
policyAllowsActionFn = policyAllowsAction,
|
||||
scanWorkspaceFilesForProhibitedBrowserStorageFn =
|
||||
scanWorkspaceFilesForProhibitedBrowserStorage,
|
||||
resolvePathFn = path.resolve,
|
||||
pathSeparator = path.sep,
|
||||
existsSyncFn = fs.existsSync,
|
||||
readFileSyncFn = fs.readFileSync,
|
||||
} = {}) {
|
||||
if (!h5Root) {
|
||||
throw new Error(
|
||||
'createPortalRunDeliverablesValidator requires h5Root',
|
||||
);
|
||||
}
|
||||
|
||||
return async function validateRunDeliverables({
|
||||
userId,
|
||||
deliverables,
|
||||
}) {
|
||||
const publishDir =
|
||||
resolveMindSpaceUserPublishDirFn(h5Root, {
|
||||
id: userId,
|
||||
});
|
||||
const resolvedPublishDir =
|
||||
resolvePathFn(publishDir);
|
||||
const pageDataErrors = [];
|
||||
|
||||
for (const page of deliverables?.pages ?? []) {
|
||||
const relativePath =
|
||||
normalizeWorkspaceRelativePathFn(
|
||||
page.workspaceRelativePath,
|
||||
);
|
||||
if (!relativePath?.startsWith('public/')) continue;
|
||||
const filePath = resolvePathFn(
|
||||
publishDir,
|
||||
relativePath,
|
||||
);
|
||||
if (
|
||||
!filePath.startsWith(
|
||||
`${resolvedPublishDir}${pathSeparator}`,
|
||||
) ||
|
||||
!existsSyncFn(filePath)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const html = readFileSyncFn(filePath, 'utf8');
|
||||
const evaluation =
|
||||
evaluatePageDataHtmlContentFn(html, {
|
||||
relativePath,
|
||||
});
|
||||
if (!evaluation.usesPageDataApi) continue;
|
||||
|
||||
for (const issue of evaluation.issues) {
|
||||
pageDataErrors.push({
|
||||
code: issue,
|
||||
message: `${relativePath} Page Data HTML 不可交付:${issue}`,
|
||||
});
|
||||
}
|
||||
const policy = page.pageId
|
||||
? readPageAccessPolicyFn(
|
||||
publishDir,
|
||||
page.pageId,
|
||||
)
|
||||
: null;
|
||||
for (const [dataset, actions] of
|
||||
detectPageDataDatasetUsageFromHtmlFn(html)) {
|
||||
for (const action of ['read', 'insert']) {
|
||||
if (
|
||||
actions?.[action] &&
|
||||
!policyAllowsActionFn(
|
||||
policy,
|
||||
dataset,
|
||||
action,
|
||||
)
|
||||
) {
|
||||
pageDataErrors.push({
|
||||
code: 'page_data_policy_action_missing',
|
||||
message: `${relativePath} 的 ${dataset}.${action} 未获最终 policy 授权或 dataset 已关闭`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const violations =
|
||||
scanWorkspaceFilesForProhibitedBrowserStorageFn({
|
||||
publishDir,
|
||||
relativePaths: (deliverables?.pages ?? [])
|
||||
.map((page) => page.workspaceRelativePath)
|
||||
.filter(Boolean),
|
||||
});
|
||||
return {
|
||||
errors: [
|
||||
...pageDataErrors,
|
||||
...violations.map((violation) => ({
|
||||
code: 'browser_storage_forbidden',
|
||||
message: `${violation.relativePath} 使用 ${violation.apis.join(', ')}`,
|
||||
})),
|
||||
],
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function bootstrapPortalGatewayServices({
|
||||
pool,
|
||||
h5Root,
|
||||
env = process.env,
|
||||
apiTarget,
|
||||
apiTargets,
|
||||
apiSecret,
|
||||
userAuth,
|
||||
sessionAccess,
|
||||
sessionStreamStore,
|
||||
llmProviderService,
|
||||
subscriptionService,
|
||||
sessionSnapshotService,
|
||||
conversationMemoryService,
|
||||
memoryV2,
|
||||
systemDisclosurePolicyService,
|
||||
mindSpaceAssets,
|
||||
directChatService,
|
||||
chatIntentRouter,
|
||||
syncUserGeneratedPages,
|
||||
isSessionPageDeliveryActive,
|
||||
createTkmindProxyFn = createTkmindProxy,
|
||||
createToolGatewayFn = createToolGateway,
|
||||
createAgentRunGatewayFn = createAgentRunGateway,
|
||||
createRunDeliverablesValidatorFn =
|
||||
createPortalRunDeliverablesValidator,
|
||||
readAssetFileFn = fs.promises.readFile,
|
||||
} = {}) {
|
||||
if (
|
||||
!pool ||
|
||||
!h5Root ||
|
||||
!userAuth ||
|
||||
!sessionAccess ||
|
||||
!llmProviderService ||
|
||||
typeof syncUserGeneratedPages !== 'function' ||
|
||||
typeof isSessionPageDeliveryActive !== 'function'
|
||||
) {
|
||||
throw new Error(
|
||||
'bootstrapPortalGatewayServices requires gateway dependencies',
|
||||
);
|
||||
}
|
||||
|
||||
// GOOSED PROXY BOUNDARY: H5 chat → goosed unique entry.
|
||||
const tkmindProxy = createTkmindProxyFn({
|
||||
apiTarget,
|
||||
apiTargets,
|
||||
apiSecret,
|
||||
userAuth,
|
||||
sessionAccess,
|
||||
sessionStreamStore,
|
||||
llmProviderService,
|
||||
subscriptionService,
|
||||
sessionSnapshotService,
|
||||
conversationMemoryService,
|
||||
memoryV2,
|
||||
systemDisclosurePolicyService,
|
||||
localFetchAsset: mindSpaceAssets
|
||||
? async (userId, assetId) => {
|
||||
const { asset, path: assetPath } =
|
||||
await mindSpaceAssets.readAsset(
|
||||
userId,
|
||||
assetId,
|
||||
);
|
||||
const buffer =
|
||||
await readAssetFileFn(assetPath);
|
||||
return {
|
||||
buffer,
|
||||
mimeType: asset.mimeType,
|
||||
};
|
||||
}
|
||||
: null,
|
||||
});
|
||||
const toolGateway = createToolGatewayFn({
|
||||
llmProviderService,
|
||||
});
|
||||
const validateRunDeliverables =
|
||||
createRunDeliverablesValidatorFn({ h5Root });
|
||||
const agentRunGateway = createAgentRunGatewayFn({
|
||||
pool,
|
||||
userAuth,
|
||||
sessionAccess,
|
||||
tkmindProxy,
|
||||
toolGateway,
|
||||
directChatService,
|
||||
systemDisclosurePolicyService,
|
||||
chatIntentRouter,
|
||||
sessionSnapshotService,
|
||||
conversationMemoryService,
|
||||
observePersonalMemoryOnSuccess: async ({
|
||||
userId,
|
||||
sessionId,
|
||||
userMessage,
|
||||
}) => {
|
||||
if (!memoryV2?.observePersonalMemory) return;
|
||||
await memoryV2.observePersonalMemory({
|
||||
userId,
|
||||
sessionId,
|
||||
messages: [userMessage],
|
||||
});
|
||||
},
|
||||
syncUserPagesOnSuccess: async ({
|
||||
userId,
|
||||
sessionId,
|
||||
runStartedAtMs,
|
||||
}) =>
|
||||
syncUserGeneratedPages(userId, {
|
||||
sessionId,
|
||||
sinceMs: runStartedAtMs,
|
||||
}),
|
||||
isSessionExternallyBusy: ({ sessionId }) =>
|
||||
isSessionPageDeliveryActive(sessionId),
|
||||
validateRunDeliverables,
|
||||
autoDispatch: isEnabledFlag(
|
||||
env.MEMIND_AGENT_RUN_AUTODISPATCH,
|
||||
'1',
|
||||
),
|
||||
maxConcurrentRuns: Number(
|
||||
env.MEMIND_AGENT_RUN_QUEUE_CONCURRENCY ?? 1,
|
||||
),
|
||||
runTimeoutMs: Number(
|
||||
env.MEMIND_AGENT_RUN_TIMEOUT_MS ??
|
||||
15 * 60 * 1000,
|
||||
),
|
||||
});
|
||||
|
||||
return {
|
||||
tkmindProxy,
|
||||
toolGateway,
|
||||
agentRunGateway,
|
||||
validateRunDeliverables,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user