feat(imitation): add article imitation create flow
Introduce 仿写创作: new list and create views, backend imitation service and prompt templates, worker task routing for imitation jobs, and one-click rewrite triggers from question citation sources. Surface source article URL/title on article list/detail, and restrict failed articles to delete-only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -94,6 +94,8 @@ type ArticleListItem struct {
|
||||
GenerationErrorMessage *string `json:"generation_error_message"`
|
||||
WordCount int `json:"word_count"`
|
||||
SourceLabel *string `json:"source_label"`
|
||||
SourceURL *string `json:"source_url,omitempty"`
|
||||
SourceTitle *string `json:"source_title,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
@@ -127,6 +129,8 @@ type ArticleDetailResponse struct {
|
||||
MarkdownContent *string `json:"markdown_content"`
|
||||
WordCount int `json:"word_count"`
|
||||
SourceLabel *string `json:"source_label"`
|
||||
SourceURL *string `json:"source_url,omitempty"`
|
||||
SourceTitle *string `json:"source_title,omitempty"`
|
||||
VersionNo *int `json:"version_no"`
|
||||
GenerationErrorMessage *string `json:"generation_error_message"`
|
||||
WizardState map[string]interface{} `json:"wizard_state,omitempty"`
|
||||
@@ -205,7 +209,11 @@ func (s *ArticleService) loadArticles(ctx context.Context, tenantID int64, param
|
||||
argIdx++
|
||||
}
|
||||
if params.Keyword != nil {
|
||||
countQuery += ` AND COALESCE(av.title, NULLIF(a.wizard_state_json ->> 'title', ''), NULLIF(gt.input_params_json ->> 'title', '')) ILIKE '%' || $` + strconv.Itoa(argIdx) + ` || '%'`
|
||||
countQuery += ` AND (
|
||||
COALESCE(av.title, NULLIF(a.wizard_state_json ->> 'title', ''), NULLIF(gt.input_params_json ->> 'title', '')) ILIKE '%' || $` + strconv.Itoa(argIdx) + ` || '%'
|
||||
OR COALESCE(NULLIF(a.wizard_state_json ->> 'source_title', ''), NULLIF(gt.input_params_json ->> 'source_title', '')) ILIKE '%' || $` + strconv.Itoa(argIdx) + ` || '%'
|
||||
OR COALESCE(NULLIF(a.wizard_state_json ->> 'source_url', ''), NULLIF(gt.input_params_json ->> 'source_url', '')) ILIKE '%' || $` + strconv.Itoa(argIdx) + ` || '%'
|
||||
)`
|
||||
countArgs = append(countArgs, *params.Keyword)
|
||||
argIdx++
|
||||
}
|
||||
@@ -274,7 +282,11 @@ func (s *ArticleService) loadArticles(ctx context.Context, tenantID int64, param
|
||||
listIdx++
|
||||
}
|
||||
if params.Keyword != nil {
|
||||
listQuery += ` AND COALESCE(av.title, NULLIF(a.wizard_state_json ->> 'title', ''), NULLIF(gt.input_params_json ->> 'title', '')) ILIKE '%' || $` + strconv.Itoa(listIdx) + ` || '%'`
|
||||
listQuery += ` AND (
|
||||
COALESCE(av.title, NULLIF(a.wizard_state_json ->> 'title', ''), NULLIF(gt.input_params_json ->> 'title', '')) ILIKE '%' || $` + strconv.Itoa(listIdx) + ` || '%'
|
||||
OR COALESCE(NULLIF(a.wizard_state_json ->> 'source_title', ''), NULLIF(gt.input_params_json ->> 'source_title', '')) ILIKE '%' || $` + strconv.Itoa(listIdx) + ` || '%'
|
||||
OR COALESCE(NULLIF(a.wizard_state_json ->> 'source_url', ''), NULLIF(gt.input_params_json ->> 'source_url', '')) ILIKE '%' || $` + strconv.Itoa(listIdx) + ` || '%'
|
||||
)`
|
||||
listArgs = append(listArgs, *params.Keyword)
|
||||
listIdx++
|
||||
}
|
||||
@@ -311,6 +323,7 @@ func (s *ArticleService) loadArticles(ctx context.Context, tenantID int64, param
|
||||
item.SourceType = dbSourceType
|
||||
item.GenerationMode = resolveArticleGenerationMode(dbSourceType, inputParamsJSON)
|
||||
item.Platforms = resolveArticlePlatforms(wizardStateJSON, inputParamsJSON)
|
||||
item.SourceURL, item.SourceTitle = resolveArticleImitationSourceInfo(dbSourceType, wizardStateJSON, inputParamsJSON)
|
||||
items = append(items, item)
|
||||
}
|
||||
return &ArticleListResponse{Items: items, Total: total, Page: params.Page, PageSize: params.PageSize}, nil
|
||||
@@ -361,6 +374,7 @@ func (s *ArticleService) loadArticleDetail(ctx context.Context, tenantID, articl
|
||||
}
|
||||
item.GenerationMode = resolveArticleGenerationMode(dbSourceType, inputParamsJSON)
|
||||
item.Platforms = resolveArticlePlatforms(wizardStateJSON, inputParamsJSON)
|
||||
item.SourceURL, item.SourceTitle = resolveArticleImitationSourceInfo(dbSourceType, wizardStateJSON, inputParamsJSON)
|
||||
item.CoverAssetURL = resolveArticleCoverAssetURL(wizardStateJSON)
|
||||
item.CoverImageAssetID = resolveArticleCoverImageAssetID(wizardStateJSON)
|
||||
if item.MarkdownContent != nil || item.CoverImageAssetID != nil {
|
||||
@@ -1080,6 +1094,24 @@ func resolveArticleGenerationMode(sourceType string, inputParamsJSON []byte) *st
|
||||
return &mode
|
||||
}
|
||||
|
||||
func resolveArticleImitationSourceInfo(sourceType string, wizardStateJSON, inputParamsJSON []byte) (*string, *string) {
|
||||
if sourceType != articleImitationSourceType {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
sourceURL := strings.TrimSpace(extractStringFromJSONPayload(wizardStateJSON, "source_url"))
|
||||
if sourceURL == "" {
|
||||
sourceURL = strings.TrimSpace(extractStringFromJSONPayload(inputParamsJSON, "source_url"))
|
||||
}
|
||||
|
||||
sourceTitle := strings.TrimSpace(extractStringFromJSONPayload(wizardStateJSON, "source_title"))
|
||||
if sourceTitle == "" {
|
||||
sourceTitle = strings.TrimSpace(extractStringFromJSONPayload(inputParamsJSON, "source_title"))
|
||||
}
|
||||
|
||||
return nilIfEmptyString(sourceURL), nilIfEmptyString(sourceTitle)
|
||||
}
|
||||
|
||||
func nilIfEmptyString(value string) *string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
|
||||
Reference in New Issue
Block a user