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 { getJimengCapabilities } from "@/lib/jimeng/capabilities"; import { getSeedanceConfig, shouldMockSeedance } from "@/lib/seedance/client"; import { rootDir } from "@/lib/server/runtime"; import { shouldMockVisualApi } from "@/lib/volcengine/visual-client"; import type { EnabledImageCapability } from "@/lib/types"; 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; mode: string; modeLabel: string; reqKey: string; configurable: boolean; field?: SettingsField; }; type FieldDefinition = Omit & { defaultValue?: string; }; const settingDefinitions: Array<{ id: string; title: string; description: string; fields: FieldDefinition[]; }> = [ { 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" }, { key: "EVOLINK_IMAGE_RESOLUTION", label: "分辨率", defaultValue: "2K" }, { key: "EVOLINK_MOCK", label: "Mock 策略", type: "select", defaultValue: "auto", options: [ { label: "自动", value: "auto" }, { label: "总是 Mock", value: "true" }, { label: "总是真实接口", value: "false" } ] } ] }, { id: "seedance", title: "Seedance 视频 API", description: "只需填写火山方舟 API Key;Base URL、模型和默认参数使用系统默认值。", fields: [ { key: "SEEDANCE_API_KEY", label: "方舟 API Key", secret: true, type: "password" } ] }, { id: "oss", title: "OSS 资产存储", description: "用于上传素材、mask、生成图和视频结果的公网转存;对象前缀使用默认值。", 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" } ] }, { key: "IMAGE_INPAINT_ENGINE", label: "局部重绘", type: "select", defaultValue: "jimeng", options: [ { label: "即梦 / 火山视觉", value: "jimeng" }, { label: "EvoLink GPT Image 2", value: "evolink" } ] } ]; 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 engineAssignments = buildEngineAssignments(fileEnv); return { envPath: envFilePath(), modes: { visual: shouldMockVisualApi() ? "mock" : "real", evolink: shouldMockEvolinkApi() ? "mock" : "real", seedance: shouldMockSeedance() ? "mock" : "real", data: process.env.SUPABASE_SERVICE_ROLE_KEY ? "supabase" : "local" }, 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: "Seedance 视频生成", reqKey: seedance.model, engine: "seedance", 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 generateEngine = currentEngineValue(engineFieldDefinitions[0], fileEnv); const inpaintEngine = currentEngineValue(engineFieldDefinitions[1], fileEnv); return [ imageEngineAssignment({ capability: "image.generate", label: "图片生成", engine: generateEngine, reqKey: generateEngine === "evolink" ? evolink.model : capabilities["image.generate"].reqKey, field: engineFieldDefinitions[0], fileEnv }), imageEngineAssignment({ capability: "image.inpaint", label: "局部重绘", engine: inpaintEngine, reqKey: inpaintEngine === "evolink" ? evolink.model : capabilities["image.inpaint"].reqKey, field: engineFieldDefinitions[1], fileEnv }), { id: "image.upscale", label: "智能超清", engine: "jimeng", engineLabel: "即梦", mode: shouldMockVisualApi() ? "mock" : "volcengine", modeLabel: shouldMockVisualApi() ? "Mock" : "即梦真实接口", reqKey: capabilities["image.upscale"].reqKey, configurable: false }, { id: "video.generate", label: "视频生成", engine: "seedance", engineLabel: "Seedance", mode: shouldMockSeedance() ? "mock" : "seedance", modeLabel: shouldMockSeedance() ? "Mock" : "Seedance 真实接口", reqKey: seedance.model, configurable: false } ]; } function imageEngineAssignment(input: { capability: EnabledImageCapability; label: string; engine: ImageCreationEngine; reqKey: string; field: FieldDefinition; fileEnv: Map; }): EngineAssignment { const mode = modeForImageEngine(input.engine); return { id: input.capability, label: input.label, engine: input.engine, engineLabel: imageEngineLabel(input.engine), mode, modeLabel: modeLabel(mode, input.engine), 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 { return value?.trim().toLowerCase() === "evolink" ? "evolink" : "jimeng"; } function imageEngineLabel(engine: ImageCreationEngine) { return engine === "evolink" ? "EvoLink" : "即梦"; } function modeForImageEngine(engine: ImageCreationEngine) { if (engine === "evolink") return shouldMockEvolinkApi() ? "mock" : "evolink"; return shouldMockVisualApi() ? "mock" : "volcengine"; } function modeLabel(mode: string, engine: ImageCreationEngine) { if (mode === "mock") return "Mock"; return engine === "evolink" ? "EvoLink 真实接口" : "即梦真实接口"; } 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; }