Files
memind/admin-bootstrap.mjs
john 205b5fd8be
Memind CI / Test, build, and release guards (pull_request) Successful in 3m14s
feat: add system disclosure policy gate
2026-07-23 22:53:15 +08:00

169 lines
7.4 KiB
JavaScript

// 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,
};
}