feat(canvas): 画布模型与连线能力 - 合并同名模型并按输入动态限制节点关联

Closes #220
This commit is contained in:
ddCat
2026-08-14 12:41:55 +08:00
parent cfdf08d05f
commit e7af466a16
16 changed files with 509 additions and 80 deletions

View File

@@ -14,6 +14,7 @@ import { createClientId } from "@/lib/client-id";
import { generationErrorMessage } from "@/lib/generation-error";
import { VIDEO_RESOLUTION_OPTIONS } from "@/lib/video-generation-options";
import { modelCapabilityConfigFor, normalizeImageValue, normalizeVideoValue, videoDurationAllowed, videoDurationOptions, type ImageCapabilityConfig, type VideoCapabilityConfig } from "@/lib/model-capabilities";
import { resolveCompatibleModel, type ModelRequirements } from "@/lib/model-selection";
import { parseBackendGenerationResult, runBackendGenerationTask, runBackendGenerationTaskBatch, type BackendGenerationResult } from "@/services/api/generation-task";
import { requestImageQuestion } from "@/services/api/image";
import { listAddedSkills, type Skill } from "@/services/api/skills";
@@ -156,7 +157,20 @@ export default function CreatePage() {
() => conversations.filter((conversation) => conversation.id === activeId || conversation.messages.length > 0).sort((left, right) => conversationTimestamp(right.updatedAt) - conversationTimestamp(left.updatedAt)),
[activeId, conversations],
);
const selectedModel = mode === "text" ? config.textModel : mode === "image" ? config.imageModel : config.videoModel;
const preferredModel = mode === "text" ? config.textModel : mode === "image" ? config.imageModel : config.videoModel;
const hasPrompt = Boolean(prompt.trim());
const modelRequirements = useMemo<ModelRequirements>(() => ({
capability: mode,
input: {
textCount: hasPrompt ? 1 : 0,
imageCount: attachments.filter(isImageAttachment).length,
videoCount: attachments.filter(isVideoAttachment).length,
audioCount: 0,
characterCount: 0,
},
videoSeconds: seconds,
}), [attachments, hasPrompt, mode, seconds]);
const selectedModel = resolveCompatibleModel(config, preferredModel, modelRequirements) || preferredModel;
const imageProfile = useMemo(() => modelCapabilityConfigFor(config, selectedModel).image!, [config, selectedModel]);
const videoProfile = useMemo(() => modelCapabilityConfigFor(config, selectedModel).video!, [config, selectedModel]);
const mentionReferences = useMemo(() => buildCreationMentionReferences(addedSkills, attachments, draftReferences), [addedSkills, attachments, draftReferences]);
@@ -568,6 +582,7 @@ export default function CreatePage() {
onFileChange: handleFileChange,
onModeChange: selectMode,
model: selectedModel,
modelRequirements,
imageProfile,
videoProfile,
config,
@@ -784,6 +799,7 @@ type ComposerProps = {
onFileChange: (event: ChangeEvent<HTMLInputElement>) => void;
onModeChange: (mode: CreationMode) => void;
model: string;
modelRequirements: ModelRequirements;
videoProfile: VideoCapabilityConfig;
imageProfile: ImageCapabilityConfig;
config: ReturnType<typeof useEffectiveConfig>;
@@ -838,7 +854,7 @@ function CreationComposer(props: ComposerProps) {
/>
<ModePicker mode={props.mode} onModeChange={props.onModeChange} />
<Tooltip title={!referencesSupported ? "当前模型不支持参考媒体" : "从素材库选择参考内容"}><button type="button" className="creation-chat-control" onClick={props.onOpenLibrary} disabled={props.busy || !referencesSupported} aria-label="打开素材库选择参考内容"><FolderOpen /><span>素材库</span></button></Tooltip>
<ModelPicker config={props.config} value={props.model} onChange={props.onModelChange} capability={props.mode} className="creation-model-picker" placeholder={`选择${modeLabels[props.mode]}模型`} showSelectedPrice={false} variant="creation" />
<ModelPicker config={props.config} value={props.model} onChange={props.onModelChange} capability={props.mode} requirements={props.modelRequirements} className="creation-model-picker" placeholder={`选择${modeLabels[props.mode]}模型`} showSelectedPrice={false} variant="creation" />
{props.mode === "video" || (props.mode === "image" && imageSettingsSupported) ? <GenerationSettingsMenu {...props} /> : null}
{props.mode === "video" ? <DurationMenu profile={props.videoProfile} seconds={props.seconds} onChange={props.setSeconds} /> : null}
</div>