feat: improve video mode toolbar and agent video analysis
This commit is contained in:
parent
85a2fe468f
commit
6bad214c3d
@ -1160,8 +1160,28 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
|
||||
// 所有可选模型(Agent模式 + 可视化模型)
|
||||
const allModels = [SMART_AUTO_MODEL, ...visualModels];
|
||||
const videoOnlyModels = useMemo(
|
||||
() => visualModels.filter((model) => VIDEO_PROVIDER_TYPES.has(model.providerType)),
|
||||
[visualModels]
|
||||
);
|
||||
const currentModel = allModels.find((model) => model.id === selectedModelId);
|
||||
const isVideoModel = Boolean(currentModel && VIDEO_PROVIDER_TYPES.has(currentModel.providerType));
|
||||
const selectableModels = useMemo(() => {
|
||||
if (isDetailGenerationMode) {
|
||||
return [SMART_AUTO_MODEL];
|
||||
}
|
||||
if (isVideoModel) {
|
||||
return videoOnlyModels;
|
||||
}
|
||||
return allModels;
|
||||
}, [SMART_AUTO_MODEL, allModels, isDetailGenerationMode, isVideoModel, videoOnlyModels]);
|
||||
const modelSelectValue = selectableModels.some((model) => model.id === selectedModelId)
|
||||
? selectedModelId
|
||||
: selectableModels[0]?.id || selectedModelId;
|
||||
const selectDisplayModel =
|
||||
selectableModels.find((model) => model.id === modelSelectValue) ||
|
||||
currentModel ||
|
||||
SMART_AUTO_MODEL;
|
||||
const videoFrameSlots = useMemo(() => {
|
||||
if (!isVideoModel || !videoParams) return [] as string[];
|
||||
if (videoParams.mode === "first-last-frame") return ["首帧", "尾帧"] as string[];
|
||||
@ -1210,6 +1230,11 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
}
|
||||
}, [aiDetailGenerationEnabled, isDetailGenerationMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!modelSelectValue || modelSelectValue === selectedModelId) return;
|
||||
setSelectedModelId(modelSelectValue);
|
||||
}, [modelSelectValue, selectedModelId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDetailGenerationMode) {
|
||||
setAttachments([]);
|
||||
@ -2845,11 +2870,12 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
);
|
||||
const shouldShowImageLoading = hasGenerationStart && !hasGenerationEnd && !!currentModel;
|
||||
const loadingImageCount = imageParams.imageCount || 1;
|
||||
const isDetailEmptyState = isDetailGenerationMode && visibleMessages.length === 0;
|
||||
|
||||
const renderModelSelectContent = () => (
|
||||
<SelectContent className="w-[238px] max-h-[340px] overflow-hidden rounded-[18px] border border-[#cfd4dc] bg-[#f3f4f6] p-1.5 shadow-[0_8px_20px_rgba(15,23,42,0.13)]">
|
||||
<div className="max-h-[312px] space-y-0.5 overflow-y-auto pr-1">
|
||||
{allModels.map((model) => (
|
||||
{selectableModels.map((model) => (
|
||||
<SelectItem
|
||||
key={model.id}
|
||||
value={model.id}
|
||||
@ -2868,12 +2894,15 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
</span>
|
||||
<span className="truncate text-[12px] font-medium leading-none">{model.displayName}</span>
|
||||
</span>
|
||||
{selectedModelId === model.id ? (
|
||||
{modelSelectValue === model.id ? (
|
||||
<span className="text-sm font-semibold text-[#1f2937]">✓</span>
|
||||
) : null}
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
{selectableModels.length === 0 ? (
|
||||
<div className="px-2 py-2 text-xs text-[#6b7280]">暂无可用模型</div>
|
||||
) : null}
|
||||
</div>
|
||||
</SelectContent>
|
||||
);
|
||||
@ -3000,6 +3029,9 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
</div>
|
||||
|
||||
{/* 消息列表 */}
|
||||
{isDetailEmptyState ? (
|
||||
<div className="h-16 shrink-0" />
|
||||
) : (
|
||||
<ScrollArea
|
||||
className={cn(
|
||||
"flex-1 p-4",
|
||||
@ -3273,6 +3305,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
|
||||
{fullscreenPreview && createPortal(
|
||||
<div
|
||||
@ -3766,7 +3799,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
|
||||
{/* Bottom Toolbar */}
|
||||
<div className="absolute left-2 bottom-2 right-2 flex items-center gap-2">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-1.5 overflow-x-visible pr-0.5">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-1.5 pr-0.5">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
@ -3886,16 +3919,20 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<div className="shrink-0">
|
||||
{isVideoModel ? (
|
||||
<VideoParamsBar
|
||||
modelName={currentModel?.displayName}
|
||||
initialParams={videoParams || undefined}
|
||||
onParamsChange={handleVideoParamsChange}
|
||||
inline={true}
|
||||
providerType={currentModel?.providerType}
|
||||
/>
|
||||
) : (
|
||||
{isVideoModel ? (
|
||||
<div className="min-w-0 flex-1 overflow-x-auto scrollbar-hide">
|
||||
<div className="w-max pr-1">
|
||||
<VideoParamsBar
|
||||
modelName={currentModel?.displayName}
|
||||
initialParams={videoParams || undefined}
|
||||
onParamsChange={handleVideoParamsChange}
|
||||
inline={true}
|
||||
providerType={currentModel?.providerType}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="shrink-0">
|
||||
<ImageParamsBar
|
||||
modelName={currentModel?.displayName}
|
||||
initialParams={imageParams}
|
||||
@ -3903,13 +3940,13 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
inline={true}
|
||||
providerType={currentModel?.providerType}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<Select
|
||||
value={selectedModelId}
|
||||
value={modelSelectValue}
|
||||
onValueChange={(value) => {
|
||||
setSelectedModelId(value);
|
||||
if (value !== "auto" || !aiDetailGenerationEnabled) {
|
||||
@ -3919,15 +3956,15 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
>
|
||||
<SelectTrigger className="h-8 w-8 rounded-full border border-[#cfd4dc] bg-white p-0 justify-center [&>svg]:hidden">
|
||||
<span className="h-6 w-6 overflow-hidden rounded-full bg-white flex items-center justify-center">
|
||||
{currentModel?.avatarUrl ? (
|
||||
{selectDisplayModel?.avatarUrl ? (
|
||||
<img
|
||||
src={currentModel.avatarUrl}
|
||||
alt={currentModel.displayName || "模型头像"}
|
||||
src={selectDisplayModel.avatarUrl}
|
||||
alt={selectDisplayModel.displayName || "模型头像"}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<span className="flex h-full w-full items-center justify-center text-[10px] font-medium text-[#4b5563]">
|
||||
{(currentModel?.displayName || "A").slice(0, 1)}
|
||||
{(selectDisplayModel?.displayName || "A").slice(0, 1)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import { generateText } from "ai";
|
||||
import sharp from "sharp";
|
||||
// 使用动态导入避免循环依赖问题
|
||||
// import { getLanguageModelById } from "./dynamic-provider";
|
||||
// import { getEnabledAiModelsWithProvider } from "@/lib/db/queries";
|
||||
@ -33,6 +34,12 @@ function inferDefaultImageEditSubIntent(text: string): AnalysisResult["subIntent
|
||||
return "enhance";
|
||||
}
|
||||
|
||||
function isVideoGenerationRequest(text: string): boolean {
|
||||
return /视频|动画|动起来|生成视频|做视频|视频生成|图生视频|文生视频|镜头|分镜|运镜|首帧|尾帧/.test(
|
||||
text.toLowerCase(),
|
||||
);
|
||||
}
|
||||
|
||||
function applyDirectExecutionPolicy(
|
||||
result: AnalysisResult,
|
||||
params: {
|
||||
@ -42,6 +49,22 @@ function applyDirectExecutionPolicy(
|
||||
imageFromHistory?: boolean;
|
||||
}
|
||||
): AnalysisResult {
|
||||
if (isVideoGenerationRequest(params.text)) {
|
||||
return {
|
||||
...result,
|
||||
intent: "video_gen",
|
||||
subIntent: undefined,
|
||||
confidence: Math.max(result.confidence, 0.8),
|
||||
needsClarification: false,
|
||||
clarifyingQuestions: [],
|
||||
conversationalReply:
|
||||
result.conversationalReply ||
|
||||
(params.hasImage
|
||||
? "收到,我先按你这轮图片做视频需求分析,再自动优化提示词执行视频生成。"
|
||||
: "收到,我先做视频需求分析并自动优化提示词后执行生成。"),
|
||||
};
|
||||
}
|
||||
|
||||
const hasUsableImage =
|
||||
params.hasImage || Boolean(params.imageFromHistory) || (params.imageUrls?.length || 0) > 0;
|
||||
if (!hasUsableImage) return result;
|
||||
@ -149,6 +172,7 @@ const ANALYZER_SYSTEM_PROMPT = `你是一个友好的AI图片编辑助手。用
|
||||
- 说明你理解了什么需求,让用户知道你在做什么
|
||||
- 有图片时优先直接执行,不要提问
|
||||
- 如果提供了画布上下文,仅用于理解,不要在 conversationalReply 中复述坐标或技术细节
|
||||
- video_gen 场景要明确说明:会先分析视频需求/分镜,再自动优化提示词并执行生成
|
||||
|
||||
## 示例
|
||||
用户:"帮我把背景换成白色"
|
||||
@ -203,8 +227,340 @@ const ANALYZER_SYSTEM_PROMPT = `你是一个友好的AI图片编辑助手。用
|
||||
"confidence": 0.9,
|
||||
"conversationalReply": "好的,我再帮你提升一下清晰度~",
|
||||
"needsClarification": false
|
||||
}
|
||||
|
||||
用户:"把这张图做成5秒短视频,镜头慢慢推进"
|
||||
{
|
||||
"intent": "video_gen",
|
||||
"confidence": 0.9,
|
||||
"conversationalReply": "收到,我先按你的目标做视频需求分析和分镜规划,再自动优化提示词执行生成~",
|
||||
"needsClarification": false
|
||||
}`;
|
||||
|
||||
const ANALYZER_MAX_IMAGE_COUNT = 2;
|
||||
const ANALYZER_IMAGE_MAX_SIDE = 500;
|
||||
const ANALYZER_IMAGE_FETCH_TIMEOUT_MS = 10000;
|
||||
const ZHIPU_VISION_MODEL_CANDIDATES = [
|
||||
"glm-4.6v-flash",
|
||||
"glm-4.6v",
|
||||
"glm-4v-plus-0111",
|
||||
"glm-4v-plus",
|
||||
"glm-4v",
|
||||
];
|
||||
|
||||
function isHttpUrl(value: string) {
|
||||
return /^https?:\/\//i.test(value);
|
||||
}
|
||||
|
||||
async function fetchWithTimeout(url: string, timeoutMs: number): Promise<Response> {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
return await fetch(url, {
|
||||
signal: controller.signal,
|
||||
cache: "no-store",
|
||||
headers: {
|
||||
"user-agent": "ai-chatbot-smart-analyzer/1.0",
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
async function toAnalyzerDataUrl(imageUrl: string): Promise<string | null> {
|
||||
try {
|
||||
const response = await fetchWithTimeout(imageUrl, ANALYZER_IMAGE_FETCH_TIMEOUT_MS);
|
||||
if (!response.ok) {
|
||||
console.warn(
|
||||
`[Smart Analyzer] Failed to fetch image for inline payload (${response.status}): ${imageUrl}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const contentType = response.headers.get("content-type") || "";
|
||||
if (!contentType.startsWith("image/")) {
|
||||
console.warn(`[Smart Analyzer] Skip non-image content-type: ${contentType}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const inputBuffer = Buffer.from(arrayBuffer);
|
||||
|
||||
const optimized = await sharp(inputBuffer)
|
||||
.rotate()
|
||||
.resize({
|
||||
width: ANALYZER_IMAGE_MAX_SIDE,
|
||||
height: ANALYZER_IMAGE_MAX_SIDE,
|
||||
fit: "inside",
|
||||
withoutEnlargement: true,
|
||||
})
|
||||
.jpeg({
|
||||
quality: 80,
|
||||
mozjpeg: true,
|
||||
})
|
||||
.toBuffer();
|
||||
|
||||
return `data:image/jpeg;base64,${optimized.toString("base64")}`;
|
||||
} catch (error) {
|
||||
console.warn("[Smart Analyzer] Failed to convert image URL to inline base64:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function buildAnalyzerVisionImages(imageUrls?: string[]) {
|
||||
const urls = (imageUrls || []).filter((url) => isHttpUrl(url)).slice(0, ANALYZER_MAX_IMAGE_COUNT);
|
||||
if (urls.length === 0) return [] as Array<{ type: "image"; image: string }>;
|
||||
|
||||
const convertedResults = await Promise.all(urls.map((url) => toAnalyzerDataUrl(url)));
|
||||
|
||||
return urls.map((originalUrl, index) => {
|
||||
const inlineDataUrl = convertedResults[index];
|
||||
return {
|
||||
type: "image" as const,
|
||||
image: inlineDataUrl || originalUrl,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function parseAnalysisResultFromModelText(
|
||||
responseText: string,
|
||||
userText: string,
|
||||
hasImage: boolean,
|
||||
): AnalysisResult | null {
|
||||
const jsonMatch = responseText.match(/\{[\s\S]*\}/);
|
||||
if (!jsonMatch) return null;
|
||||
|
||||
try {
|
||||
const result = JSON.parse(jsonMatch[0]) as AnalysisResult;
|
||||
return normalizeIntentForImageContext(result, userText, hasImage);
|
||||
} catch (error) {
|
||||
console.error("[Smart Analyzer] Failed to parse model response:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isLikelyZhipuEndpoint(baseUrl?: string | null): boolean {
|
||||
if (!baseUrl) return false;
|
||||
return /open\.bigmodel\.cn/i.test(baseUrl);
|
||||
}
|
||||
|
||||
function normalizeZhipuChatEndpoint(baseUrl: string): string {
|
||||
const cleaned = baseUrl.replace(/\/+$/, "");
|
||||
if (/\/chat\/completions$/i.test(cleaned)) return cleaned;
|
||||
if (/\/api\/paas\/v4$/i.test(cleaned)) return `${cleaned}/chat/completions`;
|
||||
if (/\/api\/paas\/v4\//i.test(cleaned)) return cleaned;
|
||||
if (/open\.bigmodel\.cn/i.test(cleaned)) return `${cleaned}/api/paas/v4/chat/completions`;
|
||||
return `${cleaned}/chat/completions`;
|
||||
}
|
||||
|
||||
function toZhipuImageUrl(image: string): string {
|
||||
const dataUrlPrefix = /^data:image\/[a-zA-Z0-9.+-]+;base64,/i;
|
||||
if (dataUrlPrefix.test(image)) {
|
||||
return image.replace(dataUrlPrefix, "");
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
async function analyzeWithZhipuOfficialApi(params: {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
userMessage: string;
|
||||
visionImages: Array<{ type: "image"; image: string }>;
|
||||
userText: string;
|
||||
hasImage: boolean;
|
||||
}): Promise<AnalysisResult | null> {
|
||||
const endpoint = normalizeZhipuChatEndpoint(params.baseUrl);
|
||||
|
||||
const content = [
|
||||
{ type: "text", text: params.userMessage },
|
||||
...params.visionImages.map((imagePart) => ({
|
||||
type: "image_url",
|
||||
image_url: {
|
||||
url: toZhipuImageUrl(imagePart.image),
|
||||
},
|
||||
})),
|
||||
];
|
||||
|
||||
for (const modelId of ZHIPU_VISION_MODEL_CANDIDATES) {
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: modelId,
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: ANALYZER_SYSTEM_PROMPT,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content,
|
||||
},
|
||||
],
|
||||
temperature: 0.1,
|
||||
max_tokens: 360,
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok || payload?.error) {
|
||||
const errorCode = String(payload?.error?.code || "");
|
||||
const errorMessage = String(payload?.error?.message || "");
|
||||
if (errorCode === "1211" || /模型不存在/.test(errorMessage)) {
|
||||
continue;
|
||||
}
|
||||
console.warn(
|
||||
`[Smart Analyzer] Zhipu official vision request failed (model=${modelId})`,
|
||||
payload?.error || payload,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const message = payload?.choices?.[0]?.message;
|
||||
const responseText = String(message?.content || message?.reasoning_content || "").trim();
|
||||
if (!responseText) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const parsed = parseAnalysisResultFromModelText(
|
||||
responseText,
|
||||
params.userText,
|
||||
params.hasImage,
|
||||
);
|
||||
if (parsed) {
|
||||
console.log(`[Smart Analyzer] Zhipu official vision model used: ${modelId}`);
|
||||
return parsed;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[Smart Analyzer] Zhipu official vision request exception (model=${modelId})`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function summarizeWithZhipuOfficialApi(params: {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
promptText: string;
|
||||
visionImages: Array<{ type: "image"; image: string }>;
|
||||
}): Promise<string | null> {
|
||||
const endpoint = normalizeZhipuChatEndpoint(params.baseUrl);
|
||||
const content = [
|
||||
{ type: "text", text: params.promptText },
|
||||
...params.visionImages.map((imagePart) => ({
|
||||
type: "image_url",
|
||||
image_url: {
|
||||
url: toZhipuImageUrl(imagePart.image),
|
||||
},
|
||||
})),
|
||||
];
|
||||
|
||||
for (const modelId of ZHIPU_VISION_MODEL_CANDIDATES) {
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: modelId,
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content:
|
||||
"你是图像观察助手。请根据图片给出执行前说明,输出严格两行中文:第1行“画面:...”,第2行“关键要素:...”。不要输出其他内容。",
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content,
|
||||
},
|
||||
],
|
||||
temperature: 0.2,
|
||||
max_tokens: 140,
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok || payload?.error) {
|
||||
const errorCode = String(payload?.error?.code || "");
|
||||
const errorMessage = String(payload?.error?.message || "");
|
||||
if (errorCode === "1211" || /模型不存在/.test(errorMessage)) {
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const message = payload?.choices?.[0]?.message;
|
||||
const responseText = String(message?.content || message?.reasoning_content || "").trim();
|
||||
if (responseText) {
|
||||
console.log(`[Smart Analyzer] Zhipu official summary model used: ${modelId}`);
|
||||
return responseText;
|
||||
}
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function summarizeImageForExecutionReport(params: {
|
||||
text?: string;
|
||||
imageUrls?: string[];
|
||||
context?: string;
|
||||
}): Promise<string | null> {
|
||||
const { text, imageUrls, context } = params;
|
||||
const visionImages = await buildAnalyzerVisionImages(imageUrls);
|
||||
if (visionImages.length === 0) return null;
|
||||
|
||||
const {
|
||||
getEnabledAiModelsWithProvider,
|
||||
getAgentAiModelWithProvider,
|
||||
} = await import("@/lib/db/queries");
|
||||
const allModels = await getEnabledAiModelsWithProvider();
|
||||
const agentModel = await getAgentAiModelWithProvider();
|
||||
const zhipuProviderFromAgent =
|
||||
agentModel?.provider?.apiKey && isLikelyZhipuEndpoint(agentModel.provider.baseUrl)
|
||||
? agentModel.provider
|
||||
: null;
|
||||
const zhipuProviderFallback = allModels.find(
|
||||
(m) =>
|
||||
m.provider?.apiKey &&
|
||||
(m.provider.type === "zhipu" || isLikelyZhipuEndpoint(m.provider.baseUrl)),
|
||||
)?.provider;
|
||||
const zhipuProvider = zhipuProviderFromAgent || zhipuProviderFallback;
|
||||
|
||||
const promptText = [
|
||||
text ? `用户需求:${text}` : "",
|
||||
context ? `画布上下文:${context.slice(0, 220)}` : "",
|
||||
"请给出当前画面的客观描述,便于执行前说明。",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
|
||||
if (zhipuProvider?.apiKey && zhipuProvider?.baseUrl) {
|
||||
const summary = await summarizeWithZhipuOfficialApi({
|
||||
apiKey: zhipuProvider.apiKey,
|
||||
baseUrl: zhipuProvider.baseUrl,
|
||||
promptText,
|
||||
visionImages,
|
||||
});
|
||||
if (summary) return summary;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// 历史消息类型
|
||||
interface HistoryMessage {
|
||||
role: "user" | "assistant";
|
||||
@ -239,7 +595,7 @@ export async function analyzeIntent(params: {
|
||||
return applyDirectExecutionPolicy(quickResult, { text, hasImage, imageUrls, imageFromHistory });
|
||||
}
|
||||
|
||||
// 尝试使用 Qwen3 VL 进行深度分析(传入历史对话)
|
||||
// 尝试使用多模态模型进行深度分析(传入历史对话)
|
||||
try {
|
||||
const llmResult = await analyzWithLLM(text, hasImage, imageUrls, historyMessages, context);
|
||||
if (llmResult) {
|
||||
@ -291,24 +647,45 @@ async function analyzWithLLM(
|
||||
const agentModel = await getAgentAiModelWithProvider();
|
||||
const isReasoningCompatibleProvider = (providerType: string) =>
|
||||
["zhipu", "alibaba", "openai", "custom", "local"].includes(providerType);
|
||||
const isLikelyZhipuVisionModel = (displayName: string, modelId: string, providerType: string) =>
|
||||
providerType === "zhipu" ||
|
||||
/(glm[-_.]?4v|glm[-_.]?4\.?1v|glm[-_.]?4\.?5v|vision)/i.test(
|
||||
`${displayName} ${modelId}`,
|
||||
);
|
||||
const zhipuVisionModel = allModels.find(
|
||||
(m) =>
|
||||
m.model.supportsVision &&
|
||||
m.model.isEnabled &&
|
||||
!m.model.isSubModel &&
|
||||
isLikelyZhipuVisionModel(m.model.displayName, m.model.modelId, m.provider.type),
|
||||
);
|
||||
const qwenVisionModel = allModels.find(
|
||||
(m) =>
|
||||
m.provider.type === "alibaba" &&
|
||||
m.model.supportsVision &&
|
||||
m.model.isEnabled &&
|
||||
!m.model.isSubModel &&
|
||||
/(qwen|通义|vl)/i.test(`${m.model.displayName} ${m.model.modelId}`)
|
||||
);
|
||||
const fallbackVisionModel =
|
||||
qwenVisionModel ||
|
||||
allModels.find((m) => m.provider.type === "alibaba" && m.model.supportsVision);
|
||||
allModels.find(
|
||||
(m) =>
|
||||
m.provider.type === "alibaba" &&
|
||||
m.model.supportsVision &&
|
||||
m.model.isEnabled &&
|
||||
!m.model.isSubModel,
|
||||
);
|
||||
const preferredVisionModel = zhipuVisionModel || fallbackVisionModel;
|
||||
const usableAgentModel =
|
||||
agentModel && isReasoningCompatibleProvider(agentModel.provider.type)
|
||||
? agentModel
|
||||
: undefined;
|
||||
const selectedAnalyzerModel = hasImage
|
||||
? fallbackVisionModel
|
||||
? preferredVisionModel
|
||||
: usableAgentModel ||
|
||||
allModels.find((m) => m.provider.type === "zhipu" && !m.model.isSubModel) ||
|
||||
fallbackVisionModel;
|
||||
preferredVisionModel;
|
||||
|
||||
if (!selectedAnalyzerModel) {
|
||||
console.log("[Smart Analyzer] No available analyzer model found");
|
||||
@ -348,6 +725,42 @@ ${historyText}
|
||||
? `${contextMessage}用户上传了图片,请求内容:"${text}"`
|
||||
: `${contextMessage}用户请求(无图片):"${text}"`;
|
||||
|
||||
const visionImages = hasImage ? await buildAnalyzerVisionImages(imageUrls) : [];
|
||||
if (visionImages.length > 0) {
|
||||
const inlineCount = visionImages.filter((item) => item.image.startsWith("data:image/")).length;
|
||||
console.log(
|
||||
`[Smart Analyzer] Prepared analyzer images: total=${visionImages.length}, inline=${inlineCount}, remote=${visionImages.length - inlineCount}`,
|
||||
);
|
||||
}
|
||||
|
||||
const zhipuProviderFromAgent =
|
||||
usableAgentModel &&
|
||||
usableAgentModel.provider &&
|
||||
usableAgentModel.provider.apiKey &&
|
||||
isLikelyZhipuEndpoint(usableAgentModel.provider.baseUrl)
|
||||
? usableAgentModel.provider
|
||||
: null;
|
||||
const zhipuProviderFallback = allModels.find(
|
||||
(m) =>
|
||||
m.provider?.apiKey &&
|
||||
(m.provider.type === "zhipu" || isLikelyZhipuEndpoint(m.provider.baseUrl)),
|
||||
)?.provider;
|
||||
const zhipuProvider = zhipuProviderFromAgent || zhipuProviderFallback;
|
||||
|
||||
if (hasImage && visionImages.length > 0 && zhipuProvider?.apiKey && zhipuProvider?.baseUrl) {
|
||||
const zhipuOfficialResult = await analyzeWithZhipuOfficialApi({
|
||||
apiKey: zhipuProvider.apiKey,
|
||||
baseUrl: zhipuProvider.baseUrl,
|
||||
userMessage,
|
||||
visionImages,
|
||||
userText: text,
|
||||
hasImage,
|
||||
});
|
||||
if (zhipuOfficialResult) {
|
||||
return zhipuOfficialResult;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
"[Smart Analyzer] Calling analyzer model:",
|
||||
selectedAnalyzerModel.model.displayName,
|
||||
@ -356,11 +769,6 @@ ${historyText}
|
||||
historyMessages?.length || 0
|
||||
);
|
||||
|
||||
const visionImages = (imageUrls || [])
|
||||
.filter((url) => /^https?:\/\//i.test(url))
|
||||
.slice(0, 4)
|
||||
.map((url) => ({ type: "image" as const, image: url }));
|
||||
|
||||
// 调用模型分析(有图时携带图片,确保真实视觉分析)
|
||||
const requestPayload = hasImage && visionImages.length > 0
|
||||
? {
|
||||
@ -388,18 +796,7 @@ ${historyText}
|
||||
|
||||
const { text: responseText } = await generateText(requestPayload as any);
|
||||
|
||||
// 解析JSON响应
|
||||
const jsonMatch = responseText.match(/\{[\s\S]*\}/);
|
||||
if (jsonMatch) {
|
||||
try {
|
||||
const result = JSON.parse(jsonMatch[0]) as AnalysisResult;
|
||||
return normalizeIntentForImageContext(result, text, hasImage);
|
||||
} catch (e) {
|
||||
console.error("[Smart Analyzer] Failed to parse LLM response:", e);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return parseAnalysisResultFromModelText(responseText, text, hasImage);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -414,6 +811,15 @@ export function fallbackKeywordAnalysis(
|
||||
|
||||
// === 图片编辑场景(需要有图片) ===
|
||||
if (hasImage) {
|
||||
// 视频生成(图生视频/首尾帧等)
|
||||
if (isVideoGenerationRequest(lowerText)) {
|
||||
return {
|
||||
intent: "video_gen",
|
||||
confidence: 0.9,
|
||||
conversationalReply: "收到,我先按你提供的图片做视频需求分析和分镜规划,再自动优化提示词执行生成~",
|
||||
};
|
||||
}
|
||||
|
||||
// 抠图/去背景
|
||||
if (/抠图|去背景|透明背景|去背|扣图|移除背景/.test(lowerText)) {
|
||||
return {
|
||||
@ -544,11 +950,11 @@ export function fallbackKeywordAnalysis(
|
||||
}
|
||||
|
||||
// 视频生成
|
||||
if (/视频|动画|动起来|生成视频|做视频|视频生成/.test(lowerText)) {
|
||||
if (isVideoGenerationRequest(lowerText)) {
|
||||
return {
|
||||
intent: "video_gen",
|
||||
confidence: 0.85,
|
||||
conversationalReply: "好的,我来帮你生成视频~",
|
||||
conversationalReply: "好的,我先做视频需求分析并自动优化提示词后执行生成~",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user