Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 0 additions & 80 deletions cmd/mark2note/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand All @@ -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"`
Expand Down
60 changes: 46 additions & 14 deletions cmd/mark2note/main_publish_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand All @@ -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")
Expand All @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/ai/deck_builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
12 changes: 8 additions & 4 deletions internal/ai/topic_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down