fix: lock workflow hidden models
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-04-27 23:12:23 +08:00
parent 35c4c02579
commit 83c274e8d4
2 changed files with 191 additions and 65 deletions

View File

@ -4,6 +4,7 @@ import {
getDefaultAiModel,
getEnabledAiModelsWithProvider,
getSiteSettings,
getSystemSetting,
getUserById,
} from "@/lib/db/queries";
import { buildVideoToolCapability } from "@/lib/ai/video-tool-capability";
@ -13,6 +14,11 @@ import {
} from "@/lib/ai/model-classification";
import { ChatSDKError } from "@/lib/errors";
import { isAiModelAgentAuto, isAiModelMultimodal } from "@/lib/db/ai-model-flags";
import {
AGENT_WORKFLOW_UI_CONFIG_KEY,
getAgentWorkflowBusinessModeSettings,
normalizeAgentWorkflowUiConfig,
} from "@/lib/agents/workflow-ui-config";
function isNanoBananaProModel(model: { displayName?: string | null; modelId?: string | null }) {
const displayName = String(model.displayName || "").toLowerCase();
@ -39,6 +45,28 @@ function sortModelsForDisplay<T extends { sortOrder?: number | null; displayName
});
}
function isWorkflowImageGenerationModel(model: {
providerType?: string | null;
modelId?: string | null;
capabilities?: unknown;
supportsImageGen?: boolean | null;
}) {
return (
isImageGenerationModelDescriptor({
providerType: model.providerType,
modelId: model.modelId,
capabilities: model.capabilities,
supportsImageGen: model.supportsImageGen,
}) &&
!isVideoModelDescriptor({
providerType: model.providerType,
modelId: model.modelId,
capabilities: model.capabilities,
supportsImageGen: model.supportsImageGen,
})
);
}
// 获取前端可用的 AI 模型列表(需要登录但不需要管理员权限)
export async function GET() {
try {
@ -56,27 +84,12 @@ export async function GET() {
const modelsWithProvider = await getEnabledAiModelsWithProvider(false, true);
const agentModelWithProvider = await getAgentAiModelWithProvider();
const defaultModel = await getDefaultAiModel();
const siteSettings = await getSiteSettings();
const visibleModelsWithProvider = sortModelsForDisplay(
modelsWithProvider
.filter(
({ model }) =>
!isAiModelAgentAuto(model) &&
(allowHiddenModels || !model.isHidden)
)
.map((item) => item.model)
);
const visibleModelIdSet = new Set(visibleModelsWithProvider.map((m) => m.id));
const visibleModelsWithProviderSorted = modelsWithProvider.filter(({ model }) =>
visibleModelIdSet.has(model.id)
).sort((a, b) => {
const left = visibleModelsWithProvider.findIndex((m) => m.id === a.model.id);
const right = visibleModelsWithProvider.findIndex((m) => m.id === b.model.id);
return left - right;
});
// 转换为前端需要的格式
const models = visibleModelsWithProviderSorted.map(({ model, provider }) => ({
const [siteSettings, agentWorkflowUiConfigSetting] = await Promise.all([
getSiteSettings(),
getSystemSetting({ key: AGENT_WORKFLOW_UI_CONFIG_KEY }).catch(() => null),
]);
type ModelWithProvider = (typeof modelsWithProvider)[number];
const serializeModel = ({ model, provider }: ModelWithProvider) => ({
id: model.id,
modelId: model.modelId,
displayName: model.displayName,
@ -101,7 +114,42 @@ export async function GET() {
}),
imageGenConfig: model.imageGenConfig,
sortOrder: model.sortOrder ?? 0,
}));
});
const workflowImageModelById = new Map<string, ModelWithProvider>();
modelsWithProvider.forEach((entry) => {
const { model, provider } = entry;
if (
!isAiModelAgentAuto(model) &&
isWorkflowImageGenerationModel({
providerType: provider.type,
modelId: model.modelId,
capabilities: model.capabilities,
supportsImageGen: model.supportsImageGen,
})
) {
workflowImageModelById.set(model.id, entry);
}
});
const visibleModelsWithProvider = sortModelsForDisplay(
modelsWithProvider
.filter(
({ model }) =>
!isAiModelAgentAuto(model) &&
(allowHiddenModels || !model.isHidden)
)
.map((item) => item.model)
);
const visibleModelIdSet = new Set(visibleModelsWithProvider.map((m) => m.id));
const visibleModelsWithProviderSorted = modelsWithProvider.filter(({ model }) =>
visibleModelIdSet.has(model.id)
).sort((a, b) => {
const left = visibleModelsWithProvider.findIndex((m) => m.id === a.model.id);
const right = visibleModelsWithProvider.findIndex((m) => m.id === b.model.id);
return left - right;
});
// 转换为前端需要的格式
const models = visibleModelsWithProviderSorted.map(serializeModel);
const preferredNanoBananaModel = models.find(
(model) =>
isImageGenerationModelDescriptor({
@ -147,29 +195,43 @@ export async function GET() {
const configuredAgentWorkflowModelId = String(
siteSettings?.agentWorkflowDefaultModelId || ""
).trim();
const agentWorkflowDefaultModel = configuredAgentWorkflowModelId
? models.find(
(model) =>
model.id === configuredAgentWorkflowModelId &&
isImageGenerationModelDescriptor({
providerType: model.providerType,
modelId: model.modelId,
capabilities: model.capabilities,
supportsImageGen: model.supportsImageGen,
}) &&
!isVideoModelDescriptor({
providerType: model.providerType,
modelId: model.modelId,
capabilities: model.capabilities,
supportsImageGen: model.supportsImageGen,
})
)
const hasConfiguredAgentWorkflowModel = Boolean(
configuredAgentWorkflowModelId &&
configuredAgentWorkflowModelId !== "auto" &&
configuredAgentWorkflowModelId !== "__auto"
);
const agentWorkflowDefaultModel = hasConfiguredAgentWorkflowModel
? workflowImageModelById.get(configuredAgentWorkflowModelId) || null
: null;
const workflowUiConfig = normalizeAgentWorkflowUiConfig(
agentWorkflowUiConfigSetting?.value
);
const workflowBusinessModeSettings = getAgentWorkflowBusinessModeSettings(
workflowUiConfig
);
const configuredWorkflowModelIds = new Set<string>();
if (agentWorkflowDefaultModel) {
configuredWorkflowModelIds.add(agentWorkflowDefaultModel.model.id);
}
workflowBusinessModeSettings.modes.forEach((mode) => {
const modelId = String(mode.defaultModelId || "").trim();
if (modelId && workflowImageModelById.has(modelId)) {
configuredWorkflowModelIds.add(modelId);
}
});
const agentWorkflowConfiguredModels = Array.from(configuredWorkflowModelIds)
.map((id) => workflowImageModelById.get(id))
.filter((entry): entry is ModelWithProvider => Boolean(entry))
.map(serializeModel);
return Response.json({
models,
defaultModelId: resolvedDefaultModelId,
agentWorkflowDefaultModelId: agentWorkflowDefaultModel?.id || null,
agentWorkflowConfiguredDefaultModelId: hasConfiguredAgentWorkflowModel
? configuredAgentWorkflowModelId
: null,
agentWorkflowDefaultModelId: agentWorkflowDefaultModel?.model.id || null,
agentWorkflowConfiguredModels,
agentModel: agentModelWithProvider
? {
id: agentModelWithProvider.model.id,

View File

@ -157,6 +157,15 @@ interface AgentGenerationWorkspaceDarkV2Props {
workflowStylePanel?: ReactNode;
}
interface AgentWorkflowModelOption {
id: string;
displayName: string;
providerType: string;
supportsImageGen?: boolean;
isMultimodal?: boolean;
imageGenConfig?: any;
}
interface PickedAttachment extends Attachment {}
const MAX_CHAT_TEXT_PART_LENGTH = 2000;
@ -1210,16 +1219,11 @@ export function AgentGenerationWorkspaceDarkV2({
);
const { data: modelsData } = useSWR<{
models: Array<{
id: string;
displayName: string;
providerType: string;
supportsImageGen?: boolean;
isMultimodal?: boolean;
imageGenConfig?: any;
}>;
models: AgentWorkflowModelOption[];
defaultModelId?: string | null;
agentWorkflowConfiguredDefaultModelId?: string | null;
agentWorkflowDefaultModelId?: string | null;
agentWorkflowConfiguredModels?: AgentWorkflowModelOption[];
agentModel?: any;
}>("/api/models", fetcher);
@ -1258,10 +1262,15 @@ export function AgentGenerationWorkspaceDarkV2({
const activeBusinessModeDefaultModelId = !requiresPresetSelection
? String(activeBusinessMode?.defaultModelId || "").trim() || null
: null;
const effectiveDefaultModelId =
const lockedWorkflowModelId =
activeBusinessModeDefaultModelId ||
selectedAgentPreset?.defaultModelId ||
modelsData?.agentWorkflowConfiguredDefaultModelId ||
modelsData?.agentWorkflowDefaultModelId ||
null;
const isWorkflowModelLocked = Boolean(lockedWorkflowModelId);
const effectiveDefaultModelId =
lockedWorkflowModelId ||
modelsData?.defaultModelId ||
null;
const presetDrivenResultModeType: AgentGenerationResultModeType =
@ -1353,7 +1362,8 @@ export function AgentGenerationWorkspaceDarkV2({
);
const workflowBrandName = getCurrentWorkflowBrandName();
const shouldAllowModelOverride =
isDirectModelGenerationMode || !isSceneGenerationPreset || presetModeConfig.scene.allowModelOverride;
!isWorkflowModelLocked &&
(isDirectModelGenerationMode || !isSceneGenerationPreset || presetModeConfig.scene.allowModelOverride);
const brandModelAttachmentKeys = useMemo(() => {
const keys = new Set<string>();
referenceImages
@ -1737,6 +1747,7 @@ export function AgentGenerationWorkspaceDarkV2({
selectedAgentPresetId: string | null;
selectedAgentPresetType: AgentPreset["type"] | null;
selectedModelId: string;
lockedWorkflowModelId: string | null;
imageParams: ImageGenerationParams;
isDetailReplacementPreset: boolean;
isSkuReplacementPreset: boolean;
@ -1754,6 +1765,7 @@ export function AgentGenerationWorkspaceDarkV2({
selectedAgentPresetId: null,
selectedAgentPresetType: null,
selectedModelId: "auto",
lockedWorkflowModelId: null,
imageParams: {
aspectRatio: "auto",
imageSize: "2K",
@ -1776,6 +1788,7 @@ export function AgentGenerationWorkspaceDarkV2({
selectedAgentPresetId: selectedAgentPresetId || null,
selectedAgentPresetType: selectedAgentPreset?.type || null,
selectedModelId,
lockedWorkflowModelId,
imageParams,
isDetailReplacementPreset,
isSkuReplacementPreset,
@ -1807,10 +1820,40 @@ export function AgentGenerationWorkspaceDarkV2({
return [autoOption, ...imageModels];
}, [modelsData?.agentModel?.imageGenConfig, modelsData?.models]);
const currentModel = useMemo(
() => selectableModels.find((model) => model.id === selectedModelId) || selectableModels[0],
[selectableModels, selectedModelId]
const configuredWorkflowModels = useMemo(
() => modelsData?.agentWorkflowConfiguredModels || [],
[modelsData?.agentWorkflowConfiguredModels]
);
const executableModelOptions = useMemo(() => {
const seen = new Set<string>();
return [...selectableModels, ...configuredWorkflowModels].filter((model) => {
if (seen.has(model.id)) return false;
seen.add(model.id);
return true;
});
}, [configuredWorkflowModels, selectableModels]);
const executableModelIdSet = useMemo(
() => new Set(executableModelOptions.map((model) => model.id)),
[executableModelOptions]
);
const lockedWorkflowModel = useMemo(
() =>
lockedWorkflowModelId
? executableModelOptions.find((model) => model.id === lockedWorkflowModelId) || null
: null,
[executableModelOptions, lockedWorkflowModelId]
);
const currentModel = useMemo(
() =>
lockedWorkflowModel ||
executableModelOptions.find((model) => model.id === selectedModelId) ||
selectableModels[0],
[executableModelOptions, lockedWorkflowModel, selectableModels, selectedModelId]
);
const lockedWorkflowModelLabel =
lockedWorkflowModel?.displayName ||
selectedAgentPreset?.defaultModel?.displayName ||
(lockedWorkflowModelId ? `配置模型不可用(${lockedWorkflowModelId})` : "未配置");
const [activeToolStep, setActiveToolStep] = useState<ToolStepKey>("settings");
const hasBrandModelStep = Boolean(brandModelPanel) && modelSlotEnabled;
const hasBrandStoreStep = Boolean(brandStorePanel);
@ -1862,9 +1905,10 @@ export function AgentGenerationWorkspaceDarkV2({
? transportState.selectedAgentPresetId || selectedAgentPresetId || undefined
: undefined;
const resolvedSelectedModelId =
transportState.selectedModelId !== "auto"
transportState.lockedWorkflowModelId ||
(transportState.selectedModelId !== "auto"
? transportState.selectedModelId
: effectiveDefaultModelId || transportState.selectedModelId;
: effectiveDefaultModelId || transportState.selectedModelId);
const replacementModePayload =
transportState.isDetailReplacementPreset || transportState.isSkuReplacementPreset
? {
@ -1958,6 +2002,10 @@ export function AgentGenerationWorkspaceDarkV2({
}, [historyMessages, setMessages]);
useEffect(() => {
if (isWorkflowModelLocked) {
setSelectedModelId((current) => (current === "auto" ? current : "auto"));
return;
}
if (normalizedIncomingWorkspaceState.selectedModelId) {
const nextModelId = normalizedIncomingWorkspaceState.selectedModelId;
setSelectedModelId((current) =>
@ -1973,6 +2021,7 @@ export function AgentGenerationWorkspaceDarkV2({
}
}, [
effectiveDefaultModelId,
isWorkflowModelLocked,
normalizedIncomingWorkspaceState.selectedModelId,
]);
@ -2625,7 +2674,9 @@ export function AgentGenerationWorkspaceDarkV2({
return arePickedAttachmentsEqual(current, nextAttachments) ? current : nextAttachments;
});
setSelectedModelId((current) => {
const nextModelId = normalizedIncomingWorkspaceState.selectedModelId || "auto";
const nextModelId = isWorkflowModelLocked
? "auto"
: normalizedIncomingWorkspaceState.selectedModelId || "auto";
return current === nextModelId ? current : nextModelId;
});
setImageParams((current) => {
@ -2636,7 +2687,7 @@ export function AgentGenerationWorkspaceDarkV2({
};
return areImageGenerationParamsEqual(current, nextParams) ? current : nextParams;
});
}, [normalizedIncomingWorkspaceState]);
}, [isWorkflowModelLocked, normalizedIncomingWorkspaceState]);
useEffect(() => {
const nextWorkspaceState = {
@ -2987,16 +3038,17 @@ export function AgentGenerationWorkspaceDarkV2({
return;
}
const resolvedModelId =
selectedModelId !== "auto"
lockedWorkflowModelId ||
(selectedModelId !== "auto"
? selectedModelId
: effectiveDefaultModelId || selectedModelId;
: effectiveDefaultModelId || selectedModelId);
if (!resolvedModelId) {
toast.error("当前风格化未配置模型");
return;
}
if (
resolvedModelId !== "auto" &&
!selectableModels.some((model) => model.id === resolvedModelId)
!executableModelIdSet.has(resolvedModelId)
) {
toast.error("当前风格化绑定的模型不存在或已停用,请重新编辑风格化模型配置");
return;
@ -3113,11 +3165,12 @@ export function AgentGenerationWorkspaceDarkV2({
isSelectedBusinessModeUnavailable,
isSkuReplacementPreset,
effectiveDefaultModelId,
executableModelIdSet,
lockedWorkflowModelId,
requiredProductUploadCount,
requiresPresetSelection,
selectedAgentPreset,
selectedAgentPresetId,
selectableModels,
selectedModelId,
sendMessage,
setPendingGeneration,
@ -3670,9 +3723,18 @@ export function AgentGenerationWorkspaceDarkV2({
? "en"
: "zh";
const resolvedModelId =
selectedModelId !== "auto"
lockedWorkflowModelId ||
(selectedModelId !== "auto"
? selectedModelId
: effectiveDefaultModelId || undefined;
: effectiveDefaultModelId || undefined);
if (
resolvedModelId &&
resolvedModelId !== "auto" &&
!executableModelIdSet.has(resolvedModelId)
) {
toast.error("当前风格化绑定的模型不存在或已停用,请重新编辑风格化模型配置");
return;
}
detailGenerationCancelRef.current = false;
setDetailGenerationRunResults([]);
@ -4918,9 +4980,11 @@ export function AgentGenerationWorkspaceDarkV2({
</Select>
) : (
<div className="rounded-[24px] border border-white/10 bg-black/30 px-4 py-3 text-sm text-white/70">
当前生图模式已锁定模型:
{isWorkflowModelLocked ? "后台已锁定生图模型:" : "当前生图模式使用默认模型:"}
<span className="ml-2 font-medium text-white">
{selectedAgentPreset?.defaultModel?.displayName || currentModel?.displayName || "未配置"}
{isWorkflowModelLocked
? lockedWorkflowModelLabel
: currentModel?.displayName || "自动选择"}
</span>
</div>
)}