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

261
server/modelRegistry.ts Normal file
View File

@@ -0,0 +1,261 @@
export type ProviderKind = "openai-compatible" | "anthropic" | "gemini";
export type ProviderId =
| "deepseek"
| "zhipu"
| "openai"
| "anthropic"
| "gemini"
| "custom-openai";
export type ModelProvider = {
id: ProviderId;
name: string;
kind: ProviderKind;
baseUrl?: string;
apiKeyEnv: string;
apiKey?: string;
configured: boolean;
};
export type ChatModel = {
id: string;
label: string;
providerId: ProviderId;
providerName: string;
providerKind: ProviderKind;
model: string;
baseUrl: string;
enabled: boolean;
configured: boolean;
apiKeyConfigured: boolean;
apiKeyPreview: string;
apiKeySource: "model" | "provider-env" | "none";
configUpdatedAt?: string;
accent: "red" | "green" | "blue" | "gold" | "ink";
};
import { getSavedModelConfig, maskApiKey } from "./modelConfigStore.js";
const env = (key: string, fallback = "") => process.env[key]?.trim() || fallback;
export const providers = (): ModelProvider[] => [
{
id: "deepseek",
name: "DeepSeek",
kind: "openai-compatible",
baseUrl: env("DEEPSEEK_BASE_URL", "https://api.deepseek.com"),
apiKeyEnv: "DEEPSEEK_API_KEY",
configured: Boolean(env("DEEPSEEK_API_KEY"))
},
{
id: "zhipu",
name: "Zhipu GLM",
kind: "openai-compatible",
baseUrl: env("ZHIPU_BASE_URL", "https://open.bigmodel.cn/api/paas/v4"),
apiKeyEnv: "ZHIPU_API_KEY",
configured: Boolean(env("ZHIPU_API_KEY"))
},
{
id: "openai",
name: "OpenAI",
kind: "openai-compatible",
baseUrl: env("OPENAI_BASE_URL", "https://api.openai.com/v1"),
apiKeyEnv: "OPENAI_API_KEY",
configured: Boolean(env("OPENAI_API_KEY"))
},
{
id: "anthropic",
name: "Anthropic",
kind: "anthropic",
baseUrl: "https://api.anthropic.com/v1",
apiKeyEnv: "ANTHROPIC_API_KEY",
configured: Boolean(env("ANTHROPIC_API_KEY"))
},
{
id: "gemini",
name: "Google Gemini",
kind: "gemini",
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
apiKeyEnv: "GEMINI_API_KEY",
configured: Boolean(env("GEMINI_API_KEY"))
}
];
const resolveModel = (
model: Omit<
ChatModel,
| "providerName"
| "providerKind"
| "configured"
| "baseUrl"
| "enabled"
| "apiKeyConfigured"
| "apiKeyPreview"
| "apiKeySource"
>,
provider: ModelProvider
): ChatModel => {
const saved = getSavedModelConfig(model.id);
const envApiKey = env(provider.apiKeyEnv);
const apiKey = saved?.apiKey || envApiKey;
const apiKeySource = saved?.apiKey ? "model" : envApiKey ? "provider-env" : "none";
return {
...model,
providerName: provider.name,
providerKind: provider.kind,
model: saved?.model || model.model,
baseUrl: saved?.baseUrl || provider.baseUrl || "",
enabled: saved?.enabled ?? true,
configured: Boolean(apiKey),
apiKeyConfigured: Boolean(apiKey),
apiKeyPreview: maskApiKey(apiKey),
apiKeySource,
configUpdatedAt: saved?.updatedAt
};
};
export const registry = (): ChatModel[] => {
const providerMap = new Map(providers().map((provider) => [provider.id, provider]));
const baseModels: Array<
Omit<
ChatModel,
| "providerName"
| "providerKind"
| "configured"
| "baseUrl"
| "enabled"
| "apiKeyConfigured"
| "apiKeyPreview"
| "apiKeySource"
>
> = [
{
id: "deepseek-v4-pro",
label: "DeepSeekV4-pro",
providerId: "deepseek",
model: env("DEEPSEEK_MODEL_V4_PRO", "DeepSeekV4-pro"),
accent: "red"
},
{
id: "deepseek-v4-flash",
label: "DeepSeekV4-flash",
providerId: "deepseek",
model: env("DEEPSEEK_MODEL_V4_FLASH", "DeepSeekV4-flash"),
accent: "red"
},
{
id: "glm-5-1",
label: "GLM5.1",
providerId: "zhipu",
model: env("ZHIPU_MODEL_GLM_5_1", "GLM5.1"),
accent: "green"
},
{
id: "gpt-5-5",
label: "GPT-5.5",
providerId: "openai",
model: env("OPENAI_MODEL_GPT_5_5", "gpt-5.5"),
accent: "blue"
},
{
id: "claude-opus-4-7",
label: "Claude Opus 4.7",
providerId: "anthropic",
model: env("ANTHROPIC_MODEL_CLAUDE_OPUS_4_7", "claude-opus-4.7"),
accent: "gold"
},
{
id: "gemini-3-1-pro",
label: "Gemini-3.1-pro",
providerId: "gemini",
model: env("GEMINI_MODEL_3_1_PRO", "Gemini-3.1-pro"),
accent: "ink"
},
{
id: "gemini-3-5-flash",
label: "gemini3.5-flash",
providerId: "gemini",
model: env("GEMINI_MODEL_3_5_FLASH", "gemini3.5-flash"),
accent: "ink"
}
];
const models = baseModels.map((model) => {
const provider = providerMap.get(model.providerId);
if (!provider) {
throw new Error(`Missing provider for ${model.id}`);
}
return resolveModel(model, provider);
});
const customIds = env("CUSTOM_OPENAI_MODEL_IDS")
.split(",")
.map((value) => value.trim())
.filter(Boolean);
if (customIds.length && env("CUSTOM_OPENAI_BASE_URL")) {
const customConfigured = Boolean(env("CUSTOM_OPENAI_API_KEY"));
models.push(
...customIds.map((modelId) =>
resolveModel(
{
id: `custom-${modelId.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`,
label: modelId,
providerId: "custom-openai" as const,
model: modelId,
configUpdatedAt: undefined,
accent: "blue" as const
},
{
id: "custom-openai",
name: env("CUSTOM_OPENAI_NAME", "Custom OpenAI-compatible"),
kind: "openai-compatible",
baseUrl: env("CUSTOM_OPENAI_BASE_URL"),
apiKeyEnv: "CUSTOM_OPENAI_API_KEY",
configured: customConfigured
}
)
)
);
}
return models;
};
export const findModel = (modelId: string) => registry().find((model) => model.id === modelId);
export const providerForModel = (model: ChatModel): ModelProvider => {
const saved = getSavedModelConfig(model.id);
if (model.providerId === "custom-openai") {
const envApiKey = env("CUSTOM_OPENAI_API_KEY");
return {
id: "custom-openai",
name: env("CUSTOM_OPENAI_NAME", "Custom OpenAI-compatible"),
kind: "openai-compatible",
baseUrl: saved?.baseUrl || env("CUSTOM_OPENAI_BASE_URL"),
apiKeyEnv: "CUSTOM_OPENAI_API_KEY",
apiKey: saved?.apiKey,
configured: Boolean(saved?.apiKey || envApiKey)
};
}
const provider = providers().find((item) => item.id === model.providerId);
if (!provider) {
throw new Error(`Provider not found for ${model.label}`);
}
const envApiKey = env(provider.apiKeyEnv);
return {
...provider,
baseUrl: saved?.baseUrl || provider.baseUrl,
apiKey: saved?.apiKey,
configured: Boolean(saved?.apiKey || envApiKey)
};
};
export const apiKeyForProvider = (provider: ModelProvider) => provider.apiKey || env(provider.apiKeyEnv);