diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx index c93163bfa..d8a3c621e 100644 --- a/app/(admin)/settings/admin-settings-client.tsx +++ b/app/(admin)/settings/admin-settings-client.tsx @@ -771,6 +771,7 @@ type AiModel = { isEnabled: boolean; isHidden: boolean; isDefault: boolean; + isDefaultTextEditModel?: boolean; isSubModel?: boolean; parentModelId?: string | null; sortOrder: number; @@ -3260,6 +3261,7 @@ export function AdminSettingsClient() { isEnabled: true, isHidden: false, isDefault: false, + isDefaultTextEditModel: false, isAgentModel: false, agentCodingModelId: "", isMultimodal: false, @@ -6499,6 +6501,7 @@ export function AdminSettingsClient() { isEnabled: true, isHidden: false, isDefault: false, + isDefaultTextEditModel: false, isAgentModel: false, agentCodingModelId: "", isMultimodal: false, @@ -14194,6 +14197,10 @@ export function AdminSettingsClient() { setModelForm({ ...modelForm, supportsImageGen: checked, + isDefaultTextEditModel: + checked + ? modelForm.isDefaultTextEditModel + : false, }) } /> @@ -14231,6 +14238,30 @@ export function AdminSettingsClient() { +
+ + setModelForm({ + ...modelForm, + isDefaultTextEditModel: checked, + }) + } + /> + +
+

+ 建议给可用的火山生图模型开启。未配置时,系统仍会回退到当前可用的火山生图模型。 +

)} + {model.isDefaultTextEditModel && ( + + 文字编辑默认 + + )} {model.isAgentModel && ( Agent @@ -15040,6 +15076,8 @@ export function AdminSettingsClient() { isEnabled: model.isEnabled, isHidden: model.isHidden || false, isDefault: model.isDefault, + isDefaultTextEditModel: + model.isDefaultTextEditModel || false, isAgentModel: model.isAgentModel || false, agentCodingModelId: model.agentCodingModelId || "", diff --git a/app/api/admin/ai-models/route.ts b/app/api/admin/ai-models/route.ts index 7038e4dc1..5383f7f81 100644 --- a/app/api/admin/ai-models/route.ts +++ b/app/api/admin/ai-models/route.ts @@ -74,6 +74,7 @@ const createModelSchema = z.object({ isEnabled: z.boolean().optional(), isHidden: z.boolean().optional(), isDefault: z.boolean().optional(), + isDefaultTextEditModel: z.boolean().optional(), sortOrder: z.number().optional(), fallbackModelId1: z.string().max(128).optional(), fallbackModelId2: z.string().max(128).optional(), @@ -97,6 +98,7 @@ const updateModelSchema = z.object({ isEnabled: z.boolean().optional(), isHidden: z.boolean().optional(), isDefault: z.boolean().optional(), + isDefaultTextEditModel: z.boolean().optional(), sortOrder: z.number().optional(), fallbackModelId1: z.string().max(128).optional(), fallbackModelId2: z.string().max(128).optional(), diff --git a/app/api/canvas/text-edit/route.ts b/app/api/canvas/text-edit/route.ts index 311e27843..1ffb68119 100644 --- a/app/api/canvas/text-edit/route.ts +++ b/app/api/canvas/text-edit/route.ts @@ -19,15 +19,17 @@ const requestSchema = z.object({ items: z.array(textItemSchema).min(1, "至少需要一条文字修改"), }); -function pickVolcanoModel(models: Awaited>) { +function pickTextEditModel(models: Awaited>) { const candidates = models .filter((item) => item.model.supportsImageGen && item.provider.type === "volcano-vision") .sort((a, b) => { const aScore = + (a.model.isDefaultTextEditModel ? 100 : 0) + (a.model.isDefault ? 10 : 0) + (a.model.modelId?.toLowerCase().includes("seedream") ? 5 : 0) + (a.model.displayName?.includes("火山") ? 3 : 0); const bScore = + (b.model.isDefaultTextEditModel ? 100 : 0) + (b.model.isDefault ? 10 : 0) + (b.model.modelId?.toLowerCase().includes("seedream") ? 5 : 0) + (b.model.displayName?.includes("火山") ? 3 : 0); @@ -148,12 +150,27 @@ export async function POST(request: Request) { } const models = await getEnabledAiModelsWithProvider(); - const selected = pickVolcanoModel(models); + const selected = pickTextEditModel(models); if (!selected) { - return NextResponse.json({ success: false, error: "未配置可用的火山生图模型" }, { status: 500 }); + return NextResponse.json( + { + success: false, + error: "未配置可用的文字编辑生图模型,请在系统设置-AI 模型中启用一个火山生图模型并设为默认文字编辑生图模型", + }, + { status: 500 }, + ); } + console.info("[Canvas Text Edit] selected model:", { + providerType: selected.provider.type, + providerName: selected.provider.name, + modelId: selected.model.modelId, + displayName: selected.model.displayName, + isDefault: selected.model.isDefault, + isDefaultTextEditModel: selected.model.isDefaultTextEditModel, + }); + const provider = createVolcanoVision({ baseURL: selected.provider.baseUrl, apiKey: selected.provider.apiKey, diff --git a/lib/db/migrate.ts b/lib/db/migrate.ts index b5b445b7e..3aa4d011b 100644 --- a/lib/db/migrate.ts +++ b/lib/db/migrate.ts @@ -105,6 +105,10 @@ const runMigrate = async () => { ALTER TABLE "AiModel" ADD COLUMN IF NOT EXISTS "excludeFromFailover" boolean NOT NULL DEFAULT false; `); + await db.execute(sql` + ALTER TABLE "AiModel" + ADD COLUMN IF NOT EXISTS "isDefaultTextEditModel" boolean NOT NULL DEFAULT false; + `); for (const item of providerBaseUrlDefaults) { await db.execute(sql` UPDATE "AiProvider" diff --git a/lib/db/migrations/0076_add_ai_model_text_edit_default.sql b/lib/db/migrations/0076_add_ai_model_text_edit_default.sql new file mode 100644 index 000000000..7372a9020 --- /dev/null +++ b/lib/db/migrations/0076_add_ai_model_text_edit_default.sql @@ -0,0 +1,2 @@ +ALTER TABLE "AiModel" +ADD COLUMN IF NOT EXISTS "isDefaultTextEditModel" boolean NOT NULL DEFAULT false; diff --git a/lib/db/migrations/meta/_journal.json b/lib/db/migrations/meta/_journal.json index 0e291c8e9..cc74ac8df 100644 --- a/lib/db/migrations/meta/_journal.json +++ b/lib/db/migrations/meta/_journal.json @@ -435,6 +435,13 @@ "when": 1777276800000, "tag": "0075_add_agent_workflow_default_model", "breakpoints": true + }, + { + "idx": 62, + "version": "7", + "when": 1777305600000, + "tag": "0076_add_ai_model_text_edit_default", + "breakpoints": true } ] } diff --git a/lib/db/queries.ts b/lib/db/queries.ts index 01e70f462..0ec95b293 100644 --- a/lib/db/queries.ts +++ b/lib/db/queries.ts @@ -3941,6 +3941,7 @@ export async function createAiModel({ isEnabled, isHidden, isDefault, + isDefaultTextEditModel, sortOrder, fallbackModelId1, fallbackModelId2, @@ -3961,6 +3962,7 @@ export async function createAiModel({ isEnabled?: boolean; isHidden?: boolean; isDefault?: boolean; + isDefaultTextEditModel?: boolean; sortOrder?: number; fallbackModelId1?: string; fallbackModelId2?: string; @@ -3980,6 +3982,13 @@ export async function createAiModel({ .where(eq(aiModel.isDefault, true)); } + if (isDefaultTextEditModel) { + await db + .update(aiModel) + .set({ isDefaultTextEditModel: false }) + .where(eq(aiModel.isDefaultTextEditModel, true)); + } + const defaultParams = agentCodingModelId !== undefined ? withAgentCodingModelId( @@ -4003,6 +4012,7 @@ export async function createAiModel({ isEnabled: isEnabled ?? true, isHidden: isHidden ?? false, isDefault: isDefault ?? false, + isDefaultTextEditModel: isDefaultTextEditModel ?? false, sortOrder: sortOrder ?? 0, fallbackModelId1: fallbackModelId1 || null, fallbackModelId2: fallbackModelId2 || null, @@ -4035,6 +4045,7 @@ export async function updateAiModel({ isEnabled, isHidden, isDefault, + isDefaultTextEditModel, sortOrder, fallbackModelId1, fallbackModelId2, @@ -4056,6 +4067,7 @@ export async function updateAiModel({ isEnabled?: boolean; isHidden?: boolean; isDefault?: boolean; + isDefaultTextEditModel?: boolean; sortOrder?: number; fallbackModelId1?: string; fallbackModelId2?: string; @@ -4075,6 +4087,13 @@ export async function updateAiModel({ .where(eq(aiModel.isDefault, true)); } + if (isDefaultTextEditModel) { + await db + .update(aiModel) + .set({ isDefaultTextEditModel: false }) + .where(eq(aiModel.isDefaultTextEditModel, true)); + } + const updateData: any = { updatedAt: new Date() }; if (providerId !== undefined) updateData.providerId = providerId; if (modelId !== undefined) updateData.modelId = modelId; @@ -4087,6 +4106,9 @@ export async function updateAiModel({ if (isEnabled !== undefined) updateData.isEnabled = isEnabled; if (isHidden !== undefined) updateData.isHidden = isHidden; if (isDefault !== undefined) updateData.isDefault = isDefault; + if (isDefaultTextEditModel !== undefined) { + updateData.isDefaultTextEditModel = isDefaultTextEditModel; + } if (sortOrder !== undefined) updateData.sortOrder = sortOrder; // 备用模型 ID(空字符串转为 null) if (fallbackModelId1 !== undefined) updateData.fallbackModelId1 = fallbackModelId1 || null; diff --git a/lib/db/schema.ts b/lib/db/schema.ts index a750be413..06ff7fc51 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -304,6 +304,7 @@ export const aiModel = pgTable("AiModel", { isEnabled: boolean("isEnabled").notNull().default(true), // 是否启用 isHidden: boolean("isHidden").notNull().default(false), // 是否在前端模型选择中隐藏 isDefault: boolean("isDefault").notNull().default(false), // 是否为默认模型 + isDefaultTextEditModel: boolean("isDefaultTextEditModel").notNull().default(false), // 是否为默认文字编辑生图模型 sortOrder: integer("sortOrder").notNull().default(0), // 排序顺序 capabilities: jsonb("capabilities"), // 🆕 模型能力配置(用于视频模型) defaultParams: jsonb("defaultParams"), // 🆕 默认参数配置