Initial 智念AIGC platform

This commit is contained in:
inman
2026-05-29 10:26:02 +08:00
commit f9c3393f84
86 changed files with 14741 additions and 0 deletions

371
lib/server/app-settings.ts Normal file
View File

@@ -0,0 +1,371 @@
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<SettingsField, "value" | "configured"> & {
defaultValue?: string;
};
const settingDefinitions: Array<{
id: string;
title: string;
description: string;
fields: FieldDefinition[];
}> = [
{
id: "visual",
title: "即梦图片 API",
description: "只需填写火山 AK/SKRegion、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 KeyBase 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<string, unknown>) {
const updates = new Map<string, string>();
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<string, string>) {
return process.env[field.key] ?? fileEnv.get(field.key) ?? field.defaultValue ?? "";
}
function buildEngineAssignments(fileEnv: Map<string, string>): 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<string, string>;
}): 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<string, string>): 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<string, string>) {
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<string, string>();
throw error;
}
}
async function writeEnvFile(updates: Map<string, string>) {
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<string>();
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<string, string>();
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;
}