import { stat } from "node:fs/promises"; import { dbPath } from "./db.mjs"; import { backupSummary } from "./backup.mjs"; function envValue(name) { return String(process.env[name] || "").trim(); } function configuredCheck(key, label, envName, detail) { const value = envValue(envName); return { key, label, status: value ? "configured" : "not-configured", severity: value ? "info" : "warning", blocking: false, detail: value ? `${detail}:已读取 ${envName}` : `${detail}:未设置 ${envName}`, envName }; } async function databaseCheck() { try { const file = await stat(dbPath); return { key: "business-database", label: "业务数据库", status: "active-local", severity: "warning", blocking: false, provider: "Node 24 node:sqlite", detail: "当前业务真源是本地 SQLite;尚未切换 PostgreSQL 高可用运行时。", path: dbPath, bytes: Number(file.size || 0), modifiedAt: file.mtime?.toISOString?.() || null }; } catch (error) { return { key: "business-database", label: "业务数据库", status: "failed", severity: "critical", blocking: true, provider: "Node 24 node:sqlite", detail: `数据库文件不可读:${error.message}`, path: dbPath }; } } export async function systemReadiness() { const backups = await backupSummary(); const allowDevContext = process.env.AI_DRAMA_ALLOW_DEV_CONTEXT === "1"; const sessionSecret = envValue("AI_DRAMA_SESSION_SECRET"); const mfaKey = envValue("AI_DRAMA_MFA_ENCRYPTION_KEY"); const oidcKey = envValue("AI_DRAMA_OIDC_STORAGE_KEY"); const checks = [ await databaseCheck(), configuredCheck("postgres-target", "PostgreSQL 目标", "PLATFORM_POSTGRES_URL", "目标数据库连接"), configuredCheck("redis-target", "Redis 目标", "PLATFORM_REDIS_URL", "目标队列/分布式锁连接"), configuredCheck("object-storage-target", "对象存储目标", "PLATFORM_OBJECT_STORAGE_ENDPOINT", "S3-compatible 存储端点"), { key: "security-secrets", label: "生产密钥", status: sessionSecret.length >= 32 && mfaKey.length >= 16 && oidcKey.length >= 16 ? "ready" : "needs-config", severity: sessionSecret.length >= 32 && mfaKey.length >= 16 && oidcKey.length >= 16 ? "info" : "critical", blocking: sessionSecret.length < 32 || mfaKey.length < 16 || oidcKey.length < 16, detail: "Session、MFA 和 OIDC 存储密钥必须通过环境变量注入,不写入数据库。", configured: { sessionSecret: sessionSecret.length >= 32, mfaKey: mfaKey.length >= 16, oidcKey: oidcKey.length >= 16 } }, { key: "dev-context", label: "开发上下文旁路", status: allowDevContext ? "unsafe" : "ready", severity: allowDevContext ? "critical" : "info", blocking: allowDevContext, detail: allowDevContext ? "AI_DRAMA_ALLOW_DEV_CONTEXT=1,生产部署禁止启用。" : "请求头上下文旁路已关闭,使用真实 session/API client。" }, { key: "database-backup", label: "数据库快照", status: backups.count ? "ready" : "missing", severity: backups.count ? "info" : "warning", blocking: false, detail: backups.count ? `已有 ${backups.count} 个本地快照,最近一次 ${backups.latest?.modifiedAt || "未知"}。` : "还没有本地 SQLite 快照;上线前应先创建并验证备份。" } ]; const blocking = checks.filter((check) => check.blocking && !["ready", "configured"].includes(check.status)).length; const attention = checks.filter((check) => check.severity === "warning" || check.severity === "critical").length; return { profile: process.env.NODE_ENV === "production" ? "production" : "local-development", activeRuntime: { database: "node:sqlite", queue: "database-lease-worker", objectStorage: "local-filesystem" }, summary: { status: blocking ? "blocked" : attention ? "attention" : "ready", blocking, attention, ready: checks.length - attention }, checks, backups, checkedAt: new Date().toISOString() }; }