diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx index 3f109da89..09bb0ce63 100644 --- a/app/(admin)/settings/admin-settings-client.tsx +++ b/app/(admin)/settings/admin-settings-client.tsx @@ -258,6 +258,7 @@ type SiteSettings = { canvasDefaultModelId: string | null; canvasAutoModeDefaultModelId: string | null; canvasDetailDefaultModelId: string | null; + canvasDetailAgentModelId: string | null; lovartComposerPrefix: string; lovartComposerPreviewTexts: string[]; lovartPromptTagVisibility: LovartPromptTagVisibility; @@ -510,6 +511,8 @@ const PROVIDER_DEFAULT_BASE_URL: Record = { }; const LEMON_OPENAI_BASE_URL = "https://new.lemonapi.site/v1"; +const MOONSHOT_OFFICIAL_BASE_URL = "https://api.moonshot.cn/v1"; +const MOONSHOT_KIMI_K25_MODEL_ID = "kimi-k2.5"; const LEMON_PROVIDER_PRESET = { name: "LemonAPI Gemini", @@ -541,6 +544,10 @@ function isLemonApiBaseUrl(value?: string | null) { return String(value || "").trim().replace(/\/+$/, "") === LEMON_OPENAI_BASE_URL; } +function isMoonshotOfficialBaseUrl(value?: string | null) { + return String(value || "").trim().replace(/\/+$/, "") === MOONSHOT_OFFICIAL_BASE_URL; +} + export function AdminSettingsClient() { const [activeTab, setActiveTab] = useState("users"); @@ -625,6 +632,14 @@ export function AdminSettingsClient() { : null; const { data: analyticsData } = useSWR(analyticsUrl, fetcher); + const detailAgentModelOptions = useMemo( + () => + (modelsData?.models || []).filter( + (model) => !model.supportsImageGen && model.isEnabled && !model.isHidden, + ), + [modelsData?.models], + ); + // 网站设置状态 const [siteSettings, setSiteSettings] = useState({ siteName: "AI Chatbot", @@ -666,6 +681,7 @@ export function AdminSettingsClient() { canvasDefaultModelId: null, canvasAutoModeDefaultModelId: null, canvasDetailDefaultModelId: null, + canvasDetailAgentModelId: null, lovartComposerPrefix: "让Buyi-ai", lovartComposerPreviewTexts: [ "打造引人注目的社交媒体视觉", @@ -766,6 +782,7 @@ export function AdminSettingsClient() { canvasDefaultModelId: siteSettingsData.canvasDefaultModelId || null, canvasAutoModeDefaultModelId: siteSettingsData.canvasAutoModeDefaultModelId || null, canvasDetailDefaultModelId: siteSettingsData.canvasDetailDefaultModelId || null, + canvasDetailAgentModelId: (siteSettingsData as any).canvasDetailAgentModelId || null, lovartComposerPrefix: String((siteSettingsData as any).lovartComposerPrefix || "").trim() || "让Buyi-ai", @@ -852,6 +869,50 @@ export function AdminSettingsClient() { } }, [siteSettingsData]); + useEffect(() => { + const configuredModelId = siteSettings.canvasDetailAgentModelId; + const providerList = providersData?.providers || []; + const modelList = modelsData?.models || []; + + if (!configuredModelId) { + const moonshotProvider = providerList.find( + (provider) => provider.type === "openai" && isMoonshotOfficialBaseUrl(provider.baseUrl), + ); + setMoonshotDetailAgentConfig((prev) => ({ + ...prev, + providerId: moonshotProvider?.id || "", + providerName: moonshotProvider?.name || "Moonshot AI (Official)", + baseUrl: moonshotProvider?.baseUrl || MOONSHOT_OFFICIAL_BASE_URL, + modelId: prev.modelId || MOONSHOT_KIMI_K25_MODEL_ID, + })); + return; + } + + const configuredModel = modelList.find((model) => model.id === configuredModelId) || null; + const configuredProvider = configuredModel + ? providerList.find((provider) => provider.id === configuredModel.providerId) + : null; + + if (!configuredModel || !configuredProvider || !isMoonshotOfficialBaseUrl(configuredProvider.baseUrl)) { + return; + } + + setMoonshotDetailAgentConfig((prev) => ({ + ...prev, + providerId: configuredProvider.id, + modelEntryId: configuredModel.id, + providerName: configuredProvider.name || "Moonshot AI (Official)", + baseUrl: configuredProvider.baseUrl || MOONSHOT_OFFICIAL_BASE_URL, + modelId: configuredModel.modelId || "", + displayName: configuredModel.displayName || "官方 Kimi K2.5", + isEnabled: configuredModel.isEnabled !== false && configuredProvider.isEnabled !== false, + })); + }, [ + modelsData?.models, + providersData?.providers, + siteSettings.canvasDetailAgentModelId, + ]); + const featuredCategoryOptions = useMemo(() => { const values = (siteSettings.surpriseCategories || []) .map((item) => String(item || "").trim()) @@ -957,6 +1018,16 @@ export function AdminSettingsClient() { apiKey: "", isEnabled: true, }); + const [moonshotDetailAgentConfig, setMoonshotDetailAgentConfig] = useState({ + providerId: "", + modelEntryId: "", + providerName: "Moonshot AI (Official)", + baseUrl: MOONSHOT_OFFICIAL_BASE_URL, + apiKey: "", + modelId: MOONSHOT_KIMI_K25_MODEL_ID, + displayName: "官方 Kimi K2.5", + isEnabled: true, + }); // 加载抠图API配置 const { data: rmbgConfigData } = useSWR("/api/admin/settings?key=rmbg_api_config", fetcher); @@ -2623,6 +2694,147 @@ export function AdminSettingsClient() { } }; + const handleSaveMoonshotDetailAgentConfig = async () => { + try { + const trimmedBaseUrl = + moonshotDetailAgentConfig.baseUrl.trim() || MOONSHOT_OFFICIAL_BASE_URL; + const trimmedModelId = + moonshotDetailAgentConfig.modelId.trim() || MOONSHOT_KIMI_K25_MODEL_ID; + const trimmedDisplayName = + moonshotDetailAgentConfig.displayName.trim() || "官方 Kimi K2.5"; + const trimmedProviderName = + moonshotDetailAgentConfig.providerName.trim() || "Moonshot AI (Official)"; + + if (!isMoonshotOfficialBaseUrl(trimmedBaseUrl)) { + toast.error("请使用 Moonshot 官方 API 地址:https://api.moonshot.cn/v1"); + return; + } + + if (!trimmedModelId) { + toast.error("请填写官方 Kimi K2.5 的 API 模型 ID"); + return; + } + + if (!moonshotDetailAgentConfig.providerId && !moonshotDetailAgentConfig.apiKey.trim()) { + toast.error("请填写 Moonshot 官方 API Key"); + return; + } + + const providerPayload: Record = moonshotDetailAgentConfig.providerId + ? { + id: moonshotDetailAgentConfig.providerId, + name: trimmedProviderName, + type: "openai", + baseUrl: trimmedBaseUrl, + isEnabled: moonshotDetailAgentConfig.isEnabled, + } + : { + name: trimmedProviderName, + type: "openai", + baseUrl: trimmedBaseUrl, + apiKey: moonshotDetailAgentConfig.apiKey.trim(), + isEnabled: moonshotDetailAgentConfig.isEnabled, + }; + + if (moonshotDetailAgentConfig.providerId && moonshotDetailAgentConfig.apiKey.trim()) { + providerPayload.apiKey = moonshotDetailAgentConfig.apiKey.trim(); + } + + const providerResponse = await fetch("/api/admin/ai-providers", { + method: moonshotDetailAgentConfig.providerId ? "PUT" : "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(providerPayload), + }); + const providerResult = await providerResponse.json().catch(() => ({})); + if (!providerResponse.ok || !providerResult?.provider?.id) { + throw new Error(String(providerResult?.error || "保存 Moonshot 提供商失败")); + } + + const providerId = String(providerResult.provider.id); + const existingModel = + (moonshotDetailAgentConfig.modelEntryId && + modelsData?.models?.find((model) => model.id === moonshotDetailAgentConfig.modelEntryId)) || + modelsData?.models?.find( + (model) => model.providerId === providerId && model.modelId === trimmedModelId, + ) || + null; + + const modelPayload: Record = existingModel + ? { + id: existingModel.id, + providerId, + modelId: trimmedModelId, + displayName: trimmedDisplayName, + description: "Moonshot 官方 Kimi K2.5,专用于 AI详情生成 的规划与排版复审", + supportsVision: true, + supportsImageGen: false, + isEnabled: moonshotDetailAgentConfig.isEnabled, + isHidden: false, + isDefault: false, + isAgentModel: false, + isMultimodal: true, + } + : { + providerId, + modelId: trimmedModelId, + displayName: trimmedDisplayName, + description: "Moonshot 官方 Kimi K2.5,专用于 AI详情生成 的规划与排版复审", + supportsVision: true, + supportsImageGen: false, + isEnabled: moonshotDetailAgentConfig.isEnabled, + isHidden: false, + isDefault: false, + isAgentModel: false, + isMultimodal: true, + }; + + const modelResponse = await fetch("/api/admin/ai-models", { + method: existingModel ? "PUT" : "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(modelPayload), + }); + const modelResult = await modelResponse.json().catch(() => ({})); + const savedModel = modelResult?.model; + if (!modelResponse.ok || !savedModel?.id) { + throw new Error(String(modelResult?.error || "保存 Kimi K2.5 模型失败")); + } + + const settingsResponse = await fetch("/api/site-settings", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + canvasDetailAgentModelId: savedModel.id, + }), + }); + if (!settingsResponse.ok) { + throw new Error("写入 AI详情 Agent 专用模型设置失败"); + } + + setSiteSettings((prev) => ({ + ...prev, + canvasDetailAgentModelId: savedModel.id, + })); + setMoonshotDetailAgentConfig((prev) => ({ + ...prev, + providerId, + modelEntryId: String(savedModel.id), + providerName: trimmedProviderName, + baseUrl: trimmedBaseUrl, + modelId: trimmedModelId, + displayName: trimmedDisplayName, + apiKey: "", + })); + + mutateProviders(); + mutateModels(); + mutateSiteSettings(); + toast.success("Moonshot 官方 Kimi K2.5 已绑定为 AI详情 Agent 专用模型"); + } catch (error) { + console.error("[Moonshot Detail Agent Config] Error:", error); + toast.error(error instanceof Error ? error.message : "保存 Moonshot Kimi 配置失败"); + } + }; + // 快速添加火山引擎视频预设模型 const handleQuickAddVolcanoModels = async (providerId: string) => { if (!confirm("将为此服务商添加4个预设模型:\n- Seedance Pro (全功能)\n- Seedance Pro Fast (快速)\n- Seedance Lite I2V (图生视频)\n- Seedance Lite T2V (文生视频)\n\n继续吗?")) { @@ -5526,6 +5738,38 @@ export function AdminSettingsClient() { /> +
+ + +
+ 用于 AI详情生成 的分镜规划与排版复审。未设置时沿用 Agent 对话默认模型;建议选择官方 + Kimi K2.5 多模态模型。 +
+
+
+ setMoonshotDetailAgentConfig((prev) => ({ + ...prev, + providerName: e.target.value, + })) + } + placeholder="Moonshot AI (Official)" + /> +
+ +
+ + + setMoonshotDetailAgentConfig((prev) => ({ + ...prev, + baseUrl: e.target.value, + })) + } + placeholder={MOONSHOT_OFFICIAL_BASE_URL} + /> +
+ 根据 Moonshot 官方文档,OpenAI 兼容接口地址为 `{MOONSHOT_OFFICIAL_BASE_URL}` +
+
+ +
+ + + setMoonshotDetailAgentConfig((prev) => ({ + ...prev, + apiKey: e.target.value, + })) + } + placeholder={ + moonshotDetailAgentConfig.providerId + ? "留空则不修改当前 Key" + : "请输入 Moonshot 官方 API Key" + } + /> +
+ +
+ + + setMoonshotDetailAgentConfig((prev) => ({ + ...prev, + modelId: e.target.value, + })) + } + placeholder={MOONSHOT_KIMI_K25_MODEL_ID} + /> +
+ 已按你提供的 Moonshot 官方文档默认填写 `kimi-k2.5`;保存后系统会自动创建/更新对应模型记录。 +
+
+ +
+ + + setMoonshotDetailAgentConfig((prev) => ({ + ...prev, + displayName: e.target.value, + })) + } + placeholder="官方 Kimi K2.5" + /> +
+ +
+
+ +
+ 关闭后仍会保留记录,但不会被 AI详情生成 选作专用 Agent 模型。 +
+
+
+ + setMoonshotDetailAgentConfig((prev) => ({ + ...prev, + isEnabled: checked, + })) + } + /> + + {moonshotDetailAgentConfig.isEnabled ? "已开启" : "已关闭"} + +
+
+ +
+

