feat(models): 模型目录与计费 - 按展示名分组并隔离管理员积分成本

This commit is contained in:
ddCat
2026-09-19 09:54:27 +08:00
parent 5ad05efb58
commit 237f2e2a6e
49 changed files with 802 additions and 276 deletions

View File

@@ -6,7 +6,7 @@ export type ModelPickerGroup = {
label: string;
icon: string;
scope: string;
kind: "product" | "channel" | "system-channel";
kind: "product" | "channel";
models: DisplayModelGroup[];
};
@@ -20,29 +20,24 @@ export function isDirectSystemModel(config: AiConfig, value: string) {
export function modelChannelLabel(config: AiConfig, value: string) {
const channel = resolveModelChannel(config, value);
const cost = channel.modelCosts?.find((item) => item.model === modelOptionName(value));
return cost?.channelLabel?.trim() || channel.publicAlias?.trim() || channel.name || "未命名渠道";
return cost?.channelLabel?.trim() || channel.name || "未命名渠道";
}
// 系统模型按渠道聚合展示,但每个模型仍保留独立的 channelId::modelKey 选择值。
// 一级按模型展示名跨渠道聚合;二级保留每条渠道模型的独立选择值、能力和售价。
export function groupModelsForPicker(config: AiConfig, options: string[]): ModelPickerGroup[] {
const groups = new Map<string, ModelPickerGroup>();
for (const channel of config.channels) {
const models = options.filter((value) => resolveModelChannel(config, value).id === channel.id);
const directModels = models.filter((value) => isDirectSystemModel(config, value));
if (directModels.length) {
const key = JSON.stringify(["system-channel", channel.id]);
groups.set(key, {
key,
label: channel.name || "未命名渠道",
icon: modelIcon(config, directModels[0]),
scope: "平台服务",
kind: "system-channel",
models: directModels.map((value) => ({
key: value,
label: configuredModelDisplayName(config, value),
models: [value],
})),
});
for (const value of directModels) {
const label = configuredModelDisplayName(config, value);
const key = JSON.stringify(["product", label]);
let group = groups.get(key);
if (!group) {
group = { key, label, icon: modelIcon(config, value), scope: "平台服务", kind: "product", models: [] };
groups.set(key, group);
}
group.models.push({ key: value, label: modelChannelLabel(config, value), models: [value] });
}
const otherModels = models.filter((value) => !isDirectSystemModel(config, value));
if (otherModels.length) {

View File

@@ -1,12 +1,12 @@
import { getFeatureAvailability, type AuthSessionPayload } from "@/services/api/auth";
import { getModelCatalog, type CapabilitySpec, type ModelCatalogResponse, type OptionConstraint, type PublicChannelCatalog, type PublicLogicalModel } from "@/services/api/logical-models";
import { getModelCatalog, type CapabilitySpec, type ModelCatalogResponse, type OptionConstraint, type PublicChannelCatalog } from "@/services/api/logical-models";
import { localForageStorage } from "@/lib/localforage-storage";
import { appQueryClient } from "@/lib/query-client";
import { scopedLocalStorage, setActiveUserScope } from "@/lib/user-scope";
import { CANVAS_STORE_KEY, flushCanvasStorePersistence, useCanvasStore } from "@/stores/canvas/use-canvas-store";
import { CANVAS_HISTORY_STORE_KEY, useCanvasHistoryStore } from "@/stores/canvas/use-canvas-history-store";
import { ASSET_STORE_KEY, flushAssetStorePersistence, useAssetStore } from "@/stores/use-asset-store";
import { CONFIG_STORE_KEY, PUBLIC_MODEL_CATALOG_ID, defaultConfig, normalizeConfigSnapshot, useConfigStore, type ModelCapability, type ModelChannel } from "@/stores/use-config-store";
import { CONFIG_STORE_KEY, defaultConfig, normalizeConfigSnapshot, useConfigStore, type ModelCapability, type ModelChannel } from "@/stores/use-config-store";
import { CREATION_PREFERENCES_STORE_KEY, useCreationPreferencesStore } from "@/stores/use-creation-preferences-store";
import { defaultModelCapabilityConfig, STANDARD_IMAGE_SIZE_VALUES, type ModelCapabilityConfig } from "@/lib/model-capabilities";
import { imageSizeConfigWithPresets } from "@/lib/image-size-presets";
@@ -90,12 +90,8 @@ export async function refreshSystemChannels() {
useConfigStore.getState().mergeSystemChannels(modelCatalogChannels(catalog));
}
// 模型目录来源决定数据形状;这里统一做运行时收口,避免畸形响应被当成“空目录”写入用户配置。
// 目录仅接受系统渠道模型,避免畸形响应被当成空目录写入配置。
function modelCatalogChannels(catalog: ModelCatalogResponse): ModelChannel[] {
if (catalog.source === "frontend") {
if (!Array.isArray(catalog.models)) throw new Error("模型目录响应缺少前台模型列表");
return managedModelChannels(catalog.models);
}
if (catalog.source === "system") {
if (!Array.isArray(catalog.channels)) throw new Error("模型目录响应缺少系统渠道列表");
return systemChannelModelChannels(catalog.channels);
@@ -103,42 +99,6 @@ function modelCatalogChannels(catalog: ModelCatalogResponse): ModelChannel[] {
throw new Error("模型目录响应来源无效");
}
function managedModelChannels(models: PublicLogicalModel[]) {
const availableModels = models.filter((item) => item.available);
if (!availableModels.length) return [];
const managed: ModelChannel = {
id: PUBLIC_MODEL_CATALOG_ID,
name: "平台模型",
baseUrl: "/api",
apiKey: "system",
apiFormat: "openai",
scope: "system",
enabled: true,
models: availableModels.map((item) => item.id),
modelAliases: Object.fromEntries(availableModels.flatMap((item) => (item.legacyModelIds || []).map((legacyID) => [legacyID, item.id]))),
modelCosts: availableModels.map((item) => ({
model: item.id,
displayName: item.name,
description: item.description,
icon: item.icon,
capability: item.capability,
pricePolicy: item.pricePolicy,
billingMode: item.billingMode,
unitPriceMicrocredits: item.unitPriceMicrocredits,
inputTokenPriceMicrocredits: item.inputPriceMicrocredits,
outputTokenPriceMicrocredits: item.outputPriceMicrocredits,
cachedTokenPriceMicrocredits: item.cachedPriceMicrocredits,
capabilityConfig: projectLogicalCapability(item.capabilitySpec, item.defaultOptions),
logicalModelId: item.id,
logicalCapabilitySpec: item.capabilitySpec,
logicalCapabilityProfiles: item.capabilityProfiles,
logicalPriceTiers: item.priceTiers,
defaultOptions: item.defaultOptions,
})),
};
return [managed];
}
// 系统渠道模型转换为前端配置格式
export function systemChannelModelChannels(channels: PublicChannelCatalog[]): ModelChannel[] {
return channels.map((channel) => {