import express from 'express'; import crypto from 'node:crypto'; import fs from 'node:fs'; import fsPromises from 'node:fs/promises'; 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'; import { buildProductAnalyticsContext, resolveMindSpaceAnalyticsConfig, resolveProductAnalyticsConfig, sendMindSpaceAnalyticsEvent, } from './mindspace-analytics.mjs'; import { resolveMindSpaceRybbitConfig } from './mindspace-rybbit.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 { 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 { 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 { assertMindSpaceRoute, mindspaceFlags } from './mindspace-flags.mjs'; import { normalizeWorkspaceRelativePath } from './mindspace-pages.mjs'; import { registerPublicHtmlArtifactsForConversationPackage } from './mindspace-conversation-package-public-html.mjs'; import { buildMindSpacePublicUrlForUser, resolveMindSpaceRuntimeConfig, resolveMindSpacePublishRoot, resolveMindSpaceServerRuntimeOptions, resolveMindSpaceUserPublishDir, 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, materializePrivateAssetsInWorkspaceHtml, repairMissingHtmlAssetReferences, 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 { listRecentlyModifiedPublicHtmlRelativePaths } from './mindspace-run-public-html-scope.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 }); 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'; 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; 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); 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 sessionAccess = null; let sessionStreamStore = null; let tkmindProxy = null; let agentRunGateway = null; const { ownsAgentSession, unregisterAgentSessionForUser, beginSessionPageDelivery, endSessionPageDelivery, isSessionPageDeliveryActive, } = createPortalSessionCoordinator({ getSessionAccess: () => sessionAccess, getUserAuth: () => userAuth, }); let chatIntentRouter = null; let toolGateway = null; let assetGatewayConfigService = null; let imageMakeAdminConfigService = null; let mindSpaceImageGeneration = null; let directChatService = null; let sessionSnapshotService = null; let conversationMemoryService = null; let memoryV2 = null; let episodicMemoryService = null; let memoryV2ConfigService = null; let skillRuntimeConfigService = null; let systemDisclosurePolicyService = null; let agentCodeRunPolicyService = null; let wechatScheduleLlmConfigService = null; let mindSpace = null; let mindSpaceAssets = null; let mindSpaceAudit = null; let mindSpaceServiceFacade = null; 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 feedbackService = null; let scheduleReminderWorker = null; let llmProviderService = null; let wordFilterService = null; let authPool = null; let pageDataService = null; let pageDataPublicService = null; let mindSpaceAnalyticsConfig = resolveMindSpaceAnalyticsConfig(); let mindSpaceRybbitConfig = resolveMindSpaceRybbitConfig(); 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; }, registerPublicHtmlArtifactsForConversation, workspaceMaintenanceEnabled: WORKSPACE_MAINTENANCE_ENABLED, logger: console, }); const { mindSearchConfigService, mindSpaceRuntimeAdapter, resolveUserIdByDirKey, } = domainServices; mindSpaceAnalyticsConfig = domainServices.mindSpaceAnalyticsConfig; scheduleService = domainServices.scheduleService; pageDataService = domainServices.pageDataService; pageDataPublicService = domainServices.pageDataPublicService; feedbackService = domainServices.feedbackService; mindSpace = domainServices.mindSpace; mindSpaceServiceFacade = domainServices.mindSpaceServiceFacade; 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; userAuth = authServices.userAuth; sessionAccess = authServices.sessionAccess; wechatPayClient = authServices.wechatPayClient; wechatOAuthService = authServices.wechatOAuthService; rechargeService = authServices.rechargeService; const agentServices = await bootstrapPortalAgentServices({ pool, env: process.env, runtime: mindSpaceServerRuntime, apiTarget: API_TARGET, apiTargets: API_TARGETS, apiSecret: API_SECRET, userAuth, sessionAccess, 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; systemDisclosurePolicyService = memorySessionServices.systemDisclosurePolicyService; agentCodeRunPolicyService = memorySessionServices.agentCodeRunPolicyService; wechatScheduleLlmConfigService = memorySessionServices.wechatScheduleLlmConfigService; 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, sessionSnapshotService, conversationMemoryService, memoryV2, systemDisclosurePolicyService, mindSpaceAssets, directChatService, chatIntentRouter, syncUserGeneratedPages, isSessionPageDeliveryActive, }); tkmindProxy = gatewayServices.tkmindProxy; toolGateway = gatewayServices.toolGateway; agentRunGateway = gatewayServices.agentRunGateway; const integrationServices = await bootstrapPortalIntegrationServices({ pool, h5Root: H5_ROOT, env: process.env, usersRoot: USERS_ROOT, wechatMpConfig: WECHAT_MP_CONFIG, authPool, userAuth, sessionAccess, tkmindProxy, scheduleService, wechatScheduleLlmConfigService, llmProviderService, chatIntentRouter, systemDisclosurePolicyService, sessionSnapshotService, mindSpaceAnalyticsConfig, mindSpaceRybbitConfig, subscriptionService, apiTarget: API_TARGET, apiSecret: API_SECRET, mindSpacePages, mindSpacePageLiveEdit, logger: console, }); wechatMpService = integrationServices.wechatMpService; notificationDispatcher = integrationServices.notificationDispatcher; scheduleReminderWorker = integrationServices.scheduleReminderWorker; 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; } } attachPortalCoreAuthRoutes({ app, jsonBody, userAuthReady, getUserAuth: () => userAuth, getLegacyAuth: () => legacyAuth, userToken, legacySessionToken, setUserLoginCookies, isSecureRequest, resolveSkillRuntimeForClient, resolveAgentCodeRunForClient, 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, isProductAnalyticsPublicPath: (requestPath, method) => method === 'GET' && requestPath === '/analytics/context', accessPolicyMode: portalAccessPolicyMode, accessEnforcementConfig: portalAccessEnforcementConfig, accessShadowReporter: portalAccessShadowReporter, logger: console, })); attachPortalImageMakeRuntimeConfigRoute(api, { getImageMakeAdminConfigService: () => imageMakeAdminConfigService, }); 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; } const userRow = await userAuth.getUserById(req.currentUser.id); if (!userRow) { res.status(404).json({ message: '用户不存在' }); return null; } const capabilityState = await userAuth.resolveUserCapabilities(userRow); if (!capabilityState.unrestricted && !capabilityState.capabilities.memory_store) { res.status(403).json({ message: '当前账户未开通长期记忆,无法访问该 API' }); return null; } return capabilityState; } 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, ensureUserMemoryCapability, ownsAgentSession, loadUserVisibleConversation, syncUserMemoriesIntoSession, resolveUserMemoryItems, }); attachPortalMindSpaceSpaceRoutes(api, { getMindSpace: () => mindSpace, getScheduleService: () => scheduleService, getMindSpaceCleanup: () => mindSpaceCleanup, getMindSpaceAudit: () => mindSpaceAudit, ensureMindSpaceEnabled, sendData, sendError, handleMindSpaceError: mindSpaceError, }); 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. const recentWorkspaceRelativePaths = sessionId && !discoveredRelativePaths?.length ? listRecentlyModifiedPublicHtmlRelativePaths( resolveMindSpaceUserPublishDir(H5_ROOT, { id: userId }), { sinceMs }, ) : []; 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, }); if (resolvedHtml?.content && resolvedHtml.relativePath && authPool) { const { storageRoot } = resolveMindSpaceRuntimeConfig(h5Root, process.env); let htmlContent = resolvedHtml.content; const repaired = await repairMissingHtmlAssetReferences({ pool: authPool, userId: user.id, sourceContent: source.content, html: htmlContent, }).catch(() => ({ html: htmlContent, changed: false })); if (repaired.changed) { htmlContent = repaired.html; } const materialized = await materializePrivateAssetsInWorkspaceHtml({ pool: authPool, storageRoot, h5Root, userId: user.id, html: htmlContent, htmlRelativePath: resolvedHtml.relativePath, writeBack: false, }).catch(() => ({ html: htmlContent, count: 0, changed: false })); if (materialized.changed) { htmlContent = materialized.html; } if (htmlContent !== resolvedHtml.content) { const publishDir = resolveMindSpaceUserPublishDir(h5Root, { id: user.id }); await fsPromises.writeFile( path.join(publishDir, resolvedHtml.relativePath), htmlContent, 'utf8', ); resolvedHtml = { ...resolvedHtml, content: htmlContent }; } } 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 ( !mindSpaceConversationPackageRegistry || !ownerId || !sessionId || !publicationId || !Buffer.isBuffer(image) ) { return; } try { const hash = crypto .createHash('sha256') .update(`${ownerId}:${sessionId}:${publicationId}:long-image`) .digest('hex') .slice(0, 16); const artifactId = `ca_long_image_${hash}`; const filename = `${publicationId || 'published-page'}.long.png`; const relativePath = `artifacts/${artifactId}/${filename}`; const now = Date.now(); const { packageRecord, writeResult } = await mindSpaceConversationPackageRegistry.putObjectForSession({ userId: ownerId, sessionId, title: pageSource.title ?? null, relativePath, body: image, }); await mindSpaceConversationPackageRegistry.recordArtifact({ id: artifactId, packageId: packageRecord.id, artifactKind: 'long_image', role: 'assistant', pageId: pageSource.pageId ?? result?.publication?.pageId ?? null, publicationId, messageId: messageId || null, displayName: filename, mimeType: 'image/png', sizeBytes: image.length, storageKey: writeResult.key, canonicalUrl, sortOrder: now, now, }); await mindSpaceConversationPackageRegistry.writeManifestForSession({ userId: ownerId, sessionId, }); } catch (error) { console.warn('[MindSpace] failed to record published long image artifact:', error?.message ?? error); } } async function registerPublicHtmlArtifactsForConversation({ user, publishDir, sessionId, title = null, relativePaths = [], artifactRefs = [], }) { return registerPublicHtmlArtifactsForConversationPackage({ conversationPackageRegistry: mindSpaceConversationPackageRegistry, h5Root: H5_ROOT, env: process.env, user, publishDir, sessionId, title, relativePaths, artifactRefs, }); } attachPortalMindSpaceChatSaveRoutes(api, { h5Root: H5_ROOT, env: process.env, getMindSpacePages: () => mindSpacePages, getMindSpaceAssets: () => mindSpaceAssets, getAuthPool: () => authPool, getConversationPackageRegistry: () => mindSpaceConversationPackageRegistry, resolveChatSaveBundle, resolveExistingSavedPage, resolveOwnedAssistantMessage, sendData, handleMindSpaceError: mindSpaceError, }); attachPortalMindSpaceChatShareRoutes(api, { h5Root: H5_ROOT, env: process.env, getMindSpacePages: () => mindSpacePages, getMindSpacePublications: () => mindSpacePublications, getPlazaPosts: () => plazaPosts, getAuthPool: () => authPool, 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, getSessionAccess: () => sessionAccess, getMindSpaceAssetAgent: () => mindSpaceAssetAgent, getCodeRunPolicyService: () => agentCodeRunPolicyService, getUserToken: userToken, getDeepSearchInternalSecret: () => DEEP_SEARCH_INTERNAL_SECRET, bearerToken, ownsAgentSession, logger: console, }); attachPortalSessionRoutes(api, { h5Root: H5_ROOT, env: process.env, waitForUserAuthReady: () => userAuthReady, getUserAuth: () => userAuth, getTkmindProxy: () => tkmindProxy, getSessionSnapshotService: () => sessionSnapshotService, getAuthPool: () => authPool, getMindSpaceAssets: () => mindSpaceAssets, getMemoryV2: () => memoryV2, getMindSpaceAnalyticsConfig: () => mindSpaceAnalyticsConfig, getMindSpaceRybbitConfig: () => mindSpaceRybbitConfig, isWorkspaceMaintenanceEnabled: () => WORKSPACE_MAINTENANCE_ENABLED, ownsAgentSession, unregisterAgentSessionForUser, beginSessionPageDelivery, endSessionPageDelivery, syncUserGeneratedPages, registerPublicHtmlArtifactsForConversation, 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); 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}`]: '' }, })); app.use('/rybbit', createProxyMiddleware({ target: 'https://rybbit.tkmind.cn', router: () => mindSpaceRybbitConfig.rybbitUrl || process.env.MEMIND_RYBBIT_URL || process.env.RYBBIT_URL || 'https://rybbit.tkmind.cn', changeOrigin: true, secure: true, // Express strips the /rybbit mount. Rybbit serves trackers under /api/*. pathRewrite: { '^/': '/api/' }, })); const { serveUserPublishFile } = createPortalWorkspacePublicationDelivery({ h5Root: H5_ROOT, internalAgentSecret: INTERNAL_AGENT_SECRET, analyticsConfig: mindSpaceAnalyticsConfig, rybbitConfig: mindSpaceRybbitConfig, getAuthPool: () => authPool, getUserAuth: () => userAuth, getMindSpacePages: () => mindSpacePages, resolveRequestOrigin, removeQueryParam, 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); }); app.use('/mindspace', api); const sendPublishedPage = createPortalPublishedPageDelivery({ registerLongImageArtifact: registerPublishedLongImageArtifactForConversation, }); attachPortalPublicationRoutes({ app, urlencodedBody: express.urlencoded({ extended: false, limit: '2kb', }), userAuthReady, h5Root: H5_ROOT, getAuthPool: () => authPool, getMindSpacePages: () => mindSpacePages, getMindSpacePublications: () => mindSpacePublications, 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; }); });