import { mkdir, readFile, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import { getEvolinkImageSettings, getSelectedImageEngine, shouldMockEvolinkApi, type ImageCreationEngine } from "@/lib/evolink/image-client"; import { authConfigSummary, getAuthRuntimeConfig } from "@/lib/auth/config"; import { getJimengCapabilities } from "@/lib/jimeng/capabilities"; import { getSeedanceConfig, shouldMockSeedance } from "@/lib/seedance/client"; import { platformAccountStoreConfigured } from "@/lib/server/account-store"; import { rootDir } from "@/lib/server/runtime"; import { shouldMockVisualApi } from "@/lib/volcengine/visual-client"; import type { EnabledImageCapability } from "@/lib/types"; import { getBailianConfig, shouldMockBailian } from "@/lib/bailian/client"; export type SettingsField = { key: string; label: string; description?: string; secret?: boolean; type?: "text" | "password" | "number" | "select"; options?: Array<{ label: string; value: string }>; value: string; configured: boolean; }; export type SettingsGroup = { id: string; title: string; description: string; fields: SettingsField[]; }; export type EngineAssignment = { id: string; label: string; engine: string; engineLabel: string; connected: boolean; connectionLabel: string; reqKey: string; configurable: boolean; field?: SettingsField; }; type FieldDefinition = Omit & { defaultValue?: string; }; const settingDefinitions: Array<{ id: string; title: string; description: string; fields: FieldDefinition[]; }> = [ { id: "auth", title: "平台账号安全", description: "平台自建手机号账号登录;密码哈希只保存在服务端,登录会话使用 HttpOnly 签名 Cookie。", fields: [ { key: "ZHINIAN_AUTH_REQUIRED", label: "登录保护", type: "select", defaultValue: "auto", options: [ { label: "自动", value: "auto" }, { label: "启用", value: "1" }, { label: "停用", value: "0" } ] }, { key: "ZHINIAN_AUTH_SESSION_SECRET", label: "会话签名密钥", secret: true, type: "password" } ] }, { id: "billing", title: "企业计费", description: "管理真实任务计费开关和成员线下转账时看到的对公账户信息。", fields: [ { key: "ZHINIAN_BILLING_REQUIRED", label: "真实任务计费", type: "select", defaultValue: "1", options: [ { label: "启用", value: "1" }, { label: "停用(免计费)", value: "0" } ] }, { key: "ZHINIAN_BILLING_ACCOUNT_NAME", label: "对公账户名称" }, { key: "ZHINIAN_BILLING_ACCOUNT_BANK", label: "开户行" }, { key: "ZHINIAN_BILLING_ACCOUNT_NUMBER", label: "银行账号" }, { key: "ZHINIAN_BILLING_CONTACT", label: "充值对接信息" } ] }, { id: "visual", title: "即梦图片 API", description: "只需填写火山 AK/SK;Region、Endpoint 和 req_key 使用系统默认值。", fields: [ { key: "VOLCENGINE_ACCESS_KEY_ID", label: "Access Key ID", secret: true, type: "password" }, { key: "VOLCENGINE_SECRET_ACCESS_KEY", label: "Secret Access Key", secret: true, type: "password" } ] }, { id: "evolink", title: "EvoLink 图片 API", description: "用于 GPT Image 2 图片生成;Base URL 和模型使用系统默认值即可。", fields: [ { key: "EVOLINK_API_KEY", label: "EvoLink API Key", secret: true, type: "password" }, { key: "EVOLINK_BASE_URL", label: "Base URL", defaultValue: "https://api.evolink.ai" }, { key: "EVOLINK_IMAGE_MODEL", label: "图片模型", defaultValue: "gpt-image-2" }, { key: "EVOLINK_IMAGE_QUALITY", label: "质量", defaultValue: "medium" } ] }, { id: "seedance", title: "Seedance 视频 API", description: "只需填写火山方舟 API Key;Base URL、模型和默认参数使用系统默认值。", fields: [ { key: "SEEDANCE_API_KEY", label: "方舟 API Key", secret: true, type: "password" } ] }, { id: "bailian", title: "阿里云百炼 API", description: "用于万相 2.7 文生图、参考图生图和图生视频。Base URL 会自动派生原生异步接口。", fields: [ { key: "BAILIAN_API_KEY", label: "百炼 API Key", secret: true, type: "password" }, { key: "BAILIAN_BASE_URL", label: "Base URL", defaultValue: "https://llm-126wneubbdo6dbr5.cn-beijing.maas.aliyuncs.com/compatible-mode/v1" }, { key: "BAILIAN_IMAGE_MODEL", label: "图片模型", defaultValue: "wan2.7-image-pro" }, { key: "BAILIAN_VIDEO_MODEL", label: "视频模型", defaultValue: "wan2.7-i2v-2026-04-25" } ] }, { id: "oss", title: "OSS 资产存储", description: "用于上传素材、生成图和视频结果的公网转存;对象前缀使用默认值。", fields: [ { key: "ALI_OSS_ENDPOINT", label: "Endpoint" }, { key: "ALI_OSS_BUCKET", label: "Bucket" }, { key: "ALI_OSS_ACCESS_KEY_ID", label: "Access Key ID", secret: true, type: "password" }, { key: "ALI_OSS_ACCESS_KEY_SECRET", label: "Access Key Secret", secret: true, type: "password" }, { key: "ALI_OSS_PUBLIC_BASE_URL", label: "公开访问 Base URL" } ] } ]; const engineFieldDefinitions: FieldDefinition[] = [ { key: "IMAGE_GENERATE_ENGINE", label: "图片生成", type: "select", defaultValue: "jimeng", options: [ { label: "即梦 / 火山视觉", value: "jimeng" }, { label: "EvoLink GPT Image 2", value: "evolink" }, { label: "阿里云百炼 Wan 2.7", value: "bailian" } ] }, { key: "VIDEO_GENERATE_ENGINE", label: "视频生成", type: "select", defaultValue: "bailian", options: [ { label: "Seedance", value: "seedance" }, { label: "阿里云百炼 Wan 2.7", value: "bailian" } ] }, ]; const allowedKeys = new Set([ ...settingDefinitions.flatMap((group) => group.fields.map((field) => field.key)), ...engineFieldDefinitions.map((field) => field.key) ]); export async function getApiSettings() { const fileEnv = await readEnvFile(); const groups: SettingsGroup[] = settingDefinitions.map((group) => ({ ...group, fields: group.fields.map((field) => { const rawValue = currentValue(field, fileEnv); return { ...field, value: field.secret ? "" : rawValue, configured: Boolean(rawValue) }; }) })); const seedance = getSeedanceConfig(); const auth = getAuthRuntimeConfig(); const engineAssignments = buildEngineAssignments(fileEnv); return { services: { visual: !shouldMockVisualApi(), evolink: !shouldMockEvolinkApi(), seedance: !shouldMockSeedance(), bailian: !shouldMockBailian() && Boolean(getBailianConfig().apiKey), auth: authConfigSummary(auth) === "configured", organization: platformAccountStoreConfigured() }, capabilities: [ ...Object.values(getJimengCapabilities()).map((capability) => { const assignment = engineAssignments.find((item) => item.id === capability.id); return { id: capability.id, label: capability.label, reqKey: assignment?.reqKey || capability.reqKey, engine: assignment?.engine, engineLabel: assignment?.engineLabel, enabled: capability.enabled }; }), { id: "video.generate", label: "视频生成", reqKey: engineAssignments.find((item) => item.id === "video.generate")?.reqKey || seedance.model, engine: engineAssignments.find((item) => item.id === "video.generate")?.engine || "seedance", engineLabel: engineAssignments.find((item) => item.id === "video.generate")?.engineLabel || "Seedance", enabled: true } ], engineAssignments, groups }; } export async function saveApiSettings(values: Record) { const updates = new Map(); for (const field of [...settingDefinitions.flatMap((group) => group.fields), ...engineFieldDefinitions]) { if (!allowedKeys.has(field.key)) continue; const nextValue = values[field.key]; if (typeof nextValue !== "string") continue; const normalized = nextValue.trim(); if (field.secret && !normalized) continue; updates.set(field.key, normalized); } if (updates.size) { await writeEnvFile(updates); for (const [key, value] of updates) process.env[key] = value; } return getApiSettings(); } function currentValue(field: FieldDefinition, fileEnv: Map) { return process.env[field.key] ?? fileEnv.get(field.key) ?? field.defaultValue ?? ""; } function buildEngineAssignments(fileEnv: Map): EngineAssignment[] { const capabilities = getJimengCapabilities(); const evolink = getEvolinkImageSettings(); const seedance = getSeedanceConfig(); const bailian = getBailianConfig(); const generateEngine = currentEngineValue(engineFieldDefinitions[0], fileEnv); const videoEngine = currentVideoEngineValue(engineFieldDefinitions[1], fileEnv); const videoConnected = videoEngine === "bailian" ? !shouldMockBailian() : !shouldMockSeedance(); return [ imageEngineAssignment({ capability: "image.generate", label: "图片生成", engine: generateEngine, reqKey: generateEngine === "bailian" ? bailian.imageModel : generateEngine === "evolink" ? evolink.model : capabilities["image.generate"].reqKey, field: engineFieldDefinitions[0], fileEnv }), { id: "video.generate", label: "视频生成", engine: videoEngine, engineLabel: videoEngine === "bailian" ? "阿里云百炼" : "Seedance", connected: videoConnected, connectionLabel: videoConnected ? "已连接" : "待配置", reqKey: videoEngine === "bailian" ? bailian.videoModel : seedance.model, configurable: true, field: fieldWithValue(engineFieldDefinitions[1], videoEngine, isConfigured(engineFieldDefinitions[1].key, fileEnv)) } ]; } function imageEngineAssignment(input: { capability: EnabledImageCapability; label: string; engine: ImageCreationEngine; reqKey: string; field: FieldDefinition; fileEnv: Map; }): EngineAssignment { const connected = imageEngineConnected(input.engine); return { id: input.capability, label: input.label, engine: input.engine, engineLabel: imageEngineLabel(input.engine), connected, connectionLabel: connected ? "已连接" : "待配置", reqKey: input.reqKey, configurable: true, field: fieldWithValue(input.field, currentEngineValue(input.field, input.fileEnv), isConfigured(input.field.key, input.fileEnv)) }; } function currentEngineValue(field: FieldDefinition, fileEnv: Map): ImageCreationEngine { const explicit = process.env[field.key] ?? fileEnv.get(field.key); if (explicit) return normalizeImageEngine(explicit); return normalizeImageEngine(process.env.IMAGE_CREATION_ENGINE ?? fileEnv.get("IMAGE_CREATION_ENGINE") ?? process.env.IMAGE_PROVIDER ?? fileEnv.get("IMAGE_PROVIDER") ?? field.defaultValue); } function fieldWithValue(field: FieldDefinition, value: string, configured: boolean): SettingsField { return { ...field, value, configured }; } function isConfigured(key: string, fileEnv: Map) { return Boolean(process.env[key] ?? fileEnv.get(key)); } function normalizeImageEngine(value: string | undefined): ImageCreationEngine { const normalized = value?.trim().toLowerCase(); return normalized === "evolink" || normalized === "bailian" ? normalized : "jimeng"; } function imageEngineLabel(engine: ImageCreationEngine) { return engine === "evolink" ? "EvoLink" : engine === "bailian" ? "阿里云百炼" : "即梦"; } function imageEngineConnected(engine: ImageCreationEngine) { if (engine === "evolink") return !shouldMockEvolinkApi(); if (engine === "bailian") return !shouldMockBailian(); return !shouldMockVisualApi(); } function currentVideoEngineValue(field: FieldDefinition, fileEnv: Map) { return (process.env[field.key] ?? fileEnv.get(field.key) ?? field.defaultValue) === "bailian" ? "bailian" : "seedance"; } function envFilePath() { return join(rootDir(), ".env.local"); } async function readEnvFile() { try { return parseEnvText(await readFile(envFilePath(), "utf8")); } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return new Map(); throw error; } } async function writeEnvFile(updates: Map) { const path = envFilePath(); let text = ""; try { text = await readFile(path, "utf8"); } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; text = "# 智念AIGC平台 API 配置\n"; } const seen = new Set(); const lines = text.split(/\r?\n/).map((line) => { const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/); if (!match) return line; const key = match[1]; if (!updates.has(key)) return line; seen.add(key); return `${key}=${formatEnvValue(updates.get(key) || "")}`; }); const missing = [...updates.entries()].filter(([key]) => !seen.has(key)); if (missing.length) { if (lines.length && lines[lines.length - 1] !== "") lines.push(""); lines.push("# Managed by 智念AIGC平台 设置页"); for (const [key, value] of missing) lines.push(`${key}=${formatEnvValue(value)}`); } await mkdir(dirname(path), { recursive: true }); await writeFile(path, `${lines.join("\n").replace(/\n+$/, "")}\n`); } function parseEnvText(text: string) { const env = new Map(); for (const line of text.split(/\r?\n/)) { const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/); if (!match) continue; env.set(match[1], parseEnvValue(match[2])); } return env; } function parseEnvValue(value: string) { const trimmed = value.trim(); if (!trimmed) return ""; if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) { return trimmed.slice(1, -1).replace(/\\n/g, "\n").replace(/\\"/g, '"').replace(/\\\\/g, "\\"); } return trimmed.replace(/\s+#.*$/, ""); } function formatEnvValue(value: string) { if (!value) return ""; if (/[\s#"'\\]/.test(value)) return JSON.stringify(value); return value; }