2026-06-15 15:04:43 -07:00
|
|
|
|
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';
|
2026-07-24 18:39:24 +08:00
|
|
|
|
import { createAuthManager } from './auth.mjs';
|
|
|
|
|
|
import { createDbPool, isDatabaseConfigured } from './db.mjs';
|
2026-07-19 18:34:50 +08:00
|
|
|
|
import { attachMindSpaceImageGenerationRoutes } from './mindspace-image-generation-routes.mjs';
|
2026-07-24 18:39:24 +08:00
|
|
|
|
import { sanitizeSessionConversationPublicHtmlLinks } from './tkmind-proxy.mjs';
|
2026-06-15 15:04:43 -07:00
|
|
|
|
import { createWikiAuth } from './wiki-auth.mjs';
|
2026-06-17 16:39:39 -07:00
|
|
|
|
import { isLocalDevHostname } from './scripts/local-test-config.mjs';
|
2026-07-24 18:39:24 +08:00
|
|
|
|
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';
|
2026-06-15 15:04:43 -07:00
|
|
|
|
import { startWorkspaceAssetSyncWatcher } from './mindspace-workspace-sync.mjs';
|
|
|
|
|
|
import { attachRequestId, sendData, sendError } from './api-response.mjs';
|
2026-07-24 18:39:24 +08:00
|
|
|
|
import { attachPortalAgentJobRoutes } from './server/portal-agent-job-routes.mjs';
|
|
|
|
|
|
import { attachPortalAgentRuntimeRoutes } from './server/portal-agent-runtime-routes.mjs';
|
2026-07-03 08:40:28 +08:00
|
|
|
|
import {
|
2026-07-24 18:39:24 +08:00
|
|
|
|
attachPortalBlockedWordsRoute,
|
|
|
|
|
|
attachPortalImageMakeRuntimeConfigRoute,
|
|
|
|
|
|
} from './server/portal-config-routes.mjs';
|
2026-07-03 08:40:28 +08:00
|
|
|
|
import {
|
2026-07-24 18:39:24 +08:00
|
|
|
|
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';
|
2026-08-10 08:06:12 +08:00
|
|
|
|
import { attachPortalTemplateCatalogRoutes } from './server/portal-template-catalog-routes.mjs';
|
2026-07-24 18:39:24 +08:00
|
|
|
|
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';
|
2026-07-03 08:40:28 +08:00
|
|
|
|
import {
|
2026-07-24 18:39:24 +08:00
|
|
|
|
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';
|
2026-08-10 08:06:12 +08:00
|
|
|
|
import { attachPortalSeoDiscoveryRoutes } from './server/portal-seo-discovery-routes.mjs';
|
2026-07-24 18:39:24 +08:00
|
|
|
|
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';
|
2026-08-01 17:03:16 +08:00
|
|
|
|
import { attachPortalGoalRunRoutes } from './server/portal-goal-run-routes.mjs';
|
2026-07-24 18:39:24 +08:00
|
|
|
|
import { assertMindSpaceRoute, mindspaceFlags } from './mindspace-flags.mjs';
|
2026-08-10 08:06:12 +08:00
|
|
|
|
import { loadMindSpaceConfigCached } from './mindspace-config.mjs';
|
|
|
|
|
|
import { createMindspaceSeoDiscoveryService } from './mindspace-seo-discovery-service.mjs';
|
2026-07-24 18:39:24 +08:00
|
|
|
|
import { normalizeWorkspaceRelativePath } from './mindspace-pages.mjs';
|
2026-07-03 08:40:28 +08:00
|
|
|
|
import {
|
|
|
|
|
|
buildMindSpacePublicUrlForUser,
|
|
|
|
|
|
resolveMindSpacePublishRoot,
|
|
|
|
|
|
resolveMindSpaceServerRuntimeOptions,
|
2026-07-24 18:39:24 +08:00
|
|
|
|
resolvePortalH5Root,
|
2026-07-03 08:40:28 +08:00
|
|
|
|
} from './mindspace-runtime-config.mjs';
|
2026-07-02 23:46:05 +08:00
|
|
|
|
import {
|
|
|
|
|
|
publicationInternals,
|
|
|
|
|
|
} from './mindspace-publications.mjs';
|
2026-07-24 18:39:24 +08:00
|
|
|
|
import { mapPlazaError } from './plaza-posts.mjs';
|
|
|
|
|
|
import { createNoopPlazaRedis } from './plaza-redis.mjs';
|
2026-06-15 15:04:43 -07:00
|
|
|
|
import {
|
|
|
|
|
|
buildChatSavePreviewFrameUrl,
|
|
|
|
|
|
buildChatSaveThumbnailUrl,
|
|
|
|
|
|
buildWorkspaceAssetUrl,
|
|
|
|
|
|
buildWorkspaceThumbnailUrl,
|
|
|
|
|
|
resolveChatSaveAnalysis,
|
|
|
|
|
|
} from './mindspace-chat-save.mjs';
|
2026-06-30 00:27:15 +08:00
|
|
|
|
import {
|
2026-07-02 23:46:05 +08:00
|
|
|
|
normalizePublicHtmlRelativePath,
|
2026-06-30 00:27:15 +08:00
|
|
|
|
} from './mindspace-public-finish-sync.mjs';
|
2026-07-05 23:14:41 +08:00
|
|
|
|
import { resolvePlazaPostPath, resolvePlazaPublicBase } from './src/utils/public-site-bases.mjs';
|
2026-07-04 00:17:58 +08:00
|
|
|
|
import { DEFAULT_IMAGE_UPLOAD_MAX_BYTES } from './user-image-normalize.mjs';
|
2026-07-04 12:52:59 +08:00
|
|
|
|
import { loadWechatMpConfig } from './wechat-mp-config.mjs';
|
2026-07-22 10:55:58 +08:00
|
|
|
|
import {
|
|
|
|
|
|
filterUserVisibleConversation,
|
|
|
|
|
|
repairSessionConversationFromDb,
|
|
|
|
|
|
restoreConversationUserMessagesFromAgentRunsFailOpen,
|
|
|
|
|
|
} from './conversation-repair.mjs';
|
2026-06-15 15:04:43 -07:00
|
|
|
|
import { attachAsrRoutes } from './asr-proxy.mjs';
|
2026-07-15 10:56:22 +08:00
|
|
|
|
import { attachPageDataRoutes, isLegacyPageDataApiPath } from './page-data-routes.mjs';
|
2026-07-24 18:39:24 +08:00
|
|
|
|
import { isPageDataPublicPath } from './page-data-public-service.mjs';
|
2026-07-10 20:05:52 +08:00
|
|
|
|
import { attachShenmeiOpinionFormRoutes } from './shenmei-opinion-form-routes.mjs';
|
2026-06-15 15:04:43 -07:00
|
|
|
|
import { isNativeH5ApiPath } from './policies.mjs';
|
2026-07-13 14:00:11 +08:00
|
|
|
|
import {
|
|
|
|
|
|
applyMemindRuntimeProfile,
|
|
|
|
|
|
describeMemindRuntimeProfile,
|
|
|
|
|
|
loadMemindEnvFiles,
|
|
|
|
|
|
} from './scripts/memind-runtime-profile.mjs';
|
2026-06-15 15:04:43 -07:00
|
|
|
|
|
|
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
2026-07-13 14:00:11 +08:00
|
|
|
|
loadMemindEnvFiles(__dirname);
|
2026-07-24 18:39:24 +08:00
|
|
|
|
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-15 15:04:43 -07:00
|
|
|
|
|
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])];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 15:04:43 -07:00
|
|
|
|
const PORT = Number(process.env.H5_PORT ?? 8081);
|
2026-06-29 06:59:37 +08:00
|
|
|
|
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';
|
2026-06-15 15:04:43 -07:00
|
|
|
|
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;
|
2026-07-24 18:39:24 +08:00
|
|
|
|
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);
|
2026-06-15 15:04:43 -07:00
|
|
|
|
const ACCESS_PASSWORD = process.env.H5_ACCESS_PASSWORD;
|
2026-06-19 23:06:43 +08:00
|
|
|
|
const WECHAT_MP_CONFIG = loadWechatMpConfig();
|
2026-06-17 16:39:39 -07:00
|
|
|
|
// 无状态前端节点(如 105,MindSpace 经 rclone 挂载)需设 MEMIND_WORKSPACE_MAINTENANCE=0,
|
|
|
|
|
|
// 否则启动时对挂载树做 readdir/递归 fs.watch 会占满 libuv 线程池导致 boot 卡死。
|
|
|
|
|
|
const WORKSPACE_MAINTENANCE_ENABLED = process.env.MEMIND_WORKSPACE_MAINTENANCE !== '0';
|
2026-07-24 18:39:24 +08:00
|
|
|
|
const USERS_ROOT = process.env.H5_USERS_ROOT ?? path.join(H5_ROOT, 'users');
|
2026-06-15 15:04:43 -07:00
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-11 12:41:37 +08:00
|
|
|
|
function isWechatMiniProgramSource(value) {
|
|
|
|
|
|
if (!value) return false;
|
|
|
|
|
|
try {
|
|
|
|
|
|
return new URL(value).hostname.endsWith('servicewechat.com');
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 15:04:43 -07:00
|
|
|
|
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];
|
2026-07-11 12:41:37 +08:00
|
|
|
|
if ([origin, referer].some(isWechatMiniProgramSource)) {
|
|
|
|
|
|
return next();
|
|
|
|
|
|
}
|
2026-06-15 15:04:43 -07:00
|
|
|
|
const allowed = [origin, referer].some((value) => {
|
|
|
|
|
|
if (!value) return false;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const source = new URL(value);
|
|
|
|
|
|
if (source.host === host) return true;
|
|
|
|
|
|
return (
|
2026-06-17 16:39:39 -07:00
|
|
|
|
isLocalDevHostname(requestHostname) && isLocalDevHostname(source.hostname)
|
2026-06-15 15:04:43 -07:00
|
|
|
|
);
|
|
|
|
|
|
} 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);
|
2026-06-15 15:04:43 -07:00
|
|
|
|
|
|
|
|
|
|
const jsonBody = express.json({ limit: '1mb' });
|
2026-06-15 22:09:38 -07:00
|
|
|
|
const jsonUnlessMultipart = (req, res, next) => {
|
|
|
|
|
|
if ((req.headers['content-type'] ?? '').includes('multipart/form-data')) return next();
|
|
|
|
|
|
return jsonBody(req, res, next);
|
|
|
|
|
|
};
|
2026-06-15 15:04:43 -07:00
|
|
|
|
const rawUploadBody = express.raw({
|
|
|
|
|
|
type: 'application/octet-stream',
|
2026-07-04 00:17:58 +08:00
|
|
|
|
limit: Math.max(mindSpaceServerRuntime.maxFileBytes, DEFAULT_IMAGE_UPLOAD_MAX_BYTES),
|
2026-06-15 15:04:43 -07:00
|
|
|
|
});
|
2026-07-24 18:39:24 +08:00
|
|
|
|
const wikiAuth = createWikiAuth(path.join(resolveMindSpacePublishRoot(H5_ROOT), 'wiki-db'));
|
2026-06-15 15:04:43 -07:00
|
|
|
|
|
|
|
|
|
|
let legacyAuth = null;
|
2026-06-29 20:49:15 +08:00
|
|
|
|
if (ACCESS_PASSWORD && !isDatabaseConfigured()) {
|
2026-06-15 15:04:43 -07:00
|
|
|
|
legacyAuth = createAuthManager({ password: ACCESS_PASSWORD });
|
2026-06-29 20:49:15 +08:00
|
|
|
|
} else if (ACCESS_PASSWORD && isDatabaseConfigured()) {
|
|
|
|
|
|
console.log('H5_ACCESS_PASSWORD ignored: multi-user database auth is configured');
|
2026-06-15 15:04:43 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let userAuth = null;
|
2026-08-10 08:06:12 +08:00
|
|
|
|
let templateCatalogService = null;
|
2026-07-06 14:19:48 +08:00
|
|
|
|
let sessionAccess = null;
|
2026-07-06 16:06:26 +08:00
|
|
|
|
let sessionStreamStore = null;
|
2026-06-15 15:04:43 -07:00
|
|
|
|
let tkmindProxy = null;
|
2026-07-06 14:19:48 +08:00
|
|
|
|
|
2026-06-29 22:50:00 +08:00
|
|
|
|
let agentRunGateway = null;
|
2026-08-01 17:03:16 +08:00
|
|
|
|
let goalRunService = null;
|
2026-07-24 18:39:24 +08:00
|
|
|
|
const {
|
|
|
|
|
|
ownsAgentSession,
|
|
|
|
|
|
unregisterAgentSessionForUser,
|
|
|
|
|
|
beginSessionPageDelivery,
|
|
|
|
|
|
endSessionPageDelivery,
|
|
|
|
|
|
isSessionPageDeliveryActive,
|
|
|
|
|
|
} = createPortalSessionCoordinator({
|
|
|
|
|
|
getSessionAccess: () => sessionAccess,
|
|
|
|
|
|
getUserAuth: () => userAuth,
|
|
|
|
|
|
});
|
2026-07-04 22:32:57 +08:00
|
|
|
|
let chatIntentRouter = null;
|
2026-07-02 09:23:55 +08:00
|
|
|
|
let toolGateway = null;
|
2026-07-19 18:34:50 +08:00
|
|
|
|
let assetGatewayConfigService = null;
|
2026-07-20 20:04:44 +08:00
|
|
|
|
let imageMakeAdminConfigService = null;
|
2026-07-19 18:34:50 +08:00
|
|
|
|
let mindSpaceImageGeneration = null;
|
2026-07-04 14:01:00 +08:00
|
|
|
|
let directChatService = null;
|
2026-06-26 15:19:03 +08:00
|
|
|
|
let sessionSnapshotService = null;
|
2026-06-29 06:59:37 +08:00
|
|
|
|
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;
|
2026-07-13 14:00:11 +08:00
|
|
|
|
let skillRuntimeConfigService = null;
|
2026-07-23 22:53:15 +08:00
|
|
|
|
let systemDisclosurePolicyService = null;
|
2026-07-23 21:33:27 +08:00
|
|
|
|
let agentCodeRunPolicyService = null;
|
2026-07-07 12:10:21 +08:00
|
|
|
|
let wechatScheduleLlmConfigService = null;
|
2026-08-01 20:57:41 +08:00
|
|
|
|
let wechatIntentRouter = null;
|
2026-06-15 15:04:43 -07:00
|
|
|
|
let mindSpace = null;
|
|
|
|
|
|
let mindSpaceAssets = null;
|
|
|
|
|
|
let mindSpaceAudit = null;
|
2026-07-02 23:46:05 +08:00
|
|
|
|
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;
|
2026-06-15 15:04:43 -07:00
|
|
|
|
let mindSpacePages = null;
|
2026-07-05 07:57:04 +08:00
|
|
|
|
let mindSpacePageSync = null;
|
2026-06-15 22:09:38 -07:00
|
|
|
|
let mindSpacePageLiveEdit = null;
|
2026-06-30 09:30:51 +08:00
|
|
|
|
let mindSpaceAssetAgent = null;
|
2026-06-15 22:09:38 -07:00
|
|
|
|
let mindSpacePageEditSession = null;
|
2026-06-15 15:04:43 -07:00
|
|
|
|
let mindSpacePublications = null;
|
2026-07-08 21:10:47 +08:00
|
|
|
|
let workspacePageDeliver = null;
|
2026-06-15 15:04:43 -07:00
|
|
|
|
let plazaPosts = null;
|
2026-06-19 23:06:43 +08:00
|
|
|
|
let plazaEvents = null;
|
|
|
|
|
|
let plazaRecommend = null;
|
2026-06-15 15:04:43 -07:00
|
|
|
|
let plazaInteractions = null;
|
|
|
|
|
|
let plazaSeo = null;
|
|
|
|
|
|
let plazaOps = null;
|
|
|
|
|
|
let plazaRedis = createNoopPlazaRedis();
|
|
|
|
|
|
let mindSpaceCleanup = null;
|
|
|
|
|
|
let mindSpaceAgentJobs = null;
|
|
|
|
|
|
let mindSpaceAgentRunner = null;
|
|
|
|
|
|
let rechargeService = null;
|
2026-06-26 15:19:03 +08:00
|
|
|
|
let subscriptionService = null;
|
2026-06-15 15:04:43 -07:00
|
|
|
|
let wechatPayClient = null;
|
|
|
|
|
|
let wechatOAuthService = null;
|
2026-06-19 23:06:43 +08:00
|
|
|
|
let wechatMpService = null;
|
2026-06-30 21:26:46 +08:00
|
|
|
|
let notificationDispatcher = null;
|
2026-06-19 23:06:43 +08:00
|
|
|
|
let scheduleService = null;
|
2026-07-31 08:02:49 +08:00
|
|
|
|
let scheduledTaskService = null;
|
2026-08-24 15:59:17 +08:00
|
|
|
|
let intentDraftService = null;
|
|
|
|
|
|
let taskUnifiedService = null;
|
2026-06-29 20:49:15 +08:00
|
|
|
|
let feedbackService = null;
|
2026-06-19 23:06:43 +08:00
|
|
|
|
let scheduleReminderWorker = null;
|
2026-07-31 08:02:49 +08:00
|
|
|
|
let scheduledTaskWorker = null;
|
2026-06-15 15:04:43 -07:00
|
|
|
|
let llmProviderService = null;
|
2026-06-26 15:19:03 +08:00
|
|
|
|
let wordFilterService = null;
|
2026-06-15 15:04:43 -07:00
|
|
|
|
let authPool = null;
|
2026-07-08 12:44:23 +08:00
|
|
|
|
let pageDataService = null;
|
|
|
|
|
|
let pageDataPublicService = null;
|
2026-07-16 21:06:28 +08:00
|
|
|
|
let mindSpaceAnalyticsConfig = resolveMindSpaceAnalyticsConfig();
|
2026-06-15 15:04:43 -07:00
|
|
|
|
|
|
|
|
|
|
async function bootstrapUserAuth() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (!isDatabaseConfigured()) return false;
|
|
|
|
|
|
const pool = createDbPool();
|
2026-07-24 18:39:24 +08:00
|
|
|
|
const domainServices =
|
|
|
|
|
|
await bootstrapPortalDomainServices({
|
|
|
|
|
|
pool,
|
|
|
|
|
|
h5Root: H5_ROOT,
|
2026-07-20 20:04:44 +08:00
|
|
|
|
env: process.env,
|
2026-07-24 18:39:24 +08:00
|
|
|
|
runtime: mindSpaceServerRuntime,
|
|
|
|
|
|
analyticsConfig: mindSpaceAnalyticsConfig,
|
|
|
|
|
|
getUserAuth: () => userAuth,
|
|
|
|
|
|
getSessionSnapshotService: () =>
|
|
|
|
|
|
sessionSnapshotService,
|
|
|
|
|
|
onMindSearchSchemaReady: () => {
|
|
|
|
|
|
authPool = pool;
|
|
|
|
|
|
},
|
|
|
|
|
|
workspaceMaintenanceEnabled:
|
|
|
|
|
|
WORKSPACE_MAINTENANCE_ENABLED,
|
2026-07-20 20:04:44 +08:00
|
|
|
|
logger: console,
|
2026-06-15 15:04:43 -07:00
|
|
|
|
});
|
2026-07-24 18:39:24 +08:00
|
|
|
|
const {
|
|
|
|
|
|
mindSearchConfigService,
|
|
|
|
|
|
mindSpaceRuntimeAdapter,
|
|
|
|
|
|
resolveUserIdByDirKey,
|
|
|
|
|
|
} = domainServices;
|
2026-07-27 22:10:07 +08:00
|
|
|
|
Object.assign(
|
|
|
|
|
|
mindSpaceAnalyticsConfig,
|
|
|
|
|
|
domainServices.mindSpaceAnalyticsConfig,
|
|
|
|
|
|
);
|
2026-07-24 18:39:24 +08:00
|
|
|
|
scheduleService = domainServices.scheduleService;
|
2026-07-31 08:02:49 +08:00
|
|
|
|
scheduledTaskService = domainServices.scheduledTaskService;
|
2026-08-24 15:59:17 +08:00
|
|
|
|
intentDraftService = domainServices.intentDraftService;
|
|
|
|
|
|
taskUnifiedService = domainServices.taskUnifiedService;
|
2026-07-24 18:39:24 +08:00
|
|
|
|
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;
|
2026-07-24 18:39:24 +08:00
|
|
|
|
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;
|
2026-08-06 15:29:16 +08:00
|
|
|
|
const billingConfigService =
|
|
|
|
|
|
authServices.billingConfigService;
|
2026-07-24 18:39:24 +08:00
|
|
|
|
userAuth = authServices.userAuth;
|
|
|
|
|
|
sessionAccess = authServices.sessionAccess;
|
|
|
|
|
|
wechatPayClient = authServices.wechatPayClient;
|
|
|
|
|
|
wechatOAuthService =
|
|
|
|
|
|
authServices.wechatOAuthService;
|
|
|
|
|
|
rechargeService = authServices.rechargeService;
|
2026-08-10 08:06:12 +08:00
|
|
|
|
templateCatalogService = authServices.templateCatalogService;
|
2026-07-24 18:39:24 +08:00
|
|
|
|
const agentServices =
|
|
|
|
|
|
await bootstrapPortalAgentServices({
|
|
|
|
|
|
pool,
|
|
|
|
|
|
env: process.env,
|
|
|
|
|
|
runtime: mindSpaceServerRuntime,
|
|
|
|
|
|
apiTarget: API_TARGET,
|
|
|
|
|
|
apiTargets: API_TARGETS,
|
|
|
|
|
|
apiSecret: API_SECRET,
|
|
|
|
|
|
userAuth,
|
|
|
|
|
|
sessionAccess,
|
2026-08-03 14:58:38 +08:00
|
|
|
|
subscriptionService,
|
2026-07-24 18:39:24 +08:00
|
|
|
|
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;
|
2026-07-24 18:39:24 +08:00
|
|
|
|
agentCodeRunPolicyService =
|
|
|
|
|
|
memorySessionServices.agentCodeRunPolicyService;
|
|
|
|
|
|
wechatScheduleLlmConfigService =
|
|
|
|
|
|
memorySessionServices.wechatScheduleLlmConfigService;
|
2026-08-01 20:57:41 +08:00
|
|
|
|
wechatIntentRouter =
|
|
|
|
|
|
memorySessionServices.wechatIntentRouter;
|
2026-07-24 18:39:24 +08:00
|
|
|
|
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,
|
2026-08-06 15:29:16 +08:00
|
|
|
|
billingConfigService,
|
2026-07-24 18:39:24 +08:00
|
|
|
|
sessionSnapshotService,
|
|
|
|
|
|
conversationMemoryService,
|
|
|
|
|
|
memoryV2,
|
2026-07-24 19:18:14 +08:00
|
|
|
|
systemDisclosurePolicyService,
|
2026-07-24 18:39:24 +08:00
|
|
|
|
mindSpaceAssets,
|
2026-07-27 15:34:35 +08:00
|
|
|
|
getWorkspacePublicationDelivery: () =>
|
|
|
|
|
|
mindSpaceWorkspacePublicationDelivery,
|
2026-07-24 18:39:24 +08:00
|
|
|
|
directChatService,
|
|
|
|
|
|
chatIntentRouter,
|
|
|
|
|
|
syncUserGeneratedPages,
|
|
|
|
|
|
isSessionPageDeliveryActive,
|
|
|
|
|
|
});
|
|
|
|
|
|
tkmindProxy = gatewayServices.tkmindProxy;
|
|
|
|
|
|
toolGateway = gatewayServices.toolGateway;
|
|
|
|
|
|
agentRunGateway =
|
|
|
|
|
|
gatewayServices.agentRunGateway;
|
2026-08-01 17:03:16 +08:00
|
|
|
|
goalRunService = gatewayServices.goalRunService;
|
2026-07-24 18:39:24 +08:00
|
|
|
|
const integrationServices =
|
|
|
|
|
|
await bootstrapPortalIntegrationServices({
|
|
|
|
|
|
pool,
|
|
|
|
|
|
h5Root: H5_ROOT,
|
2026-07-28 09:33:25 +08:00
|
|
|
|
codeRoot: __dirname,
|
2026-07-24 18:39:24 +08:00
|
|
|
|
env: process.env,
|
|
|
|
|
|
usersRoot: USERS_ROOT,
|
|
|
|
|
|
wechatMpConfig: WECHAT_MP_CONFIG,
|
2026-07-27 15:34:35 +08:00
|
|
|
|
mindSpacePublicFinish,
|
2026-07-24 18:39:24 +08:00
|
|
|
|
userAuth,
|
|
|
|
|
|
sessionAccess,
|
|
|
|
|
|
tkmindProxy,
|
2026-06-19 23:06:43 +08:00
|
|
|
|
scheduleService,
|
2026-07-31 08:02:49 +08:00
|
|
|
|
scheduledTaskService,
|
2026-08-24 15:59:17 +08:00
|
|
|
|
intentDraftService,
|
|
|
|
|
|
taskUnifiedService,
|
2026-07-31 08:02:49 +08:00
|
|
|
|
sessionSnapshotService,
|
2026-07-24 18:39:24 +08:00
|
|
|
|
wechatScheduleLlmConfigService,
|
2026-08-01 20:57:41 +08:00
|
|
|
|
wechatIntentRouter,
|
2026-07-24 18:39:24 +08:00
|
|
|
|
llmProviderService,
|
|
|
|
|
|
chatIntentRouter,
|
2026-07-24 19:18:14 +08:00
|
|
|
|
systemDisclosurePolicyService,
|
2026-07-24 18:39:24 +08:00
|
|
|
|
sessionSnapshotService,
|
|
|
|
|
|
mindSpaceAnalyticsConfig,
|
|
|
|
|
|
subscriptionService,
|
|
|
|
|
|
apiTarget: API_TARGET,
|
|
|
|
|
|
apiSecret: API_SECRET,
|
|
|
|
|
|
mindSpacePages,
|
|
|
|
|
|
mindSpacePageLiveEdit,
|
|
|
|
|
|
logger: console,
|
2026-06-19 23:06:43 +08:00
|
|
|
|
});
|
2026-07-24 18:39:24 +08:00
|
|
|
|
wechatMpService =
|
|
|
|
|
|
integrationServices.wechatMpService;
|
|
|
|
|
|
notificationDispatcher =
|
|
|
|
|
|
integrationServices.notificationDispatcher;
|
|
|
|
|
|
scheduleReminderWorker =
|
|
|
|
|
|
integrationServices.scheduleReminderWorker;
|
2026-07-31 08:02:49 +08:00
|
|
|
|
scheduledTaskWorker =
|
|
|
|
|
|
integrationServices.scheduledTaskWorker;
|
2026-07-24 18:39:24 +08:00
|
|
|
|
mindSpacePageEditSession =
|
|
|
|
|
|
integrationServices.mindSpacePageEditSession;
|
2026-06-15 15:04:43 -07:00
|
|
|
|
return true;
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('User auth bootstrap failed:', err);
|
2026-06-29 20:49:15 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-06-15 15:04:43 -07:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const userAuthReady = bootstrapUserAuth();
|
|
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
const {
|
|
|
|
|
|
legacySessionToken,
|
|
|
|
|
|
userToken,
|
|
|
|
|
|
setUserLoginCookies,
|
|
|
|
|
|
clearUserLoginCookies,
|
|
|
|
|
|
attachUserSession,
|
|
|
|
|
|
} = createPortalAuthSessionHelpers({
|
|
|
|
|
|
isSecureRequest,
|
|
|
|
|
|
getLegacyAuth: () => legacyAuth,
|
|
|
|
|
|
getUserAuth: () => userAuth,
|
|
|
|
|
|
logger: console,
|
|
|
|
|
|
});
|
2026-06-15 15:04:43 -07:00
|
|
|
|
|
|
|
|
|
|
app.use(attachUserSession);
|
|
|
|
|
|
|
|
|
|
|
|
// ============ Legacy password auth ============
|
|
|
|
|
|
|
2026-07-13 14:00:11 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-23 21:33:27 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-01 17:03:16 +08:00
|
|
|
|
async function resolveGoalRunForClient(userId) {
|
|
|
|
|
|
const { isGoalRunEnabledForUser } = await import('./goal-run-intent.mjs');
|
|
|
|
|
|
return { enabled: isGoalRunEnabledForUser(userId, process.env) };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
attachPortalCoreAuthRoutes({
|
|
|
|
|
|
app,
|
|
|
|
|
|
jsonBody,
|
|
|
|
|
|
userAuthReady,
|
|
|
|
|
|
getUserAuth: () => userAuth,
|
|
|
|
|
|
getLegacyAuth: () => legacyAuth,
|
|
|
|
|
|
userToken,
|
|
|
|
|
|
legacySessionToken,
|
|
|
|
|
|
setUserLoginCookies,
|
|
|
|
|
|
isSecureRequest,
|
|
|
|
|
|
resolveSkillRuntimeForClient,
|
|
|
|
|
|
resolveAgentCodeRunForClient,
|
2026-08-01 17:03:16 +08:00
|
|
|
|
resolveGoalRunForClient,
|
2026-07-24 18:39:24 +08:00
|
|
|
|
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,
|
2026-06-15 15:04:43 -07:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
attachPortalBillingRoutes({
|
|
|
|
|
|
app,
|
|
|
|
|
|
jsonBody,
|
|
|
|
|
|
userAuthReady,
|
|
|
|
|
|
getUserAuth: () => userAuth,
|
|
|
|
|
|
getRechargeService: () => rechargeService,
|
|
|
|
|
|
getSubscriptionService: () => subscriptionService,
|
|
|
|
|
|
getMindSpace: () => mindSpace,
|
|
|
|
|
|
userToken,
|
|
|
|
|
|
});
|
2026-06-15 15:04:43 -07:00
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
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,
|
|
|
|
|
|
});
|
2026-06-15 15:04:43 -07:00
|
|
|
|
|
|
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
// ============ Wiki API ============
|
|
|
|
|
|
const wikiApi = express.Router();
|
|
|
|
|
|
attachPortalWikiRoutes({
|
|
|
|
|
|
router: wikiApi,
|
|
|
|
|
|
jsonBody,
|
|
|
|
|
|
wikiAuth,
|
|
|
|
|
|
isSecureRequest,
|
2026-06-15 15:04:43 -07:00
|
|
|
|
});
|
2026-07-24 18:39:24 +08:00
|
|
|
|
app.use('/wiki-api', wikiApi);
|
2026-06-15 15:04:43 -07:00
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
// ============ TKMind API proxy ============
|
2026-07-11 12:41:37 +08:00
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
const api = express.Router();
|
|
|
|
|
|
api.use(jsonUnlessMultipart);
|
|
|
|
|
|
attachShenmeiOpinionFormRoutes(api, { rootDir: __dirname });
|
2026-06-15 15:04:43 -07:00
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
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',
|
2026-07-24 18:39:24 +08:00
|
|
|
|
accessPolicyMode: portalAccessPolicyMode,
|
|
|
|
|
|
accessEnforcementConfig: portalAccessEnforcementConfig,
|
|
|
|
|
|
accessShadowReporter: portalAccessShadowReporter,
|
|
|
|
|
|
logger: console,
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
attachPortalImageMakeRuntimeConfigRoute(api, {
|
|
|
|
|
|
getImageMakeAdminConfigService: () => imageMakeAdminConfigService,
|
2026-06-15 15:04:43 -07:00
|
|
|
|
});
|
|
|
|
|
|
|
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 }));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
attachAsrRoutes(api, { sendError, sendData });
|
|
|
|
|
|
attachMindSpaceImageGenerationRoutes(api, {
|
|
|
|
|
|
getService: () => mindSpaceImageGeneration,
|
|
|
|
|
|
requireInternal: requireInternalAgentSecret,
|
|
|
|
|
|
});
|
|
|
|
|
|
attachPageDataRoutes(api, {
|
|
|
|
|
|
sendError,
|
|
|
|
|
|
sendData,
|
|
|
|
|
|
getPageDataService: () => pageDataService,
|
|
|
|
|
|
getPageDataPublicService: () => pageDataPublicService,
|
2026-06-15 15:04:43 -07:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
attachPortalBlockedWordsRoute(api, {
|
|
|
|
|
|
waitForUserAuthReady: () => userAuthReady,
|
|
|
|
|
|
getWordFilterService: () => wordFilterService,
|
|
|
|
|
|
});
|
2026-06-19 23:06:43 +08:00
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
attachPortalRuntimeRoutes(api, {
|
|
|
|
|
|
waitForUserAuthReady: () => userAuthReady,
|
|
|
|
|
|
getUserAuth: () => userAuth,
|
|
|
|
|
|
getTkmindProxy: () => tkmindProxy,
|
|
|
|
|
|
getEpisodicMemoryService: () => episodicMemoryService,
|
|
|
|
|
|
getAgentRunGateway: () => agentRunGateway,
|
|
|
|
|
|
getCodeRunPolicyService: () => agentCodeRunPolicyService,
|
|
|
|
|
|
env: process.env,
|
2026-06-19 23:06:43 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
async function ensureUserMemoryCapability(req, res) {
|
|
|
|
|
|
if (!userAuth) {
|
|
|
|
|
|
res.status(503).json({ message: '未启用用户系统' });
|
|
|
|
|
|
return null;
|
2026-06-29 22:20:04 +08:00
|
|
|
|
}
|
2026-07-24 18:39:24 +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
|
|
|
|
}
|
2026-07-24 18:39:24 +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
|
|
|
|
}
|
2026-07-24 18:39:24 +08:00
|
|
|
|
return capabilityState;
|
|
|
|
|
|
}
|
2026-06-29 22:20:04 +08:00
|
|
|
|
|
2026-07-24 18:39:24 +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 || '读取会话失败');
|
2026-06-15 15:47:19 -07:00
|
|
|
|
}
|
2026-07-24 18:39:24 +08:00
|
|
|
|
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,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-06-15 15:47:19 -07:00
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
async function syncUserMemoriesIntoSession(userId, sessionId) {
|
|
|
|
|
|
if (!tkmindProxy || !sessionId) return false;
|
|
|
|
|
|
await tkmindProxy.reconcileSessionPolicyForUser(userId, sessionId);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-06-19 23:06:43 +08:00
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
async function resolveUserMemoryItems(userId, { sessionId = null, limit = 200 } = {}) {
|
|
|
|
|
|
const resolved = await memoryV2.resolve({
|
|
|
|
|
|
userId,
|
|
|
|
|
|
sessionId,
|
|
|
|
|
|
limit,
|
2026-06-19 23:06:43 +08:00
|
|
|
|
});
|
2026-07-24 18:39:24 +08:00
|
|
|
|
const memories = Array.isArray(resolved?.memories) ? resolved.memories : [];
|
|
|
|
|
|
if (memories.length || !conversationMemoryService?.listMemories) {
|
|
|
|
|
|
return memories;
|
|
|
|
|
|
}
|
|
|
|
|
|
return conversationMemoryService.listMemories(userId, { limit }).catch(() => []);
|
2026-06-19 23:06:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
attachPortalUserMemoryRoutes(api, {
|
|
|
|
|
|
getMemoryV2: () => memoryV2,
|
|
|
|
|
|
getTkmindProxy: () => tkmindProxy,
|
2026-08-01 17:03:16 +08:00
|
|
|
|
getPool: () => pool,
|
2026-07-24 18:39:24 +08:00
|
|
|
|
ensureUserMemoryCapability,
|
|
|
|
|
|
ownsAgentSession,
|
|
|
|
|
|
loadUserVisibleConversation,
|
|
|
|
|
|
syncUserMemoriesIntoSession,
|
|
|
|
|
|
resolveUserMemoryItems,
|
2026-06-15 15:47:19 -07:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-08-01 17:03:16 +08:00
|
|
|
|
attachPortalGoalRunRoutes(api, {
|
|
|
|
|
|
getGoalRunService: () => goalRunService,
|
|
|
|
|
|
getAgentRunGateway: () => agentRunGateway,
|
|
|
|
|
|
env: process.env,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
attachPortalMindSpaceSpaceRoutes(api, {
|
|
|
|
|
|
getMindSpace: () => mindSpace,
|
|
|
|
|
|
getScheduleService: () => scheduleService,
|
|
|
|
|
|
getMindSpaceCleanup: () => mindSpaceCleanup,
|
|
|
|
|
|
getMindSpaceAudit: () => mindSpaceAudit,
|
|
|
|
|
|
ensureMindSpaceEnabled,
|
|
|
|
|
|
sendData,
|
|
|
|
|
|
sendError,
|
|
|
|
|
|
handleMindSpaceError: mindSpaceError,
|
2026-06-15 15:47:19 -07:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-08-10 08:06:12 +08:00
|
|
|
|
attachPortalTemplateCatalogRoutes(api, {
|
|
|
|
|
|
getTemplateCatalog: () => templateCatalogService,
|
|
|
|
|
|
ensureMindSpaceEnabled,
|
|
|
|
|
|
sendData,
|
|
|
|
|
|
sendError,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
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,
|
2026-06-15 15:47:19 -07:00
|
|
|
|
});
|
2026-07-24 18:39:24 +08:00
|
|
|
|
return resolvePlazaPostPath(base, postId);
|
|
|
|
|
|
}
|
2026-06-15 15:47:19 -07:00
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-06-15 15:47:19 -07:00
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
function plazaClientIp(req) {
|
|
|
|
|
|
const forwarded = req.headers['x-forwarded-for'];
|
|
|
|
|
|
if (typeof forwarded === 'string' && forwarded.length > 0) {
|
|
|
|
|
|
return forwarded.split(',')[0].trim();
|
2026-06-15 15:47:19 -07:00
|
|
|
|
}
|
2026-07-24 18:39:24 +08:00
|
|
|
|
return req.ip;
|
|
|
|
|
|
}
|
2026-06-15 15:47:19 -07:00
|
|
|
|
|
2026-06-15 15:04:43 -07:00
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-06-15 15:47:19 -07:00
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
function isRequestBodyTooLarge(error) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
error?.type === 'entity.too.large' ||
|
|
|
|
|
|
error?.status === 413 ||
|
|
|
|
|
|
error?.statusCode === 413
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-06-15 15:47:19 -07:00
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
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
|
|
|
|
|
|
? '上传文件超过单文件大小限制,请压缩后重试'
|
|
|
|
|
|
: '请求内容过大,请缩小后重试',
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-06-15 15:47:19 -07:00
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-15 15:47:19 -07:00
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
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();
|
|
|
|
|
|
}
|
2026-06-15 15:47:19 -07:00
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
function requireInternalAgentSecret(req, res) {
|
|
|
|
|
|
if (bearerToken(req) === INTERNAL_AGENT_SECRET) return true;
|
|
|
|
|
|
sendError(res, req, 401, 'agent_job_token_invalid', '内部 Agent 凭据无效');
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2026-06-15 15:04:43 -07:00
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
attachPortalAgentJobRoutes(api, {
|
|
|
|
|
|
getMindSpaceAgentJobs: () => mindSpaceAgentJobs,
|
|
|
|
|
|
getMindSpaceAgentRunner: () => mindSpaceAgentRunner,
|
|
|
|
|
|
getMindSpaceAudit: () => mindSpaceAudit,
|
|
|
|
|
|
ensureMindSpaceEnabled,
|
|
|
|
|
|
requireInternalAgentSecret,
|
|
|
|
|
|
bearerToken,
|
|
|
|
|
|
sendData,
|
|
|
|
|
|
handleMindSpaceError: mindSpaceError,
|
|
|
|
|
|
getSsePollMs: () => mindSpaceServerRuntime.agentWorker.ssePollMs,
|
|
|
|
|
|
logger: console,
|
2026-06-15 15:04:43 -07:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
attachPortalMindSpaceAssetDeliveryRoutes(api, {
|
|
|
|
|
|
rawUploadBody,
|
|
|
|
|
|
getConversationPackageRegistry: () => mindSpaceConversationPackageRegistry,
|
|
|
|
|
|
getUserAuth: () => userAuth,
|
|
|
|
|
|
getSessionAccess: () => sessionAccess,
|
|
|
|
|
|
beforeReadManifest: ({ req, sessionId }) =>
|
|
|
|
|
|
mindSpaceServiceFacade?.prepareConversationPackageRead({
|
|
|
|
|
|
user: req.currentUser,
|
|
|
|
|
|
sessionId,
|
|
|
|
|
|
}),
|
|
|
|
|
|
getMindSpaceAssets: () => mindSpaceAssets,
|
|
|
|
|
|
ensureMindSpaceEnabled,
|
|
|
|
|
|
sendData,
|
|
|
|
|
|
handleMindSpaceError: mindSpaceError,
|
2026-06-15 15:04:43 -07:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
attachPortalMindSpaceAssetRoutes(api, {
|
|
|
|
|
|
getMindSpacePublications: () => mindSpacePublications,
|
|
|
|
|
|
getDatabasePool: () => authPool,
|
|
|
|
|
|
getMindSpaceAssets: () => mindSpaceAssets,
|
|
|
|
|
|
getMindSpacePages: () => mindSpacePages,
|
|
|
|
|
|
getMindSpaceAudit: () => mindSpaceAudit,
|
|
|
|
|
|
getInternalAgentSecret: () => INTERNAL_AGENT_SECRET,
|
|
|
|
|
|
ensureMindSpaceEnabled,
|
|
|
|
|
|
resolveRequestOrigin,
|
|
|
|
|
|
sendData,
|
|
|
|
|
|
handleMindSpaceError: mindSpaceError,
|
2026-06-15 15:04:43 -07:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
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',
|
2026-06-29 20:49:15 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-07-24 18:39:24 +08:00
|
|
|
|
if (!(await ownsAgentSession(userId, sessionId))) {
|
|
|
|
|
|
throw Object.assign(new Error('来源会话不存在'), { code: 'source_message_not_found' });
|
2026-06-29 20:49:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-06-29 20:49:15 +08:00
|
|
|
|
}
|
2026-06-26 15:19:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
if (!session) {
|
|
|
|
|
|
const target = await tkmindProxy.resolveTarget(sessionId);
|
|
|
|
|
|
let upstream;
|
2026-06-26 15:19:03 +08:00
|
|
|
|
try {
|
2026-07-24 18:39:24 +08:00
|
|
|
|
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);
|
2026-06-26 15:19:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
const message = (session.conversation ?? []).find((item) => item.id === messageId);
|
|
|
|
|
|
if (!message) {
|
|
|
|
|
|
throw Object.assign(new Error('来源消息不存在'), { code: 'source_message_not_found' });
|
2026-06-26 15:19:03 +08:00
|
|
|
|
}
|
2026-07-24 18:39:24 +08:00
|
|
|
|
const content = messageText(message);
|
|
|
|
|
|
if (message.role !== 'assistant' || !message.metadata?.userVisible || !content) {
|
|
|
|
|
|
throw Object.assign(new Error('只有可见的 AI 文本消息可以保存为页面'), {
|
|
|
|
|
|
code: 'invalid_source_message',
|
|
|
|
|
|
});
|
2026-06-26 15:19:03 +08:00
|
|
|
|
}
|
2026-07-24 18:39:24 +08:00
|
|
|
|
return { session, message, content };
|
|
|
|
|
|
}
|
2026-06-26 15:19:03 +08:00
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-06-15 22:09:38 -07:00
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
// 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')))];
|
|
|
|
|
|
}
|
2026-06-26 15:19:03 +08:00
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
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 })
|
2026-06-26 15:19:03 +08:00
|
|
|
|
: null;
|
2026-07-24 18:39:24 +08:00
|
|
|
|
// 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 ?? []
|
|
|
|
|
|
: [];
|
2026-07-24 18:39:24 +08:00
|
|
|
|
const pageDataRelativePaths = sessionId
|
|
|
|
|
|
? (discoveredRelativePaths?.length ? discoveredRelativePaths : recentWorkspaceRelativePaths)
|
|
|
|
|
|
: null;
|
|
|
|
|
|
if (workspacePageDeliver?.syncAndDeliver) {
|
|
|
|
|
|
return await workspacePageDeliver.syncAndDeliver(userId, { pageDataRelativePaths });
|
2026-06-26 15:19:03 +08:00
|
|
|
|
}
|
2026-07-24 18:39:24 +08:00
|
|
|
|
if (!mindSpacePageSync) return;
|
|
|
|
|
|
return await mindSpacePageSync.syncUserGeneratedPages(userId);
|
|
|
|
|
|
}
|
2026-06-26 15:19:03 +08:00
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
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();
|
2026-06-15 15:04:43 -07:00
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
const source = await resolveOwnedAssistantMessage(user, sessionId, messageId);
|
|
|
|
|
|
let { analysis, resolvedHtml } = await resolveChatSaveAnalysis({
|
|
|
|
|
|
content: source.content,
|
|
|
|
|
|
userId: user.id,
|
|
|
|
|
|
username: user.username,
|
|
|
|
|
|
h5Root,
|
|
|
|
|
|
selectedLinkIndex,
|
2026-06-15 15:04:43 -07:00
|
|
|
|
});
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
};
|
2026-06-15 15:04:43 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
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,
|
2026-06-15 15:04:43 -07:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
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();
|
2026-07-04 23:15:44 +08:00
|
|
|
|
if (
|
2026-07-27 15:34:35 +08:00
|
|
|
|
!mindSpaceConversationArtifacts ||
|
2026-07-24 18:39:24 +08:00
|
|
|
|
!ownerId ||
|
|
|
|
|
|
!sessionId ||
|
|
|
|
|
|
!publicationId ||
|
|
|
|
|
|
!Buffer.isBuffer(image)
|
2026-07-04 23:15:44 +08:00
|
|
|
|
) {
|
2026-07-24 18:39:24 +08:00
|
|
|
|
return;
|
2026-07-04 22:32:57 +08:00
|
|
|
|
}
|
2026-07-02 06:55:26 +08:00
|
|
|
|
try {
|
2026-07-27 15:34:35 +08:00
|
|
|
|
await mindSpaceConversationArtifacts
|
|
|
|
|
|
.registerPublishedLongImageArtifact({
|
|
|
|
|
|
ownerId,
|
2026-07-24 18:39:24 +08:00
|
|
|
|
sessionId,
|
2026-07-27 15:34:35 +08:00
|
|
|
|
messageId: messageId || null,
|
|
|
|
|
|
publicationId,
|
|
|
|
|
|
pageId:
|
|
|
|
|
|
pageSource.pageId ??
|
|
|
|
|
|
result?.publication?.pageId ??
|
|
|
|
|
|
null,
|
2026-07-24 18:39:24 +08:00
|
|
|
|
title: pageSource.title ?? null,
|
|
|
|
|
|
body: image,
|
2026-07-27 15:34:35 +08:00
|
|
|
|
canonicalUrl,
|
2026-07-16 21:06:28 +08:00
|
|
|
|
});
|
2026-07-24 18:39:24 +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
|
|
|
|
}
|
2026-06-29 20:49:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-24 18:39:24 +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,
|
2026-07-24 18:39:24 +08:00
|
|
|
|
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,
|
2026-07-24 18:39:24 +08:00
|
|
|
|
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,
|
2026-08-12 15:21:42 +08:00
|
|
|
|
getAuthPool: () => authPool,
|
2026-07-24 18:39:24 +08:00
|
|
|
|
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,
|
2026-08-01 17:03:16 +08:00
|
|
|
|
getGoalRunService: () => goalRunService,
|
|
|
|
|
|
getChatIntentRouter: () => chatIntentRouter,
|
2026-08-10 08:06:12 +08:00
|
|
|
|
getTemplateCatalog: () => templateCatalogService,
|
2026-07-24 18:39:24 +08:00
|
|
|
|
getSessionAccess: () => sessionAccess,
|
|
|
|
|
|
getMindSpaceAssetAgent: () => mindSpaceAssetAgent,
|
|
|
|
|
|
getCodeRunPolicyService: () => agentCodeRunPolicyService,
|
|
|
|
|
|
getUserToken: userToken,
|
2026-07-24 19:18:14 +08:00
|
|
|
|
getDeepSearchInternalSecret: () =>
|
|
|
|
|
|
DEEP_SEARCH_INTERNAL_SECRET,
|
|
|
|
|
|
bearerToken,
|
2026-07-24 18:39:24 +08:00
|
|
|
|
ownsAgentSession,
|
2026-07-24 19:18:14 +08:00
|
|
|
|
logger: console,
|
2026-07-24 18:39:24 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
attachPortalSessionRoutes(api, {
|
|
|
|
|
|
waitForUserAuthReady: () => userAuthReady,
|
|
|
|
|
|
getUserAuth: () => userAuth,
|
|
|
|
|
|
getTkmindProxy: () => tkmindProxy,
|
|
|
|
|
|
getSessionSnapshotService: () => sessionSnapshotService,
|
|
|
|
|
|
getAuthPool: () => authPool,
|
2026-07-27 15:34:35 +08:00
|
|
|
|
getMindSpacePublicFinish: () =>
|
|
|
|
|
|
mindSpacePublicFinish,
|
2026-07-24 18:39:24 +08:00
|
|
|
|
getMemoryV2: () => memoryV2,
|
|
|
|
|
|
getMindSpaceAnalyticsConfig: () => mindSpaceAnalyticsConfig,
|
|
|
|
|
|
ownsAgentSession,
|
|
|
|
|
|
unregisterAgentSessionForUser,
|
|
|
|
|
|
beginSessionPageDelivery,
|
|
|
|
|
|
endSessionPageDelivery,
|
|
|
|
|
|
syncUserGeneratedPages,
|
|
|
|
|
|
logger: console,
|
2026-06-15 15:04:43 -07:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-04 00:17:58 +08:00
|
|
|
|
api.use(apiRequestBodyError);
|
|
|
|
|
|
|
2026-06-15 15:04:43 -07:00
|
|
|
|
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}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-30 00:27:15 +08:00
|
|
|
|
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',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 15:04:43 -07:00
|
|
|
|
const sessionMatch = req.path.match(/^\/sessions\/([^/]+)/);
|
|
|
|
|
|
if (sessionMatch) {
|
|
|
|
|
|
const sessionId = sessionMatch[1];
|
|
|
|
|
|
if (req.path.endsWith('/events') && req.method === 'GET') {
|
|
|
|
|
|
return next();
|
|
|
|
|
|
}
|
2026-06-29 22:50:00 +08:00
|
|
|
|
if (req.path.endsWith('/reply') && req.method === 'POST') {
|
|
|
|
|
|
return res.status(410).json({
|
|
|
|
|
|
message: '聊天提交入口已统一为 POST /agent/runs',
|
|
|
|
|
|
code: 'AGENT_RUNS_REQUIRED',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-07-06 14:19:48 +08:00
|
|
|
|
const owns = await ownsAgentSession(req.currentUser.id, sessionId);
|
2026-06-15 15:04:43 -07:00
|
|
|
|
if (!owns) {
|
|
|
|
|
|
return res.status(403).json({ message: '无权访问该会话' });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (req.method === 'POST' && req.path === '/agent/resume' && req.body?.session_id) {
|
2026-07-06 14:19:48 +08:00
|
|
|
|
const owns = await ownsAgentSession(req.currentUser.id, req.body.session_id);
|
2026-06-15 15:04:43 -07:00
|
|
|
|
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}`]: '' },
|
|
|
|
|
|
}));
|
2026-07-24 18:39:24 +08:00
|
|
|
|
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,
|
2026-07-24 18:39:24 +08:00
|
|
|
|
resolveRequestOrigin,
|
|
|
|
|
|
removeQueryParam,
|
2026-08-10 08:06:12 +08:00
|
|
|
|
getMindSpaceConfig: () =>
|
|
|
|
|
|
authPool
|
|
|
|
|
|
? loadMindSpaceConfigCached(authPool)
|
|
|
|
|
|
: Promise.resolve({ seoGeo: { enabled: false } }),
|
2026-07-24 18:39:24 +08:00
|
|
|
|
logger: console,
|
|
|
|
|
|
});
|
2026-07-02 23:46:05 +08:00
|
|
|
|
// 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);
|
2026-06-15 15:04:43 -07:00
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
const sendPublishedPage =
|
|
|
|
|
|
createPortalPublishedPageDelivery({
|
|
|
|
|
|
registerLongImageArtifact:
|
|
|
|
|
|
registerPublishedLongImageArtifactForConversation,
|
2026-07-27 22:10:07 +08:00
|
|
|
|
analyticsConfig: mindSpaceAnalyticsConfig,
|
2026-08-12 01:34:05 +08:00
|
|
|
|
analyticsConfig: mindSpaceAnalyticsConfig,
|
2026-07-27 22:10:07 +08:00
|
|
|
|
getAuthPool: () => authPool,
|
|
|
|
|
|
getUserAuth: () => userAuth,
|
|
|
|
|
|
getMindSpacePages: () => mindSpacePages,
|
2026-08-10 08:06:12 +08:00
|
|
|
|
getMindSpaceConfig: () =>
|
|
|
|
|
|
authPool
|
|
|
|
|
|
? loadMindSpaceConfigCached(authPool)
|
|
|
|
|
|
: Promise.resolve({ seoGeo: { enabled: false } }),
|
2026-06-15 15:04:43 -07:00
|
|
|
|
});
|
2026-06-29 20:49:15 +08:00
|
|
|
|
|
2026-08-10 08:06:12 +08:00
|
|
|
|
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,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-24 18:39:24 +08:00
|
|
|
|
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,
|
2026-07-24 18:39:24 +08:00
|
|
|
|
getUserAuth: () => userAuth,
|
|
|
|
|
|
sendPublishedPage,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
attachPortalStaticDeliveryRoutes({
|
|
|
|
|
|
app,
|
|
|
|
|
|
h5Root: __dirname,
|
|
|
|
|
|
host: HOST,
|
|
|
|
|
|
port: PORT,
|
|
|
|
|
|
publishRootDir: PUBLISH_ROOT_DIR,
|
|
|
|
|
|
usersRoot: USERS_ROOT,
|
|
|
|
|
|
userAuthReady,
|
|
|
|
|
|
resolveRequestOrigin,
|
2026-06-15 15:04:43 -07:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-05 11:26:34 +08:00
|
|
|
|
userAuthReady.then((enabled) => {
|
2026-06-29 20:49:15 +08:00
|
|
|
|
if (isDatabaseConfigured() && !enabled && process.env.NODE_ENV === 'production') {
|
|
|
|
|
|
console.error('Refusing to start portal without user auth while database is configured');
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
|
}
|
2026-07-05 11:26:34 +08:00
|
|
|
|
const server = app.listen(PORT, HOST, () => {
|
2026-07-13 14:00:11 +08:00
|
|
|
|
console.log(`[Portal] Runtime profile: ${describeMemindRuntimeProfile()}`);
|
2026-06-29 06:59:37 +08:00
|
|
|
|
console.log(`TKMind H5 @ http://${HOST}:${PORT}`);
|
2026-06-19 23:06:43 +08:00
|
|
|
|
console.log(`Proxy -> ${API_TARGETS.join(', ')}`);
|
2026-06-15 15:04:43 -07:00
|
|
|
|
console.log(`Auth -> ${enabled ? 'multi-user (MySQL)' : legacyAuth ? 'legacy password' : 'disabled'}`);
|
2026-06-29 06:59:37 +08:00
|
|
|
|
console.log(`Wiki @ http://${HOST}:${PORT}/${PUBLISH_ROOT_DIR}/wiki`);
|
2026-06-15 15:04:43 -07:00
|
|
|
|
});
|
2026-07-05 11:26:34 +08:00
|
|
|
|
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;
|
|
|
|
|
|
});
|
2026-06-15 15:04:43 -07:00
|
|
|
|
});
|