main: 初始化 Face to API 项目

- 添加前端抽卡台与模型配置界面
- 添加 Express API 服务与多厂商模型调用适配
- 配置本地环境示例、构建脚本和忽略规则
This commit is contained in:
2026-05-27 11:10:40 +08:00
commit cf457e8349
17 changed files with 7304 additions and 0 deletions

121
server/modelConfigStore.ts Normal file
View File

@@ -0,0 +1,121 @@
import fs from "node:fs";
import path from "node:path";
export type SavedModelConfig = {
baseUrl?: string;
model?: string;
apiKey?: string;
enabled?: boolean;
updatedAt?: string;
};
type ConfigFile = {
version: 1;
models: Record<string, SavedModelConfig>;
};
export type SaveModelConfigInput = {
baseUrl?: string;
model?: string;
apiKey?: string;
enabled?: boolean;
clearApiKey?: boolean;
};
const defaultConfigPath = () =>
path.resolve(process.cwd(), process.env.MODEL_CONFIG_PATH?.trim() || ".face-to-api/model-configs.json");
let cache: ConfigFile | undefined;
const emptyStore = (): ConfigFile => ({ version: 1, models: {} });
const readStore = (): ConfigFile => {
if (cache) {
return cache;
}
const filePath = defaultConfigPath();
if (!fs.existsSync(filePath)) {
cache = emptyStore();
return cache;
}
try {
const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8")) as ConfigFile;
cache = {
version: 1,
models: parsed.models ?? {}
};
return cache;
} catch {
cache = emptyStore();
return cache;
}
};
const writeStore = (store: ConfigFile) => {
const filePath = defaultConfigPath();
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, `${JSON.stringify(store, null, 2)}\n`);
cache = store;
};
const trimOrUndefined = (value?: string) => {
const trimmed = value?.trim();
return trimmed ? trimmed : undefined;
};
export const getSavedModelConfig = (modelId: string): SavedModelConfig | undefined => {
const saved = readStore().models[modelId];
return saved ? { ...saved } : undefined;
};
export const saveModelConfig = (modelId: string, input: SaveModelConfigInput) => {
const store = readStore();
const existing = store.models[modelId] ?? {};
const next: SavedModelConfig = { ...existing };
const baseUrl = trimOrUndefined(input.baseUrl);
const model = trimOrUndefined(input.model);
const apiKey = trimOrUndefined(input.apiKey);
if (input.baseUrl !== undefined) {
if (baseUrl) {
next.baseUrl = baseUrl;
} else {
delete next.baseUrl;
}
}
if (input.model !== undefined) {
if (model) {
next.model = model;
} else {
delete next.model;
}
}
if (input.clearApiKey) {
delete next.apiKey;
} else if (apiKey) {
next.apiKey = apiKey;
}
if (input.enabled !== undefined) {
next.enabled = input.enabled;
}
next.updatedAt = new Date().toISOString();
store.models[modelId] = next;
writeStore(store);
return { ...next };
};
export const maskApiKey = (apiKey?: string) => {
if (!apiKey) {
return "";
}
const tail = apiKey.slice(-4);
return `•••• ${tail}`;
};