From 27c8066e6da9f0d4754dc0e5c40097049fe730cf Mon Sep 17 00:00:00 2001
From: ddcat666 <26564566+ddcat666@users.noreply.github.com>
Date: Mon, 12 Oct 2026 00:37:15 +0800
Subject: [PATCH] =?UTF-8?q?fix(*):=20=E8=B4=A8=E9=87=8F=E6=A3=80=E6=9F=A5?=
=?UTF-8?q?=20-=20=E4=BF=AE=E5=A4=8D=E9=A2=84=E6=BC=94=E4=BF=9D=E5=AD=98?=
=?UTF-8?q?=E3=80=81=E5=A2=9E=E9=87=8F=E5=90=8C=E6=AD=A5=E4=B8=8E=E7=BC=BA?=
=?UTF-8?q?=E5=A4=B1=E8=B5=84=E6=BA=90=E6=81=A2=E5=A4=8D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
CHANGELOG.md | 8 +++
VERSION | 2 +-
.../app/channel_model_pricing_test.go | 24 ++++---
.../internal/app/channel_presentation_test.go | 4 +-
backend/internal/app/model_catalog.go | 6 +-
backend/internal/app/resource_generated.go | 18 +++--
.../canvas-agent-video-approval-settings.tsx | 46 ++++++++----
.../canvas/canvas-cloud-agent-panel-parts.tsx | 6 +-
.../canvas/previs/canvas-previs-workbench.tsx | 21 +++++-
.../previs/use-previs-save-coordinator.ts | 16 ++++-
.../lib/canvas/canvas-generation-result.ts | 4 +-
.../lib/canvas/canvas-generation-task-sync.ts | 8 +--
web/src/lib/model-picker-groups.ts | 3 +-
web/src/pages/settings/model-default-grid.tsx | 4 +-
web/src/services/agent-canvas-sync.ts | 37 ++++++++--
web/src/services/api/resources.ts | 5 +-
web/src/services/canvas-missing-resources.ts | 70 +++++++++++++++++++
web/src/stores/use-config-store.ts | 2 +-
web/test/canvas-operations-build.test.ts | 42 +++++++----
web/test/model-picker-groups.test.ts | 2 +-
20 files changed, 257 insertions(+), 71 deletions(-)
create mode 100644 web/src/services/canvas-missing-resources.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ad0b7b813..42ac40a70 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# CHANGELOG
+## v1.6.6
+
+- 画布云端写入改为可幂等重放的 operations 事务,支持字段级并发合并、冲突保护和删除后迟到更新拦截;完善预演场景保存、版本同步与 Agent 变更撤销。
+- Agent 视频审批可在确认前调整模型与生成规格;完善分镜校验、生成资源访问与任务结果恢复。
+- 优化大画布节点渲染预算和性能模式,统一工作区与管理后台侧栏体验。
+- 修复 PostgreSQL 连接池 keepalive 与闲置事务限制、短剧关闭时误隐藏角色库相关接口、灵感页刷新重定向和模型图片分辨率价格档识别。
+- 升级注意:数据库 schema 升至 v50,新增画布操作幂等回执表,启动时自动迁移。
+
## v1.6.5
- Agent 重复读取同一结果或同一张图时改为软收敛:运行保持进行中,下一步暂时收起只读工具,仍可回复、提问或执行写操作。
diff --git a/VERSION b/VERSION
index ef0519020..0591f3366 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-v1.6.5
+v1.6.6
diff --git a/backend/internal/app/channel_model_pricing_test.go b/backend/internal/app/channel_model_pricing_test.go
index aa5e61ee2..81058b5af 100644
--- a/backend/internal/app/channel_model_pricing_test.go
+++ b/backend/internal/app/channel_model_pricing_test.go
@@ -219,17 +219,7 @@ func TestRepriceTierFieldValidation(t *testing.T) {
}
}
-func TestNormalizeChannelModelTierSelectorAcceptsResolutionTypo(t *testing.T) {
- selector, resolution, videoSeconds, err := normalizeChannelModelTierSelector("image", ChannelModelPriceTierRequest{
- Selector: map[string]string{"resoluton": "2K"},
- })
- if err != nil {
- t.Fatal(err)
- }
- if selector["resolution"] != "2k" || selector["resoluton"] != "" || resolution != "*" || videoSeconds != 0 {
- t.Fatalf("selector=%v resolution=%q videoSeconds=%d", selector, resolution, videoSeconds)
- }
-}
+func TestBatchRepricingRollsBackOnConcurrentChange(t *testing.T) {
for _, target := range []string{"model", "tier"} {
t.Run(target, func(t *testing.T) {
svc, db, _ := seedRepricingModels(t)
@@ -265,3 +255,15 @@ func TestNormalizeChannelModelTierSelectorAcceptsResolutionTypo(t *testing.T) {
})
}
}
+
+func TestNormalizeChannelModelTierSelectorAcceptsResolutionTypo(t *testing.T) {
+ selector, resolution, videoSeconds, err := normalizeChannelModelTierSelector("image", ChannelModelPriceTierRequest{
+ Selector: map[string]string{"resoluton": "2K"},
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ if selector["resolution"] != "2k" || selector["resoluton"] != "" || resolution != "*" || videoSeconds != 0 {
+ t.Fatalf("selector=%v resolution=%q videoSeconds=%d", selector, resolution, videoSeconds)
+ }
+}
diff --git a/backend/internal/app/channel_presentation_test.go b/backend/internal/app/channel_presentation_test.go
index ca87d7587..7955a9340 100644
--- a/backend/internal/app/channel_presentation_test.go
+++ b/backend/internal/app/channel_presentation_test.go
@@ -49,7 +49,7 @@ func TestChannelPresentationUsesModelLabelsAndPublicOrder(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- if len(catalog) != 2 || catalog[0].ID != "second" || catalog[0].Name != "渠道second" || catalog[0].DisplayName != "渠道second" {
+ if len(catalog) != 2 || catalog[0].ID != "second" || catalog[0].Name != "" || catalog[0].DisplayName != "" {
t.Fatalf("public catalog: %#v", catalog)
}
legacy, err := svc.PublicSystemChannels()
@@ -74,7 +74,7 @@ func TestChannelPresentationUsesModelLabelsAndPublicOrder(t *testing.T) {
t.Fatal(err)
}
catalog, err = svc.publicSystemChannelCatalog(nil)
- if err != nil || catalog[0].Name != "渠道second" {
+ if err != nil || catalog[0].Name != "" || catalog[0].DisplayName != "" {
t.Fatalf("channel name: %v %v", catalog, err)
}
for _, invalid := range []int{-1, 1000000} {
diff --git a/backend/internal/app/model_catalog.go b/backend/internal/app/model_catalog.go
index e5466b27e..d4c2ef6ac 100644
--- a/backend/internal/app/model_catalog.go
+++ b/backend/internal/app/model_catalog.go
@@ -169,10 +169,12 @@ func (s *Service) publicSystemChannelCatalog(intent *ModelRequestIntent) ([]Publ
}
if len(publicModels) > 0 {
+ // 系统渠道的真实名称属于内部部署信息(供应商、中转站、内部命名等),
+ // 不得通过普通用户可访问的创作目录泄漏。前端只依赖 ID 做分组,名称留空后由客户端统一展示为“系统模型”。
result = append(result, PublicChannelCatalog{
ID: channel.ID,
- Name: channel.Name,
- DisplayName: channel.Name,
+ Name: "",
+ DisplayName: "",
SortOrder: channel.SortOrder,
Models: publicModels,
})
diff --git a/backend/internal/app/resource_generated.go b/backend/internal/app/resource_generated.go
index db3e082d9..cc611f560 100644
--- a/backend/internal/app/resource_generated.go
+++ b/backend/internal/app/resource_generated.go
@@ -6,8 +6,10 @@ package app
import (
"bytes"
+ "context"
"encoding/base64"
"encoding/json"
+ "errors"
"fmt"
"image"
"io"
@@ -182,22 +184,30 @@ func downloadRemoteResource(rawURL string, maxBytes int64) (remoteResourcePayloa
if err != nil {
return remoteResourcePayload{}, err
}
- client := OutboundHTTPClient(90 * time.Second)
+ client := OutboundHTTPClient(3 * time.Minute)
req, err := http.NewRequest(http.MethodGet, parsed.String(), nil)
if err != nil {
return remoteResourcePayload{}, err
}
resp, err := client.Do(req)
if err != nil {
- return remoteResourcePayload{}, err
+ // 把下载层网络错误统一映射为可公开的 502/504 文案,避免在恢复接口上被 failService
+ // 折叠成通用的“系统处理失败”。同时保留真实原因在 Cause 里供诊断日志追踪。
+ if errors.Is(err, context.DeadlineExceeded) {
+ return remoteResourcePayload{}, WrapAppError(http.StatusGatewayTimeout, "远程资源下载超时,请检查地址是否可访问或稍后重试", err)
+ }
+ return remoteResourcePayload{}, WrapAppError(http.StatusBadGateway, "远程资源下载失败,请检查地址是否可访问", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
- return remoteResourcePayload{}, fmt.Errorf("远程资源下载失败:%s", resp.Status)
+ return remoteResourcePayload{}, WrapAppError(http.StatusBadGateway, fmt.Sprintf("远程资源下载失败:%s", resp.Status), nil)
}
data, err := io.ReadAll(io.LimitReader(resp.Body, maxBytes))
if err != nil {
- return remoteResourcePayload{}, err
+ if errors.Is(err, context.DeadlineExceeded) {
+ return remoteResourcePayload{}, WrapAppError(http.StatusGatewayTimeout, "远程资源下载超时,请检查地址是否可访问或稍后重试", err)
+ }
+ return remoteResourcePayload{}, WrapAppError(http.StatusBadGateway, "远程资源读取失败,请检查地址是否可访问", err)
}
if int64(len(data)) >= maxBytes {
return remoteResourcePayload{}, BadAuthRequest(fmt.Sprintf("远程资源必须小于 %s", formatStorageLimit(maxBytes)))
diff --git a/web/src/components/canvas/canvas-agent-video-approval-settings.tsx b/web/src/components/canvas/canvas-agent-video-approval-settings.tsx
index da83aa349..38debac75 100644
--- a/web/src/components/canvas/canvas-agent-video-approval-settings.tsx
+++ b/web/src/components/canvas/canvas-agent-video-approval-settings.tsx
@@ -8,13 +8,7 @@ import { resolveCompatibleModel, type ModelRequirements } from "@/lib/model-sele
import type { AgentMediaSettings } from "@/services/api/agent";
import { PUBLIC_MODEL_CATALOG_ID, useEffectiveConfig } from "@/stores/use-config-store";
-export function CanvasAgentVideoApprovalSettings({ initial, value, theme, disabled, onChange }: {
- initial: AgentVideoApproval;
- value?: AgentMediaSettings;
- theme: CanvasTheme;
- disabled: boolean;
- onChange: (value: AgentMediaSettings) => void;
-}) {
+export function CanvasAgentVideoApprovalSettings({ initial, value, theme, disabled, onChange }: { initial: AgentVideoApproval; value?: AgentMediaSettings; theme: CanvasTheme; disabled: boolean; onChange: (value: AgentMediaSettings) => void }) {
const effective = useEffectiveConfig();
const config = useMemo(() => ({ ...effective, channels: effective.channels.filter((channel) => channel.scope === "system" || channel.id === PUBLIC_MODEL_CATALOG_ID) }), [effective]);
const settings = value || initial;
@@ -42,7 +36,15 @@ export function CanvasAgentVideoApprovalSettings({ initial, value, theme, disabl
const profile = modelCapabilityConfigFor(config, next).video;
if (!profile) return;
const normalized = normalizeVideoValue(profile, { seconds: duration, ratio: settings.size, resolution: settings.quality });
- onChange({ ...agentApprovalModelSelection(config, next), size: normalized.ratio, quality: normalized.resolution, durationSeconds: Number(normalized.seconds), videoGenerateAudio: settings.videoGenerateAudio, videoWatermark: settings.videoWatermark, videoArkPrivateAssetUpload: settings.videoArkPrivateAssetUpload });
+ onChange({
+ ...agentApprovalModelSelection(config, next),
+ size: normalized.ratio,
+ quality: normalized.resolution,
+ durationSeconds: Number(normalized.seconds),
+ videoGenerateAudio: settings.videoGenerateAudio,
+ videoWatermark: settings.videoWatermark,
+ videoArkPrivateAssetUpload: settings.videoArkPrivateAssetUpload,
+ });
};
const changeOption = (key: "vquality" | "size" | "videoSeconds" | "videoGenerateAudio" | "videoWatermark" | "videoArkPrivateAssetUpload", next: string) => {
@@ -75,12 +77,32 @@ export function CanvasAgentVideoApprovalSettings({ initial, value, theme, disabl
return (
);
}
diff --git a/web/src/components/canvas/canvas-cloud-agent-panel-parts.tsx b/web/src/components/canvas/canvas-cloud-agent-panel-parts.tsx
index 85cf58c70..1a978fd25 100644
--- a/web/src/components/canvas/canvas-cloud-agent-panel-parts.tsx
+++ b/web/src/components/canvas/canvas-cloud-agent-panel-parts.tsx
@@ -571,7 +571,11 @@ export function ApprovalCard({
无法确认具体目标,继续前请重新读取画布。
)}
- {imageApproval ? : videoApproval ? : null}
+ {imageApproval ? (
+
+ ) : videoApproval ? (
+
+ ) : null}
diff --git a/web/src/components/canvas/previs/canvas-previs-workbench.tsx b/web/src/components/canvas/previs/canvas-previs-workbench.tsx
index 6312fb126..8ac6b80c1 100644
--- a/web/src/components/canvas/previs/canvas-previs-workbench.tsx
+++ b/web/src/components/canvas/previs/canvas-previs-workbench.tsx
@@ -79,7 +79,7 @@ export function CanvasPrevisWorkbench({
onDeleteImageNode: (nodeId: string) => void;
onFlush?: () => void | Promise;
}) {
- const { message } = App.useApp();
+ const { message, modal } = App.useApp();
const theme = canvasThemes[useActiveTheme()];
const effectiveCanvasId = useMemo(() => {
const explicit = canvasId?.trim();
@@ -241,6 +241,7 @@ export function CanvasPrevisWorkbench({
resetWorkbench();
}, [open, resetWorkbench, scene, writeDraft]);
+ // 打开会话时检查合法本地恢复候选
// 父级项目收到 Agent 的预演台语义补丁后,scene.id 不变但 updatedAt 会变化;
// 不能只按 scene.id 忽略,否则打开着的工作台永远看不到 Agent 的修改。
useEffect(() => {
@@ -257,7 +258,7 @@ export function CanvasPrevisWorkbench({
setSelectedBone(null);
setPlayhead(0);
message.info("Agent 已更新预演场景");
- }, [message, open, scene, setPlayhead, setSelectedBone, setSelectedLightId, setSelectedObjectId, writeDraft]);
+ }, [message, modal, open, scene, writeDraft]);
const activeShot = draft?.shots?.find((item) => item.id === draft.activeShotId) || draft?.shots?.[0] || null;
const activeCamera = draft?.cameras?.find((item) => item.id === activeShot?.cameraId) || draft?.cameras?.[0] || null;
@@ -355,6 +356,7 @@ export function CanvasPrevisWorkbench({
[writeAndPublish],
);
+ // 打开会话时检查合法本地恢复候选之后,挂载暂存手势生命周期。
// 暂存手势的终止生命周期:常驻安装,非活跃时 end 为空操作。
useEffect(
() =>
@@ -466,7 +468,14 @@ export function CanvasPrevisWorkbench({
return;
}
recordPrevisDiagnostic("PREVIS_CLOSE_BLOCKED", { sceneId: draftRef.current?.id, saveOutcome: "stay", revision: saveController.progress.revision });
- message.error(decision.message);
+ modal.confirm({
+ title: "保存未完成",
+ content: decision.message,
+ okText: "放弃修改并关闭",
+ cancelText: "留在预演台",
+ okButtonProps: { danger: true },
+ onOk: onClose,
+ });
closingRef.current = false;
})();
};
@@ -1246,6 +1255,12 @@ export function CanvasPrevisWorkbench({
{saveIndicator.label}
+ {saveController.progress.status === "error" ? (
+
+ ) : null}
{onboardingScope ? (