• 专用于 AI详情生成 的分镜规划与排版复审,不影响普通 Agent 对话。

+

• 系统会按多模态 Agent 模型写入:支持视觉、支持结构化规划、支持协议复审。

+

• 保存成功后会自动写入“AI详情生成 - Agent专用模型”。

+
+ +
+ +
+ + + Tripo3D API 配置 diff --git a/app/api/canvas/detail-plan/route.ts b/app/api/canvas/detail-plan/route.ts index 2916d64ba..521dcf51d 100644 --- a/app/api/canvas/detail-plan/route.ts +++ b/app/api/canvas/detail-plan/route.ts @@ -4,6 +4,7 @@ import { auth } from "@/app/(auth)/auth"; import { getAgentAiModelWithProvider, getEnabledAiModelsWithProvider, + getSiteSettings, } from "@/lib/db/queries"; import { getAiModelAgentCodingModelId, @@ -1800,19 +1801,32 @@ export async function POST(request: Request) { bundleImageCount: bundleImageUrls.length, }); - const allModels = await getEnabledAiModelsWithProvider(false); - const agentModel = await getAgentAiModelWithProvider(); + const [allModels, agentModel, currentSiteSettings] = await Promise.all([ + getEnabledAiModelsWithProvider(false), + getAgentAiModelWithProvider(), + getSiteSettings(), + ]); const reasoningCompatible = allModels.filter( (item) => !item.model.isSubModel && item.model.isEnabled && ["zhipu", "alibaba", "openai", "custom", "local"].includes(item.provider.type), ); - const preferredAgentConfig = + const configuredDetailAgentConfig = + currentSiteSettings?.canvasDetailAgentModelId && + reasoningCompatible.some( + (item) => item.model.id === currentSiteSettings.canvasDetailAgentModelId, + ) + ? reasoningCompatible.find( + (item) => item.model.id === currentSiteSettings.canvasDetailAgentModelId, + ) + : undefined; + const fallbackAgentConfig = agentModel && reasoningCompatible.some((item) => item.model.id === agentModel.model.id) ? reasoningCompatible.find((item) => item.model.id === agentModel.model.id) : undefined; + const preferredAgentConfig = configuredDetailAgentConfig || fallbackAgentConfig; let plannerConfig = preferredAgentConfig; if (referenceImages.length > 0) { @@ -1835,6 +1849,13 @@ export async function POST(request: Request) { codingModelId: getAiModelAgentCodingModelId(plannerConfig.model), providerType: plannerConfig.provider.type, supportsVision: plannerConfig.model.supportsVision, + detailAgentSettingModelId: currentSiteSettings?.canvasDetailAgentModelId || null, + selectionSource: + configuredDetailAgentConfig?.model.id === plannerConfig.model.id + ? "detail-agent-setting" + : fallbackAgentConfig?.model.id === plannerConfig.model.id + ? "agent-default" + : "vision-fallback", multimodalFlag: isAiModelMultimodal({ capabilities: plannerConfig.model.capabilities, defaultParams: plannerConfig.model.defaultParams, diff --git a/app/api/canvas/detail-review/route.ts b/app/api/canvas/detail-review/route.ts index 17dc5b3b1..a4bd2f502 100644 --- a/app/api/canvas/detail-review/route.ts +++ b/app/api/canvas/detail-review/route.ts @@ -4,6 +4,7 @@ import { auth } from "@/app/(auth)/auth"; import { getAgentAiModelWithProvider, getEnabledAiModelsWithProvider, + getSiteSettings, } from "@/lib/db/queries"; import { getAiModelAgentCodingModelId, @@ -189,20 +190,33 @@ export async function POST(request: Request) { const screenPrompt = String(body.screen?.visualPrompt || "").trim(); const layoutNote = String(body.screen?.layoutNote || "").trim(); const referenceImages = sanitizeImageUrls(body.referenceImages); - const allModels = await getEnabledAiModelsWithProvider(false); - const agentModel = await getAgentAiModelWithProvider(); + const [allModels, agentModel, currentSiteSettings] = await Promise.all([ + getEnabledAiModelsWithProvider(false), + getAgentAiModelWithProvider(), + getSiteSettings(), + ]); const reviewCompatible = allModels.filter( (item) => !item.model.isSubModel && item.model.isEnabled && ["zhipu", "alibaba", "openai", "custom", "local"].includes(item.provider.type), ); + const configuredDetailAgentConfig = + currentSiteSettings?.canvasDetailAgentModelId && + reviewCompatible.some( + (item) => item.model.id === currentSiteSettings.canvasDetailAgentModelId, + ) + ? reviewCompatible.find( + (item) => item.model.id === currentSiteSettings.canvasDetailAgentModelId, + ) + : undefined; - let reviewerConfig = + const fallbackAgentConfig = agentModel && reviewCompatible.some((item) => item.model.id === agentModel.model.id) ? reviewCompatible.find((item) => item.model.id === agentModel.model.id) : undefined; + let reviewerConfig = configuredDetailAgentConfig || fallbackAgentConfig; if (!reviewerConfig || !supportsVisionReview(reviewerConfig.model)) { reviewerConfig = reviewCompatible.find((item) => supportsVisionReview(item.model)); } @@ -263,6 +277,21 @@ export async function POST(request: Request) { reviewerConfig.model.id, codingModelId ); + console.log("[Canvas Detail Review] Reviewer model selected:", { + id: reviewerConfig.model.id, + displayName: reviewerConfig.model.displayName, + modelId: reviewerConfig.model.modelId, + codingModelId, + providerType: reviewerConfig.provider.type, + supportsVision: reviewerConfig.model.supportsVision, + detailAgentSettingModelId: currentSiteSettings?.canvasDetailAgentModelId || null, + selectionSource: + configuredDetailAgentConfig?.model.id === reviewerConfig.model.id + ? "detail-agent-setting" + : fallbackAgentConfig?.model.id === reviewerConfig.model.id + ? "agent-default" + : "vision-fallback", + }); if (!model) { return NextResponse.json({ success: true, diff --git a/app/api/site-settings/route.ts b/app/api/site-settings/route.ts index 937d0fe1e..aa4d47aa6 100644 --- a/app/api/site-settings/route.ts +++ b/app/api/site-settings/route.ts @@ -484,6 +484,7 @@ export async function GET() { canvasDefaultModelId: null, canvasAutoModeDefaultModelId: null, canvasDetailDefaultModelId: null, + canvasDetailAgentModelId: null, lovartPromoText: "现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠", lovartPromoActionText: "立即升级", lovartHeroTitle: "由壹点AI 让设计更简单", @@ -735,6 +736,7 @@ const updateSchema = z.object({ canvasDefaultModelId: z.string().nullable().optional(), canvasAutoModeDefaultModelId: z.string().nullable().optional(), canvasDetailDefaultModelId: z.string().nullable().optional(), + canvasDetailAgentModelId: z.string().nullable().optional(), lovartPromoText: z.string().max(300).optional(), lovartPromoActionText: z.string().max(32).optional(), lovartHeroTitle: z.string().max(300).optional(), @@ -1389,6 +1391,7 @@ export async function PUT(request: NextRequest) { canvasDefaultModelId: siteSettingsPayload.canvasDefaultModelId, canvasAutoModeDefaultModelId: siteSettingsPayload.canvasAutoModeDefaultModelId, canvasDetailDefaultModelId: siteSettingsPayload.canvasDetailDefaultModelId, + canvasDetailAgentModelId: siteSettingsPayload.canvasDetailAgentModelId, lovartPromoText: siteSettingsPayload.lovartPromoText ?? "现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠", diff --git a/lib/db/migrations/0060_add_canvas_detail_agent_model.sql b/lib/db/migrations/0060_add_canvas_detail_agent_model.sql new file mode 100644 index 000000000..e4b314aab --- /dev/null +++ b/lib/db/migrations/0060_add_canvas_detail_agent_model.sql @@ -0,0 +1,2 @@ +ALTER TABLE "SiteSettings" + ADD COLUMN IF NOT EXISTS "canvasDetailAgentModelId" varchar(128); diff --git a/lib/db/migrations/meta/_journal.json b/lib/db/migrations/meta/_journal.json index 058dfc288..b58a8cee5 100644 --- a/lib/db/migrations/meta/_journal.json +++ b/lib/db/migrations/meta/_journal.json @@ -323,6 +323,13 @@ "when": 1774070400000, "tag": "0059_add_canvas_detail_surface_snapshot", "breakpoints": true + }, + { + "idx": 46, + "version": "7", + "when": 1774074000000, + "tag": "0060_add_canvas_detail_agent_model", + "breakpoints": true } ] } diff --git a/lib/db/queries.ts b/lib/db/queries.ts index 712315553..3bbd667b9 100644 --- a/lib/db/queries.ts +++ b/lib/db/queries.ts @@ -4082,6 +4082,7 @@ export async function getSiteSettings() { tutorialEnabled: false, rankingEnabled: true, agentEnabled: true, + canvasDetailAgentModelId: null, lovartPromoText: "现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠", lovartPromoActionText: "立即升级", lovartHeroTitle: "由壹点AI 让设计更简单", diff --git a/lib/db/schema.ts b/lib/db/schema.ts index 067e021a6..27e14489b 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -381,6 +381,7 @@ export const siteSettings = pgTable("SiteSettings", { canvasDefaultModelId: varchar("canvasDefaultModelId", { length: 128 }), // 画布默认选择的模型ID(null表示智能自动) canvasAutoModeDefaultModelId: varchar("canvasAutoModeDefaultModelId", { length: 128 }), // 智能自动模式下实际使用的默认生图模型ID canvasDetailDefaultModelId: varchar("canvasDetailDefaultModelId", { length: 128 }), // AI详情生成专用默认生图模型ID + canvasDetailAgentModelId: varchar("canvasDetailAgentModelId", { length: 128 }), // AI详情生成专用Agent模型ID(规划/复审) lovartPromoText: text("lovartPromoText").default( "现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠" ),