From 61979c695ec86ee7197a75be0cce82efc50daa35 Mon Sep 17 00:00:00 2001 From: walker <13750528578@163.com> Date: Thu, 11 Jun 2026 11:12:08 +0800 Subject: [PATCH] =?UTF-8?q?fix(xhs):=20=E7=BA=A6=E6=9D=9F=E7=94=B5?= =?UTF-8?q?=E5=AD=90=E6=A6=A8=E8=8F=9C=E8=AF=9D=E9=A2=98=E7=94=9F=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/mark2note/main.go | 80 ------------------------------ cmd/mark2note/main_publish_test.go | 60 ++++++++++++++++------ internal/ai/deck_builder_test.go | 2 +- internal/ai/topic_builder.go | 12 +++-- 4 files changed, 55 insertions(+), 99 deletions(-) diff --git a/cmd/mark2note/main.go b/cmd/mark2note/main.go index 67ff514..ccdecae 100644 --- a/cmd/mark2note/main.go +++ b/cmd/mark2note/main.go @@ -924,9 +924,6 @@ func buildAutoPublishXHSTopics(cfg config.Config, markdown string, title string, if len(overrideTags) > 0 { return xhs.NormalizePublishTopics(overrideTags), nil } - if topics := buildElectronicPicklesXHSTopics(markdown); len(topics) > 0 { - return topics, nil - } if cfg.XHS.Publish.TopicGeneration.Enabled == nil || !*cfg.XHS.Publish.TopicGeneration.Enabled { return nil, fmt.Errorf("xhs publish topic generation is disabled; pass --xhs-tags or enable xhs.publish.topic_generation.enabled") } @@ -945,83 +942,6 @@ func isElectronicPicklesMarkdown(markdown string) bool { return strings.Contains(markdown, "电子榨菜") && strings.Contains(markdown, "## 小红书卡片") } -func buildElectronicPicklesXHSTopics(markdown string) []string { - if !isElectronicPicklesMarkdown(markdown) { - return nil - } - topics := []string{"电子榨菜"} - topics = append(topics, electronicPicklesRelatedTopics(markdown)...) - return xhs.NormalizePublishTopics(topics) -} - -func electronicPicklesRelatedTopics(markdown string) []string { - sectionStart := strings.Index(markdown, "## 小红书卡片") - if sectionStart < 0 { - return nil - } - lines := strings.Split(markdown[sectionStart:], "\n") - out := make([]string, 0, 4) - seen := map[string]bool{} - add := func(topic string) { - if len(out) >= 4 { - return - } - normalized := xhs.NormalizePublishTopics([]string{topic}) - if len(normalized) == 0 || seen[normalized[0]] { - return - } - seen[normalized[0]] = true - out = append(out, normalized[0]) - } - for _, line := range lines { - trimmed := strings.TrimSpace(line) - if strings.HasPrefix(trimmed, "## ") && !strings.HasPrefix(trimmed, "## 小红书卡片") { - break - } - if !strings.HasPrefix(trimmed, "### ") { - continue - } - if _, title, ok := strings.Cut(trimmed, "|"); ok { - for _, topic := range electronicPicklesTitleTopics(title) { - add(topic) - } - } - } - return out -} - -func electronicPicklesTitleTopics(title string) []string { - title = strings.TrimSpace(title) - if before, _, ok := strings.Cut(title, "(点赞 "); ok { - title = strings.TrimSpace(before) - } - checks := []struct { - marker string - topic string - }{ - {"乘风破浪", "乘风破浪的姐姐"}, - {"高考", "高考数学"}, - {"新一卷", "高考数学"}, - {"数学", "高考数学"}, - {"特朗普", "特朗普"}, - {"盛世天下", "盛世天下"}, - {"三国", "三国"}, - {"职场", "职场内耗"}, - {"妆容", "妆容教程"}, - {"发型", "妆容教程"}, - {"美食", "美食制作"}, - {"日本", "日本生活"}, - {"藏獒", "萌宠"}, - {"鬼獒", "萌宠"}, - } - for _, check := range checks { - if strings.Contains(title, check.marker) { - return []string{check.topic} - } - } - return []string{title} -} - type xhsPublishMeta struct { Title string `json:"title"` Content string `json:"content"` diff --git a/cmd/mark2note/main_publish_test.go b/cmd/mark2note/main_publish_test.go index 13fe41d..960de74 100644 --- a/cmd/mark2note/main_publish_test.go +++ b/cmd/mark2note/main_publish_test.go @@ -83,6 +83,25 @@ func TestParsePublishXHSOptionsTracksOriginalityFlagPresence(t *testing.T) { } } +func TestBuildAutoPublishXHSTopicsKeepsManualTagsForElectronicPickles(t *testing.T) { + originalBuildPublishTopics := buildPublishTopics + defer func() { buildPublishTopics = originalBuildPublishTopics }() + buildPublishTopics = func(cfg *config.Config, markdown string, title string) ([]string, error) { + t.Fatal("buildPublishTopics called for manual XHS tags") + return nil, nil + } + markdown := "# 每日电子榨菜|2026-06-11\n\n## 小红书卡片\n\n### 第 1 页|视频标题\n" + + got, err := buildAutoPublishXHSTopics(config.Config{}, markdown, "每日电子榨菜", []string{"#手动 话题", "AI工具"}) + if err != nil { + t.Fatalf("buildAutoPublishXHSTopics() error = %v", err) + } + want := []string{"手动话题", "AI工具"} + if !reflect.DeepEqual(got, want) { + t.Fatalf("topics = %#v, want %#v", got, want) + } +} + func TestRunAutoPublishXHSWritesMetadataBeforePublishing(t *testing.T) { originalPublishXHS := publishXHS originalReadFile := readFile @@ -388,24 +407,31 @@ func TestRunAutoPublishXHSRewritesLongTitleWithAI(t *testing.T) { } } -func TestBuildAutoPublishXHSTopicsUsesElectronicPicklesTitles(t *testing.T) { +func TestBuildAutoPublishXHSTopicsUsesAIForElectronicPickles(t *testing.T) { originalBuildPublishTopics := buildPublishTopics defer func() { buildPublishTopics = originalBuildPublishTopics }() - buildPublishTopics = func(cfg *config.Config, markdown string, title string) ([]string, error) { - t.Fatal("buildPublishTopics called for electronic pickles markdown") - return nil, nil - } + topicGenerationEnabled := true + cfg := config.Config{XHS: config.XHSCfg{Publish: config.XHSPublishCfg{TopicGeneration: config.XHSTopicGenerationCfg{Enabled: &topicGenerationEnabled}}}} markdown := "# 每日电子榨菜|2026-06-10\n\n## 小红书卡片\n\n" + "### 第 1 页|歌剧老师锐评《乘风破浪的姐姐2026》四公(上)(点赞 1|收藏 1|投币 1)\n\n" + - "### 第 2 页|2026新一卷数学!难出天际???!(点赞 1|收藏 1|投币 1)\n\n" + - "### 第 3 页|摔麦喷媒体,空降看球赛,特朗普还有多少活要整?(点赞 1|收藏 1|投币 1)\n\n" + - "### 第 4 页|【某幻】《盛世天下》再返深宫,活下来!(点赞 1|收藏 1|投币 1)\n" + "### 第 2 页|2026新一卷数学!难出天际???!(点赞 1|收藏 1|投币 1)\n" + called := false + buildPublishTopics = func(cfg *config.Config, gotMarkdown string, title string) ([]string, error) { + called = true + if gotMarkdown != markdown || title != "每日电子榨菜" { + t.Fatalf("AI topic inputs = title %q markdown %q", title, gotMarkdown) + } + return []string{"电子榨菜", "综艺", "高考 数学", "游戏!"}, nil + } - got, err := buildAutoPublishXHSTopics(config.Config{}, markdown, "每日电子榨菜", nil) + got, err := buildAutoPublishXHSTopics(cfg, markdown, "每日电子榨菜", nil) if err != nil { t.Fatalf("buildAutoPublishXHSTopics() error = %v", err) } - want := []string{"电子榨菜", "乘风破浪的姐姐", "高考数学", "特朗普", "盛世天下"} + if !called { + t.Fatal("buildPublishTopics was not called") + } + want := []string{"电子榨菜", "综艺", "高考数学", "游戏"} if !reflect.DeepEqual(got, want) { t.Fatalf("topics = %#v, want %#v", got, want) } @@ -431,11 +457,17 @@ func TestBuildAutoPublishXHSOptionsDeclaresElectronicPicklesAIGenerated(t *testi return []byte("# 每日电子榨菜|2026-06-10\n\n## 小红书卡片\n\n### 第 1 页|歌剧老师锐评《乘风破浪的姐姐2026》四公(上)(点赞 1|收藏 1|投币 1)\n"), nil } loadConfig = func(path string) (*config.Config, error) { - return &config.Config{XHS: config.XHSCfg{Publish: config.XHSPublishCfg{Account: "walker"}}}, nil + topicGenerationEnabled := true + return &config.Config{XHS: config.XHSCfg{Publish: config.XHSPublishCfg{ + Account: "walker", + TopicGeneration: config.XHSTopicGenerationCfg{Enabled: &topicGenerationEnabled}, + }}}, nil } buildPublishTopics = func(cfg *config.Config, markdown string, title string) ([]string, error) { - t.Fatal("buildPublishTopics called for electronic pickles markdown") - return nil, nil + if title != "每日电子榨菜|2026-06-10" { + t.Fatalf("topic title = %q", title) + } + return []string{"电子榨菜", "综艺"}, nil } buildPublishTitle = func(cfg *config.Config, markdown string, title string, maxRunes int) (string, error) { t.Fatal("buildPublishTitle called for title within limit") @@ -449,7 +481,7 @@ func TestBuildAutoPublishXHSOptionsDeclaresElectronicPicklesAIGenerated(t *testi if !got.DeclareOriginal || got.OriginalDeclarationType != "ai_generated" || got.AllowContentCopy { t.Fatalf("originality flags = declare:%v type:%q copy:%v", got.DeclareOriginal, got.OriginalDeclarationType, got.AllowContentCopy) } - wantTags := []string{"电子榨菜", "乘风破浪的姐姐"} + wantTags := []string{"电子榨菜", "综艺"} if !reflect.DeepEqual(got.Tags, wantTags) { t.Fatalf("Tags = %#v, want %#v", got.Tags, wantTags) } diff --git a/internal/ai/deck_builder_test.go b/internal/ai/deck_builder_test.go index 1c91af8..8867bb0 100644 --- a/internal/ai/deck_builder_test.go +++ b/internal/ai/deck_builder_test.go @@ -347,7 +347,7 @@ func TestBuildPublishTopicsUsesConfiguredCommand(t *testing.T) { t.Fatalf("args = %#v, want codex --bare -p prompt", runner.args) } prompt := runner.args[3] - for _, want := range []string{"只能输出 JSON", `{"topics":["话题1","话题2"]}`, "优先生成 3 个", "最多 4 个", "更可能已经存在的泛话题", "不要使用纯数字", "标题:标题", "Markdown 如下:\n# 标题"} { + for _, want := range []string{"只能输出 JSON", `{"topics":["话题1","话题2"]}`, "优先生成 3 个", "最多 4 个", "每个话题不能包含空格", "每个话题不能包含特殊符号", "不要直接复制很长的视频标题", "不要包含点赞、收藏、投币", "如果 Markdown 是“电子榨菜”内容", "更可能已经存在的泛话题", "标题:标题", "Markdown 如下:\n# 标题"} { if !strings.Contains(prompt, want) { t.Fatalf("topic prompt missing %q: %q", want, prompt) } diff --git a/internal/ai/topic_builder.go b/internal/ai/topic_builder.go index f9a084f..36a8ef8 100644 --- a/internal/ai/topic_builder.go +++ b/internal/ai/topic_builder.go @@ -14,10 +14,14 @@ const topicPromptTemplate = `你是一个小红书发布话题生成器。 2. JSON 结构必须是 {"topics":["话题1","话题2"]} 3. topics 优先生成 3 个字符串,最多 4 个字符串 4. 每个话题不要包含 # -5. 优先选择小红书上更可能已经存在的泛话题,避免过细、过新或自造的组合词 -6. 不要使用纯数字、日期、GitHub PR/issue 编号、链接、文件名或路径作为话题 -7. 优先选择能概括文章主题、创作场景、技术领域或经验复盘的短话题 -8. 每个话题建议 2-10 个中文字符或常见中英混合短语 +5. 每个话题必须短,建议 2-10 个中文字符或常见中英混合短语 +6. 每个话题不能包含空格、制表符或换行 +7. 每个话题不能包含特殊符号或标点,例如:|、【】、()、[]、!、?、/、\\、:、:、#、@ +8. 不要直接复制很长的视频标题、文章标题或章节标题 +9. 不要包含点赞、收藏、投币、播放量、日期、序号、纯数字、链接、文件名或路径 +10. 优先选择小红书上更可能已经存在的泛话题,避免过细、过新或自造的组合词 +11. 优先选择能概括文章主题、创作场景、技术领域、娱乐类别或经验复盘的短话题 +12. 如果 Markdown 是“电子榨菜”内容,请根据每个视频标题归纳短话题,例如影视、综艺、游戏、动画、足球、理财、生活记录、AI工具等;不要输出原始长视频标题 标题:%s