Files

1609 lines
54 KiB
JavaScript
Raw Permalink Normal View History

import express from 'express';
import fs from 'node:fs';
import { createProxyMiddleware } from 'http-proxy-middleware';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createAuthManager } from './auth.mjs';
import { createDbPool, isDatabaseConfigured } from './db.mjs';
import { attachMindSpaceImageGenerationRoutes } from './mindspace-image-generation-routes.mjs';
import { sanitizeSessionConversationPublicHtmlLinks } from './tkmind-proxy.mjs';
import { createWikiAuth } from './wiki-auth.mjs';
import { isLocalDevHostname } from './scripts/local-test-config.mjs';
import { PUBLISH_ROOT_DIR } from './user-publish.mjs';
import { startWorkspaceThumbnailWatcher } from './mindspace-workspace-thumbnails.mjs';
2026-07-20 20:19:52 +08:00
import {
buildProductAnalyticsContext,
resolveMindSpaceAnalyticsConfig,
resolveProductAnalyticsConfig,
sendMindSpaceAnalyticsEvent,
} from './mindspace-analytics.mjs';
import { startWorkspaceAssetSyncWatcher } from './mindspace-workspace-sync.mjs';
import { attachRequestId, sendData, sendError } from './api-response.mjs';
import { attachPortalAgentJobRoutes } from './server/portal-agent-job-routes.mjs';
import { attachPortalAgentRuntimeRoutes } from './server/portal-agent-runtime-routes.mjs';
import {
attachPortalBlockedWordsRoute,
attachPortalImageMakeRuntimeConfigRoute,
} from './server/portal-config-routes.mjs';
import {
createPortalAccessShadowReporter,
resolvePortalAccessEnforcementConfig,
resolvePortalAccessPolicyMode,
resolvePortalAccessShadowLogIntervalMs,
} from './server/portal-access-policy.mjs';
import { createPortalApiAuthMiddleware } from './server/portal-api-auth-middleware.mjs';
import { attachPortalAccountFeedbackRoutes } from './server/portal-account-feedback-routes.mjs';
import { attachPortalBillingRoutes } from './server/portal-billing-routes.mjs';
import { attachPortalCoreAuthRoutes } from './server/portal-core-auth-routes.mjs';
import { attachPortalWechatAuthRoutes } from './server/portal-wechat-auth-routes.mjs';
import { attachPortalWebhookRoutes } from './server/portal-webhook-routes.mjs';
import { attachPortalWikiRoutes } from './server/portal-wiki-routes.mjs';
import { bootstrapPortalAgentServices } from './server/portal-agent-services-bootstrap.mjs';
import { bootstrapPortalAuthServices } from './server/portal-auth-services-bootstrap.mjs';
import { bootstrapPortalDomainServices } from './server/portal-domain-services-bootstrap.mjs';
import { bootstrapPortalGatewayServices } from './server/portal-gateway-services-bootstrap.mjs';
import { bootstrapPortalIntegrationServices } from './server/portal-integration-services-bootstrap.mjs';
import { bootstrapPortalMemorySessionServices } from './server/portal-memory-session-services-bootstrap.mjs';
import { attachPortalNotificationRoutes } from './server/portal-notification-routes.mjs';
import { attachPortalMindSpaceAssetDeliveryRoutes } from './server/portal-mindspace-asset-delivery-routes.mjs';
import { attachPortalMindSpaceAgentOperationRoutes } from './server/portal-mindspace-agent-operation-routes.mjs';
import { attachPortalMindSpaceAssetRoutes } from './server/portal-mindspace-asset-routes.mjs';
import { attachPortalMindSpaceChatSaveRoutes } from './server/portal-mindspace-chat-save-routes.mjs';
import { attachPortalMindSpaceChatShareRoutes } from './server/portal-mindspace-chat-share-routes.mjs';
import { attachPortalMindSpacePageCoreRoutes } from './server/portal-mindspace-page-core-routes.mjs';
import { attachPortalMindSpacePagePublishRoutes } from './server/portal-mindspace-page-publish-routes.mjs';
import { attachPortalMindSpaceSpaceRoutes } from './server/portal-mindspace-space-routes.mjs';
import { attachPortalTemplateCatalogRoutes } from './server/portal-template-catalog-routes.mjs';
import { attachPortalPlazaDiscoveryRoutes } from './server/portal-plaza-discovery-routes.mjs';
import { attachPortalPlazaRoutes } from './server/portal-plaza-routes.mjs';
import { attachPortalPublicationRoutes } from './server/portal-publication-routes.mjs';
import { createPortalPublishedPageDelivery } from './server/portal-published-page-delivery.mjs';
import {
removeQueryParam,
resolveRequestOrigin,
} from './server/portal-publication-shell.mjs';
import { attachPortalRuntimeRoutes } from './server/portal-runtime-routes.mjs';
import { attachPortalStaticDeliveryRoutes } from './server/portal-static-delivery-routes.mjs';
import { createPortalWorkspacePublicationDelivery } from './server/portal-workspace-publication-delivery.mjs';
import { attachPortalSeoDiscoveryRoutes } from './server/portal-seo-discovery-routes.mjs';
import { createPortalAuthSessionHelpers } from './server/portal-auth-session-helpers.mjs';
import { createPortalSessionCoordinator } from './server/portal-session-coordinator.mjs';
import { attachPortalSessionRoutes } from './server/portal-session-routes.mjs';
import { attachPortalUserMemoryRoutes } from './server/portal-user-memory-routes.mjs';
import { attachPortalGoalRunRoutes } from './server/portal-goal-run-routes.mjs';
import { assertMindSpaceRoute, mindspaceFlags } from './mindspace-flags.mjs';
import { loadMindSpaceConfigCached } from './mindspace-config.mjs';
import { createMindspaceSeoDiscoveryService } from './mindspace-seo-discovery-service.mjs';
import { normalizeWorkspaceRelativePath } from './mindspace-pages.mjs';
import {
buildMindSpacePublicUrlForUser,
resolveMindSpacePublishRoot,
resolveMindSpaceServerRuntimeOptions,
resolvePortalH5Root,
} from './mindspace-runtime-config.mjs';
import {
publicationInternals,
} from './mindspace-publications.mjs';
import { mapPlazaError } from './plaza-posts.mjs';
import { createNoopPlazaRedis } from './plaza-redis.mjs';
import {
buildChatSavePreviewFrameUrl,
buildChatSaveThumbnailUrl,
buildWorkspaceAssetUrl,
buildWorkspaceThumbnailUrl,
resolveChatSaveAnalysis,
} from './mindspace-chat-save.mjs';
import {
normalizePublicHtmlRelativePath,
} from './mindspace-public-finish-sync.mjs';
import { resolvePlazaPostPath, resolvePlazaPublicBase } from './src/utils/public-site-bases.mjs';
import { DEFAULT_IMAGE_UPLOAD_MAX_BYTES } from './user-image-normalize.mjs';
import { loadWechatMpConfig } from './wechat-mp-config.mjs';
import {
filterUserVisibleConversation,
repairSessionConversationFromDb,
restoreConversationUserMessagesFromAgentRunsFailOpen,
} from './conversation-repair.mjs';
import { attachAsrRoutes } from './asr-proxy.mjs';
import { attachPageDataRoutes, isLegacyPageDataApiPath } from './page-data-routes.mjs';
import { isPageDataPublicPath } from './page-data-public-service.mjs';
import { attachShenmeiOpinionFormRoutes } from './shenmei-opinion-form-routes.mjs';
import { isNativeH5ApiPath } from './policies.mjs';
import {
applyMemindRuntimeProfile,
describeMemindRuntimeProfile,
loadMemindEnvFiles,
} from './scripts/memind-runtime-profile.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
loadMemindEnvFiles(__dirname);
const H5_ROOT = resolvePortalH5Root(__dirname, process.env);
if (!fs.existsSync(H5_ROOT) || !fs.statSync(H5_ROOT).isDirectory()) {
throw new Error(
`MEMIND_PORTAL_H5_ROOT must reference an existing directory: ${H5_ROOT}`,
);
}
if (H5_ROOT !== __dirname) {
console.log(`[Portal] Code root: ${__dirname}`);
console.log(`[Portal] Persistent H5 root: ${H5_ROOT}`);
loadMemindEnvFiles(H5_ROOT);
}
applyMemindRuntimeProfile({ rootDir: H5_ROOT });
2026-06-27 21:31:02 +08:00
function parseApiTargets() {
const csvTargets = (process.env.TKMIND_API_TARGETS ?? '')
.split(',')
.map((value) => value.trim())
.filter(Boolean);
const legacyTargets = [
process.env.TKMIND_API_TARGET ?? 'https://127.0.0.1:18006',
process.env.TKMIND_API_TARGET_1,
].filter(Boolean);
return [...new Set([...csvTargets, ...legacyTargets])];
}
const PORT = Number(process.env.H5_PORT ?? 8081);
const HOST = String(process.env.H5_HOST ?? '127.0.0.1').trim() || '127.0.0.1';
2026-06-27 21:31:02 +08:00
const API_TARGETS = parseApiTargets();
const API_TARGET = API_TARGETS[0] ?? 'https://127.0.0.1:18006';
const API_SECRET = process.env.TKMIND_SERVER__SECRET_KEY ?? 'local-dev-secret';
const INTERNAL_AGENT_SECRET = process.env.MINDSPACE_INTERNAL_AGENT_SECRET ?? API_SECRET;
2026-07-24 19:18:14 +08:00
const DEEP_SEARCH_INTERNAL_SECRET =
process.env.TKMIND_DEEP_SEARCH_SECRET ?? INTERNAL_AGENT_SECRET;
const portalAccessPolicyMode = resolvePortalAccessPolicyMode(process.env);
const portalAccessEnforcementConfig = resolvePortalAccessEnforcementConfig(process.env);
const portalAccessShadowReporter = createPortalAccessShadowReporter({
intervalMs: resolvePortalAccessShadowLogIntervalMs(process.env),
emit: (payload) => {
console.warn('[Auth][PortalAccessShadow]', JSON.stringify(payload));
},
});
if (portalAccessEnforcementConfig.killSwitch) {
console.warn('[Auth][PortalAccessEnforce] kill switch active; all policy groups use legacy auth');
} else if (portalAccessEnforcementConfig.enabled) {
console.log(
`[Auth][PortalAccessEnforce] groups enabled: ${portalAccessEnforcementConfig.activeGroups.join(', ')}`,
);
}
const mindSpaceServerRuntime = resolveMindSpaceServerRuntimeOptions(H5_ROOT, process.env);
const ACCESS_PASSWORD = process.env.H5_ACCESS_PASSWORD;
const WECHAT_MP_CONFIG = loadWechatMpConfig();
// 无状态前端节点(如 105,MindSpace 经 rclone 挂载)需设 MEMIND_WORKSPACE_MAINTENANCE=0,
// 否则启动时对挂载树做 readdir/递归 fs.watch 会占满 libuv 线程池导致 boot 卡死。
const WORKSPACE_MAINTENANCE_ENABLED = process.env.MEMIND_WORKSPACE_MAINTENANCE !== '0';
const USERS_ROOT = process.env.H5_USERS_ROOT ?? path.join(H5_ROOT, 'users');
const app = express();
app.set('trust proxy', 1);
const isSecureRequest = (req) =>
req.secure || req.get('x-forwarded-proto')?.split(',')[0]?.trim() === 'https';
const msFlags = mindspaceFlags();
app.use(attachRequestId);
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
res.setHeader('Permissions-Policy', 'camera=(), microphone=(self), geolocation=()');
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
if (isSecureRequest(req)) {
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
}
next();
});
function isWechatMiniProgramSource(value) {
if (!value) return false;
try {
return new URL(value).hostname.endsWith('servicewechat.com');
} catch {
return false;
}
}
function csrfOriginCheck(req, res, next) {
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) return next();
const host = req.get('host');
const origin = req.get('origin');
const referer = req.get('referer');
if (!origin && !referer) return next();
const requestHostname = (host ?? '').split(':')[0];
if ([origin, referer].some(isWechatMiniProgramSource)) {
return next();
}
const allowed = [origin, referer].some((value) => {
if (!value) return false;
try {
const source = new URL(value);
if (source.host === host) return true;
return (
isLocalDevHostname(requestHostname) && isLocalDevHostname(source.hostname)
);
} catch {
return false;
}
});
if (!allowed) {
return sendError(res, req, 403, 'csrf_failed', '来源校验失败');
}
return next();
}
app.use('/api', csrfOriginCheck);
app.use('/auth', csrfOriginCheck);
2026-07-02 19:52:26 +08:00
app.use('/mindspace', csrfOriginCheck);
const jsonBody = express.json({ limit: '1mb' });
const jsonUnlessMultipart = (req, res, next) => {
if ((req.headers['content-type'] ?? '').includes('multipart/form-data')) return next();
return jsonBody(req, res, next);
};
const rawUploadBody = express.raw({
type: 'application/octet-stream',
limit: Math.max(mindSpaceServerRuntime.maxFileBytes, DEFAULT_IMAGE_UPLOAD_MAX_BYTES),
});
const wikiAuth = createWikiAuth(path.join(resolveMindSpacePublishRoot(H5_ROOT), 'wiki-db'));
let legacyAuth = null;
if (ACCESS_PASSWORD && !isDatabaseConfigured()) {
legacyAuth = createAuthManager({ password: ACCESS_PASSWORD });
} else if (ACCESS_PASSWORD && isDatabaseConfigured()) {
console.log('H5_ACCESS_PASSWORD ignored: multi-user database auth is configured');
}
let userAuth = null;
let templateCatalogService = null;
let sessionAccess = null;
let sessionStreamStore = null;
let tkmindProxy = null;
let agentRunGateway = null;
let goalRunService = null;
const {
ownsAgentSession,
unregisterAgentSessionForUser,
beginSessionPageDelivery,
endSessionPageDelivery,
isSessionPageDeliveryActive,
} = createPortalSessionCoordinator({
getSessionAccess: () => sessionAccess,
getUserAuth: () => userAuth,
});
let chatIntentRouter = null;
2026-07-02 09:23:55 +08:00
let toolGateway = null;
let assetGatewayConfigService = null;
let imageMakeAdminConfigService = null;
let mindSpaceImageGeneration = null;
let directChatService = null;
let sessionSnapshotService = null;
let conversationMemoryService = null;
2026-07-03 09:46:03 +08:00
let memoryV2 = null;
2026-07-22 12:36:13 +08:00
let episodicMemoryService = null;
2026-07-03 09:46:03 +08:00
let memoryV2ConfigService = null;
let skillRuntimeConfigService = null;
2026-07-23 22:53:15 +08:00
let systemDisclosurePolicyService = null;
let agentCodeRunPolicyService = null;
2026-07-07 12:10:21 +08:00
let wechatScheduleLlmConfigService = null;
let wechatIntentRouter = null;
let mindSpace = null;
let mindSpaceAssets = null;
let mindSpaceAudit = null;
let mindSpaceServiceFacade = null;
2026-07-27 15:34:35 +08:00
let mindSpaceChatSave = null;
let mindSpaceConversationArtifacts = null;
let mindSpacePublicFinish = null;
let mindSpaceWorkspacePublicationDelivery =
null;
2026-07-02 21:32:34 +08:00
let mindSpaceConversationPackageRegistry = null;
let mindSpacePages = null;
let mindSpacePageSync = null;
let mindSpacePageLiveEdit = null;
let mindSpaceAssetAgent = null;
let mindSpacePageEditSession = null;
let mindSpacePublications = null;
let workspacePageDeliver = null;
let plazaPosts = null;
let plazaEvents = null;
let plazaRecommend = null;
let plazaInteractions = null;
let plazaSeo = null;
let plazaOps = null;
let plazaRedis = createNoopPlazaRedis();
let mindSpaceCleanup = null;
let mindSpaceAgentJobs = null;
let mindSpaceAgentRunner = null;
let rechargeService = null;
let subscriptionService = null;
let wechatPayClient = null;
let wechatOAuthService = null;
let wechatMpService = null;
let notificationDispatcher = null;
let scheduleService = null;
let scheduledTaskService = null;
let feedbackService = null;
let scheduleReminderWorker = null;
let scheduledTaskWorker = null;
let llmProviderService = null;
let wordFilterService = null;
let authPool = null;
let pageDataService = null;
let pageDataPublicService = null;
2026-07-16 21:06:28 +08:00
let mindSpaceAnalyticsConfig = resolveMindSpaceAnalyticsConfig();
async function bootstrapUserAuth() {
try {
if (!isDatabaseConfigured()) return false;
const pool = createDbPool();
const domainServices =
await bootstrapPortalDomainServices({
pool,
h5Root: H5_ROOT,
env: process.env,
runtime: mindSpaceServerRuntime,
analyticsConfig: mindSpaceAnalyticsConfig,
getUserAuth: () => userAuth,
getSessionSnapshotService: () =>
sessionSnapshotService,
onMindSearchSchemaReady: () => {
authPool = pool;
},
workspaceMaintenanceEnabled:
WORKSPACE_MAINTENANCE_ENABLED,
logger: console,
});
const {
mindSearchConfigService,
mindSpaceRuntimeAdapter,
resolveUserIdByDirKey,
} = domainServices;
Object.assign(
mindSpaceAnalyticsConfig,
domainServices.mindSpaceAnalyticsConfig,
);
scheduleService = domainServices.scheduleService;
scheduledTaskService = domainServices.scheduledTaskService;
pageDataService = domainServices.pageDataService;
pageDataPublicService =
domainServices.pageDataPublicService;
feedbackService = domainServices.feedbackService;
mindSpace = domainServices.mindSpace;
mindSpaceServiceFacade =
domainServices.mindSpaceServiceFacade;
2026-07-27 15:34:35 +08:00
mindSpaceChatSave =
domainServices.mindSpaceChatSave;
mindSpaceConversationArtifacts =
domainServices.mindSpaceConversationArtifacts;
mindSpacePublicFinish =
domainServices.mindSpacePublicFinish;
mindSpaceWorkspacePublicationDelivery =
domainServices
.mindSpaceWorkspacePublicationDelivery;
mindSpaceConversationPackageRegistry =
domainServices.mindSpaceConversationPackageRegistry;
mindSpaceAssets = domainServices.mindSpaceAssets;
mindSpacePages = domainServices.mindSpacePages;
mindSpacePageSync = domainServices.mindSpacePageSync;
mindSpacePageLiveEdit =
domainServices.mindSpacePageLiveEdit;
mindSpaceAssetAgent =
domainServices.mindSpaceAssetAgent;
mindSpacePublications =
domainServices.mindSpacePublications;
workspacePageDeliver =
domainServices.workspacePageDeliver;
plazaRedis = domainServices.plazaRedis;
plazaSeo = domainServices.plazaSeo;
plazaInteractions = domainServices.plazaInteractions;
plazaEvents = domainServices.plazaEvents;
plazaRecommend = domainServices.plazaRecommend;
plazaPosts = domainServices.plazaPosts;
plazaOps = domainServices.plazaOps;
mindSpaceCleanup = domainServices.mindSpaceCleanup;
const authServices =
await bootstrapPortalAuthServices({
pool,
h5Root: H5_ROOT,
usersRoot: USERS_ROOT,
mindSearchConfigService,
env: process.env,
logger: console,
});
subscriptionService =
authServices.subscriptionService;
const billingConfigService =
authServices.billingConfigService;
userAuth = authServices.userAuth;
sessionAccess = authServices.sessionAccess;
wechatPayClient = authServices.wechatPayClient;
wechatOAuthService =
authServices.wechatOAuthService;
rechargeService = authServices.rechargeService;
templateCatalogService = authServices.templateCatalogService;
const agentServices =
await bootstrapPortalAgentServices({
pool,
env: process.env,
runtime: mindSpaceServerRuntime,
apiTarget: API_TARGET,
apiTargets: API_TARGETS,
apiSecret: API_SECRET,
userAuth,
sessionAccess,
subscriptionService,
mindSpaceRuntimeAdapter,
mindSpaceAssets,
resolveUserIdByDirKey,
workspaceMaintenanceEnabled:
WORKSPACE_MAINTENANCE_ENABLED,
startWorkspaceThumbnailWatcher,
startWorkspaceAssetSyncWatcher,
logger: console,
});
mindSpaceAgentJobs =
agentServices.mindSpaceAgentJobs;
mindSpaceAgentRunner =
agentServices.mindSpaceAgentRunner;
mindSpaceAudit = agentServices.mindSpaceAudit;
llmProviderService =
agentServices.llmProviderService;
assetGatewayConfigService =
agentServices.assetGatewayConfigService;
imageMakeAdminConfigService =
agentServices.imageMakeAdminConfigService;
mindSpaceImageGeneration =
agentServices.mindSpaceImageGeneration;
wordFilterService =
agentServices.wordFilterService;
const memorySessionServices =
await bootstrapPortalMemorySessionServices({
pool,
h5Root: H5_ROOT,
env: process.env,
llmProviderService,
userAuth,
sessionAccess,
logger: console,
});
memoryV2ConfigService =
memorySessionServices.memoryV2ConfigService;
skillRuntimeConfigService =
memorySessionServices.skillRuntimeConfigService;
2026-07-24 19:18:14 +08:00
systemDisclosurePolicyService =
memorySessionServices.systemDisclosurePolicyService;
agentCodeRunPolicyService =
memorySessionServices.agentCodeRunPolicyService;
wechatScheduleLlmConfigService =
memorySessionServices.wechatScheduleLlmConfigService;
wechatIntentRouter =
memorySessionServices.wechatIntentRouter;
conversationMemoryService =
memorySessionServices.conversationMemoryService;
memoryV2 = memorySessionServices.memoryV2;
episodicMemoryService =
memorySessionServices.episodicMemoryService;
sessionSnapshotService =
memorySessionServices.sessionSnapshotService;
sessionStreamStore =
memorySessionServices.sessionStreamStore;
directChatService =
memorySessionServices.directChatService;
chatIntentRouter =
memorySessionServices.chatIntentRouter;
const gatewayServices =
bootstrapPortalGatewayServices({
pool,
h5Root: H5_ROOT,
env: process.env,
apiTarget: API_TARGET,
apiTargets: API_TARGETS,
apiSecret: API_SECRET,
userAuth,
sessionAccess,
sessionStreamStore,
llmProviderService,
subscriptionService,
billingConfigService,
sessionSnapshotService,
conversationMemoryService,
memoryV2,
2026-07-24 19:18:14 +08:00
systemDisclosurePolicyService,
mindSpaceAssets,
2026-07-27 15:34:35 +08:00
getWorkspacePublicationDelivery: () =>
mindSpaceWorkspacePublicationDelivery,
directChatService,
chatIntentRouter,
syncUserGeneratedPages,
isSessionPageDeliveryActive,
});
tkmindProxy = gatewayServices.tkmindProxy;
toolGateway = gatewayServices.toolGateway;
agentRunGateway =
gatewayServices.agentRunGateway;
goalRunService = gatewayServices.goalRunService;
const integrationServices =
await bootstrapPortalIntegrationServices({
pool,
h5Root: H5_ROOT,
2026-07-28 09:33:25 +08:00
codeRoot: __dirname,
env: process.env,
usersRoot: USERS_ROOT,
wechatMpConfig: WECHAT_MP_CONFIG,
2026-07-27 15:34:35 +08:00
mindSpacePublicFinish,
userAuth,
sessionAccess,
tkmindProxy,
scheduleService,
scheduledTaskService,
sessionSnapshotService,
wechatScheduleLlmConfigService,
wechatIntentRouter,
llmProviderService,
chatIntentRouter,
2026-07-24 19:18:14 +08:00
systemDisclosurePolicyService,
sessionSnapshotService,
mindSpaceAnalyticsConfig,
subscriptionService,
apiTarget: API_TARGET,
apiSecret: API_SECRET,
mindSpacePages,
mindSpacePageLiveEdit,
logger: console,
});
wechatMpService =
integrationServices.wechatMpService;
notificationDispatcher =
integrationServices.notificationDispatcher;
scheduleReminderWorker =
integrationServices.scheduleReminderWorker;
scheduledTaskWorker =
integrationServices.scheduledTaskWorker;
mindSpacePageEditSession =
integrationServices.mindSpacePageEditSession;
return true;
} catch (err) {
console.error('User auth bootstrap failed:', err);
if (isDatabaseConfigured() && process.env.NODE_ENV === 'production') {
console.error(
'Fatal: database is configured but user auth bootstrap failed; exiting so launchd can retry',
);
process.exit(1);
}
return false;
}
}
const userAuthReady = bootstrapUserAuth();
const {
legacySessionToken,
userToken,
setUserLoginCookies,
clearUserLoginCookies,
attachUserSession,
} = createPortalAuthSessionHelpers({
isSecureRequest,
getLegacyAuth: () => legacyAuth,
getUserAuth: () => userAuth,
logger: console,
});
app.use(attachUserSession);
// ============ Legacy password auth ============
async function resolveSkillRuntimeForClient() {
if (!skillRuntimeConfigService?.getPublicRuntimeConfig) return null;
try {
return await skillRuntimeConfigService.getPublicRuntimeConfig();
} catch (err) {
console.warn('[SkillRuntime] public config unavailable:', err instanceof Error ? err.message : err);
return null;
}
}
async function resolveAgentCodeRunForClient(userId) {
if (!agentCodeRunPolicyService?.getPublicClientPolicy) return null;
try {
return await agentCodeRunPolicyService.getPublicClientPolicy(userId);
} catch (err) {
console.warn('[AgentCodeRun] public policy unavailable:', err instanceof Error ? err.message : err);
return null;
}
}
async function resolveGoalRunForClient(userId) {
const { isGoalRunEnabledForUser } = await import('./goal-run-intent.mjs');
return { enabled: isGoalRunEnabledForUser(userId, process.env) };
}
attachPortalCoreAuthRoutes({
app,
jsonBody,
userAuthReady,
getUserAuth: () => userAuth,
getLegacyAuth: () => legacyAuth,
userToken,
legacySessionToken,
setUserLoginCookies,
isSecureRequest,
resolveSkillRuntimeForClient,
resolveAgentCodeRunForClient,
resolveGoalRunForClient,
getPlazaSeo: () => plazaSeo,
plazaClientIp,
logger: console,
});
attachPortalWechatAuthRoutes({
app,
jsonBody,
userAuthReady,
getUserAuth: () => userAuth,
getWechatOAuthService: () => wechatOAuthService,
getWechatMpService: () => wechatMpService,
wechatMpConfig: WECHAT_MP_CONFIG,
userToken,
setUserLoginCookies,
isSecureRequest,
getPlazaSeo: () => plazaSeo,
plazaClientIp,
logger: console,
});
attachPortalAccountFeedbackRoutes({
app,
jsonBody,
userAuthReady,
getUserAuth: () => userAuth,
getLegacyAuth: () => legacyAuth,
getSubscriptionService: () => subscriptionService,
getFeedbackService: () => feedbackService,
userToken,
legacySessionToken,
clearUserLoginCookies,
resolveSkillRuntimeForClient,
resolveAgentCodeRunForClient,
logger: console,
});
attachPortalNotificationRoutes({
app,
userAuthReady,
getUserAuth: () => userAuth,
getScheduleService: () => scheduleService,
userToken,
logger: console,
});
attachPortalBillingRoutes({
app,
jsonBody,
userAuthReady,
getUserAuth: () => userAuth,
getRechargeService: () => rechargeService,
getSubscriptionService: () => subscriptionService,
getMindSpace: () => mindSpace,
userToken,
});
const wechatNotifyBody = express.raw({
type: ['application/json', 'text/xml', 'application/xml'],
limit: '64kb',
});
const wechatMpBody = express.text({
type: ['text/xml', 'application/xml'],
limit: '128kb',
});
attachPortalWebhookRoutes({
app,
userAuthReady,
wechatNotifyBody,
wechatMpBody,
wechatMpConfig: WECHAT_MP_CONFIG,
getRechargeService: () => rechargeService,
getWechatPayClient: () => wechatPayClient,
getWechatMpService: () => wechatMpService,
logger: console,
});
// ============ Wiki API ============
const wikiApi = express.Router();
attachPortalWikiRoutes({
router: wikiApi,
jsonBody,
wikiAuth,
isSecureRequest,
});
app.use('/wiki-api', wikiApi);
// ============ TKMind API proxy ============
const api = express.Router();
api.use(jsonUnlessMultipart);
attachShenmeiOpinionFormRoutes(api, { rootDir: __dirname });
api.use(createPortalApiAuthMiddleware({
waitForUserAuthReady: () => userAuthReady,
getUserAuth: () => userAuth,
getTkmindProxy: () => tkmindProxy,
getLegacyAuth: () => legacyAuth,
getLegacySessionToken: legacySessionToken,
isPageDataPublicPath,
isLegacyPageDataApiPath,
2026-07-20 20:19:52 +08:00
isProductAnalyticsPublicPath: (requestPath, method) =>
method === 'GET' && requestPath === '/analytics/context',
accessPolicyMode: portalAccessPolicyMode,
accessEnforcementConfig: portalAccessEnforcementConfig,
accessShadowReporter: portalAccessShadowReporter,
logger: console,
}));
attachPortalImageMakeRuntimeConfigRoute(api, {
getImageMakeAdminConfigService: () => imageMakeAdminConfigService,
});
2026-07-20 20:19:52 +08:00
api.get('/analytics/context', (req, res) => {
const config = resolveProductAnalyticsConfig(process.env, mindSpaceAnalyticsConfig);
res.set('Cache-Control', 'private, no-store');
res.json(buildProductAnalyticsContext({ config, user: req.currentUser ?? null }));
});
attachAsrRoutes(api, { sendError, sendData });
attachMindSpaceImageGenerationRoutes(api, {
getService: () => mindSpaceImageGeneration,
requireInternal: requireInternalAgentSecret,
});
attachPageDataRoutes(api, {
sendError,
sendData,
getPageDataService: () => pageDataService,
getPageDataPublicService: () => pageDataPublicService,
});
attachPortalBlockedWordsRoute(api, {
waitForUserAuthReady: () => userAuthReady,
getWordFilterService: () => wordFilterService,
});
attachPortalRuntimeRoutes(api, {
waitForUserAuthReady: () => userAuthReady,
getUserAuth: () => userAuth,
getTkmindProxy: () => tkmindProxy,
getEpisodicMemoryService: () => episodicMemoryService,
getAgentRunGateway: () => agentRunGateway,
getCodeRunPolicyService: () => agentCodeRunPolicyService,
env: process.env,
});
async function ensureUserMemoryCapability(req, res) {
if (!userAuth) {
res.status(503).json({ message: '未启用用户系统' });
return null;
2026-06-29 22:20:04 +08:00
}
const userRow = await userAuth.getUserById(req.currentUser.id);
if (!userRow) {
res.status(404).json({ message: '用户不存在' });
return null;
2026-06-29 22:20:04 +08:00
}
const capabilityState = await userAuth.resolveUserCapabilities(userRow);
if (!capabilityState.unrestricted && !capabilityState.capabilities.memory_store) {
res.status(403).json({ message: '当前账户未开通长期记忆,无法访问该 API' });
return null;
2026-06-29 22:20:04 +08:00
}
return capabilityState;
}
2026-06-29 22:20:04 +08:00
async function loadUserVisibleConversation(sessionId, userId) {
const target = await tkmindProxy.resolveTarget(sessionId);
const upstream = await tkmindProxy.apiFetchTo(target, `/sessions/${encodeURIComponent(sessionId)}`, {
method: 'GET',
});
if (!upstream.ok) {
const message = await upstream.text().catch(() => '');
throw new Error(message || '读取会话失败');
}
let session = await upstream.json();
if (authPool && userId) {
session = await repairSessionConversationFromDb(authPool, session, sessionId, userId);
}
const visible = filterUserVisibleConversation(session?.conversation ?? []);
if (!authPool || !userId) return visible;
return restoreConversationUserMessagesFromAgentRunsFailOpen(
authPool,
visible,
sessionId,
userId,
);
}
async function syncUserMemoriesIntoSession(userId, sessionId) {
if (!tkmindProxy || !sessionId) return false;
await tkmindProxy.reconcileSessionPolicyForUser(userId, sessionId);
return true;
}
async function resolveUserMemoryItems(userId, { sessionId = null, limit = 200 } = {}) {
const resolved = await memoryV2.resolve({
userId,
sessionId,
limit,
});
const memories = Array.isArray(resolved?.memories) ? resolved.memories : [];
if (memories.length || !conversationMemoryService?.listMemories) {
return memories;
}
return conversationMemoryService.listMemories(userId, { limit }).catch(() => []);
}
attachPortalUserMemoryRoutes(api, {
getMemoryV2: () => memoryV2,
getTkmindProxy: () => tkmindProxy,
getPool: () => pool,
ensureUserMemoryCapability,
ownsAgentSession,
loadUserVisibleConversation,
syncUserMemoriesIntoSession,
resolveUserMemoryItems,
});
attachPortalGoalRunRoutes(api, {
getGoalRunService: () => goalRunService,
getAgentRunGateway: () => agentRunGateway,
env: process.env,
});
attachPortalMindSpaceSpaceRoutes(api, {
getMindSpace: () => mindSpace,
getScheduleService: () => scheduleService,
getMindSpaceCleanup: () => mindSpaceCleanup,
getMindSpaceAudit: () => mindSpaceAudit,
ensureMindSpaceEnabled,
sendData,
sendError,
handleMindSpaceError: mindSpaceError,
});
attachPortalTemplateCatalogRoutes(api, {
getTemplateCatalog: () => templateCatalogService,
ensureMindSpaceEnabled,
sendData,
sendError,
});
function resolvePlazaPostUrlForRequest(postId, req) {
const host = String(req.headers['x-forwarded-host'] || req.headers.host || '').split(':')[0];
const base = resolvePlazaPublicBase({
configuredBase: process.env.PLAZA_PUBLIC_BASE ?? '',
dev: process.env.NODE_ENV !== 'production',
hostname: host,
});
return resolvePlazaPostPath(base, postId);
}
function plazaRouteError(res, req, error) {
const status = mapPlazaError(error);
const code = error?.code ?? 'internal_error';
const message = error instanceof Error ? error.message : 'Plaza 请求失败';
return sendError(res, req, status, code, message, error?.details);
}
function plazaClientIp(req) {
const forwarded = req.headers['x-forwarded-for'];
if (typeof forwarded === 'string' && forwarded.length > 0) {
return forwarded.split(',')[0].trim();
}
return req.ip;
}
function mindSpaceError(res, req, error) {
const statusByCode = {
invalid_filename: 400,
invalid_file_size: 400,
category_not_uploadable: 400,
file_size_mismatch: 409,
invalid_upload_state: 409,
file_too_large: 413,
unsupported_file_type: 415,
category_not_found: 404,
upload_not_found: 404,
asset_not_found: 404,
asset_in_use: 409,
page_not_found: 404,
upload_expired: 410,
quota_exceeded: 429,
public_page_limit_exceeded: 429,
space_unavailable: 423,
invalid_page_input: 400,
page_content_too_large: 413,
source_message_not_found: 404,
invalid_source_message: 409,
version_conflict: 409,
slug_conflict: 409,
page_already_online: 409,
invalid_state_transition: 409,
invalid_publish_input: 400,
publication_not_found: 404,
security_scan_required: 422,
security_ack_required: 422,
security_risk_blocked: 422,
invalid_agent_job_input: 400,
invalid_agent_job_output: 400,
agent_job_not_found: 404,
agent_job_token_invalid: 401,
agent_job_expired: 410,
feature_disabled: 503,
cover_ai_unavailable: 503,
llm_not_configured: 503,
cover_ai_failed: 502,
cover_ai_invalid_output: 422,
thumbnail_not_supported: 422,
thumbnail_image_required: 400,
thumbnail_image_invalid: 400,
category_not_pageable: 400,
redaction_not_needed: 409,
invalid_category_code: 400,
invalid_page_path: 400,
empty_page_content: 400,
static_page_not_found: 404,
preview_not_supported: 422,
};
const code = error?.code ?? 'internal_error';
const status = statusByCode[code] ?? 500;
const message =
status === 500 && (!error?.code || code === 'internal_error')
? 'MindSpace 服务异常'
: error?.message || 'MindSpace 服务异常';
return sendError(res, req, status, code, message, error?.details);
}
function isRequestBodyTooLarge(error) {
return (
error?.type === 'entity.too.large' ||
error?.status === 413 ||
error?.statusCode === 413
);
}
function apiRequestBodyError(error, req, res, next) {
if (!isRequestBodyTooLarge(error)) return next(error);
const isMindSpaceUpload = req.path.startsWith('/mindspace/v1/uploads/');
return sendError(
res,
req,
413,
isMindSpaceUpload ? 'file_too_large' : 'request_body_too_large',
isMindSpaceUpload
? '上传文件超过单文件大小限制,请压缩后重试'
: '请求内容过大,请缩小后重试',
);
}
function ensureMindSpaceEnabled(res, req, { upload = false, agent = false } = {}) {
try {
assertMindSpaceRoute(msFlags, upload ? 'upload' : agent ? 'agent' : undefined);
return true;
} catch (error) {
mindSpaceError(res, req, error);
return false;
}
}
function bearerToken(req) {
const header = req.get('authorization') ?? '';
const [scheme, value] = header.split(/\s+/, 2);
if (scheme?.toLowerCase() !== 'bearer' || !value) return null;
return value.trim();
}
function requireInternalAgentSecret(req, res) {
if (bearerToken(req) === INTERNAL_AGENT_SECRET) return true;
sendError(res, req, 401, 'agent_job_token_invalid', '内部 Agent 凭据无效');
return false;
}
attachPortalAgentJobRoutes(api, {
getMindSpaceAgentJobs: () => mindSpaceAgentJobs,
getMindSpaceAgentRunner: () => mindSpaceAgentRunner,
getMindSpaceAudit: () => mindSpaceAudit,
ensureMindSpaceEnabled,
requireInternalAgentSecret,
bearerToken,
sendData,
handleMindSpaceError: mindSpaceError,
getSsePollMs: () => mindSpaceServerRuntime.agentWorker.ssePollMs,
logger: console,
});
attachPortalMindSpaceAssetDeliveryRoutes(api, {
rawUploadBody,
getConversationPackageRegistry: () => mindSpaceConversationPackageRegistry,
getUserAuth: () => userAuth,
getSessionAccess: () => sessionAccess,
beforeReadManifest: ({ req, sessionId }) =>
mindSpaceServiceFacade?.prepareConversationPackageRead({
user: req.currentUser,
sessionId,
}),
getMindSpaceAssets: () => mindSpaceAssets,
ensureMindSpaceEnabled,
sendData,
handleMindSpaceError: mindSpaceError,
});
attachPortalMindSpaceAssetRoutes(api, {
getMindSpacePublications: () => mindSpacePublications,
getDatabasePool: () => authPool,
getMindSpaceAssets: () => mindSpaceAssets,
getMindSpacePages: () => mindSpacePages,
getMindSpaceAudit: () => mindSpaceAudit,
getInternalAgentSecret: () => INTERNAL_AGENT_SECRET,
ensureMindSpaceEnabled,
resolveRequestOrigin,
sendData,
handleMindSpaceError: mindSpaceError,
});
function messageText(message) {
return (message?.content ?? [])
.filter((item) => item?.type === 'text' && typeof item.text === 'string')
.map((item) => item.text)
.join('')
.trim();
}
async function resolveOwnedAssistantMessage(user, sessionId, messageId) {
const userId = user?.id;
if (!sessionId || !messageId) {
throw Object.assign(new Error('缺少来源会话或消息'), {
code: 'invalid_page_input',
});
}
if (!(await ownsAgentSession(userId, sessionId))) {
throw Object.assign(new Error('来源会话不存在'), { code: 'source_message_not_found' });
}
let session = null;
if (sessionSnapshotService?.isEnabled()) {
const snapshot = await sessionSnapshotService.get(sessionId).catch(() => null);
if (snapshot?.messages?.length) {
session = {
...snapshot.session,
conversation: sanitizeSessionConversationPublicHtmlLinks(snapshot.messages, user),
};
if (authPool) {
session = await repairSessionConversationFromDb(authPool, session, sessionId, userId);
}
}
}
if (!session) {
const target = await tkmindProxy.resolveTarget(sessionId);
let upstream;
try {
upstream = await tkmindProxy.apiFetchTo(
target,
`/sessions/${encodeURIComponent(sessionId)}`,
{ method: 'GET', signal: AbortSignal.timeout(15_000) },
);
} catch (error) {
throw Object.assign(
new Error(
error?.name === 'TimeoutError' || error?.name === 'AbortError'
? '读取来源会话超时,请稍后重试'
: '无法读取来源会话',
),
{ code: 'source_message_not_found' },
);
}
if (!upstream.ok) {
throw Object.assign(new Error('无法读取来源会话'), { code: 'source_message_not_found' });
}
session = await upstream.json();
if (Array.isArray(session.conversation)) {
session.conversation = sanitizeSessionConversationPublicHtmlLinks(session.conversation, user);
}
if (authPool) {
session = await repairSessionConversationFromDb(authPool, session, sessionId, userId);
}
}
const message = (session.conversation ?? []).find((item) => item.id === messageId);
if (!message) {
throw Object.assign(new Error('来源消息不存在'), { code: 'source_message_not_found' });
}
const content = messageText(message);
if (message.role !== 'assistant' || !message.metadata?.userVisible || !content) {
throw Object.assign(new Error('只有可见的 AI 文本消息可以保存为页面'), {
code: 'invalid_source_message',
});
}
return { session, message, content };
}
async function resolveExistingSavedPage(userId, { sessionId, messageId, relativePath } = {}) {
if (!mindSpacePages || !userId) return null;
const byMessage = await mindSpacePages
.findPageBySourceMessage(userId, sessionId, messageId)
.catch(() => null);
if (byMessage) return byMessage;
const normalizedPath = normalizeWorkspaceRelativePath(relativePath);
if (!normalizedPath) return null;
return mindSpacePages.findPageByRelativePath(userId, normalizedPath).catch(() => null);
}
// REGRESSION GUARD: mindspace-page-sync-thumbnail — remote 也经 pageSyncService RPC 同步 public HTML
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 discoveredRelativePaths = sessionId
? await listSessionPublicHtmlRelativePaths(userId, sessionId, { sinceMs })
: null;
// A normal static-page-publish flow may only leave a workspace HTML file and
// no conversation artifact. Discover only files written around this run;
// `null` would rescan every historical page and let stale Page Data pages
// block an unrelated delivery.
2026-07-27 15:34:35 +08:00
const recentWorkspaceRelativePaths =
sessionId &&
!discoveredRelativePaths?.length &&
mindSpaceWorkspacePublicationDelivery
? (
await mindSpaceWorkspacePublicationDelivery
.listRecentlyModifiedPublicHtml({
userId,
sinceMs,
})
)?.relativePaths ?? []
: [];
const pageDataRelativePaths = sessionId
? (discoveredRelativePaths?.length ? discoveredRelativePaths : recentWorkspaceRelativePaths)
: null;
if (workspacePageDeliver?.syncAndDeliver) {
return await workspacePageDeliver.syncAndDeliver(userId, { pageDataRelativePaths });
}
if (!mindSpacePageSync) return;
return await mindSpacePageSync.syncUserGeneratedPages(userId);
}
async function resolveChatSaveBundle(user, h5Root, input = {}) {
const sessionId = input.sessionId ?? input.session_id;
const messageId = input.messageId ?? input.message_id;
const selectedLinkIndex = Number(input.selectedLinkIndex ?? input.selected_link_index ?? 0);
const previewTitle = String(input.previewTitle ?? input.preview_title ?? '').trim();
const previewSummary = String(input.previewSummary ?? input.preview_summary ?? '').trim();
const source = await resolveOwnedAssistantMessage(user, sessionId, messageId);
let { analysis, resolvedHtml } = await resolveChatSaveAnalysis({
content: source.content,
userId: user.id,
username: user.username,
h5Root,
selectedLinkIndex,
});
2026-07-27 15:34:35 +08:00
if (
resolvedHtml?.content &&
resolvedHtml.relativePath &&
mindSpaceChatSave
) {
const materialized =
await mindSpaceChatSave.materializeWorkspaceHtml({
userId: user.id,
sourceContent: source.content,
html: resolvedHtml.content,
relativePath: resolvedHtml.relativePath,
});
resolvedHtml = {
...resolvedHtml,
relativePath: materialized.relativePath,
content: materialized.html,
};
}
return {
source,
analysis,
resolvedHtml,
selectedLinkIndex,
previewTitle,
previewSummary,
previewFrameUrl:
resolvedHtml != null
? buildChatSavePreviewFrameUrl({ sessionId, messageId, selectedLinkIndex })
: null,
thumbnailUrl:
resolvedHtml != null
? buildChatSaveThumbnailUrl({
sessionId,
messageId,
selectedLinkIndex,
previewTitle,
previewSummary,
})
: null,
localPreviewUrl:
resolvedHtml?.relativePath != null
? buildWorkspaceAssetUrl(user.id, resolvedHtml.relativePath)
: null,
localThumbnailUrl:
resolvedHtml?.relativePath != null
? buildWorkspaceThumbnailUrl(user.id, resolvedHtml.relativePath)
: null,
};
}
async function registerPublishedLongImageArtifactForConversation({
result,
image,
canonicalUrl,
}) {
const ownerId = String(result?.ownerId ?? '').trim();
const pageSource = result?.pageSource ?? {};
const sessionId = String(pageSource.sourceSessionId ?? '').trim();
const messageId = String(pageSource.sourceMessageId ?? '').trim();
const publicationId = String(result?.publication?.id ?? '').trim();
if (
2026-07-27 15:34:35 +08:00
!mindSpaceConversationArtifacts ||
!ownerId ||
!sessionId ||
!publicationId ||
!Buffer.isBuffer(image)
) {
return;
}
2026-07-02 06:55:26 +08:00
try {
2026-07-27 15:34:35 +08:00
await mindSpaceConversationArtifacts
.registerPublishedLongImageArtifact({
ownerId,
sessionId,
2026-07-27 15:34:35 +08:00
messageId: messageId || null,
publicationId,
pageId:
pageSource.pageId ??
result?.publication?.pageId ??
null,
title: pageSource.title ?? null,
body: image,
2026-07-27 15:34:35 +08:00
canonicalUrl,
2026-07-16 21:06:28 +08:00
});
} catch (error) {
console.warn('[MindSpace] failed to record published long image artifact:', error?.message ?? error);
2026-07-02 06:55:26 +08:00
}
}
attachPortalMindSpaceChatSaveRoutes(api, {
h5Root: H5_ROOT,
env: process.env,
getMindSpacePages: () => mindSpacePages,
getMindSpaceAssets: () => mindSpaceAssets,
2026-07-27 15:34:35 +08:00
getMindSpaceChatSave: () => mindSpaceChatSave,
getConversationArtifactService: () =>
mindSpaceConversationArtifacts,
getAuthPool: () => authPool,
resolveChatSaveBundle,
resolveExistingSavedPage,
resolveOwnedAssistantMessage,
sendData,
handleMindSpaceError: mindSpaceError,
});
attachPortalMindSpaceChatShareRoutes(api, {
h5Root: H5_ROOT,
getMindSpacePages: () => mindSpacePages,
2026-07-27 15:34:35 +08:00
getMindSpaceChatSave: () => mindSpaceChatSave,
getMindSpacePublications: () => mindSpacePublications,
getPlazaPosts: () => plazaPosts,
resolveChatSaveBundle,
resolvePlazaPostUrl: resolvePlazaPostUrlForRequest,
mapPlazaError,
sendData,
sendError,
handlePlazaError: plazaRouteError,
handleMindSpaceError: mindSpaceError,
logger: console,
});
attachPortalMindSpacePageCoreRoutes(api, {
getMindSpacePages: () => mindSpacePages,
getMindSpacePublications: () => mindSpacePublications,
getMindSpace: () => mindSpace,
getMindSpaceAudit: () => mindSpaceAudit,
getMindSpacePageLiveEdit: () => mindSpacePageLiveEdit,
getMindSpacePageEditSession: () => mindSpacePageEditSession,
getUserAuth: () => userAuth,
syncUserGeneratedPages,
ownsAgentSession,
ensureMindSpaceEnabled,
sendData,
sendError,
handleMindSpaceError: mindSpaceError,
});
attachPortalMindSpaceAgentOperationRoutes(api, {
getMindSpacePageLiveEdit: () => mindSpacePageLiveEdit,
getMindSpaceAssetAgent: () => mindSpaceAssetAgent,
getMindSpaceAudit: () => mindSpaceAudit,
sendData,
sendError,
handleMindSpaceError: mindSpaceError,
});
attachPortalMindSpacePagePublishRoutes(api, {
env: process.env,
getMindSpacePages: () => mindSpacePages,
getMindSpacePublications: () => mindSpacePublications,
getMindSpaceAudit: () => mindSpaceAudit,
getAuthPool: () => authPool,
sendData,
handleMindSpaceError: mindSpaceError,
});
attachPortalPlazaDiscoveryRoutes(api, {
getPlazaPosts: () => plazaPosts,
getPlazaSeo: () => plazaSeo,
sendData,
sendError,
handleRouteError: plazaRouteError,
});
attachPortalPlazaRoutes(api, {
getPlazaSeo: () => plazaSeo,
getPlazaOps: () => plazaOps,
getPlazaPosts: () => plazaPosts,
getPlazaEvents: () => plazaEvents,
getPlazaInteractions: () => plazaInteractions,
getPlazaRedis: () => plazaRedis,
resolveClientIp: plazaClientIp,
sendData,
sendError,
handleRouteError: plazaRouteError,
});
attachPortalAgentRuntimeRoutes(api, {
waitForUserAuthReady: () => userAuthReady,
getUserAuth: () => userAuth,
getTkmindProxy: () => tkmindProxy,
getLlmProviderService: () => llmProviderService,
getAgentRunGateway: () => agentRunGateway,
getGoalRunService: () => goalRunService,
getChatIntentRouter: () => chatIntentRouter,
getTemplateCatalog: () => templateCatalogService,
getSessionAccess: () => sessionAccess,
getMindSpaceAssetAgent: () => mindSpaceAssetAgent,
getCodeRunPolicyService: () => agentCodeRunPolicyService,
getUserToken: userToken,
2026-07-24 19:18:14 +08:00
getDeepSearchInternalSecret: () =>
DEEP_SEARCH_INTERNAL_SECRET,
bearerToken,
ownsAgentSession,
2026-07-24 19:18:14 +08:00
logger: console,
});
attachPortalSessionRoutes(api, {
waitForUserAuthReady: () => userAuthReady,
getUserAuth: () => userAuth,
getTkmindProxy: () => tkmindProxy,
getSessionSnapshotService: () => sessionSnapshotService,
getAuthPool: () => authPool,
2026-07-27 15:34:35 +08:00
getMindSpacePublicFinish: () =>
mindSpacePublicFinish,
getMemoryV2: () => memoryV2,
getMindSpaceAnalyticsConfig: () => mindSpaceAnalyticsConfig,
ownsAgentSession,
unregisterAgentSessionForUser,
beginSessionPageDelivery,
endSessionPageDelivery,
syncUserGeneratedPages,
logger: console,
});
api.use(apiRequestBodyError);
api.use(async (req, res, next) => {
await userAuthReady;
if (!userAuth || !tkmindProxy) return next();
if (isNativeH5ApiPath(req.path)) {
return sendError(res, req, 404, 'not_found', `接口不存在:${req.method} ${req.path}`);
}
if (req.method === 'GET' && /^\/agent\/runs\/[^/]+\/events$/.test(req.path)) {
return res.status(503).json({
message: 'Agent Run SSE 接口需在 Portal 本地处理,请确认 server.mjs 已更新并重启后端',
code: 'AGENT_RUNS_NATIVE_REQUIRED',
});
}
const sessionMatch = req.path.match(/^\/sessions\/([^/]+)/);
if (sessionMatch) {
const sessionId = sessionMatch[1];
if (req.path.endsWith('/events') && req.method === 'GET') {
return next();
}
if (req.path.endsWith('/reply') && req.method === 'POST') {
return res.status(410).json({
message: '聊天提交入口已统一为 POST /agent/runs',
code: 'AGENT_RUNS_REQUIRED',
});
}
const owns = await ownsAgentSession(req.currentUser.id, sessionId);
if (!owns) {
return res.status(403).json({ message: '无权访问该会话' });
}
}
if (req.method === 'POST' && req.path === '/agent/resume' && req.body?.session_id) {
const owns = await ownsAgentSession(req.currentUser.id, req.body.session_id);
if (!owns) {
return res.status(403).json({ message: '无权访问该会话' });
}
}
if (req.body?.working_dir) {
const allowed = await userAuth.isPathAllowed(req.currentUser.id, req.body.working_dir);
if (!allowed) {
return res.status(403).json({ message: '工作目录不在授权范围内' });
}
}
return tkmindProxy.proxyFallback(req, res);
});
api.use(
createProxyMiddleware({
target: API_TARGET,
changeOrigin: true,
secure: false,
pathRewrite: { '^/api': '' },
on: {
proxyReq: (proxyReq) => {
proxyReq.setHeader('X-Secret-Key', API_SECRET);
},
},
}),
);
app.use('/api', api);
2026-07-16 21:06:28 +08:00
app.use('/analytics', createProxyMiddleware({
target: 'http://127.0.0.1:3100',
router: () => mindSpaceAnalyticsConfig.analyticsUrl || process.env.MEMIND_ANALYTICS_URL || 'http://127.0.0.1:3100',
changeOrigin: true,
secure: false,
pathRewrite: { [`^${mindSpaceAnalyticsConfig.hostPath}`]: '' },
}));
const { serveUserPublishFile } =
createPortalWorkspacePublicationDelivery({
internalAgentSecret:
INTERNAL_AGENT_SECRET,
analyticsConfig:
mindSpaceAnalyticsConfig,
getAuthPool: () => authPool,
getUserAuth: () => userAuth,
getMindSpacePages: () => mindSpacePages,
2026-07-27 15:34:35 +08:00
getWorkspacePublicationDelivery: () =>
mindSpaceWorkspacePublicationDelivery,
resolveRequestOrigin,
removeQueryParam,
getMindSpaceConfig: () =>
authPool
? loadMindSpaceConfigCached(authPool)
: Promise.resolve({ seoGeo: { enabled: false } }),
logger: console,
});
// Express routing is case-insensitive by default, so the lowercase /mindspace API
// mount would otherwise capture public /MindSpace/... page URLs.
app.use(`/${PUBLISH_ROOT_DIR}`, async (req, res, next) => {
await userAuthReady;
return serveUserPublishFile(req, res, next);
});
2026-07-02 19:52:26 +08:00
app.use('/mindspace', api);
const sendPublishedPage =
createPortalPublishedPageDelivery({
registerLongImageArtifact:
registerPublishedLongImageArtifactForConversation,
analyticsConfig: mindSpaceAnalyticsConfig,
analyticsConfig: mindSpaceAnalyticsConfig,
getAuthPool: () => authPool,
getUserAuth: () => userAuth,
getMindSpacePages: () => mindSpacePages,
getMindSpaceConfig: () =>
authPool
? loadMindSpaceConfigCached(authPool)
: Promise.resolve({ seoGeo: { enabled: false } }),
});
let mindspaceSeoDiscoveryService = null;
function getMindspaceSeoDiscoveryService() {
if (!authPool) return null;
if (!mindspaceSeoDiscoveryService) {
mindspaceSeoDiscoveryService = createMindspaceSeoDiscoveryService(authPool);
}
return mindspaceSeoDiscoveryService;
}
attachPortalSeoDiscoveryRoutes(app, {
getAuthPool: () => authPool,
getMindSpaceConfig: () =>
authPool
? loadMindSpaceConfigCached(authPool)
: Promise.resolve({ seoGeo: { enabled: false } }),
getSeoDiscoveryService: getMindspaceSeoDiscoveryService,
resolveRequestOrigin,
});
attachPortalPublicationRoutes({
app,
urlencodedBody: express.urlencoded({
extended: false,
limit: '2kb',
}),
userAuthReady,
h5Root: H5_ROOT,
getAuthPool: () => authPool,
getMindSpacePages: () => mindSpacePages,
getMindSpacePublications: () =>
mindSpacePublications,
2026-07-27 15:34:35 +08:00
getWorkspacePublicationDelivery: () =>
mindSpaceWorkspacePublicationDelivery,
getUserAuth: () => userAuth,
sendPublishedPage,
});
attachPortalStaticDeliveryRoutes({
app,
h5Root: __dirname,
host: HOST,
port: PORT,
publishRootDir: PUBLISH_ROOT_DIR,
usersRoot: USERS_ROOT,
userAuthReady,
resolveRequestOrigin,
});
userAuthReady.then((enabled) => {
if (isDatabaseConfigured() && !enabled && process.env.NODE_ENV === 'production') {
console.error('Refusing to start portal without user auth while database is configured');
process.exit(1);
}
const server = app.listen(PORT, HOST, () => {
console.log(`[Portal] Runtime profile: ${describeMemindRuntimeProfile()}`);
console.log(`TKMind H5 @ http://${HOST}:${PORT}`);
console.log(`Proxy -> ${API_TARGETS.join(', ')}`);
console.log(`Auth -> ${enabled ? 'multi-user (MySQL)' : legacyAuth ? 'legacy password' : 'disabled'}`);
console.log(`Wiki @ http://${HOST}:${PORT}/${PUBLISH_ROOT_DIR}/wiki`);
});
server.on('error', (err) => {
if (err?.code === 'EADDRINUSE') {
console.error(
`Port ${PORT} already in use (${HOST}:${PORT}); exiting so LaunchAgent can retry after ThrottleInterval`,
);
process.exit(1);
}
throw err;
});
});