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

View File

@ -1,9 +1,16 @@
import { z } from "zod";
import { requireAdmin } from "@/lib/middleware/admin";
import {
createAiModel,
createAiProvider,
getAiModelById,
getAiProviderById,
getAllAiModels,
getAllSystemSettings,
getSystemSetting,
upsertSystemSetting,
updateAiModel,
updateAiProvider,
} from "@/lib/db/queries";
import { ChatSDKError } from "@/lib/errors";
import { probeBflModelSupport } from "@/lib/realtime/bfl-capability";
@ -12,12 +19,33 @@ import {
normalizeRealtimeCanvasConfigForStorage,
resolveRealtimeProviders,
} from "@/lib/realtime/config";
import { db } from "@/lib/db";
import { siteSettings } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
const settingSchema = z.object({
key: z.string(),
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> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
@ -58,6 +86,132 @@ export async function POST(request: Request) {
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 (!isObjectLike(value)) {
return new ChatSDKError("bad_request:api", "realtime_canvas_config value 必须为对象").toResponse();

View File

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

View File

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