fix(canvas): 画布任务恢复与价格邮件展示 - 修复刷新找回和名称同步

This commit is contained in:
Rou
2026-09-06 00:44:08 +08:00
parent 61f13d8d4a
commit d4d8a9decd
8 changed files with 80 additions and 24 deletions

View File

@@ -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(

View File

@@ -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)
}

View File

@@ -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) {

View File

@@ -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
}
// 列表只暴露首个可访问媒体地址,避免把完整生成结果和内嵌数据带回前端。

View File

@@ -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" {

View File

@@ -5,6 +5,15 @@ description: 当前项目已实现功能的手工验证清单与未确认边界
# 待测试
## 验证码邮件
- [ ] 邮件配置保存自定义发件名称后,新发送的注册验证码邮件发件名称、标题和正文保持一致;密码重置验证码邮件同样使用该名称。
- [ ] 发件名称留空或使用默认名称时,验证码邮件继续继承站点品牌名称;再次修改自定义发件名称后,新邮件使用最新名称。
## 画布任务恢复
- [ ] 画布中存在较多历史生成任务时,刷新页面后仍能恢复当前节点对应的任务;任务完成后结果正常回填,不再误显示“页面刷新后找不到对应任务,请重新生成”。
## 图片规格价格
- [ ] 仅配置 1K、2K、4K 三档价格(无统一兜底),画布逐一切换分辨率,显示与对应档位一致的积分价格。

View File

@@ -354,6 +354,7 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
requirements={resolvedRequirements}
onMissingConfig={() => navigateToSettings({ continueCreation: true })}
showSelectedPrice={false}
showOptionPrices={creditsEnabled}
variant="creation"
showConfiguredModelName
/>

View File

@@ -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 [];
})