Files
ai-drama-platform/server/readiness.mjs
T

99 lines
4.1 KiB
JavaScript

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()
};
}