fix(canvas): 画布任务恢复与价格邮件展示 - 修复刷新找回和名称同步
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
// 列表只暴露首个可访问媒体地址,避免把完整生成结果和内嵌数据带回前端。
|
||||
|
||||
@@ -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" {
|
||||
|
||||
@@ -5,6 +5,15 @@ description: 当前项目已实现功能的手工验证清单与未确认边界
|
||||
|
||||
# 待测试
|
||||
|
||||
## 验证码邮件
|
||||
|
||||
- [ ] 邮件配置保存自定义发件名称后,新发送的注册验证码邮件发件名称、标题和正文保持一致;密码重置验证码邮件同样使用该名称。
|
||||
- [ ] 发件名称留空或使用默认名称时,验证码邮件继续继承站点品牌名称;再次修改自定义发件名称后,新邮件使用最新名称。
|
||||
|
||||
## 画布任务恢复
|
||||
|
||||
- [ ] 画布中存在较多历史生成任务时,刷新页面后仍能恢复当前节点对应的任务;任务完成后结果正常回填,不再误显示“页面刷新后找不到对应任务,请重新生成”。
|
||||
|
||||
## 图片规格价格
|
||||
|
||||
- [ ] 仅配置 1K、2K、4K 三档价格(无统一兜底),画布逐一切换分辨率,显示与对应档位一致的积分价格。
|
||||
|
||||
@@ -354,6 +354,7 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
|
||||
requirements={resolvedRequirements}
|
||||
onMissingConfig={() => navigateToSettings({ continueCreation: true })}
|
||||
showSelectedPrice={false}
|
||||
showOptionPrices={creditsEnabled}
|
||||
variant="creation"
|
||||
showConfiguredModelName
|
||||
/>
|
||||
|
||||
@@ -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 [];
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user