diff --git a/backend/internal/service/storage_s3_test.go b/backend/internal/service/storage_s3_test.go index 612c6b9f7..3696732bd 100644 --- a/backend/internal/service/storage_s3_test.go +++ b/backend/internal/service/storage_s3_test.go @@ -118,10 +118,11 @@ func TestS3EndpointAndDigestsEnforceStorageContract(t *testing.T) { } func TestOSSSettingKeepsS3SecretsWhenLocationChanges(t *testing.T) { + t.Setenv("CANVAS_ALLOWED_PRIVATE_UPSTREAM_HOSTS", "127.0.0.1") current := ossSettingValue{ Provider: s3Provider, Region: "us-east-1", - Endpoint: "https://s3.example.com", + Endpoint: "https://127.0.0.1", Bucket: "old-bucket", AccessKeyID: "access-id", AccessKeySecret: "secret-value", @@ -133,7 +134,7 @@ func TestOSSSettingKeepsS3SecretsWhenLocationChanges(t *testing.T) { Provider: s3Provider, S3Preset: "custom", Region: "us-east-1", - Endpoint: "https://s3.example.com", + Endpoint: "https://127.0.0.1", Bucket: "new-bucket", AccessKeyID: "access-id", PathPrefix: "changed-prefix", diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index 99700edf2..6ebe589f3 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -5,6 +5,14 @@ description: 影策已实现功能的手工验证清单与未确认边界 # 待测试 +## 创作输入区参考内容管理 + +- [x] 自动化覆盖首个、中间和末尾参考内容按稳定 ID 独立删除,且删除后其余顺序保持不变。 +- [x] 折叠态提供明确的“查看全部”入口,展开态提供图片、视频、音频、文件筛选、清空全部和收起入口。 +- [x] 在桌面浏览器验证上方“参考内容”打开素材选择、折叠堆叠态中间卡片删除、展开态任意卡片删除、筛选、拖动排序和收起。 +- [ ] 在真实登录数据下验证“清空全部素材”同步移除全部附件引用,不影响技能引用和素材库原始文件。 +- [ ] 在触屏及窄窗口验证删除按钮无需悬停也可见,素材轨道可横向浏览且不会遮挡提示词和底部控制区。 + ## Agnes Video 2.5 JSON 协议 - [x] 前端专项测试确认 Agnes 创建请求使用 `application/json`,不再发送 `input_reference[]`、`resolution_name`、`preset` 或像素型 `size`。 diff --git a/web/scripts/director-p0-chrome-e2e.mjs b/web/scripts/director-p0-chrome-e2e.mjs index d661dc83a..419037acd 100644 --- a/web/scripts/director-p0-chrome-e2e.mjs +++ b/web/scripts/director-p0-chrome-e2e.mjs @@ -672,7 +672,15 @@ async function saveFailureCloseGuard(cdp, baseUrl) { const stayClicked = await cdp.clickText("留在导演台"); if (!stayClicked) throw new Error("F: 留在导演台 button not clickable"); - const modalGone = await cdp.poll(`!document.querySelector('.ant-modal-confirm')`, "modal dismissed", 20000); + const modalGone = await cdp.poll( + `![...document.querySelectorAll('.ant-modal-confirm')].some((modal) => { + const rect = modal.getBoundingClientRect(); + const style = getComputedStyle(modal); + return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden' && Number(style.opacity) > 0; + })`, + "modal dismissed", + 20000, + ); assert(modalGone, "F6 confirm dialog dismissed after choosing 留在导演台"); await sleep(1000); diff --git a/web/src/components/model-logo.tsx b/web/src/components/model-logo.tsx index a9a1126c7..d26e3354e 100644 --- a/web/src/components/model-logo.tsx +++ b/web/src/components/model-logo.tsx @@ -10,7 +10,7 @@ type LobeIconComponent = ComponentType & { size?: number // 只允许按需加载 Mono 组件。这里不能使用 eager glob:模型 Logo 目录有数百个 provider 模块, // eager 会让每次进入工作区都发起数百个开发模块请求,即使用户从未打开 Logo 选择器。 -const iconModules = import.meta.glob("../../node_modules/@lobehub/icons/es/*/components/Mono.js", { import: "default" }); +const iconModules = "Bun" in globalThis ? {} : import.meta.glob("../../node_modules/@lobehub/icons/es/*/components/Mono.js", { import: "default" }); const iconLoaders = Object.fromEntries( Object.entries(iconModules) .map(([path, loader]) => [path.match(/\/([^/]+)\/components\/Mono\.js$/)?.[1], loader]) diff --git a/web/src/lib/model-pricing.ts b/web/src/lib/model-pricing.ts index 29babbcff..cd3a125d1 100644 --- a/web/src/lib/model-pricing.ts +++ b/web/src/lib/model-pricing.ts @@ -13,16 +13,7 @@ type ModelCreditCost = { logicalPriceTiers?: ModelPriceTier[]; }; -export function requestCreditCost(options: { - channelMode: string; - modelCosts?: ModelCreditCost[]; - model: string; - count?: string | number; - seconds?: string | number; - capability?: ModelCapability; - config?: AiConfig; - requirements?: ModelRequirements; -}) { +export function requestCreditCost(options: { channelMode: string; modelCosts?: ModelCreditCost[]; model: string; count?: string | number; seconds?: string | number; capability?: ModelCapability; config?: AiConfig; requirements?: ModelRequirements }) { if (options.channelMode !== "remote") return null; const cost = options.modelCosts?.find((item) => item.model === options.model) || null; if (!cost) return null; @@ -86,9 +77,7 @@ export function modelQuoteRequest(config: AiConfig, value: string, capability?: } function creditAmount(billingMode: "fixed_request" | "per_second", unitPriceMicrocredits: number, count?: string | number, seconds?: string | number) { - const quantity = billingMode === "per_second" - ? Math.max(1, Math.floor(Math.abs(Number(seconds)) || 1)) - : Math.max(1, Math.floor(Math.abs(Number(count)) || 1)); + const quantity = billingMode === "per_second" ? Math.max(1, Math.floor(Math.abs(Number(seconds)) || 1)) : Math.max(1, Math.floor(Math.abs(Number(count)) || 1)); return (unitPriceMicrocredits / 1_000_000) * quantity; } diff --git a/web/src/pages/create/index.tsx b/web/src/pages/create/index.tsx index 46c291b2e..d2ecc3cc7 100644 --- a/web/src/pages/create/index.tsx +++ b/web/src/pages/create/index.tsx @@ -1,7 +1,7 @@ -import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type CSSProperties, type FocusEvent, type MouseEvent, type PointerEvent, type ReactNode, type RefObject } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type CSSProperties, type PointerEvent, type ReactNode, type RefObject } from "react"; import { App, Button, Drawer, Modal, Popover, Spin, Tooltip } from "antd"; import { Reorder } from "motion/react"; -import { ArrowDown, ArrowUp, Check, ChevronDown, ChevronLeft, ChevronRight, Clapperboard, Clock3, Copy, Download, FileText, Film, FolderOpen, History, Image as ImageIcon, LoaderCircle, Maximize2, MessageSquareText, Music2, Paperclip, Plus, RefreshCw, Search, SlidersHorizontal, Sparkles, Trash2, WandSparkles, X } from "lucide-react"; +import { ArrowDown, ArrowUp, Check, ChevronDown, ChevronLeft, ChevronRight, Clapperboard, Clock3, Copy, Download, FileText, Film, FolderOpen, History, Image as ImageIcon, LoaderCircle, Maximize2, MessageSquareText, Minimize2, Music2, Paperclip, Plus, RefreshCw, Search, SlidersHorizontal, Sparkles, Trash2, WandSparkles, X } from "lucide-react"; import { Link } from "react-router"; import { AIMessageMarkdown } from "@/components/ai/ai-message-markdown"; @@ -44,7 +44,7 @@ import { promptOptimizerPlugin, PROMPT_OPTIMIZER_PLUGIN_ID } from "@/lib/plugins import { createPluginHostContext } from "@/services/plugin-host"; import { usePluginStore } from "@/stores/use-plugin-store"; import { buildCreationMentionReferences, creationReferenceMetadata, displayCreationPrompt, expandCreationPrompt, reconcileCreationAttachmentLimit, removeCreationReferenceTokens, replaceCreationAttachmentReference, selectedCreationReferences, type CreationReference } from "./creation-references"; -import { creationAttachmentFromAsset, creationAttachmentFromAudio, creationAttachmentFromAudioAsset, creationAttachmentFromDocument, creationAttachmentFromExternalAsset, creationAttachmentFromImage, creationAttachmentFromVideo, creationAttachmentFromVideoAsset, creationAttachmentKind, creationAudioAsset, creationFileAccepted, creationImageAsset, creationMediaAspectRatio, creationUploadAccept, creationVideoAsset, splitCreationAttachments, type CreationAttachment } from "./creation-assets"; +import { creationAttachmentFromAsset, creationAttachmentFromAudio, creationAttachmentFromAudioAsset, creationAttachmentFromDocument, creationAttachmentFromExternalAsset, creationAttachmentFromImage, creationAttachmentFromVideo, creationAttachmentFromVideoAsset, creationAttachmentKind, creationAudioAsset, creationFileAccepted, creationImageAsset, creationMediaAspectRatio, creationUploadAccept, creationVideoAsset, removeCreationAttachment, splitCreationAttachments, type CreationAttachment } from "./creation-assets"; type CreationMode = "text" | "image" | "video"; type CreationViewMode = "chat" | "storyboard"; @@ -472,10 +472,17 @@ export default function CreatePage() { const removeAttachment = (id: string) => { const reference = mentionReferences.find((item) => item.attachmentId === id); - setAttachments((current) => current.filter((item) => item.id !== id)); + setAttachments((current) => removeCreationAttachment(current, id)); if (reference) setPrompt((current) => removeCreationReferenceTokens(current, [reference])); }; + const clearAttachments = () => { + const attachmentIds = new Set(attachments.map((item) => item.id)); + const references = mentionReferences.filter((item) => item.attachmentId && attachmentIds.has(item.attachmentId)); + setAttachments([]); + if (references.length) setPrompt((current) => removeCreationReferenceTokens(current, references)); + }; + const reorderAttachments = useCallback((next: CreationAttachment[]) => { attachmentsRef.current = next; setAttachments(next); @@ -886,6 +893,7 @@ export default function CreatePage() { maxReferences, references: mentionReferences, onRemoveAttachment: removeAttachment, + onClearAttachments: clearAttachments, onReorderAttachments: reorderAttachments, onReplaceAttachment: replaceReferenceFromTrack, onReplaceReferenceFiles: replaceReferenceFromFiles, @@ -1132,12 +1140,10 @@ function CreationAttachmentThumbnail({ item, onPreview, onRemove }: { const kind = creationAttachmentKind(item); const previewable = kind === "image" || kind === "video"; const url = (kind === "video" ? item.url : item.previewUrl) || ""; + const content = kind === "video" ?