fix: enable local moonshot detail agent option
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-03-21 22:27:00 +08:00
parent b702440347
commit b99eb6cf7a
6 changed files with 427 additions and 178 deletions

View File

@ -22,6 +22,13 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import {
DETAIL_AGENT_LOCAL_MOONSHOT_BASE_URL,
DETAIL_AGENT_LOCAL_MOONSHOT_MODEL_ID,
DETAIL_AGENT_LOCAL_MOONSHOT_OPTION_ID,
DETAIL_AGENT_LOCAL_MOONSHOT_SETTING_KEY,
normalizeDetailAgentLocalMoonshotConfig,
} from "@/lib/ai/detail-agent-model-config";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { import {
@ -511,9 +518,6 @@ const PROVIDER_DEFAULT_BASE_URL: Record<string, string> = {
}; };
const LEMON_OPENAI_BASE_URL = "https://new.lemonapi.site/v1"; 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 = { const LEMON_PROVIDER_PRESET = {
name: "LemonAPI Gemini", name: "LemonAPI Gemini",
type: "openai" as const, type: "openai" as const,
@ -545,7 +549,10 @@ function isLemonApiBaseUrl(value?: string | null) {
} }
function isMoonshotOfficialBaseUrl(value?: string | null) { function isMoonshotOfficialBaseUrl(value?: string | null) {
return String(value || "").trim().replace(/\/+$/, "") === MOONSHOT_OFFICIAL_BASE_URL; return (
String(value || "").trim().replace(/\/+$/, "") ===
DETAIL_AGENT_LOCAL_MOONSHOT_BASE_URL
);
} }
export function AdminSettingsClient() { export function AdminSettingsClient() {
@ -605,6 +612,11 @@ export function AdminSettingsClient() {
"/api/auth-settings", "/api/auth-settings",
fetcher, fetcher,
); );
const { data: moonshotDetailAgentSettingData, mutate: mutateMoonshotDetailAgentSetting } =
useSWR<{ setting?: { value?: unknown } }>(
`/api/admin/settings?key=${DETAIL_AGENT_LOCAL_MOONSHOT_SETTING_KEY}`,
fetcher,
);
// 🆕 生图记录数据 // 🆕 生图记录数据
const imageQueryParams = new URLSearchParams({ const imageQueryParams = new URLSearchParams({
@ -870,48 +882,20 @@ export function AdminSettingsClient() {
}, [siteSettingsData]); }, [siteSettingsData]);
useEffect(() => { useEffect(() => {
const configuredModelId = siteSettings.canvasDetailAgentModelId; const normalized = normalizeDetailAgentLocalMoonshotConfig(
const providerList = providersData?.providers || []; moonshotDetailAgentSettingData?.setting?.value,
const modelList = modelsData?.models || []; );
setMoonshotDetailAgentConfig({
if (!configuredModelId) { providerId: "",
const moonshotProvider = providerList.find( modelEntryId: "",
(provider) => provider.type === "openai" && isMoonshotOfficialBaseUrl(provider.baseUrl), providerName: normalized.providerName,
); baseUrl: normalized.baseUrl,
setMoonshotDetailAgentConfig((prev) => ({ apiKey: "",
...prev, modelId: normalized.modelId,
providerId: moonshotProvider?.id || "", displayName: normalized.displayName,
providerName: moonshotProvider?.name || "Moonshot AI (Official)", isEnabled: normalized.enabled,
baseUrl: moonshotProvider?.baseUrl || MOONSHOT_OFFICIAL_BASE_URL, });
modelId: prev.modelId || MOONSHOT_KIMI_K25_MODEL_ID, }, [moonshotDetailAgentSettingData]);
}));
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 featuredCategoryOptions = useMemo(() => {
const values = (siteSettings.surpriseCategories || []) const values = (siteSettings.surpriseCategories || [])
@ -1022,9 +1006,9 @@ export function AdminSettingsClient() {
providerId: "", providerId: "",
modelEntryId: "", modelEntryId: "",
providerName: "Moonshot AI (Official)", providerName: "Moonshot AI (Official)",
baseUrl: MOONSHOT_OFFICIAL_BASE_URL, baseUrl: DETAIL_AGENT_LOCAL_MOONSHOT_BASE_URL,
apiKey: "", apiKey: "",
modelId: MOONSHOT_KIMI_K25_MODEL_ID, modelId: DETAIL_AGENT_LOCAL_MOONSHOT_MODEL_ID,
displayName: "官方 Kimi K2.5", displayName: "官方 Kimi K2.5",
isEnabled: true, isEnabled: true,
}); });
@ -2697,9 +2681,11 @@ export function AdminSettingsClient() {
const handleSaveMoonshotDetailAgentConfig = async () => { const handleSaveMoonshotDetailAgentConfig = async () => {
try { try {
const trimmedBaseUrl = const trimmedBaseUrl =
moonshotDetailAgentConfig.baseUrl.trim() || MOONSHOT_OFFICIAL_BASE_URL; moonshotDetailAgentConfig.baseUrl.trim() ||
DETAIL_AGENT_LOCAL_MOONSHOT_BASE_URL;
const trimmedModelId = const trimmedModelId =
moonshotDetailAgentConfig.modelId.trim() || MOONSHOT_KIMI_K25_MODEL_ID; moonshotDetailAgentConfig.modelId.trim() ||
DETAIL_AGENT_LOCAL_MOONSHOT_MODEL_ID;
const trimmedDisplayName = const trimmedDisplayName =
moonshotDetailAgentConfig.displayName.trim() || "官方 Kimi K2.5"; moonshotDetailAgentConfig.displayName.trim() || "官方 Kimi K2.5";
const trimmedProviderName = const trimmedProviderName =
@ -2720,90 +2706,31 @@ export function AdminSettingsClient() {
return; return;
} }
const providerPayload: Record<string, any> = moonshotDetailAgentConfig.providerId const settingResponse = await fetch("/api/admin/settings", {
? { method: "POST",
id: moonshotDetailAgentConfig.providerId, headers: { "Content-Type": "application/json" },
name: trimmedProviderName, body: JSON.stringify({
type: "openai", key: DETAIL_AGENT_LOCAL_MOONSHOT_SETTING_KEY,
baseUrl: trimmedBaseUrl, value: {
isEnabled: moonshotDetailAgentConfig.isEnabled, enabled: moonshotDetailAgentConfig.isEnabled,
} providerName: trimmedProviderName,
: {
name: trimmedProviderName,
type: "openai",
baseUrl: trimmedBaseUrl, baseUrl: trimmedBaseUrl,
apiKey: moonshotDetailAgentConfig.apiKey.trim(), 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<string, any> = existingModel
? {
id: existingModel.id,
providerId,
modelId: trimmedModelId, modelId: trimmedModelId,
displayName: trimmedDisplayName, 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 settingResult = await settingResponse.json().catch(() => ({}));
const savedModel = modelResult?.model; if (!settingResponse.ok) {
if (!modelResponse.ok || !savedModel?.id) { throw new Error(String(settingResult?.message || "保存 Moonshot 本地AI配置失败"));
throw new Error(String(modelResult?.error || "保存 Kimi K2.5 模型失败"));
} }
const settingsResponse = await fetch("/api/site-settings", { const settingsResponse = await fetch("/api/site-settings", {
method: "PUT", method: "PUT",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({
canvasDetailAgentModelId: savedModel.id, canvasDetailAgentModelId: DETAIL_AGENT_LOCAL_MOONSHOT_OPTION_ID,
}), }),
}); });
if (!settingsResponse.ok) { if (!settingsResponse.ok) {
@ -2812,12 +2739,10 @@ export function AdminSettingsClient() {
setSiteSettings((prev) => ({ setSiteSettings((prev) => ({
...prev, ...prev,
canvasDetailAgentModelId: savedModel.id, canvasDetailAgentModelId: DETAIL_AGENT_LOCAL_MOONSHOT_OPTION_ID,
})); }));
setMoonshotDetailAgentConfig((prev) => ({ setMoonshotDetailAgentConfig((prev) => ({
...prev, ...prev,
providerId,
modelEntryId: String(savedModel.id),
providerName: trimmedProviderName, providerName: trimmedProviderName,
baseUrl: trimmedBaseUrl, baseUrl: trimmedBaseUrl,
modelId: trimmedModelId, modelId: trimmedModelId,
@ -2825,8 +2750,7 @@ export function AdminSettingsClient() {
apiKey: "", apiKey: "",
})); }));
mutateProviders(); mutateMoonshotDetailAgentSetting();
mutateModels();
mutateSiteSettings(); mutateSiteSettings();
toast.success("Moonshot 官方 Kimi K2.5 已绑定为 AI详情 Agent 专用模型"); toast.success("Moonshot 官方 Kimi K2.5 已绑定为 AI详情 Agent 专用模型");
} catch (error) { } catch (error) {
@ -5756,6 +5680,11 @@ export function AdminSettingsClient() {
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="agent-default">沿用Agent对话默认模型</SelectItem> <SelectItem value="agent-default">沿用Agent对话默认模型</SelectItem>
{moonshotDetailAgentConfig.isEnabled ? (
<SelectItem value={DETAIL_AGENT_LOCAL_MOONSHOT_OPTION_ID}>
{moonshotDetailAgentConfig.displayName} · 本地AI配置
</SelectItem>
) : null}
{detailAgentModelOptions.map((model) => ( {detailAgentModelOptions.map((model) => (
<SelectItem key={model.id} value={model.id}> <SelectItem key={model.id} value={model.id}>
{model.displayName} {model.displayName}
@ -8310,10 +8239,10 @@ export function AdminSettingsClient() {
baseUrl: e.target.value, baseUrl: e.target.value,
})) }))
} }
placeholder={MOONSHOT_OFFICIAL_BASE_URL} placeholder={DETAIL_AGENT_LOCAL_MOONSHOT_BASE_URL}
/> />
<div className="text-xs text-muted-foreground"> <div className="text-xs text-muted-foreground">
根据 Moonshot 官方文档,OpenAI 兼容接口地址为 `{MOONSHOT_OFFICIAL_BASE_URL}` 根据 Moonshot 官方文档,OpenAI 兼容接口地址为 `{DETAIL_AGENT_LOCAL_MOONSHOT_BASE_URL}`
</div> </div>
</div> </div>
@ -8350,7 +8279,7 @@ export function AdminSettingsClient() {
modelId: e.target.value, modelId: e.target.value,
})) }))
} }
placeholder={MOONSHOT_KIMI_K25_MODEL_ID} placeholder={DETAIL_AGENT_LOCAL_MOONSHOT_MODEL_ID}
/> />
<div className="text-xs text-muted-foreground"> <div className="text-xs text-muted-foreground">
已按你提供的 Moonshot 官方文档默认填写 `kimi-k2.5`;保存后系统会自动创建/更新对应模型记录。 已按你提供的 Moonshot 官方文档默认填写 `kimi-k2.5`;保存后系统会自动创建/更新对应模型记录。

View File

@ -1,9 +1,16 @@
import { z } from "zod"; import { z } from "zod";
import { requireAdmin } from "@/lib/middleware/admin"; import { requireAdmin } from "@/lib/middleware/admin";
import { import {
createAiModel,
createAiProvider,
getAiModelById,
getAiProviderById,
getAllAiModels,
getAllSystemSettings, getAllSystemSettings,
getSystemSetting, getSystemSetting,
upsertSystemSetting, upsertSystemSetting,
updateAiModel,
updateAiProvider,
} from "@/lib/db/queries"; } from "@/lib/db/queries";
import { ChatSDKError } from "@/lib/errors"; import { ChatSDKError } from "@/lib/errors";
import { probeBflModelSupport } from "@/lib/realtime/bfl-capability"; import { probeBflModelSupport } from "@/lib/realtime/bfl-capability";
@ -12,12 +19,33 @@ import {
normalizeRealtimeCanvasConfigForStorage, normalizeRealtimeCanvasConfigForStorage,
resolveRealtimeProviders, resolveRealtimeProviders,
} from "@/lib/realtime/config"; } from "@/lib/realtime/config";
import { db } from "@/lib/db";
import { siteSettings } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
const settingSchema = z.object({ const settingSchema = z.object({
key: z.string(), key: z.string(),
value: z.any(), value: z.any(),
}); });
const moonshotDetailAgentConfigSchema = z.object({
providerId: z.string().uuid().optional(),
modelEntryId: z.string().uuid().optional(),
providerName: z.string().trim().min(1).max(128).optional(),
baseUrl: z.string().trim().min(1),
apiKey: z.string().optional(),
modelId: z.string().trim().min(1).max(128).optional(),
displayName: z.string().trim().min(1).max(128).optional(),
isEnabled: z.boolean().optional(),
});
const MOONSHOT_OFFICIAL_BASE_URL = "https://api.moonshot.cn/v1";
const MOONSHOT_KIMI_K25_MODEL_ID = "kimi-k2.5";
function isMoonshotOfficialBaseUrl(value: string) {
return String(value || "").trim().replace(/\/+$/, "") === MOONSHOT_OFFICIAL_BASE_URL;
}
function isObjectLike(value: unknown): value is Record<string, unknown> { function isObjectLike(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value); return Boolean(value) && typeof value === "object" && !Array.isArray(value);
} }
@ -58,6 +86,132 @@ export async function POST(request: Request) {
let nextValue = value; let nextValue = value;
if (key === "moonshot_detail_agent_config") {
const config = moonshotDetailAgentConfigSchema.parse(value || {});
const normalizedBaseUrl = String(config.baseUrl || "").trim() || MOONSHOT_OFFICIAL_BASE_URL;
const normalizedModelId = String(config.modelId || "").trim() || MOONSHOT_KIMI_K25_MODEL_ID;
const normalizedDisplayName = String(config.displayName || "").trim() || "官方 Kimi K2.5";
const normalizedProviderName =
String(config.providerName || "").trim() || "Moonshot AI (Official)";
const normalizedApiKey = String(config.apiKey || "").trim();
const enabled = config.isEnabled !== false;
if (!isMoonshotOfficialBaseUrl(normalizedBaseUrl)) {
return new ChatSDKError(
"bad_request:api",
`Moonshot 官方地址必须为 ${MOONSHOT_OFFICIAL_BASE_URL}`,
).toResponse();
}
if (!config.providerId && !normalizedApiKey) {
return new ChatSDKError("bad_request:api", "缺少 Moonshot 官方 API Key").toResponse();
}
const [existingSiteSettings] = await db.select().from(siteSettings).limit(1);
const providerRows = config.providerId
? await updateAiProvider({
id: config.providerId,
name: normalizedProviderName,
type: "openai",
baseUrl: normalizedBaseUrl,
apiKey: normalizedApiKey || undefined,
isEnabled: enabled,
updatedBy: session.user.id,
})
: await createAiProvider({
name: normalizedProviderName,
type: "openai",
baseUrl: normalizedBaseUrl,
apiKey: normalizedApiKey,
isEnabled: enabled,
updatedBy: session.user.id,
});
const provider = providerRows[0];
if (!provider) {
return new ChatSDKError("bad_request:database", "保存 Moonshot 提供商失败").toResponse();
}
let existingModel =
config.modelEntryId ? await getAiModelById({ id: config.modelEntryId }) : undefined;
if (!existingModel) {
const allModels = await getAllAiModels();
existingModel =
allModels.find(
(model) => model.providerId === provider.id && model.modelId === normalizedModelId,
) || undefined;
}
const modelRows = existingModel
? await updateAiModel({
id: existingModel.id,
providerId: provider.id,
modelId: normalizedModelId,
displayName: normalizedDisplayName,
description: "Moonshot 官方 Kimi K2.5,专用于 AI详情生成 的规划与排版复审",
supportsVision: true,
supportsImageGen: false,
isEnabled: enabled,
isHidden: false,
isDefault: false,
isAgentModel: false,
isMultimodal: true,
})
: await createAiModel({
providerId: provider.id,
modelId: normalizedModelId,
displayName: normalizedDisplayName,
description: "Moonshot 官方 Kimi K2.5,专用于 AI详情生成 的规划与排版复审",
supportsVision: true,
supportsImageGen: false,
isEnabled: enabled,
isHidden: false,
isDefault: false,
isAgentModel: false,
isMultimodal: true,
});
const model = modelRows[0];
if (!model) {
return new ChatSDKError("bad_request:database", "保存 Kimi K2.5 模型失败").toResponse();
}
if (existingSiteSettings) {
await db
.update(siteSettings)
.set({
canvasDetailAgentModelId: model.id,
updatedAt: new Date(),
updatedBy: session.user.id,
})
.where(eq(siteSettings.id, existingSiteSettings.id));
} else {
await db.insert(siteSettings).values({
siteName: "AI Chatbot",
canvasDetailAgentModelId: model.id,
updatedBy: session.user.id,
});
}
return Response.json({
setting: {
key: "moonshot_detail_agent_config",
value: {
providerId: provider.id,
modelEntryId: model.id,
providerName: provider.name,
baseUrl: provider.baseUrl,
modelId: model.modelId,
displayName: model.displayName,
isEnabled: model.isEnabled,
canvasDetailAgentModelId: model.id,
},
},
});
}
if (key === "realtime_canvas_config") { if (key === "realtime_canvas_config") {
if (!isObjectLike(value)) { if (!isObjectLike(value)) {
return new ChatSDKError("bad_request:api", "realtime_canvas_config value 必须为对象").toResponse(); return new ChatSDKError("bad_request:api", "realtime_canvas_config value 必须为对象").toResponse();

View File

@ -11,6 +11,7 @@ import {
isAiModelMultimodal, isAiModelMultimodal,
} from "@/lib/db/ai-model-flags"; } from "@/lib/db/ai-model-flags";
import { getLanguageModelById } from "@/lib/ai/dynamic-provider"; import { getLanguageModelById } from "@/lib/ai/dynamic-provider";
import { resolveDetailAgentLocalMoonshotModel } from "@/lib/ai/detail-agent-model-runtime";
import { inlineVisionImages } from "@/lib/ai/vision-inline"; import { inlineVisionImages } from "@/lib/ai/vision-inline";
import { filterReachableRemoteUrls } from "@/lib/net/url-access"; import { filterReachableRemoteUrls } from "@/lib/net/url-access";
import { import {
@ -1806,6 +1807,9 @@ export async function POST(request: Request) {
getAgentAiModelWithProvider(), getAgentAiModelWithProvider(),
getSiteSettings(), getSiteSettings(),
]); ]);
const localDetailAgentResolved = await resolveDetailAgentLocalMoonshotModel({
siteSettingModelId: currentSiteSettings?.canvasDetailAgentModelId,
});
const reasoningCompatible = allModels.filter( const reasoningCompatible = allModels.filter(
(item) => (item) =>
!item.model.isSubModel && !item.model.isSubModel &&
@ -1829,45 +1833,65 @@ export async function POST(request: Request) {
const preferredAgentConfig = configuredDetailAgentConfig || fallbackAgentConfig; const preferredAgentConfig = configuredDetailAgentConfig || fallbackAgentConfig;
let plannerConfig = preferredAgentConfig; let plannerConfig = preferredAgentConfig;
if (referenceImages.length > 0) { if (!localDetailAgentResolved && referenceImages.length > 0) {
if (!plannerConfig || !supportsVisionPlanning(plannerConfig.model)) { if (!plannerConfig || !supportsVisionPlanning(plannerConfig.model)) {
plannerConfig = reasoningCompatible.find((item) => supportsVisionPlanning(item.model)); plannerConfig = reasoningCompatible.find((item) => supportsVisionPlanning(item.model));
} }
} }
if (!plannerConfig) { if (!localDetailAgentResolved && !plannerConfig) {
plannerConfig = reasoningCompatible[0]; plannerConfig = reasoningCompatible[0];
} }
if (!plannerConfig) { if (!localDetailAgentResolved && !plannerConfig) {
return NextResponse.json({ success: true, data: fallback }); return NextResponse.json({ success: true, data: fallback });
} }
const selectedModelRef = localDetailAgentResolved
? localDetailAgentResolved.modelRef
: plannerConfig!.model;
const selectedProviderRef = localDetailAgentResolved
? localDetailAgentResolved.providerRef
: {
id: plannerConfig!.provider.id,
name: plannerConfig!.provider.name,
type: plannerConfig!.provider.type,
baseUrl: plannerConfig!.provider.baseUrl,
};
console.log("[Canvas Detail Plan] Planner model selected:", { console.log("[Canvas Detail Plan] Planner model selected:", {
id: plannerConfig.model.id, id: selectedModelRef.id,
displayName: plannerConfig.model.displayName, displayName: selectedModelRef.displayName,
modelId: plannerConfig.model.modelId, modelId: selectedModelRef.modelId,
codingModelId: getAiModelAgentCodingModelId(plannerConfig.model), codingModelId: localDetailAgentResolved
providerType: plannerConfig.provider.type, ? null
supportsVision: plannerConfig.model.supportsVision, : getAiModelAgentCodingModelId(plannerConfig!.model),
providerType: selectedProviderRef.type,
supportsVision: selectedModelRef.supportsVision,
detailAgentSettingModelId: currentSiteSettings?.canvasDetailAgentModelId || null, detailAgentSettingModelId: currentSiteSettings?.canvasDetailAgentModelId || null,
selectionSource: selectionSource:
configuredDetailAgentConfig?.model.id === plannerConfig.model.id localDetailAgentResolved
? "detail-agent-local-config"
: configuredDetailAgentConfig?.model.id === plannerConfig?.model.id
? "detail-agent-setting" ? "detail-agent-setting"
: fallbackAgentConfig?.model.id === plannerConfig.model.id : fallbackAgentConfig?.model.id === plannerConfig?.model.id
? "agent-default" ? "agent-default"
: "vision-fallback", : "vision-fallback",
multimodalFlag: isAiModelMultimodal({ multimodalFlag: isAiModelMultimodal({
capabilities: plannerConfig.model.capabilities, capabilities: selectedModelRef.capabilities,
defaultParams: plannerConfig.model.defaultParams, defaultParams: selectedModelRef.defaultParams,
}), }),
hasReferenceImages: referenceImages.length > 0, hasReferenceImages: referenceImages.length > 0,
}); });
const codingModelId = getAiModelAgentCodingModelId(plannerConfig.model); const codingModelId = localDetailAgentResolved
const model = await getLanguageModelById( ? null
plannerConfig.model.id, : getAiModelAgentCodingModelId(plannerConfig!.model);
codingModelId const model = localDetailAgentResolved
); ? localDetailAgentResolved.model
: await getLanguageModelById(
plannerConfig!.model.id,
codingModelId
);
if (!model) { if (!model) {
return NextResponse.json({ success: true, data: fallback }); return NextResponse.json({ success: true, data: fallback });
} }
@ -1898,9 +1922,12 @@ export async function POST(request: Request) {
.join("\n"); .join("\n");
let imageInputs: Array<{ type: "image"; image: string }> = []; let imageInputs: Array<{ type: "image"; image: string }> = [];
if (supportsVisionPlanning(plannerConfig.model) && referenceImages.length > 0) { if (
(localDetailAgentResolved || supportsVisionPlanning(plannerConfig!.model)) &&
referenceImages.length > 0
) {
try { try {
if (isVolcanoResponsesPlanner(plannerConfig)) { if (!localDetailAgentResolved && isVolcanoResponsesPlanner(plannerConfig!)) {
imageInputs = referenceImages imageInputs = referenceImages
.slice(0, 6) .slice(0, 6)
.map((url) => ({ type: "image" as const, image: url })); .map((url) => ({ type: "image" as const, image: url }));
@ -2042,9 +2069,9 @@ export async function POST(request: Request) {
issues: [fallbackReason], issues: [fallbackReason],
}, },
model: { model: {
id: plannerConfig.model.id, id: selectedModelRef.id,
displayName: plannerConfig.model.displayName, displayName: selectedModelRef.displayName,
providerType: plannerConfig.provider.type, providerType: selectedProviderRef.type,
}, },
preflight: { preflight: {
passed: false, passed: false,
@ -2224,9 +2251,9 @@ export async function POST(request: Request) {
}) })
: undefined, : undefined,
model: { model: {
id: plannerConfig.model.id, id: selectedModelRef.id,
displayName: plannerConfig.model.displayName, displayName: selectedModelRef.displayName,
providerType: plannerConfig.provider.type, providerType: selectedProviderRef.type,
}, },
}; };

View File

@ -11,6 +11,7 @@ import {
isAiModelMultimodal, isAiModelMultimodal,
} from "@/lib/db/ai-model-flags"; } from "@/lib/db/ai-model-flags";
import { getLanguageModelById } from "@/lib/ai/dynamic-provider"; import { getLanguageModelById } from "@/lib/ai/dynamic-provider";
import { resolveDetailAgentLocalMoonshotModel } from "@/lib/ai/detail-agent-model-runtime";
import { inlineVisionImages } from "@/lib/ai/vision-inline"; import { inlineVisionImages } from "@/lib/ai/vision-inline";
import { import {
buildDetailUiProtocol, buildDetailUiProtocol,
@ -195,6 +196,9 @@ export async function POST(request: Request) {
getAgentAiModelWithProvider(), getAgentAiModelWithProvider(),
getSiteSettings(), getSiteSettings(),
]); ]);
const localDetailAgentResolved = await resolveDetailAgentLocalMoonshotModel({
siteSettingModelId: currentSiteSettings?.canvasDetailAgentModelId,
});
const reviewCompatible = allModels.filter( const reviewCompatible = allModels.filter(
(item) => (item) =>
!item.model.isSubModel && !item.model.isSubModel &&
@ -217,7 +221,7 @@ export async function POST(request: Request) {
? reviewCompatible.find((item) => item.model.id === agentModel.model.id) ? reviewCompatible.find((item) => item.model.id === agentModel.model.id)
: undefined; : undefined;
let reviewerConfig = configuredDetailAgentConfig || fallbackAgentConfig; let reviewerConfig = configuredDetailAgentConfig || fallbackAgentConfig;
if (!reviewerConfig || !supportsVisionReview(reviewerConfig.model)) { if (!localDetailAgentResolved && (!reviewerConfig || !supportsVisionReview(reviewerConfig.model))) {
reviewerConfig = reviewCompatible.find((item) => supportsVisionReview(item.model)); reviewerConfig = reviewCompatible.find((item) => supportsVisionReview(item.model));
} }
@ -253,7 +257,7 @@ export async function POST(request: Request) {
}); });
const currentUiValidation = validateDetailUiProtocol(currentUiProtocol); const currentUiValidation = validateDetailUiProtocol(currentUiProtocol);
if (!reviewerConfig) { if (!localDetailAgentResolved && !reviewerConfig) {
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
data: { data: {
@ -272,23 +276,40 @@ export async function POST(request: Request) {
}); });
} }
const codingModelId = getAiModelAgentCodingModelId(reviewerConfig.model); const selectedModelRef = localDetailAgentResolved
const model = await getLanguageModelById( ? localDetailAgentResolved.modelRef
reviewerConfig.model.id, : reviewerConfig!.model;
codingModelId const selectedProviderRef = localDetailAgentResolved
); ? localDetailAgentResolved.providerRef
: {
id: reviewerConfig!.provider.id,
name: reviewerConfig!.provider.name,
type: reviewerConfig!.provider.type,
baseUrl: reviewerConfig!.provider.baseUrl,
};
const codingModelId = localDetailAgentResolved
? null
: getAiModelAgentCodingModelId(reviewerConfig!.model);
const model = localDetailAgentResolved
? localDetailAgentResolved.model
: await getLanguageModelById(
reviewerConfig!.model.id,
codingModelId
);
console.log("[Canvas Detail Review] Reviewer model selected:", { console.log("[Canvas Detail Review] Reviewer model selected:", {
id: reviewerConfig.model.id, id: selectedModelRef.id,
displayName: reviewerConfig.model.displayName, displayName: selectedModelRef.displayName,
modelId: reviewerConfig.model.modelId, modelId: selectedModelRef.modelId,
codingModelId, codingModelId,
providerType: reviewerConfig.provider.type, providerType: selectedProviderRef.type,
supportsVision: reviewerConfig.model.supportsVision, supportsVision: selectedModelRef.supportsVision,
detailAgentSettingModelId: currentSiteSettings?.canvasDetailAgentModelId || null, detailAgentSettingModelId: currentSiteSettings?.canvasDetailAgentModelId || null,
selectionSource: selectionSource:
configuredDetailAgentConfig?.model.id === reviewerConfig.model.id localDetailAgentResolved
? "detail-agent-local-config"
: configuredDetailAgentConfig?.model.id === reviewerConfig?.model.id
? "detail-agent-setting" ? "detail-agent-setting"
: fallbackAgentConfig?.model.id === reviewerConfig.model.id : fallbackAgentConfig?.model.id === reviewerConfig?.model.id
? "agent-default" ? "agent-default"
: "vision-fallback", : "vision-fallback",
}); });
@ -355,7 +376,11 @@ export async function POST(request: Request) {
const reviewImageCandidates = [imageUrl, ...referenceImages].slice(0, 6); const reviewImageCandidates = [imageUrl, ...referenceImages].slice(0, 6);
let effectiveReviewImages = reviewImageCandidates; let effectiveReviewImages = reviewImageCandidates;
if (!isVolcanoResponsesReviewer(reviewerConfig)) { if (
!localDetailAgentResolved &&
reviewerConfig &&
!isVolcanoResponsesReviewer(reviewerConfig)
) {
const { inlineDataUrls } = await inlineVisionImages(reviewImageCandidates, { const { inlineDataUrls } = await inlineVisionImages(reviewImageCandidates, {
maxImages: 6, maxImages: 6,
maxSide: 900, maxSide: 900,
@ -409,9 +434,9 @@ export async function POST(request: Request) {
uiPatch: undefined, uiPatch: undefined,
uiSurfacePatch: undefined, uiSurfacePatch: undefined,
model: { model: {
id: reviewerConfig.model.id, id: selectedModelRef.id,
displayName: reviewerConfig.model.displayName, displayName: selectedModelRef.displayName,
providerType: reviewerConfig.provider.type, providerType: selectedProviderRef.type,
}, },
}, },
}); });
@ -565,9 +590,9 @@ export async function POST(request: Request) {
issues: uiValidation.issues.map((item) => item.message), issues: uiValidation.issues.map((item) => item.message),
}, },
model: { model: {
id: reviewerConfig.model.id, id: selectedModelRef.id,
displayName: reviewerConfig.model.displayName, displayName: selectedModelRef.displayName,
providerType: reviewerConfig.provider.type, providerType: selectedProviderRef.type,
}, },
}, },
}); });

View File

@ -0,0 +1,41 @@
export const DETAIL_AGENT_LOCAL_MOONSHOT_SETTING_KEY =
"canvas_detail_agent_moonshot_config";
export const DETAIL_AGENT_LOCAL_MOONSHOT_OPTION_ID =
"__moonshot_kimi_k25_local__";
export const DETAIL_AGENT_LOCAL_MOONSHOT_BASE_URL =
"https://api.moonshot.cn/v1";
export const DETAIL_AGENT_LOCAL_MOONSHOT_MODEL_ID = "kimi-k2.5";
export type DetailAgentLocalMoonshotConfig = {
enabled: boolean;
providerName: string;
baseUrl: string;
apiKey: string;
modelId: string;
displayName: string;
};
export function isDetailAgentLocalMoonshotOption(value: unknown) {
return String(value || "").trim() === DETAIL_AGENT_LOCAL_MOONSHOT_OPTION_ID;
}
export function normalizeDetailAgentLocalMoonshotConfig(
value: unknown,
): DetailAgentLocalMoonshotConfig {
const raw =
value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
return {
enabled: raw.enabled !== false,
providerName:
String(raw.providerName || "").trim() || "Moonshot AI (Official)",
baseUrl:
String(raw.baseUrl || "").trim() || DETAIL_AGENT_LOCAL_MOONSHOT_BASE_URL,
apiKey: String(raw.apiKey || "").trim(),
modelId:
String(raw.modelId || "").trim() || DETAIL_AGENT_LOCAL_MOONSHOT_MODEL_ID,
displayName: String(raw.displayName || "").trim() || "官方 Kimi K2.5",
};
}

View File

@ -0,0 +1,73 @@
import "server-only";
import { createOpenAI } from "@ai-sdk/openai";
import type { LanguageModel } from "ai";
import { getSystemSetting } from "@/lib/db/queries";
import {
DETAIL_AGENT_LOCAL_MOONSHOT_BASE_URL,
DETAIL_AGENT_LOCAL_MOONSHOT_OPTION_ID,
DETAIL_AGENT_LOCAL_MOONSHOT_SETTING_KEY,
isDetailAgentLocalMoonshotOption,
normalizeDetailAgentLocalMoonshotConfig,
} from "@/lib/ai/detail-agent-model-config";
export async function resolveDetailAgentLocalMoonshotModel(params: {
siteSettingModelId?: string | null;
}) {
if (!isDetailAgentLocalMoonshotOption(params.siteSettingModelId)) {
return null;
}
const setting = await getSystemSetting({
key: DETAIL_AGENT_LOCAL_MOONSHOT_SETTING_KEY,
}).catch(() => null);
const config = normalizeDetailAgentLocalMoonshotConfig(setting?.value);
if (!config.enabled || !config.apiKey.trim()) {
return null;
}
const provider = createOpenAI({
apiKey: config.apiKey,
baseURL: config.baseUrl || DETAIL_AGENT_LOCAL_MOONSHOT_BASE_URL,
});
const model = provider.chat(config.modelId);
return {
kind: "local_moonshot" as const,
model,
modelRef: {
id: DETAIL_AGENT_LOCAL_MOONSHOT_OPTION_ID,
displayName: config.displayName,
modelId: config.modelId,
supportsVision: true,
capabilities: { multimodalModel: true },
defaultParams: null,
},
providerRef: {
id: DETAIL_AGENT_LOCAL_MOONSHOT_OPTION_ID,
name: config.providerName,
type: "openai",
baseUrl: config.baseUrl,
},
};
}
export type DetailAgentResolvedModel = {
kind: "local_moonshot" | "db_model";
model: LanguageModel;
modelRef: {
id: string;
displayName: string;
modelId: string;
supportsVision?: boolean | null;
capabilities?: unknown;
defaultParams?: unknown;
};
providerRef: {
id: string;
name: string;
type: string;
baseUrl?: string | null;
};
};