diff --git a/backend/internal/service/email.go b/backend/internal/service/email.go index b5461a10b..188cdc13b 100644 --- a/backend/internal/service/email.go +++ b/backend/internal/service/email.go @@ -195,9 +195,8 @@ func (s *Service) SendRegistrationEmailCode(rawEmail string) error { if err := s.repo.Create(&record); err != nil { return err } - brandName := s.appearanceBrandName() - setting = resolveEmailSender(setting, brandName) - if err := s.deliverEmail(setting, email, brandName+"注册验证码", registrationEmailBody(brandName, code)); err != nil { + setting = resolveEmailSender(setting, s.appearanceBrandName()) + if err := s.deliverEmail(setting, email, setting.FromName+"注册验证码", registrationEmailBody(setting.FromName, code)); err != nil { cleanupErr := s.repo.DeleteEmailVerificationCode(record.ID) if cleanupErr != nil { return errors.Join( diff --git a/backend/internal/service/password_reset.go b/backend/internal/service/password_reset.go index f4e99648e..432c60b62 100644 --- a/backend/internal/service/password_reset.go +++ b/backend/internal/service/password_reset.go @@ -74,9 +74,8 @@ func (s *Service) SendPasswordResetEmailCode(rawEmail string) error { if err := s.repo.Create(&record); err != nil { return err } - brandName := s.appearanceBrandName() - setting = resolveEmailSender(setting, brandName) - if err := s.deliverEmail(setting, email, brandName+"密码重置验证码", passwordResetEmailBody(brandName, code)); err != nil { + setting = resolveEmailSender(setting, s.appearanceBrandName()) + if err := s.deliverEmail(setting, email, setting.FromName+"密码重置验证码", passwordResetEmailBody(setting.FromName, code)); err != nil { if cleanupErr := s.repo.DeleteEmailVerificationCode(record.ID); cleanupErr != nil { log.Printf("password reset email cleanup failed: recipient=%s error=%v", maskedEmail(email), cleanupErr) } diff --git a/backend/internal/service/password_reset_test.go b/backend/internal/service/password_reset_test.go index bcccb10e5..495247c29 100644 --- a/backend/internal/service/password_reset_test.go +++ b/backend/internal/service/password_reset_test.go @@ -187,8 +187,38 @@ func TestPasswordResetDeliveryFailureRemovesUnusableCode(t *testing.T) { } } -func TestCustomAppearanceBrandsRegistrationAndPasswordResetEmails(t *testing.T) { +func TestSenderNameBrandsRegistrationAndPasswordResetEmails(t *testing.T) { + for _, tc := range []struct { + name string + fromName string + wantName string + }{ + {name: "default inherits appearance", fromName: defaultAppearanceBrandName, wantName: "HIMA Studio"}, + {name: "empty inherits appearance", fromName: "", wantName: "HIMA Studio"}, + {name: "custom sender overrides appearance", fromName: "Custom Canvas", wantName: "Custom Canvas"}, + {name: "custom sender is trimmed", fromName: " 自定义工作台 ", wantName: "自定义工作台"}, + } { + t.Run(tc.name, func(t *testing.T) { + assertRegistrationAndPasswordResetEmailBrand(t, tc.fromName, tc.wantName) + }) + } +} + +func assertRegistrationAndPasswordResetEmailBrand(t *testing.T, fromName, wantName string) { + t.Helper() svc, db := newPasswordResetTestService(t) + _, emailSetting, err := svc.readEmailSetting() + if err != nil { + t.Fatal(err) + } + emailSetting.FromName = fromName + settingJSON, err := json.Marshal(emailSetting) + if err != nil { + t.Fatal(err) + } + if err := db.Model(&model.SystemSetting{}).Where("key = ?", emailSettingKey).Update("value_json", string(settingJSON)).Error; err != nil { + t.Fatal(err) + } appearanceJSON, err := json.Marshal(AppearanceSetting{ SchemaVersion: appearanceSchemaVersion, BrandName: "HIMA Studio", @@ -237,13 +267,16 @@ func TestCustomAppearanceBrandsRegistrationAndPasswordResetEmails(t *testing.T) t.Fatalf("deliveries = %d, want 2", len(deliveries)) } for _, delivered := range deliveries { - if delivered.fromName != "HIMA Studio" || !strings.Contains(delivered.subject, "HIMA Studio") || !strings.Contains(delivered.body, "HIMA Studio") || strings.Contains(delivered.subject, defaultAppearanceBrandName) || strings.Contains(delivered.body, defaultAppearanceBrandName) { - t.Fatalf("email did not inherit custom appearance: %#v", delivered) + if delivered.fromName != wantName || !strings.Contains(delivered.subject, wantName) || !strings.Contains(delivered.body, wantName) || strings.Contains(delivered.subject, defaultAppearanceBrandName) || strings.Contains(delivered.body, defaultAppearanceBrandName) { + t.Fatalf("email did not use resolved sender name %q: %#v", wantName, delivered) } } - if deliveries[0].subject != "HIMA Studio注册验证码" || deliveries[1].subject != "HIMA Studio密码重置验证码" { + if deliveries[0].subject != wantName+"注册验证码" || deliveries[1].subject != wantName+"密码重置验证码" { t.Fatalf("unexpected branded subjects: %#v", deliveries) } + if !strings.Contains(deliveries[0].body, "你正在注册"+wantName+"。") { + t.Fatalf("registration body did not use resolved sender name: %q", deliveries[0].body) + } } func newPasswordResetTestService(t *testing.T) (*Service, *gorm.DB) { diff --git a/backend/internal/service/task_output.go b/backend/internal/service/task_output.go index a2c4845d8..bf2dc493b 100644 --- a/backend/internal/service/task_output.go +++ b/backend/internal/service/task_output.go @@ -41,6 +41,7 @@ type TaskSummary struct { } type TaskClientContext struct { + NodeID string `json:"nodeId,omitempty"` ConversationID string `json:"conversationId,omitempty"` MessageID string `json:"messageId,omitempty"` BatchIndex int `json:"batchIndex,omitempty"` @@ -134,6 +135,7 @@ func taskClientContext(raw string) *TaskClientContext { var input struct { Metadata struct { Source string `json:"source"` + NodeID string `json:"nodeId"` ConversationID string `json:"conversationId"` MessageID string `json:"messageId"` BatchIndex int `json:"batchIndex"` @@ -150,16 +152,20 @@ func taskClientContext(raw string) *TaskClientContext { return nil } metadata := input.Metadata + context := &TaskClientContext{NodeID: metadata.NodeID} if metadata.Source == "create-page" && metadata.ConversationID != "" && metadata.MessageID != "" { - return &TaskClientContext{ - ConversationID: metadata.ConversationID, - MessageID: metadata.MessageID, - BatchIndex: metadata.BatchIndex, - BatchCount: metadata.BatchCount, - } + context.ConversationID = metadata.ConversationID + context.MessageID = metadata.MessageID + context.BatchIndex = metadata.BatchIndex + context.BatchCount = metadata.BatchCount + return context } if metadata.ShotID != "" && metadata.WorkflowStepID != "" { - return &TaskClientContext{DomainProjectID: metadata.DomainProjectID, ShotID: metadata.ShotID, WorkflowStepID: metadata.WorkflowStepID, ArtifactType: metadata.ArtifactType} + context.DomainProjectID = metadata.DomainProjectID + context.ShotID = metadata.ShotID + context.WorkflowStepID = metadata.WorkflowStepID + context.ArtifactType = metadata.ArtifactType + return context } chapterOperation := "" if metadata.Operation == "chapter_character_breakdown" { @@ -168,13 +174,15 @@ func taskClientContext(raw string) *TaskClientContext { chapterOperation = "storyboard" } if chapterOperation == "" || metadata.DomainProjectID == "" || metadata.ChapterID == "" { - return nil - } - return &TaskClientContext{ - DomainProjectID: metadata.DomainProjectID, - ChapterID: metadata.ChapterID, - ChapterOperation: chapterOperation, + if context.NodeID == "" { + return nil + } + return context } + context.DomainProjectID = metadata.DomainProjectID + context.ChapterID = metadata.ChapterID + context.ChapterOperation = chapterOperation + return context } // 列表只暴露首个可访问媒体地址,避免把完整生成结果和内嵌数据带回前端。 diff --git a/backend/internal/service/task_output_test.go b/backend/internal/service/task_output_test.go index 3d34056d9..2e4401349 100644 --- a/backend/internal/service/task_output_test.go +++ b/backend/internal/service/task_output_test.go @@ -51,6 +51,13 @@ func TestTaskClientContextRequiresCreatePageMetadata(t *testing.T) { } } +func TestTaskClientContextPreservesCanvasNodeID(t *testing.T) { + context := taskClientContext(`{"mode":"image","metadata":{"nodeId":"canvas-node-1","source":"canvas"}}`) + if context == nil || context.NodeID != "canvas-node-1" { + t.Fatalf("canvas node context was not preserved: %+v", context) + } +} + func TestTaskClientContextProjectsChapterOperations(t *testing.T) { characters := taskClientContext(`{"metadata":{"domainProjectId":"project-1","chapterId":"chapter-1","operation":"chapter_character_breakdown"}}`) if characters == nil || characters.DomainProjectID != "project-1" || characters.ChapterID != "chapter-1" || characters.ChapterOperation != "characters" { diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index 42da8c3cc..53811381d 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -5,6 +5,15 @@ description: 当前项目已实现功能的手工验证清单与未确认边界 # 待测试 +## 验证码邮件 + +- [ ] 邮件配置保存自定义发件名称后,新发送的注册验证码邮件发件名称、标题和正文保持一致;密码重置验证码邮件同样使用该名称。 +- [ ] 发件名称留空或使用默认名称时,验证码邮件继续继承站点品牌名称;再次修改自定义发件名称后,新邮件使用最新名称。 + +## 画布任务恢复 + +- [ ] 画布中存在较多历史生成任务时,刷新页面后仍能恢复当前节点对应的任务;任务完成后结果正常回填,不再误显示“页面刷新后找不到对应任务,请重新生成”。 + ## 图片规格价格 - [ ] 仅配置 1K、2K、4K 三档价格(无统一兜底),画布逐一切换分辨率,显示与对应档位一致的积分价格。 diff --git a/web/src/components/canvas/canvas-node-prompt-panel.tsx b/web/src/components/canvas/canvas-node-prompt-panel.tsx index 2ec930f1a..edaa7736c 100644 --- a/web/src/components/canvas/canvas-node-prompt-panel.tsx +++ b/web/src/components/canvas/canvas-node-prompt-panel.tsx @@ -354,6 +354,7 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi requirements={resolvedRequirements} onMissingConfig={() => navigateToSettings({ continueCreation: true })} showSelectedPrice={false} + showOptionPrices={creditsEnabled} variant="creation" showConfiguredModelName /> diff --git a/web/src/pages/canvas/use-canvas-generation.ts b/web/src/pages/canvas/use-canvas-generation.ts index 39f217609..f5b6f411d 100644 --- a/web/src/pages/canvas/use-canvas-generation.ts +++ b/web/src/pages/canvas/use-canvas-generation.ts @@ -377,7 +377,7 @@ export function useCanvasGeneration({ projectId, domainProjectId, projectLoaded, const needsDiscovery = recoveryNodes.some((node) => !node.metadata?.taskId && !node.metadata?.agentGenerationContinuation?.taskId); const projectTasks = needsDiscovery ? ( - await listGenerationTasks(30, { projectId: startedProjectId }, undefined, signal).catch((error) => { + await listGenerationTasks(100, { projectId: startedProjectId }, undefined, signal).catch((error) => { if (!isCurrentProject()) throw error; return []; })