新增Agent自动模型标签并支持智普4.7服务商
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-12 12:47:46 +08:00
parent 2913bf77be
commit 46f06819dc
16 changed files with 279 additions and 42 deletions

View File

@ -98,7 +98,7 @@ type ToolConfig = {
type AiProvider = {
id: string;
name: string;
type: "alibaba" | "openai" | "custom" | "local" | "gemini" | "huanke-gemini" | "qianwen-video" | "volcano-vision" | "volcano-video" | "alibaba-image" | "ruxa-image";
type: "alibaba" | "openai" | "custom" | "local" | "gemini" | "huanke-gemini" | "qianwen-video" | "volcano-vision" | "volcano-video" | "alibaba-image" | "ruxa-image" | "zhipu";
baseUrl: string;
apiKey: string;
isEnabled: boolean;
@ -128,6 +128,7 @@ type AiModel = {
isEnabled: boolean;
isDefault: boolean;
sortOrder: number;
isAgentModel?: boolean;
imageGenConfig?: ImageGenConfig; // 🆕 图片生成特性配置
};
@ -284,6 +285,7 @@ const PROVIDER_TYPES = [
{ value: "qianwen-video", label: "千问视频生成" },
{ value: "volcano-vision", label: "火山引擎视觉" },
{ value: "volcano-video", label: "火山引擎视频" },
{ value: "zhipu", label: "智普官方 4.7" },
];
export function AdminSettingsClient() {
@ -740,7 +742,7 @@ export function AdminSettingsClient() {
const [providerForm, setProviderForm] = useState({
id: "",
name: "",
type: "openai" as "alibaba" | "openai" | "custom" | "local" | "gemini" | "huanke-gemini" | "qianwen-video" | "volcano-vision" | "volcano-video" | "alibaba-image" | "ruxa-image",
type: "openai" as "alibaba" | "openai" | "custom" | "local" | "gemini" | "huanke-gemini" | "qianwen-video" | "volcano-vision" | "volcano-video" | "alibaba-image" | "ruxa-image" | "zhipu",
baseUrl: "",
apiKey: "",
isEnabled: true,
@ -759,6 +761,7 @@ export function AdminSettingsClient() {
supportsImageGen: false,
isEnabled: true,
isDefault: false,
isAgentModel: false,
sortOrder: 0,
fallbackModelId1: "", // 备用模型 ID 1
fallbackModelId2: "", // 备用模型 ID 2
@ -1353,6 +1356,7 @@ export function AdminSettingsClient() {
supportsImageGen: false,
isEnabled: true,
isDefault: false,
isAgentModel: false,
sortOrder: 0,
fallbackModelId1: "",
fallbackModelId2: "",
@ -2307,6 +2311,7 @@ export function AdminSettingsClient() {
"alibaba-image": "bg-amber-500",
"ruxa-image": "bg-emerald-500",
"openai": "bg-green-500",
"zhipu": "bg-lime-500",
"qianwen-video": "bg-cyan-500",
"custom": "bg-gray-500",
"local": "bg-yellow-500",
@ -3617,6 +3622,8 @@ export function AdminSettingsClient() {
baseUrl:
!prev.baseUrl && value === "ruxa-image"
? "https://api.ruxa.ai"
: !prev.baseUrl && value === "zhipu"
? "https://open.bigmodel.cn/api/paas/v4"
: prev.baseUrl,
}))
}
@ -3634,7 +3641,13 @@ export function AdminSettingsClient() {
<Input
value={providerForm.baseUrl}
onChange={(e) => setProviderForm({ ...providerForm, baseUrl: e.target.value })}
placeholder={providerForm.type === "ruxa-image" ? "https://api.ruxa.ai" : "https://api.openai.com/v1"}
placeholder={
providerForm.type === "ruxa-image"
? "https://api.ruxa.ai"
: providerForm.type === "zhipu"
? "https://open.bigmodel.cn/api/paas/v4"
: "https://api.openai.com/v1"
}
/>
</div>
<div className="space-y-2">
@ -3899,6 +3912,13 @@ export function AdminSettingsClient() {
<Label>设为默认</Label>
</div>
</div>
<div className="flex items-center gap-2">
<Switch
checked={modelForm.isAgentModel}
onCheckedChange={(checked) => setModelForm({ ...modelForm, isAgentModel: checked })}
/>
<Label>Agent 自动模型(前端隐藏,仅后台 Agent 使用)</Label>
</div>
{/* 🆕 图片生成模型特性配置 */}
{modelForm.supportsImageGen && (
@ -4029,6 +4049,7 @@ export function AdminSettingsClient() {
<div className="font-medium flex items-center gap-2">
{model.displayName}
{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>}
</div>
<div className="text-sm text-muted-foreground">
ID: {model.modelId} | 提供商: {provider?.name ?? "未知"}
@ -4046,6 +4067,7 @@ export function AdminSettingsClient() {
supportsImageGen: model.supportsImageGen,
isEnabled: model.isEnabled,
isDefault: model.isDefault,
isAgentModel: model.isAgentModel || false,
sortOrder: model.sortOrder || 0,
fallbackModelId1: (model as any).fallbackModelId1 || "",
fallbackModelId2: (model as any).fallbackModelId2 || "",

View File

@ -54,6 +54,7 @@ import { uploadFile } from "@/lib/storage/service";
import type { ChatMessage } from "@/lib/types";
import type { AppUsage } from "@/lib/usage";
import { convertToUIMessages, generateUUID } from "@/lib/utils";
import { isAiModelAgentAuto } from "@/lib/db/ai-model-flags";
import { generateTitleFromUserMessage } from "../../actions";
import { type PostRequestBody, postRequestBodySchema } from "./schema";
@ -832,7 +833,13 @@ export async function POST(request: Request) {
if (selectedModelIsUUID) {
const selectedModelConfig = await getAiModelById({ id: requestedModelId });
if (selectedModelConfig?.supportsImageGen && !selectedModelConfig?.isSubModel) {
if (selectedModelConfig && isAiModelAgentAuto(selectedModelConfig)) {
shouldRunAgentConversation = true;
shouldKeepSelectedModel = false;
console.log(
`[Smart Auto Mode] 🧠 Agent-only model detected, force auto routing: ${selectedModelConfig.displayName}`
);
} else if (selectedModelConfig?.supportsImageGen && !selectedModelConfig?.isSubModel) {
shouldRunAgentConversation = true;
shouldKeepSelectedModel = true;
console.log(

View File

@ -1,6 +1,7 @@
import { auth } from "@/app/(auth)/auth";
import { getEnabledAiModelsWithProvider, getDefaultAiModel } from "@/lib/db/queries";
import { ChatSDKError } from "@/lib/errors";
import { isAiModelAgentAuto } from "@/lib/db/ai-model-flags";
// 获取前端可用的 AI 模型列表(需要登录但不需要管理员权限)
export async function GET() {
@ -14,9 +15,12 @@ export async function GET() {
// 只获取主模型(不包括子模型),子模型由后端根据 videoParams.mode 自动选择
const modelsWithProvider = await getEnabledAiModelsWithProvider(false);
const defaultModel = await getDefaultAiModel();
const visibleModelsWithProvider = modelsWithProvider.filter(
({ model }) => !isAiModelAgentAuto(model)
);
// 转换为前端需要的格式
const models = modelsWithProvider.map(({ model, provider }) => ({
const models = visibleModelsWithProvider.map(({ model, provider }) => ({
id: model.id,
modelId: model.modelId,
displayName: model.displayName,
@ -31,9 +35,18 @@ export async function GET() {
capabilities: model.capabilities, // 🆕 返回子模型的能力配置
}));
const resolvedDefaultModelId =
(defaultModel &&
!isAiModelAgentAuto(defaultModel) &&
models.some((model) => model.id === defaultModel.id)
? defaultModel.id
: models.find((model) => model.isDefault)?.id) ??
models[0]?.id ??
null;
return Response.json({
models,
defaultModelId: defaultModel?.id ?? models[0]?.id ?? null,
defaultModelId: resolvedDefaultModelId,
});
} catch (error) {
if (error instanceof ChatSDKError) {
@ -42,4 +55,3 @@ export async function GET() {
return new ChatSDKError("offline:chat").toResponse();
}
}

View File

@ -6,6 +6,7 @@ import { DataStreamHandler } from "@/components/data-stream-handler";
import { generateUUID } from "@/lib/utils";
import { auth } from "../(auth)/auth";
import { getDefaultAiModel, getEnabledAiModelsWithProvider } from "@/lib/db/queries";
import { isAiModelAgentAuto } from "@/lib/db/ai-model-flags";
export default function Page() {
return (
@ -33,14 +34,17 @@ async function NewChatPage() {
if (!initialModelId) {
// 尝试获取默认模型
const defaultModel = await getDefaultAiModel();
if (defaultModel) {
if (defaultModel && !isAiModelAgentAuto(defaultModel)) {
initialModelId = defaultModel.id;
} else {
// 如果没有设置默认模型,获取第一个启用的模型
const enabledModels = await getEnabledAiModelsWithProvider();
if (enabledModels.length > 0) {
initialModelId = enabledModels[0].model.id;
const visibleModels = enabledModels.filter(
({ model }) => !isAiModelAgentAuto(model)
);
if (visibleModels.length > 0) {
initialModelId = visibleModels[0].model.id;
}
}
}

View File

@ -10,6 +10,7 @@ import {
getAiProviderById,
} from "@/lib/db/queries";
import { ChatSDKError } from "@/lib/errors";
import { isAiModelAgentAuto } from "@/lib/db/ai-model-flags";
// 🆕 图片生成模型特性配置 Schema
const imageGenConfigSchema = z.object({
@ -35,6 +36,7 @@ const createModelSchema = z.object({
sortOrder: z.number().optional(),
fallbackModelId1: z.string().max(128).optional(),
fallbackModelId2: z.string().max(128).optional(),
isAgentModel: z.boolean().optional(),
imageGenConfig: imageGenConfigSchema, // 🆕 图片生成特性配置
});
@ -51,9 +53,17 @@ const updateModelSchema = z.object({
sortOrder: z.number().optional(),
fallbackModelId1: z.string().max(128).optional(),
fallbackModelId2: z.string().max(128).optional(),
isAgentModel: z.boolean().optional(),
imageGenConfig: imageGenConfigSchema, // 🆕 图片生成特性配置
});
function normalizeModel(model: any) {
return {
...model,
isAgentModel: isAiModelAgentAuto(model),
};
}
export async function GET(request: Request) {
try {
await requireAdmin();
@ -67,16 +77,16 @@ export async function GET(request: Request) {
if (!model) {
return new ChatSDKError("bad_request:api", "Model not found").toResponse();
}
return Response.json({ model });
return Response.json({ model: normalizeModel(model) });
}
if (providerId) {
const models = await getAiModelsByProviderId({ providerId });
return Response.json({ models });
return Response.json({ models: models.map(normalizeModel) });
}
const models = await getAllAiModels();
return Response.json({ models });
return Response.json({ models: models.map(normalizeModel) });
} catch (error) {
if (error instanceof ChatSDKError) {
return error.toResponse();
@ -100,7 +110,7 @@ export async function POST(request: Request) {
const model = await createAiModel(data);
return Response.json({ model: model[0] });
return Response.json({ model: normalizeModel(model[0]) });
} catch (error) {
if (error instanceof ChatSDKError) {
return error.toResponse();
@ -129,7 +139,7 @@ export async function PUT(request: Request) {
}
console.log("[Admin API] Model updated successfully:", model[0].id); // Debug log
return Response.json({ model: model[0] });
return Response.json({ model: normalizeModel(model[0]) });
} catch (error) {
console.error("[Admin API] Update model error:", error); // Debug log
if (error instanceof ChatSDKError) {
@ -164,4 +174,3 @@ export async function DELETE(request: Request) {
return new ChatSDKError("offline:admin").toResponse();
}
}

View File

@ -9,9 +9,26 @@ import {
} from "@/lib/db/queries";
import { ChatSDKError } from "@/lib/errors";
const providerTypes = [
"alibaba",
"openai",
"custom",
"local",
"gemini",
"huanke-gemini",
"qianwen-video",
"volcano-vision",
"volcano-video",
"alibaba-image",
"ruxa-image",
"zhipu",
] as const;
type ProviderType = (typeof providerTypes)[number];
type StoredProviderType = Exclude<ProviderType, "zhipu">;
const createProviderSchema = z.object({
name: z.string().min(1).max(128),
type: z.enum(["alibaba", "openai", "custom", "local", "gemini", "huanke-gemini", "qianwen-video", "volcano-vision", "volcano-video", "alibaba-image", "ruxa-image"]),
type: z.enum(providerTypes),
baseUrl: z.string().min(1), // 改为简单的字符串验证,支持 localhost
apiKey: z.string().min(1),
isEnabled: z.boolean().optional(),
@ -20,12 +37,32 @@ const createProviderSchema = z.object({
const updateProviderSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1).max(128).optional(),
type: z.enum(["alibaba", "openai", "custom", "local", "gemini", "huanke-gemini", "qianwen-video", "volcano-vision", "volcano-video", "alibaba-image", "ruxa-image"]).optional(),
type: z.enum(providerTypes).optional(),
baseUrl: z.string().min(1).optional(), // 改为简单的字符串验证,支持 localhost
apiKey: z.string().min(1).optional(),
isEnabled: z.boolean().optional(),
});
const ZHIPU_BASE_URL = "https://open.bigmodel.cn/api/paas/v4";
function normalizeProviderTypeForStorage(
type?: ProviderType
): StoredProviderType | undefined {
if (!type) return undefined;
return type === "zhipu" ? "custom" : type;
}
function isZhipuBaseUrl(baseUrl: string) {
return baseUrl.includes("open.bigmodel.cn");
}
function normalizeProviderTypeForResponse(provider: Record<string, any>) {
if (provider.type === "custom" && typeof provider.baseUrl === "string" && isZhipuBaseUrl(provider.baseUrl)) {
return { ...provider, type: "zhipu" };
}
return provider;
}
function maskApiKey(apiKey?: string) {
if (!apiKey) return undefined;
if (apiKey.length <= 8) return "****";
@ -55,20 +92,22 @@ export async function GET(request: Request) {
}
// 隐藏 API Key 的部分内容
return Response.json({
provider: {
provider: normalizeProviderTypeForResponse({
...provider,
apiKey: provider.apiKey.slice(0, 8) + "****" + provider.apiKey.slice(-4),
},
}),
});
}
const providers = await getAllAiProviders();
// 隐藏 API Key 的部分内容
return Response.json({
providers: providers.map((p) => ({
...p,
apiKey: p.apiKey.slice(0, 8) + "****" + p.apiKey.slice(-4),
})),
providers: providers.map((p) =>
normalizeProviderTypeForResponse({
...p,
apiKey: p.apiKey.slice(0, 8) + "****" + p.apiKey.slice(-4),
})
),
});
} catch (error) {
if (error instanceof ChatSDKError) {
@ -87,14 +126,17 @@ export async function POST(request: Request) {
const data = createProviderSchema.parse(body);
console.log("[Admin API] Validation passed, creating provider...");
const storageType = normalizeProviderTypeForStorage(data.type) ?? "custom";
const provider = await createAiProvider({
...data,
type: storageType,
baseUrl: data.type === "zhipu" ? data.baseUrl || ZHIPU_BASE_URL : data.baseUrl,
updatedBy: session.user.id,
});
console.log("[Admin API] Provider created successfully:", provider[0].id);
return Response.json({ provider: provider[0] });
return Response.json({ provider: normalizeProviderTypeForResponse(provider[0] as any) });
} catch (error) {
console.error("[Admin API] Create provider error:", error);
if (error instanceof ChatSDKError) {
@ -117,6 +159,7 @@ export async function PUT(request: Request) {
const provider = await updateAiProvider({
...data,
type: normalizeProviderTypeForStorage(data.type),
updatedBy: session.user.id,
});
@ -124,7 +167,7 @@ export async function PUT(request: Request) {
return new ChatSDKError("bad_request:api", "Provider not found").toResponse();
}
return Response.json({ provider: provider[0] });
return Response.json({ provider: normalizeProviderTypeForResponse(provider[0] as any) });
} catch (error) {
if (error instanceof ChatSDKError) {
return error.toResponse();

View File

@ -1058,6 +1058,26 @@ function PureModelSelectorCompact({
setOptimisticModelId(selectedModelId);
}, [selectedModelId]);
useEffect(() => {
if (displayModels.length === 0) return;
const currentExists = displayModels.some((m) => m.id === selectedModelId);
if (currentExists) return;
const fallbackModelId =
dynamicModels.find((m) => m.isDefault)?.id ??
dynamicModels[0]?.id ??
(showAutoModel ? "auto" : "");
if (!fallbackModelId) return;
setOptimisticModelId(fallbackModelId);
onModelChange?.(fallbackModelId);
startTransition(() => {
saveChatModelAsCookie(fallbackModelId);
});
}, [displayModels, dynamicModels, onModelChange, selectedModelId, showAutoModel]);
// 自动选择模型的逻辑 - 必须在所有条件 return 之前定义
const selectBestModel = useCallback(() => {
const hasAttachments = attachments && attachments.length > 0;

View File

@ -65,16 +65,25 @@ async function generateAgentConversation(
state: AutoAgentStateType
): Promise<string | null> {
try {
const { getEnabledAiModelsWithProvider } = await import("@/lib/db/queries");
const {
getEnabledAiModelsWithProvider,
getAgentAiModelWithProvider,
} = await import("@/lib/db/queries");
const { getLanguageModelById } = await import("./dynamic-provider");
const allModels = await getEnabledAiModelsWithProvider();
const agentModel = await getAgentAiModelWithProvider();
const canUseAgentModel = Boolean(
agentModel && (!state.hasImage || agentModel.model.supportsVision)
);
const preferredVisionModel = allModels.find(
(m) => m.provider.type === "alibaba" && m.model.supportsVision && m.model.isEnabled
);
const candidateModel = state.hasImage
? preferredVisionModel || allModels.find((m) => m.model.supportsVision && m.model.isEnabled)
: allModels.find((m) => m.model.isEnabled && !m.model.isSubModel);
const candidateModel =
(canUseAgentModel ? agentModel : undefined) ||
(state.hasImage
? preferredVisionModel || allModels.find((m) => m.model.supportsVision && m.model.isEnabled)
: allModels.find((m) => m.model.isEnabled && !m.model.isSubModel));
if (!candidateModel) {
return analysis.conversationalReply || null;

View File

@ -66,6 +66,17 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
break;
}
case "zhipu": {
providerInstance = createOpenAI({
apiKey: provider.apiKey,
baseURL: provider.baseUrl || "https://open.bigmodel.cn/api/paas/v4",
});
console.log(
`[Provider] Created Zhipu Open Platform provider: ${provider.baseUrl || "https://open.bigmodel.cn/api/paas/v4"}`
);
break;
}
case "local": {
// 本地部署的模型(如千问2.5-VL)
// 使用自定义的本地模型接口,不兼容 OpenAI 格式

View File

@ -110,11 +110,16 @@ export async function routeToModel(params: {
// 使用动态导入避免循环依赖
const { getEnabledAiModelsWithProvider } = await import("@/lib/db/queries");
const { isAiModelAgentAuto } = await import("@/lib/db/ai-model-flags");
const { db } = await import("@/lib/db");
const { siteSettings } = await import("@/lib/db/schema");
// 获取所有可用模型
const allModels = await getEnabledAiModelsWithProvider();
const allModelsRaw = await getEnabledAiModelsWithProvider();
const nonAgentModels = allModelsRaw.filter(
(item) => !isAiModelAgentAuto(item.model)
);
const allModels = nonAgentModels.length > 0 ? nonAgentModels : allModelsRaw;
console.log("[Model Router] Available models:", allModels.length);
// 获取意图对应的配置

View File

@ -294,21 +294,26 @@ export async function optimizePromptWithLLM(params: {
try {
// 动态导入避免循环依赖
const { getEnabledAiModelsWithProvider } = await import("@/lib/db/queries");
const {
getEnabledAiModelsWithProvider,
getAgentAiModelWithProvider,
} = await import("@/lib/db/queries");
const { getLanguageModelById } = await import("./dynamic-provider");
// 获取千问3 VL 模型
const allModels = await getEnabledAiModelsWithProvider();
const agentModel = await getAgentAiModelWithProvider();
const qwenModel = allModels.find(
(m) => m.provider.type === "alibaba" && m.model.supportsVision
);
const selectedOptimizerModel = agentModel || qwenModel;
if (!qwenModel) {
if (!selectedOptimizerModel) {
console.log("[Prompt Optimizer LLM] No Qwen3 VL model, falling back to template");
return optimizePrompt({ originalPrompt, intent, subIntent });
}
const model = await getLanguageModelById(qwenModel.model.id);
const model = await getLanguageModelById(selectedOptimizerModel.model.id);
if (!model) {
console.log("[Prompt Optimizer LLM] Failed to get model, falling back to template");
return optimizePrompt({ originalPrompt, intent, subIntent });

View File

@ -206,21 +206,29 @@ async function analyzWithLLM(
context?: string
): Promise<AnalysisResult | null> {
// 使用动态导入避免循环依赖问题
const { getEnabledAiModelsWithProvider } = await import("@/lib/db/queries");
const {
getEnabledAiModelsWithProvider,
getAgentAiModelWithProvider,
} = await import("@/lib/db/queries");
const { getLanguageModelById } = await import("./dynamic-provider");
// 获取千问3 VL 模型
const allModels = await getEnabledAiModelsWithProvider();
const agentModel = await getAgentAiModelWithProvider();
const qwenModel = allModels.find(
(m) => m.provider.type === "alibaba" && m.model.supportsVision
);
const selectedAnalyzerModel =
(agentModel && (!hasImage || agentModel.model.supportsVision)
? agentModel
: undefined) || qwenModel;
if (!qwenModel) {
if (!selectedAnalyzerModel) {
console.log("[Smart Analyzer] No Qwen3 VL model found");
return null;
}
const model = await getLanguageModelById(qwenModel.model.id);
const model = await getLanguageModelById(selectedAnalyzerModel.model.id);
if (!model) {
console.log("[Smart Analyzer] Failed to get language model");
return null;

40
lib/db/ai-model-flags.ts Normal file
View File

@ -0,0 +1,40 @@
const AGENT_AUTO_MODEL_KEY = "agentAutoModel";
function toRecord(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
return value as Record<string, unknown>;
}
export function isAiModelAgentAuto(input: {
defaultParams?: unknown;
capabilities?: unknown;
}): boolean {
const defaultParams = toRecord(input.defaultParams);
if (typeof defaultParams?.[AGENT_AUTO_MODEL_KEY] === "boolean") {
return defaultParams[AGENT_AUTO_MODEL_KEY] as boolean;
}
const capabilities = toRecord(input.capabilities);
if (typeof capabilities?.[AGENT_AUTO_MODEL_KEY] === "boolean") {
return capabilities[AGENT_AUTO_MODEL_KEY] as boolean;
}
return false;
}
export function withAgentAutoModelFlag(
defaultParams: unknown,
enabled: boolean
): Record<string, unknown> | null {
const next = { ...(toRecord(defaultParams) || {}) };
if (enabled) {
next[AGENT_AUTO_MODEL_KEY] = true;
} else {
delete next[AGENT_AUTO_MODEL_KEY];
}
return Object.keys(next).length > 0 ? next : null;
}

View File

@ -28,7 +28,7 @@ const runMigrate = async () => {
ADD CONSTRAINT "AiProvider_type_check"
CHECK (type IN (
'alibaba', 'openai', 'custom', 'local', 'gemini',
'huanke-gemini', 'qianwen-video', 'volcano-vision', 'volcano-video', 'alibaba-image', 'ruxa-image'
'huanke-gemini', 'qianwen-video', 'volcano-vision', 'volcano-video', 'alibaba-image', 'ruxa-image', 'zhipu'
));
`);
const end = Date.now();

View File

@ -51,6 +51,10 @@ import {
generatedImage,
} from "./schema";
import { generateHashedPassword } from "./utils";
import {
isAiModelAgentAuto,
withAgentAutoModelFlag,
} from "./ai-model-flags";
// Optionally, if not using email/pass login, you can
// use the Drizzle adapter for Auth.js / NextAuth
@ -941,7 +945,7 @@ export async function createAiProvider({
updatedBy,
}: {
name: string;
type: "alibaba" | "openai" | "custom" | "local" | "gemini" | "huanke-gemini" | "qianwen-video" | "volcano-vision" | "volcano-video" | "alibaba-image" | "ruxa-image";
type: "alibaba" | "openai" | "custom" | "local" | "gemini" | "huanke-gemini" | "qianwen-video" | "volcano-vision" | "volcano-video" | "alibaba-image" | "ruxa-image" | "zhipu";
baseUrl: string;
apiKey: string;
isEnabled?: boolean;
@ -979,7 +983,7 @@ export async function updateAiProvider({
}: {
id: string;
name?: string;
type?: "alibaba" | "openai" | "custom" | "local" | "gemini" | "huanke-gemini" | "qianwen-video" | "volcano-vision" | "volcano-video" | "alibaba-image" | "ruxa-image";
type?: "alibaba" | "openai" | "custom" | "local" | "gemini" | "huanke-gemini" | "qianwen-video" | "volcano-vision" | "volcano-video" | "alibaba-image" | "ruxa-image" | "zhipu";
baseUrl?: string;
apiKey?: string;
isEnabled?: boolean;
@ -1126,6 +1130,7 @@ export async function createAiModel({
fallbackModelId1,
fallbackModelId2,
imageGenConfig,
isAgentModel,
}: {
providerId: string;
modelId: string;
@ -1139,6 +1144,7 @@ export async function createAiModel({
fallbackModelId1?: string;
fallbackModelId2?: string;
imageGenConfig?: ImageGenConfig;
isAgentModel?: boolean;
}): Promise<AiModel[]> {
try {
// 如果设置为默认,先取消其他默认
@ -1164,6 +1170,10 @@ export async function createAiModel({
fallbackModelId1: fallbackModelId1 || null,
fallbackModelId2: fallbackModelId2 || null,
imageGenConfig: imageGenConfig || null,
defaultParams:
isAgentModel === undefined
? null
: withAgentAutoModelFlag(null, isAgentModel),
})
.returning();
} catch (_error) {
@ -1188,6 +1198,7 @@ export async function updateAiModel({
fallbackModelId1,
fallbackModelId2,
imageGenConfig,
isAgentModel,
}: {
id: string;
providerId?: string;
@ -1202,6 +1213,7 @@ export async function updateAiModel({
fallbackModelId1?: string;
fallbackModelId2?: string;
imageGenConfig?: ImageGenConfig;
isAgentModel?: boolean;
}): Promise<AiModel[]> {
try {
// 如果设置为默认,先取消其他默认
@ -1228,6 +1240,22 @@ export async function updateAiModel({
if (fallbackModelId2 !== undefined) updateData.fallbackModelId2 = fallbackModelId2 || null;
// 🆕 图片生成配置
if (imageGenConfig !== undefined) updateData.imageGenConfig = imageGenConfig || null;
if (isAgentModel !== undefined) {
const [existingModel] = await db
.select({ defaultParams: aiModel.defaultParams })
.from(aiModel)
.where(eq(aiModel.id, id))
.limit(1);
if (!existingModel) {
return [];
}
updateData.defaultParams = withAgentAutoModelFlag(
existingModel.defaultParams,
isAgentModel
);
}
return await db
.update(aiModel)
@ -1476,6 +1504,20 @@ export async function getEnabledAiModelsWithProvider(includeSubModels: boolean =
}
}
export async function getAgentAiModelWithProvider() {
const models = await getEnabledAiModelsWithProvider(false);
const candidates = models
.filter(({ model }) => isAiModelAgentAuto(model))
.sort((a, b) => {
if (a.model.isDefault !== b.model.isDefault) {
return Number(b.model.isDefault) - Number(a.model.isDefault);
}
return (a.model.sortOrder ?? 0) - (b.model.sortOrder ?? 0);
});
return candidates[0];
}
/**
* 🆕 获取指定父模型的所有子模型
* 用于火山引擎视频等需要智能选择模型的场景

View File

@ -215,7 +215,7 @@ export type ToolConfig = InferSelectModel<typeof toolConfig>;
export const aiProvider = pgTable("AiProvider", {
id: uuid("id").primaryKey().notNull().defaultRandom(),
name: varchar("name", { length: 128 }).notNull(), // 提供商名称,如 "阿里云通义千问"
type: varchar("type", { enum: ["alibaba", "openai", "custom", "local", "gemini", "huanke-gemini", "qianwen-video", "volcano-vision", "volcano-video", "alibaba-image", "ruxa-image"] })
type: varchar("type", { enum: ["alibaba", "openai", "custom", "local", "gemini", "huanke-gemini", "qianwen-video", "volcano-vision", "volcano-video", "alibaba-image", "ruxa-image", "zhipu"] })
.notNull()
.default("custom"), // 提供商类型
baseUrl: text("baseUrl").notNull(), // API 基础 URL