feat(models): add frontend hide toggle and compact image param chips
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-02-20 20:31:40 +08:00
parent a1691fbd39
commit 8fc880fe9e
10 changed files with 50 additions and 14 deletions

View File

@ -151,6 +151,7 @@ type AiModel = {
supportsVision: boolean;
supportsImageGen: boolean;
isEnabled: boolean;
isHidden: boolean;
isDefault: boolean;
sortOrder: number;
isAgentModel?: boolean;
@ -998,6 +999,7 @@ export function AdminSettingsClient() {
supportsVision: false,
supportsImageGen: false,
isEnabled: true,
isHidden: false,
isDefault: false,
isAgentModel: false,
isMultimodal: false,
@ -1856,6 +1858,7 @@ export function AdminSettingsClient() {
supportsVision: false,
supportsImageGen: false,
isEnabled: true,
isHidden: false,
isDefault: false,
isAgentModel: false,
isMultimodal: false,
@ -5820,6 +5823,13 @@ export function AdminSettingsClient() {
/>
<Label>启用</Label>
</div>
<div className="flex items-center gap-2">
<Switch
checked={modelForm.isHidden}
onCheckedChange={(checked) => setModelForm({ ...modelForm, isHidden: checked })}
/>
<Label>前台隐藏(仅 Agent/故障迁移可用)</Label>
</div>
<div className="flex items-center gap-2">
<Switch
checked={modelForm.isDefault}
@ -6007,6 +6017,7 @@ export function AdminSettingsClient() {
{model.isDefault && <span className="text-xs bg-blue-100 text-blue-800 px-2 py-0.5 rounded">默认</span>}
{model.isAgentModel && <span className="text-xs bg-violet-100 text-violet-800 px-2 py-0.5 rounded">Agent</span>}
{model.isMultimodal && <span className="text-xs bg-emerald-100 text-emerald-800 px-2 py-0.5 rounded">多模态</span>}
{model.isHidden && <span className="text-xs bg-amber-100 text-amber-800 px-2 py-0.5 rounded">前台隐藏</span>}
</div>
<div className="text-sm text-muted-foreground truncate">
ID: {model.modelId} | 提供商: {provider?.name ?? "未知"}
@ -6025,6 +6036,7 @@ export function AdminSettingsClient() {
supportsVision: model.supportsVision,
supportsImageGen: model.supportsImageGen,
isEnabled: model.isEnabled,
isHidden: model.isHidden || false,
isDefault: model.isDefault,
isAgentModel: model.isAgentModel || false,
isMultimodal: model.isMultimodal || false,

View File

@ -30,12 +30,12 @@ export async function GET() {
}
// 只获取主模型(不包括子模型),子模型由后端根据 videoParams.mode 自动选择
const modelsWithProvider = await getEnabledAiModelsWithProvider(false);
const modelsWithProvider = await getEnabledAiModelsWithProvider(false, false);
const agentModelWithProvider = await getAgentAiModelWithProvider();
const defaultModel = await getDefaultAiModel();
const visibleModelsWithProvider = sortModelsForDisplay(
modelsWithProvider
.filter(({ model }) => !isAiModelAgentAuto(model))
.filter(({ model }) => !isAiModelAgentAuto(model) && !model.isHidden)
.map((item) => item.model)
);
const visibleModelIdSet = new Set(visibleModelsWithProvider.map((m) => m.id));

View File

@ -79,13 +79,13 @@ async function NewChatPage() {
// 尝试获取默认模型
const defaultModel = await getDefaultAiModel();
if (defaultModel && !isAiModelAgentAuto(defaultModel)) {
if (defaultModel && !isAiModelAgentAuto(defaultModel) && !defaultModel.isHidden) {
initialModelId = defaultModel.id;
} else {
// 如果没有设置默认模型,获取第一个启用的模型
const enabledModels = await getEnabledAiModelsWithProvider();
const enabledModels = await getEnabledAiModelsWithProvider(false, false);
const visibleModels = enabledModels.filter(
({ model }) => !isAiModelAgentAuto(model)
({ model }) => !isAiModelAgentAuto(model) && !model.isHidden
);
if (visibleModels.length > 0) {
initialModelId = visibleModels[0].model.id;

View File

@ -34,6 +34,7 @@ const createModelSchema = z.object({
supportsVision: z.boolean().optional(),
supportsImageGen: z.boolean().optional(),
isEnabled: z.boolean().optional(),
isHidden: z.boolean().optional(),
isDefault: z.boolean().optional(),
sortOrder: z.number().optional(),
fallbackModelId1: z.string().max(128).optional(),
@ -53,6 +54,7 @@ const updateModelSchema = z.object({
supportsVision: z.boolean().optional(),
supportsImageGen: z.boolean().optional(),
isEnabled: z.boolean().optional(),
isHidden: z.boolean().optional(),
isDefault: z.boolean().optional(),
sortOrder: z.number().optional(),
fallbackModelId1: z.string().max(128).optional(),

View File

@ -808,7 +808,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
const [videoSlotUploading, setVideoSlotUploading] = useState<number | null>(null);
const [lastImageModelId, setLastImageModelId] = useState<string | null>(null);
const [lastVideoModelId, setLastVideoModelId] = useState<string | null>(null);
const [deepThinkingEnabled, setDeepThinkingEnabled] = useState(true);
const [deepThinkingEnabled, setDeepThinkingEnabled] = useState(false);
const imageParamsRef = useRef(imageParams);
const videoParamsRef = useRef(videoParams);
const deepThinkingEnabledRef = useRef(deepThinkingEnabled);

View File

@ -8,7 +8,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Check, Image, Ratio, Monitor, Images, SlidersHorizontal, Ban } from "lucide-react";
import { Check, SlidersHorizontal } from "lucide-react";
import { cn } from "@/lib/utils";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
@ -183,10 +183,10 @@ export function ImageParamsBar({
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="h-8 px-2 focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none focus:ring-0"
className="h-7 min-w-[46px] rounded-md border border-[#d5dae2] bg-white px-2 text-[11px] font-medium text-[#374151] hover:bg-[#f7f8fa] focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none focus:ring-0"
title={`比例:${params.aspectRatio}`}
>
<Ratio size={14} />
{params.aspectRatio === "auto" ? "自动" : params.aspectRatio}
<span className="sr-only">{`比例:${params.aspectRatio}`}</span>
</Button>
</DropdownMenuTrigger>
@ -212,10 +212,10 @@ export function ImageParamsBar({
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="h-8 px-2 focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none focus:ring-0"
className="h-7 min-w-[42px] rounded-md border border-[#d5dae2] bg-white px-2 text-[11px] font-medium text-[#374151] hover:bg-[#f7f8fa] focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none focus:ring-0"
title={`分辨率:${params.imageSize}`}
>
<Monitor size={14} />
{params.imageSize}
<span className="sr-only">{`分辨率:${params.imageSize}`}</span>
</Button>
</DropdownMenuTrigger>
@ -238,10 +238,10 @@ export function ImageParamsBar({
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="h-8 px-2 focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none focus:ring-0"
className="h-7 min-w-[42px] rounded-md border border-[#d5dae2] bg-white px-2 text-[11px] font-medium text-[#374151] hover:bg-[#f7f8fa] focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none focus:ring-0"
title={`数量:${params.imageCount || 1}张`}
>
<Images size={14} />
{params.imageCount || 1}张
<span className="sr-only">{`数量:${params.imageCount || 1}张`}</span>
</Button>
</DropdownMenuTrigger>

View File

@ -0,0 +1,2 @@
ALTER TABLE "AiModel"
ADD COLUMN IF NOT EXISTS "isHidden" boolean NOT NULL DEFAULT false;

View File

@ -176,6 +176,13 @@
"when": 1771502400000,
"tag": "0038_add_canvas_detail_archives",
"breakpoints": true
},
{
"idx": 25,
"version": "7",
"when": 1771588800000,
"tag": "0039_add_ai_model_hidden",
"breakpoints": true
}
]
}

View File

@ -1352,6 +1352,7 @@ export async function createAiModel({
supportsVision,
supportsImageGen,
isEnabled,
isHidden,
isDefault,
sortOrder,
fallbackModelId1,
@ -1368,6 +1369,7 @@ export async function createAiModel({
supportsVision?: boolean;
supportsImageGen?: boolean;
isEnabled?: boolean;
isHidden?: boolean;
isDefault?: boolean;
sortOrder?: number;
fallbackModelId1?: string;
@ -1396,6 +1398,7 @@ export async function createAiModel({
supportsVision: supportsVision ?? false,
supportsImageGen: supportsImageGen ?? false,
isEnabled: isEnabled ?? true,
isHidden: isHidden ?? false,
isDefault: isDefault ?? false,
sortOrder: sortOrder ?? 0,
fallbackModelId1: fallbackModelId1 || null,
@ -1429,6 +1432,7 @@ export async function updateAiModel({
supportsVision,
supportsImageGen,
isEnabled,
isHidden,
isDefault,
sortOrder,
fallbackModelId1,
@ -1446,6 +1450,7 @@ export async function updateAiModel({
supportsVision?: boolean;
supportsImageGen?: boolean;
isEnabled?: boolean;
isHidden?: boolean;
isDefault?: boolean;
sortOrder?: number;
fallbackModelId1?: string;
@ -1473,6 +1478,7 @@ export async function updateAiModel({
if (supportsImageGen !== undefined)
updateData.supportsImageGen = supportsImageGen;
if (isEnabled !== undefined) updateData.isEnabled = isEnabled;
if (isHidden !== undefined) updateData.isHidden = isHidden;
if (isDefault !== undefined) updateData.isDefault = isDefault;
if (sortOrder !== undefined) updateData.sortOrder = sortOrder;
// 备用模型 ID(空字符串转为 null)
@ -1726,7 +1732,10 @@ export async function deleteStorageConfig({ id }: { id: string }) {
}
// 获取启用的 AI 模型及其提供商信息(用于前端展示)
export async function getEnabledAiModelsWithProvider(includeSubModels: boolean = false) {
export async function getEnabledAiModelsWithProvider(
includeSubModels: boolean = false,
includeHidden: boolean = true
) {
try {
const conditions = [
eq(aiModel.isEnabled, true),
@ -1737,6 +1746,9 @@ export async function getEnabledAiModelsWithProvider(includeSubModels: boolean =
if (!includeSubModels) {
conditions.push(eq(aiModel.isSubModel, false));
}
if (!includeHidden) {
conditions.push(eq(aiModel.isHidden, false));
}
const models = await db
.select({

View File

@ -284,6 +284,7 @@ export const aiModel = pgTable("AiModel", {
supportsVision: boolean("supportsVision").notNull().default(false), // 是否支持图片输入
supportsImageGen: boolean("supportsImageGen").notNull().default(false), // 是否支持图片生成
isEnabled: boolean("isEnabled").notNull().default(true), // 是否启用
isHidden: boolean("isHidden").notNull().default(false), // 是否在前端模型选择中隐藏
isDefault: boolean("isDefault").notNull().default(false), // 是否为默认模型
sortOrder: integer("sortOrder").notNull().default(0), // 排序顺序
capabilities: jsonb("capabilities"), // 🆕 模型能力配置(用于视频模型)