// admin-bootstrap.mjs // // Service container for the standalone memind_adm process. // // Builds only the domain services the back-office routes need (user-auth, LLM // providers, and the minimal plaza graph behind the ops console). It deliberately // does NOT start any user-facing daemons — no thumbnail watcher, no asset-sync // watcher, no plaza hot-score scheduler, no goosed auto-sync. Those belong to the // public server. Side-effecting actions (e.g. push selected LLM key to goosed) // stay behind explicit admin routes. // // The same factory functions imported here are the ones server.mjs uses, so the // domain logic has a single source of truth; only the wiring differs. import path from 'node:path'; import { createSubscriptionService } from './billing-subscription.mjs'; import { createDbPool, ensureAssetGatewaySchema, isDatabaseConfigured } from './db.mjs'; import { createUserAuth } from './user-auth.mjs'; import { createLlmProviderService } from './llm-providers.mjs'; import { createAssetGatewayConfigService } from './asset-gateway.mjs'; import { createImageMakeAdminConfigService } from './image-make-admin-config.mjs'; import { createMemoryV2AdminConfigService } from './memory-v2-admin-config.mjs'; import { createSkillRuntimeAdminConfigService } from './skill-runtime-admin-config.mjs'; import { createSystemDisclosurePolicyService } from './system-disclosure-policy.mjs'; import { createMindSearchConfigService } from './mindsearch-config.mjs'; import { createWechatScheduleLlmConfigService } from './wechat-schedule-llm-config.mjs'; import { createPlazaPostService, formatPostRow } from './plaza-posts.mjs'; import { createPlazaInteractionService } from './plaza-interactions.mjs'; import { createPlazaOpsService } from './plaza-ops.mjs'; import { createNoopPlazaRedis } from './plaza-redis.mjs'; import { ensureAlgorithmConfig, loadAlgorithmConfig } from './plaza-algorithm.mjs'; import { createNotificationDispatcher } from './notification-dispatcher.mjs'; import { createWechatAdminService } from './wechat-admin.mjs'; import { createWechatMpService, loadWechatMpConfig } from './wechat-mp.mjs'; import { createAdminSystemTestService } from './admin-system-tests.mjs'; const noop = () => {}; /** * Build the admin service container. * @param {object} [env] * @param {string} [env.h5Root] Repo root used for workspace path resolution. * @param {string} [env.usersRoot] User workspace root. * @param {string} [env.apiTarget] Goosed/relay API target. * @param {string} [env.apiSecret] Goosed/relay API secret. * @param {number} [env.defaultSignupBalanceCents] * @param {boolean} [env.ensureAdminUser] Ensure an admin account exists on boot (default true). * @returns {Promise<{pool, userAuth, llmProviderService, memoryV2ConfigService, adminSystemTestService, plazaPosts, plazaOps, wechatAdmin}>} */ export async function createAdminServices(env = {}) { if (!isDatabaseConfigured()) { throw new Error('admin-server requires a database (set DATABASE_URL or MYSQL_* env)'); } const h5Root = env.h5Root ?? process.cwd(); const usersRoot = env.usersRoot ?? process.env.H5_USERS_ROOT ?? path.join(h5Root, 'users'); const apiTarget = env.apiTarget ?? process.env.TKMIND_API_TARGET ?? 'https://127.0.0.1:18006'; const apiSecret = env.apiSecret ?? process.env.TKMIND_SERVER__SECRET_KEY ?? 'local-dev-secret'; const defaultSignupBalanceCents = Number( env.defaultSignupBalanceCents ?? process.env.H5_SIGNUP_BALANCE_CENTS ?? 500, ); const pool = createDbPool(); // The back-office process can boot before the public Portal. Create only the // optional control-plane tables here instead of requiring the public boot path. await ensureAssetGatewaySchema(pool); // --- plaza graph (review queue, reports, featured, analytics, creators) --- const plazaRedis = createNoopPlazaRedis(); await ensureAlgorithmConfig(pool); const algorithmConfig = await loadAlgorithmConfig(pool); const plazaInteractions = createPlazaInteractionService(pool, { formatPostRow, plazaRedis }); let plazaOps = null; const plazaPosts = createPlazaPostService(pool, { loadViewerReactions: (viewerId, postIds) => plazaInteractions.loadViewerReactions(viewerId, postIds), plazaRedis, algorithmConfig, onPostPublished: noop, loadFeaturedPosts: async (viewerId) => { if (!plazaOps) return { homepage_banner: [], trending: [], category_top: {} }; return plazaOps.loadActiveFeaturedPosts(viewerId); }, }); plazaOps = createPlazaOpsService(pool, { formatPostRow, reviewPost: (...args) => plazaPosts.reviewPost(...args), invalidateFeedCaches: () => plazaRedis?.invalidateFeedCaches?.(), }); // --- platform super-admin services --- const subscriptionService = createSubscriptionService(pool); const userAuth = createUserAuth(pool, { usersRoot, h5Root, defaultSignupBalanceCents, subscriptionService, }); if (env.ensureAdminUser !== false) { await userAuth.ensureAdminUser(); } const llmProviderService = createLlmProviderService(pool, { apiTarget, apiSecret }); const assetGatewayConfigService = createAssetGatewayConfigService(pool, { llmProviderService }); const imageMakeAdminConfigService = createImageMakeAdminConfigService(pool, { env: process.env, llmProviderService, }); await imageMakeAdminConfigService.ensureSchema(); const memoryV2ConfigService = createMemoryV2AdminConfigService(pool); const mindSearchConfigService = createMindSearchConfigService(pool); await mindSearchConfigService.ensureSchema(); const skillRuntimeConfigService = createSkillRuntimeAdminConfigService(pool, { h5Root }); const systemDisclosurePolicyService = createSystemDisclosurePolicyService(pool, { autoRefresh: false, }); await systemDisclosurePolicyService.initialize(); const wechatScheduleLlmConfigService = createWechatScheduleLlmConfigService(pool); const adminSystemTestService = createAdminSystemTestService({ pool, userAuth, portalBaseUrl: process.env.SYSTEM_TEST_PORTAL_BASE_URL ?? `http://127.0.0.1:${process.env.H5_PORT ?? 8081}`, }); const wechatMpConfig = loadWechatMpConfig(); const wechatMpService = createWechatMpService({ config: wechatMpConfig, userAuth, apiFetch: async () => { throw new Error('admin bootstrap does not proxy WeChat chat sessions'); }, }); const notificationDispatcher = createNotificationDispatcher({ sendWechatTextToUser: wechatMpService?.enabled ? (userId, text) => wechatMpService.sendTextToUser(userId, text) : null, }); userAuth.setRechargeNotifier(async ({ userId, title, body, dedupeKey }) => { await notificationDispatcher.sendRechargeSuccess({ userId, title, body, dedupeKey }); }); const wechatAdmin = createWechatAdminService(pool, { config: wechatMpConfig, scheduleEnabled: process.env.H5_SCHEDULE_ENABLED === '1', reminderWorkerEnabled: process.env.H5_REMINDER_WORKER_ENABLED === '1', sendWechatTextToUser: wechatMpService?.enabled ? (userId, text) => notificationDispatcher.sendScheduleNotification({ userId, text }) : null, }); return { pool, userAuth, llmProviderService, assetGatewayConfigService, imageMakeAdminConfigService, memoryV2ConfigService, mindSearchConfigService, skillRuntimeConfigService, systemDisclosurePolicyService, wechatScheduleLlmConfigService, adminSystemTestService, plazaPosts, plazaOps, wechatAdmin, subscriptionService, }; }