feat(platform): 模型管理与后台能力 - 新增逻辑模型路由和响应拦截
This commit is contained in:
@@ -4,6 +4,7 @@ import type { GenerationTask, TaskStatus } from "@/services/api/task-center";
|
||||
import type { CanvasDrawingEngineSetting } from "@/lib/canvas/canvas-drawing-engine";
|
||||
import type { FeatureAvailability } from "@/stores/use-user-store";
|
||||
import { apiClient, request } from "@/services/api/request";
|
||||
import type { PublicLogicalModel } from "@/services/api/logical-models";
|
||||
|
||||
const api = apiClient;
|
||||
|
||||
@@ -30,7 +31,7 @@ export type AdminUser = LocalUser & {
|
||||
|
||||
export type AuthSessionPayload = {
|
||||
user: LocalUser | null;
|
||||
systemChannels?: ModelChannel[];
|
||||
logicalModels?: PublicLogicalModel[];
|
||||
runtimeLimits?: RuntimeLimits;
|
||||
drawingEngine?: CanvasDrawingEngineSetting;
|
||||
features?: FeatureAvailability;
|
||||
@@ -249,7 +250,7 @@ export type UserPromptPreference = {
|
||||
|
||||
export type AdminOSSSetting = {
|
||||
enabled: boolean;
|
||||
provider: "aliyun" | "tencent";
|
||||
provider: "aliyun" | "tencent" | "qiniu";
|
||||
region: string;
|
||||
endpoint: string;
|
||||
cdnBaseUrl: string;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { LOCAL_DREAMINA_WAIT_STOPPED_CODE, LocalDreaminaGenerationClientError, r
|
||||
import { isLocalDreaminaBackgroundTask, localDreaminaTaskId, projectLocalDreaminaTask, stripLocalDreaminaTaskPrefix } from "@/services/local-dreamina-task-projection";
|
||||
import { modelCapabilityConfigFor } from "@/lib/model-capabilities";
|
||||
import { grokImagePromptLimitError } from "@/lib/grok-image-prompt-limit";
|
||||
import { resolveModelRequestConfig, type AiConfig } from "@/stores/use-config-store";
|
||||
import { modelOptionName, resolveModelChannel, resolveModelRequestConfig, type AiConfig } from "@/stores/use-config-store";
|
||||
import type { ReferenceImage } from "@/types/image";
|
||||
import type { ReferenceAudio, ReferenceVideo } from "@/types/media";
|
||||
|
||||
@@ -28,6 +28,7 @@ type BackendGenerationTaskOptions = {
|
||||
referenceImages?: ReferenceImage[];
|
||||
referenceVideos?: ReferenceVideo[];
|
||||
referenceAudios?: ReferenceAudio[];
|
||||
textHistory?: Array<{ role: "user" | "assistant"; content: string }>;
|
||||
mask?: ReferenceImage;
|
||||
signal?: AbortSignal;
|
||||
metadata?: Record<string, unknown>;
|
||||
@@ -73,6 +74,7 @@ export async function runBackendGenerationTask(
|
||||
referenceImages = [],
|
||||
referenceVideos = [],
|
||||
referenceAudios = [],
|
||||
textHistory = [],
|
||||
mask,
|
||||
signal,
|
||||
metadata,
|
||||
@@ -89,13 +91,13 @@ export async function runBackendGenerationTask(
|
||||
assertClientPromptLimit(mode, prompt, config, metadata);
|
||||
if (isLocalDreaminaModel(config.model)) {
|
||||
return await runLocalDreaminaGeneration(
|
||||
{ projectId, mode, prompt, config, referenceImages, referenceVideos, referenceAudios, mask, signal, metadata, onTaskUpdate, localIdempotencyKey, localResumeOnly, clientOperationId, retryOf, attemptGroupId },
|
||||
{ projectId, mode, prompt, config, referenceImages, referenceVideos, referenceAudios, textHistory, mask, signal, metadata, onTaskUpdate, localIdempotencyKey, localResumeOnly, clientOperationId, retryOf, attemptGroupId },
|
||||
dependencies,
|
||||
);
|
||||
}
|
||||
const prepared = await prepareGenerationReferences({ referenceImages, referenceVideos, referenceAudios, mask });
|
||||
throwIfAborted(signal);
|
||||
return createAndWaitGenerationTask({ projectId, mode, prompt, config, referenceImages, referenceVideos, referenceAudios, signal, metadata, onTaskUpdate }, prepared, dependencies);
|
||||
return createAndWaitGenerationTask({ projectId, mode, prompt, config, referenceImages, referenceVideos, referenceAudios, textHistory, signal, metadata, onTaskUpdate }, prepared, dependencies);
|
||||
}
|
||||
|
||||
export async function runBackendGenerationTaskBatch(options: BackendGenerationTaskOptions & { count: number }, dependencies: GenerationTaskDependencies = defaultDependencies) {
|
||||
@@ -331,16 +333,20 @@ async function prepareGenerationReferences({
|
||||
async function createAndWaitGenerationTask(options: BackendGenerationTaskOptions, prepared: PreparedGenerationReferences, dependencies: GenerationTaskDependencies) {
|
||||
const { projectId, mode, prompt, config, signal, metadata, onTaskUpdate } = options;
|
||||
const videoOperation = generationOperation(options);
|
||||
const logicalModelId = logicalModelIDForConfig(config);
|
||||
const task = await dependencies.createTask({
|
||||
...(projectId ? { projectId } : {}),
|
||||
type: `canvas_${mode}`,
|
||||
operation: mode === "video" ? videoOperation : mode,
|
||||
prompt,
|
||||
model: config.model,
|
||||
...(logicalModelId ? { logicalModelId } : {}),
|
||||
input: {
|
||||
mode,
|
||||
prompt,
|
||||
config: backendProviderConfig(config),
|
||||
capabilityOptions: logicalModelId ? logicalCapabilityOptions(config, mode) : undefined,
|
||||
textHistory: options.textHistory,
|
||||
referenceImages: prepared.referenceImages,
|
||||
referenceVideos: prepared.referenceVideos,
|
||||
referenceAudios: prepared.referenceAudios,
|
||||
@@ -415,15 +421,7 @@ function backendMediaReference<T extends ReferenceVideo | ReferenceAudio>(media:
|
||||
|
||||
export function backendProviderConfig(config: AiConfig) {
|
||||
const requestConfig = resolveModelRequestConfig(config, config.model);
|
||||
return {
|
||||
channelId: requestConfig.channelId,
|
||||
apiFormat: requestConfig.apiFormat,
|
||||
interfaceType: requestConfig.interfaceType,
|
||||
baseUrl: requestConfig.baseUrl,
|
||||
allowLocalChannel: requestConfig.allowLocalChannel === true,
|
||||
apiKey: requestConfig.apiKey,
|
||||
secretKey: requestConfig.secretKey,
|
||||
model: requestConfig.model,
|
||||
const generationOptions = {
|
||||
size: config.size,
|
||||
quality: config.quality,
|
||||
transparentBackground: config.transparentBackground,
|
||||
@@ -436,11 +434,41 @@ export function backendProviderConfig(config: AiConfig) {
|
||||
audioFormat: config.audioFormat,
|
||||
audioSpeed: config.audioSpeed,
|
||||
audioInstructions: config.audioInstructions,
|
||||
};
|
||||
if (logicalModelIDForConfig(config)) return generationOptions;
|
||||
return {
|
||||
channelId: requestConfig.channelId,
|
||||
apiFormat: requestConfig.apiFormat,
|
||||
interfaceType: requestConfig.interfaceType,
|
||||
baseUrl: requestConfig.baseUrl,
|
||||
allowLocalChannel: requestConfig.allowLocalChannel === true,
|
||||
apiKey: requestConfig.apiKey,
|
||||
secretKey: requestConfig.secretKey,
|
||||
model: requestConfig.model,
|
||||
...generationOptions,
|
||||
capabilityConfig: modelCapabilityConfigFor(config, requestConfig.model),
|
||||
systemPrompt: "",
|
||||
};
|
||||
}
|
||||
|
||||
export function logicalModelIDForConfig(config: AiConfig) {
|
||||
const channel = resolveModelChannel(config, config.model);
|
||||
return channel.modelCosts?.find((item) => item.model === modelOptionName(config.model))?.logicalModelId || "";
|
||||
}
|
||||
|
||||
function logicalCapabilityOptions(config: AiConfig, mode: BackendGenerationMode) {
|
||||
const channel = resolveModelChannel(config, config.model);
|
||||
const spec = channel.modelCosts?.find((item) => item.model === modelOptionName(config.model))?.logicalCapabilitySpec;
|
||||
const candidates: Record<string, unknown> = mode === "image"
|
||||
? { size: config.size, quality: config.quality, transparentBackground: config.transparentBackground === "true", count: Number(config.count) }
|
||||
: mode === "video"
|
||||
? { size: config.size, videoSeconds: Number(config.videoSeconds), vquality: config.vquality, videoGenerateAudio: config.videoGenerateAudio === "true", videoWatermark: config.videoWatermark === "true" }
|
||||
: mode === "audio"
|
||||
? { audioVoice: config.audioVoice, audioFormat: config.audioFormat, audioSpeed: Number(config.audioSpeed) }
|
||||
: {};
|
||||
return Object.fromEntries(Object.entries(candidates).filter(([key]) => Boolean(spec?.options?.[key])));
|
||||
}
|
||||
|
||||
export function parseBackendGenerationResult(task: GenerationTask): BackendGenerationResult {
|
||||
if (!task.resultJson) throw new Error("后端任务没有返回结果");
|
||||
const result = JSON.parse(task.resultJson) as BackendGenerationResult;
|
||||
|
||||
145
web/src/services/api/logical-models.ts
Normal file
145
web/src/services/api/logical-models.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { apiClient, request } from "@/services/api/request";
|
||||
|
||||
export type InputConstraint = { min: number; max: number };
|
||||
export type OptionConstraint = { values?: unknown[]; min?: number; max?: number; step?: number };
|
||||
export type CapabilitySpec = {
|
||||
version: 1;
|
||||
capability: "text" | "image" | "video" | "audio";
|
||||
operations?: string[];
|
||||
inputs?: Record<string, InputConstraint>;
|
||||
options?: Record<string, OptionConstraint>;
|
||||
};
|
||||
|
||||
export type ModelRequestIntent = {
|
||||
capability: CapabilitySpec["capability"];
|
||||
operation?: string;
|
||||
inputs?: Record<string, number>;
|
||||
options?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type PublicLogicalModel = {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
icon?: string;
|
||||
description: string;
|
||||
capability: CapabilitySpec["capability"];
|
||||
sortOrder: number;
|
||||
pricePolicy: "channel" | "unified";
|
||||
billingMode: "fixed_request" | "per_second" | "token";
|
||||
unitPriceMicrocredits: number;
|
||||
inputPriceMicrocredits: number;
|
||||
outputPriceMicrocredits: number;
|
||||
cachedPriceMicrocredits: number;
|
||||
capabilitySpec: CapabilitySpec;
|
||||
capabilityProfiles: CapabilitySpec[];
|
||||
defaultOptions: Record<string, unknown>;
|
||||
available: boolean;
|
||||
};
|
||||
|
||||
export type AdminLogicalRoute = {
|
||||
id: string;
|
||||
physicalVariantId: string;
|
||||
physicalVariantName: string;
|
||||
channelModelId: string;
|
||||
channelId: string;
|
||||
physicalModelKey: string;
|
||||
physicalModelName: string;
|
||||
enabled: boolean;
|
||||
priority: number;
|
||||
weight: number;
|
||||
available: boolean;
|
||||
capabilitySpec: CapabilitySpec;
|
||||
};
|
||||
|
||||
export type AdminLogicalModel = PublicLogicalModel & {
|
||||
enabled: boolean;
|
||||
activeRevisionId: string;
|
||||
revisionVersion: number;
|
||||
configurationError?: string;
|
||||
availabilityError?: string;
|
||||
routes: AdminLogicalRoute[];
|
||||
};
|
||||
|
||||
export type AdminPhysicalVariant = {
|
||||
id: string;
|
||||
channelModelId: string;
|
||||
channelId: string;
|
||||
modelKey: string;
|
||||
modelName: string;
|
||||
name: string;
|
||||
capability: CapabilitySpec["capability"];
|
||||
protocol: string;
|
||||
enabled: boolean;
|
||||
modelEnabled: boolean;
|
||||
usageCount: number;
|
||||
capabilitySpec: CapabilitySpec;
|
||||
};
|
||||
|
||||
export type PhysicalVariantMutation = {
|
||||
channelModelId: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
/** 能力范围由渠道模型能力参数自动生成;保留可选字段兼容旧调用方。 */
|
||||
capabilitySpec?: CapabilitySpec;
|
||||
};
|
||||
|
||||
export type LogicalModelMutation = {
|
||||
code: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
description: string;
|
||||
capability: CapabilitySpec["capability"];
|
||||
enabled: boolean;
|
||||
sortOrder: number;
|
||||
pricePolicy: PublicLogicalModel["pricePolicy"];
|
||||
billingMode: PublicLogicalModel["billingMode"];
|
||||
unitPriceMicrocredits: number;
|
||||
inputPriceMicrocredits: number;
|
||||
outputPriceMicrocredits: number;
|
||||
cachedPriceMicrocredits: number;
|
||||
capabilitySpec: CapabilitySpec;
|
||||
defaultOptions: Record<string, unknown>;
|
||||
routes: Array<{ physicalVariantId: string; enabled: boolean; priority: number; weight: number }>;
|
||||
};
|
||||
|
||||
export type RouteSimulationResult = {
|
||||
productMatch: { matched: boolean; reasons?: string[] };
|
||||
candidates: Array<{ routeId: string; variantId: string; channelModelId: string; priority: number; weight: number; enabled: boolean; matched: boolean; blocked: boolean; inPool: boolean; reasons?: string[] }>;
|
||||
};
|
||||
|
||||
export function listLogicalModels() {
|
||||
return request<{ models: PublicLogicalModel[] }>(apiClient.get("/models"));
|
||||
}
|
||||
|
||||
export function listAvailableLogicalModels(intent: ModelRequestIntent) {
|
||||
return request<{ models: PublicLogicalModel[] }>(apiClient.post("/models/available", intent));
|
||||
}
|
||||
|
||||
export function listAdminLogicalModels() {
|
||||
return request<{ models: AdminLogicalModel[] }>(apiClient.get("/admin/logical-models"));
|
||||
}
|
||||
|
||||
export function createAdminLogicalModel(input: LogicalModelMutation) {
|
||||
return request<{ model: AdminLogicalModel }>(apiClient.post("/admin/logical-models", input));
|
||||
}
|
||||
|
||||
export function updateAdminLogicalModel(id: string, input: LogicalModelMutation) {
|
||||
return request<{ model: AdminLogicalModel }>(apiClient.patch(`/admin/logical-models/${encodeURIComponent(id)}`, input));
|
||||
}
|
||||
|
||||
export function listAdminPhysicalVariants(channelModelId?: string) {
|
||||
return request<{ variants: AdminPhysicalVariant[] }>(apiClient.get("/admin/logical-models/physical-variants", { params: channelModelId ? { channelModelId } : undefined }));
|
||||
}
|
||||
|
||||
export function createAdminPhysicalVariant(input: PhysicalVariantMutation) {
|
||||
return request<{ variant: AdminPhysicalVariant }>(apiClient.post("/admin/logical-models/physical-variants", input));
|
||||
}
|
||||
|
||||
export function updateAdminPhysicalVariant(id: string, input: PhysicalVariantMutation) {
|
||||
return request<{ variant: AdminPhysicalVariant }>(apiClient.patch(`/admin/logical-models/physical-variants/${encodeURIComponent(id)}`, input));
|
||||
}
|
||||
|
||||
export function simulateAdminLogicalModel(id: string, intent: ModelRequestIntent) {
|
||||
return request<RouteSimulationResult>(apiClient.post(`/admin/logical-models/${encodeURIComponent(id)}/simulate`, intent));
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export type RemoteResource = {
|
||||
|
||||
export type UserOSSSetting = {
|
||||
enabled: boolean;
|
||||
provider: "aliyun" | "tencent";
|
||||
provider: "aliyun" | "tencent" | "qiniu";
|
||||
region: string;
|
||||
endpoint: string;
|
||||
cdnBaseUrl: string;
|
||||
|
||||
19
web/src/services/api/response-interception.ts
Normal file
19
web/src/services/api/response-interception.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { apiClient, request } from "@/services/api/request";
|
||||
|
||||
export type ResponseInterceptionRule = {
|
||||
contains: string;
|
||||
replace: string;
|
||||
};
|
||||
|
||||
export type ResponseInterceptionSetting = {
|
||||
enabled: boolean;
|
||||
rules: ResponseInterceptionRule[];
|
||||
};
|
||||
|
||||
export function getAdminResponseInterceptionSetting() {
|
||||
return request<{ setting: ResponseInterceptionSetting }>(apiClient.get("/admin/settings/response-interception"));
|
||||
}
|
||||
|
||||
export function updateAdminResponseInterceptionSetting(setting: ResponseInterceptionSetting) {
|
||||
return request<{ setting: ResponseInterceptionSetting }>(apiClient.patch("/admin/settings/response-interception", setting));
|
||||
}
|
||||
@@ -167,6 +167,7 @@ export type CreateSessionInput = {
|
||||
projectStyle?: { presetId: string; title: string; prompt: string };
|
||||
characters?: Array<{ assetId: string; versionId: string; name: string; definition: Record<string, unknown> }>;
|
||||
config?: Record<string, unknown>;
|
||||
logicalModelId?: string;
|
||||
};
|
||||
|
||||
export type CreateTaskInput = {
|
||||
@@ -177,6 +178,7 @@ export type CreateTaskInput = {
|
||||
prompt: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
logicalModelId?: string;
|
||||
input?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user