feat: support default canvas text edit image model
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-28 15:07:28 +08:00
parent 8b9d9d42e0
commit 1f1ffd2211
8 changed files with 96 additions and 3 deletions

View File

@ -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() {
</Label>
</div>
</div>
<div className="flex items-center gap-2">
<Switch
checked={modelForm.isDefaultTextEditModel}
disabled={!modelForm.supportsImageGen}
onCheckedChange={(checked) =>
setModelForm({
...modelForm,
isDefaultTextEditModel: checked,
})
}
/>
<Label
className={
modelForm.supportsImageGen
? ""
: "text-muted-foreground"
}
>
默认文字编辑生图模型(无限画布文字编辑使用)
</Label>
</div>
<p className="text-xs text-muted-foreground">
建议给可用的火山生图模型开启。未配置时,系统仍会回退到当前可用的火山生图模型。
</p>
<div className="flex items-center gap-2">
<Switch
checked={modelForm.isHidden}
@ -14981,6 +15012,11 @@ export function AdminSettingsClient() {
默认
</span>
)}
{model.isDefaultTextEditModel && (
<span className="text-xs bg-fuchsia-100 text-fuchsia-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
@ -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 || "",

View File

@ -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(),

View File

@ -19,15 +19,17 @@ const requestSchema = z.object({
items: z.array(textItemSchema).min(1, "至少需要一条文字修改"),
});
function pickVolcanoModel(models: Awaited<ReturnType<typeof getEnabledAiModelsWithProvider>>) {
function pickTextEditModel(models: Awaited<ReturnType<typeof getEnabledAiModelsWithProvider>>) {
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,

View File

@ -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"

View File

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

View File

@ -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
}
]
}

View File

@ -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;

View File

@ -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"), // 🆕 默认参数配置