Merge remote-tracking branch 'origin/pr-573' into codex/pr-557-589-security-integration
This commit is contained in:
@@ -7,6 +7,12 @@ export type CreationReference = CanvasResourceReference & {
|
||||
attachmentId?: string;
|
||||
};
|
||||
|
||||
export type CreationReferenceLimits = {
|
||||
maxImages: number;
|
||||
maxVideos: number;
|
||||
maxAudios: number;
|
||||
};
|
||||
|
||||
export function buildCreationMentionReferences(skills: Skill[], attachments: CreationAttachment[] = [], snapshots: CreationReference[] = []) {
|
||||
const counts = { image: 0, video: 0, audio: 0, file: 0 };
|
||||
const attachmentReferences = attachments.map((attachment) => {
|
||||
@@ -34,6 +40,25 @@ export function reconcileCreationAttachmentLimit(attachments: CreationAttachment
|
||||
return { attachments: nextAttachments, removedReferences };
|
||||
}
|
||||
|
||||
export function reconcileCreationAttachmentLimits(attachments: CreationAttachment[], references: CreationReference[], limits: CreationReferenceLimits) {
|
||||
const remaining = {
|
||||
image: Math.max(0, Math.floor(limits.maxImages)),
|
||||
video: Math.max(0, Math.floor(limits.maxVideos)),
|
||||
audio: Math.max(0, Math.floor(limits.maxAudios)),
|
||||
};
|
||||
const nextAttachments = attachments.filter((attachment) => {
|
||||
const kind = creationAttachmentKind(attachment);
|
||||
if (kind === "file" || remaining[kind] <= 0) return false;
|
||||
remaining[kind] -= 1;
|
||||
return true;
|
||||
});
|
||||
if (nextAttachments.length === attachments.length) return { attachments, removedReferences: [] as CreationReference[] };
|
||||
|
||||
const retainedIds = new Set(nextAttachments.map((attachment) => attachment.id));
|
||||
const removedReferences = references.filter((reference) => reference.attachmentId && !retainedIds.has(reference.attachmentId));
|
||||
return { attachments: nextAttachments, removedReferences };
|
||||
}
|
||||
|
||||
export function removeCreationReferenceTokens(value: string, references: CreationReference[]) {
|
||||
return references.reduce((current, reference) => current.split(canvasResourceMentionToken(reference)).join("").split(`@${reference.label}`).join(""), value);
|
||||
}
|
||||
|
||||
@@ -415,7 +415,7 @@ export function CreationComposer(props: ComposerProps) {
|
||||
: "描述镜头内容、运动、光线与节奏";
|
||||
const emptyPlaceholder = "输入你的镜头、画面或故事。也可以添加参考图开始创作";
|
||||
const imageReferencesSupported = props.imageProfile.references.maxImages > 0;
|
||||
const referencesSupported = props.mode === "image" ? imageReferencesSupported : props.mode !== "video" || props.videoProfile.operations.includes("image_to_video");
|
||||
const referencesSupported = props.mode === "image" ? imageReferencesSupported : props.mode !== "video" || props.maxReferences > 0;
|
||||
const canAddMoreReferences = referencesSupported && props.attachments.length < props.maxReferences;
|
||||
const addReferenceLabel = interactionBusy ? (props.referenceReplacementBusy ? "正在替换参考图" : "生成中暂不能添加参考内容") : canAddMoreReferences ? "添加更多参考内容" : `已达到当前模型的参考内容上限(${props.maxReferences} 个)`;
|
||||
const referenceCounts = useMemo(() => props.attachments.reduce((counts, attachment) => {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { getActiveUserScope } from "@/lib/user-scope";
|
||||
import { continueCreationConversationOnCanvas } from "@/services/creation-canvas-conversation";
|
||||
import { useExternalAssetSources } from "@/hooks/use-external-asset-sources";
|
||||
import { modelCapabilityConfigFor, normalizeImageValue, normalizeVideoValue, videoDurationAllowed, videoDurationOptions } from "@/lib/model-capabilities";
|
||||
import { inferVideoOperation, resolveCompatibleModel, mergedImageCapabilityConfig, type ModelRequirements } from "@/lib/model-selection";
|
||||
import { inferVideoOperation, modelGroupReferenceLimits, resolveCompatibleModel, mergedImageCapabilityConfig, type ModelRequirements } from "@/lib/model-selection";
|
||||
import type { BackendGenerationResult } from "@/services/api/generation-task";
|
||||
import type { Skill } from "@/services/api/skills";
|
||||
import type { GenerationTask } from "@/services/api/task-center";
|
||||
@@ -27,7 +27,7 @@ import type { PromptOptimizerProvider } from "@/lib/plugins/plugin-types";
|
||||
import { promptOptimizerPlugin, PROMPT_OPTIMIZER_PLUGIN_ID } from "@/lib/plugins/builtin/prompt-optimizer";
|
||||
import { createPluginHostContext } from "@/services/plugin-host";
|
||||
import { usePluginStore } from "@/stores/use-plugin-store";
|
||||
import { buildCreationMentionReferences, expandCreationPrompt, reconcileCreationAttachmentLimit, removeCreationReferenceTokens, replaceCreationAttachmentReference, selectedCreationReferences, type CreationReference } from "./creation-references";
|
||||
import { buildCreationMentionReferences, expandCreationPrompt, reconcileCreationAttachmentLimit, reconcileCreationAttachmentLimits, removeCreationReferenceTokens, replaceCreationAttachmentReference, selectedCreationReferences, type CreationReference, type CreationReferenceLimits } from "./creation-references";
|
||||
import { creationAttachmentFromAsset, creationAttachmentFromAudio, creationAttachmentFromAudioAsset, creationAttachmentFromDocument, creationAttachmentFromExternalAsset, creationAttachmentFromImage, creationAttachmentFromVideo, creationAttachmentFromVideoAsset, creationAttachmentKind, creationAudioAsset, creationFileAccepted, creationImageAsset, creationMediaAspectRatio, creationUploadAccept, creationVideoAsset, removeCreationAttachment, splitCreationAttachments, type CreationAttachment } from "./creation-assets";
|
||||
import { defaultCreationMode, modeLabels, type CreationConversation, type CreationMessage, type CreationMode, type CreationRetryContext, type CreationSettings, type CreationShotRailEntry, type CreationStatus } from "./creation-types";
|
||||
import { attachCreationTaskContexts, completedCreationGenerationTask, conversationTimestamp, creationShotRail, creationVideoShotOrdinal, isImageAttachment, isVideoAttachment, materializeCreationTaskResults, newConversation, newMessage, reconcileCreationTaskMessages } from "./creation-conversations";
|
||||
@@ -42,6 +42,18 @@ type CreationRuntime = Awaited<ReturnType<typeof loadCreationRuntime>>;
|
||||
|
||||
const TEXT_STREAMING_PREF_KEY = "creation.composer.text-streaming";
|
||||
const TEXT_THINKING_PREF_KEY = "creation.composer.text-thinking";
|
||||
|
||||
function creationLibraryDisabledReason(mode: CreationMode, kind: string | undefined, videoLimits?: CreationReferenceLimits) {
|
||||
if (!kind) return undefined;
|
||||
if (mode === "image") return kind === "image" ? undefined : "图片创作仅支持参考图";
|
||||
if (mode !== "video") return undefined;
|
||||
|
||||
const maximum = kind === "image" ? videoLimits?.maxImages : kind === "video" ? videoLimits?.maxVideos : kind === "audio" ? videoLimits?.maxAudios : 0;
|
||||
if ((maximum || 0) > 0) return undefined;
|
||||
const label = kind === "video" ? "视频" : kind === "audio" ? "音频" : kind === "image" ? "图片" : "此类素材";
|
||||
return `当前视频模型不支持参考${label}`;
|
||||
}
|
||||
|
||||
function readComposerPref(key: string, fallback: boolean): boolean {
|
||||
try {
|
||||
const stored = window.localStorage.getItem(key);
|
||||
@@ -152,7 +164,14 @@ export default function CreatePage() {
|
||||
: config, [config, mode, ratio, seconds, selectedModel, videoQuality]);
|
||||
const imageProfile = useMemo(() => modelCapabilityConfigFor(config, selectedModel).image!, [config, selectedModel]);
|
||||
const videoProfile = useMemo(() => modelCapabilityConfigFor(config, selectedModel).video!, [config, selectedModel]);
|
||||
const maxReferences = mode === "video" ? videoProfile.operations.includes("image_to_video") ? videoProfile.references.maxImages : 0 : mode === "image" ? imageProfile.references.maxImages : 6;
|
||||
// 同名逻辑模型可能把文生视频、图生视频和全模态参考拆到不同路由。
|
||||
// 入口需要展示整个模型组的引用能力,添加素材后再由兼容路由选择具体模型。
|
||||
const videoReferenceLimits = useMemo(() => mode === "video"
|
||||
? modelGroupReferenceLimits(config, preferredModel || selectedModel, "video") || { maxImages: 0, maxVideos: 0, maxAudios: 0 }
|
||||
: undefined, [config, mode, preferredModel, selectedModel]);
|
||||
const maxReferences = mode === "video"
|
||||
? (videoReferenceLimits?.maxImages || 0) + (videoReferenceLimits?.maxVideos || 0) + (videoReferenceLimits?.maxAudios || 0)
|
||||
: mode === "image" ? imageProfile.references.maxImages : 6;
|
||||
const referenceImageSize = useMemo(() => {
|
||||
const imageAttachments = attachments.filter(isImageAttachment);
|
||||
if (imageAttachments.length !== 1) return undefined;
|
||||
@@ -225,16 +244,16 @@ export default function CreatePage() {
|
||||
setSeconds(normalized.seconds);
|
||||
setRatio(normalized.ratio);
|
||||
setVideoQuality(normalized.resolution.replace(/p$/i, ""));
|
||||
const maxReferences = videoProfile.operations.includes("image_to_video") ? videoProfile.references.maxImages : 0;
|
||||
if (attachments.length > maxReferences) setAttachments((current) => current.slice(0, maxReferences));
|
||||
}, [composerPreferencesHydrated, composerPreferencesInitialized, mode, selectedModel, videoProfile]);
|
||||
|
||||
useEffect(() => {
|
||||
const reconciled = reconcileCreationAttachmentLimit(attachments, mentionReferences, maxReferences);
|
||||
const reconciled = mode === "video" && videoReferenceLimits
|
||||
? reconcileCreationAttachmentLimits(attachments, mentionReferences, videoReferenceLimits)
|
||||
: reconcileCreationAttachmentLimit(attachments, mentionReferences, maxReferences);
|
||||
if (reconciled.attachments === attachments) return;
|
||||
setAttachments(reconciled.attachments);
|
||||
if (reconciled.removedReferences.length) setPrompt((current) => removeCreationReferenceTokens(current, reconciled.removedReferences));
|
||||
}, [attachments, maxReferences, mentionReferences]);
|
||||
}, [attachments, maxReferences, mentionReferences, mode, videoReferenceLimits]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -387,9 +406,9 @@ export default function CreatePage() {
|
||||
const externalLibraryItems = useMemo<AssetLibraryPickerItem[]>(
|
||||
() => externalAssetSources.items.map((item) => ({
|
||||
...item,
|
||||
disabledReason: mode === "image" && item.external?.item.kind !== "image" ? "图片创作仅支持参考图" : undefined,
|
||||
disabledReason: creationLibraryDisabledReason(mode, item.external?.item.kind, videoReferenceLimits),
|
||||
})),
|
||||
[externalAssetSources.items, mode],
|
||||
[externalAssetSources.items, mode, videoReferenceLimits],
|
||||
);
|
||||
const libraryItems = useMemo<AssetLibraryPickerItem[]>(() => [
|
||||
...assets
|
||||
@@ -401,10 +420,10 @@ export default function CreatePage() {
|
||||
kindLabel: asset.kind === "video" ? "视频" : asset.kind === "audio" ? "音频" : "图片",
|
||||
asset,
|
||||
searchText: (asset.tags || []).join(" "),
|
||||
disabledReason: mode === "image" && asset.kind !== "image" ? "图片创作仅支持参考图" : undefined,
|
||||
disabledReason: creationLibraryDisabledReason(mode, asset.kind, videoReferenceLimits),
|
||||
})),
|
||||
...externalLibraryItems,
|
||||
], [assets, externalLibraryItems, mode]);
|
||||
], [assets, externalLibraryItems, mode, videoReferenceLimits]);
|
||||
const uploadCreationAsset = async (file: File) => {
|
||||
const { uploadImage, uploadMediaFile } = await loadCreationRuntime();
|
||||
if (file.type.startsWith("video/")) {
|
||||
@@ -455,7 +474,14 @@ export default function CreatePage() {
|
||||
return external ? [creationAttachmentFromExternalAsset(external)] : [];
|
||||
});
|
||||
if (!next.length) return;
|
||||
setAttachments((current) => [...current.filter((item) => !next.some((candidate) => candidate.id === item.id)), ...next].slice(0, maxReferences));
|
||||
setAttachments((current) => {
|
||||
const candidates = [...current.filter((item) => !next.some((candidate) => candidate.id === item.id)), ...next];
|
||||
const reconciled = mode === "video" && videoReferenceLimits
|
||||
? reconcileCreationAttachmentLimits(candidates, [], videoReferenceLimits)
|
||||
: reconcileCreationAttachmentLimit(candidates, [], maxReferences);
|
||||
if (reconciled.attachments.length < candidates.length) toast.warning("部分素材超出当前模型的参考内容上限,未添加到创作中");
|
||||
return reconciled.attachments;
|
||||
});
|
||||
setLibraryOpen(false);
|
||||
};
|
||||
|
||||
@@ -556,7 +582,10 @@ export default function CreatePage() {
|
||||
releaseSubmitGate();
|
||||
return;
|
||||
}
|
||||
if (attachments.length > maxReferences) {
|
||||
const reconciledAttachments = mode === "video" && videoReferenceLimits
|
||||
? reconcileCreationAttachmentLimits(attachments, mentionReferences, videoReferenceLimits)
|
||||
: reconcileCreationAttachmentLimit(attachments, mentionReferences, maxReferences);
|
||||
if (reconciledAttachments.attachments !== attachments) {
|
||||
toast.warning("参考内容正在按当前模型能力调整,请稍后重试");
|
||||
releaseRetryLock();
|
||||
releaseSubmitGate();
|
||||
|
||||
Reference in New Issue
Block a user