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
24 changes: 15 additions & 9 deletions cmd/mark2note/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ Flags:
--xhs-schedule-at <time> override Xiaohongshu schedule time for --publish-xhs/--prepare-xhs (YYYY-MM-DD HH:MM:SS)
--collection <name> override Xiaohongshu collection for --publish-xhs/--prepare-xhs
--declare-original override Xiaohongshu original declaration for --publish-xhs/--prepare-xhs
--original-declaration-type <type>
content type declaration: ai_generated; independent of --declare-original
--import-photos import generated PNG files into Apple Photos after export
--import-album <name> Apple Photos album name for imported PNG files
--import-timeout <d> Apple Photos PNG import timeout (default: 2m0s)
Expand Down Expand Up @@ -277,6 +279,7 @@ func parseOptions(args []string) (Options, error) {
fs.StringVar(&opts.XHSScheduleAt, "xhs-schedule-at", opts.XHSScheduleAt, "Xiaohongshu schedule time for auto publish")
fs.StringVar(&opts.XHSCollection, "collection", opts.XHSCollection, "Xiaohongshu collection name for auto publish")
fs.BoolVar(&opts.XHSDeclareOriginal, "declare-original", opts.XHSDeclareOriginal, "declare original content for auto publish")
fs.StringVar(&opts.XHSDeclarationType, "original-declaration-type", opts.XHSDeclarationType, "Xiaohongshu content declaration type for auto publish")
fs.BoolVar(&opts.ImportPhotos, "import-photos", opts.ImportPhotos, "import generated PNG files into Apple Photos after export")
fs.StringVar(&opts.ImportAlbum, "import-album", opts.ImportAlbum, "Apple Photos album name for imported PNG files")
fs.DurationVar(&opts.ImportTimeout, "import-timeout", opts.ImportTimeout, "Apple Photos PNG import timeout")
Expand Down Expand Up @@ -353,6 +356,7 @@ func parseOptions(args []string) (Options, error) {
xhsScheduleAtChanged := false
xhsCollectionChanged := false
xhsDeclareOriginalChanged := false
xhsOriginalDeclarationTypeChanged := false
xhsContentModeChanged := false
stopBeforeSubmitChanged := false
fs.Visit(func(f *flag.Flag) {
Expand Down Expand Up @@ -405,6 +409,8 @@ func parseOptions(args []string) (Options, error) {
xhsCollectionChanged = true
case "declare-original":
xhsDeclareOriginalChanged = true
case "original-declaration-type":
xhsOriginalDeclarationTypeChanged = true
case "stop-before-submit":
stopBeforeSubmitChanged = true
}
Expand Down Expand Up @@ -432,6 +438,7 @@ func parseOptions(args []string) (Options, error) {
opts.XHSScheduleAtChanged = xhsScheduleAtChanged
opts.XHSCollectionChanged = xhsCollectionChanged
opts.XHSDeclareOriginalChanged = xhsDeclareOriginalChanged
opts.XHSDeclarationTypeChanged = xhsOriginalDeclarationTypeChanged
opts.XHSContentModeChanged = xhsContentModeChanged
opts.StopBeforeSubmitChanged = stopBeforeSubmitChanged
if opts.XHSTagsChanged && !opts.PublishXHS && !opts.PrepareXHS {
Expand All @@ -446,6 +453,9 @@ func parseOptions(args []string) (Options, error) {
if opts.XHSDeclareOriginalChanged && !opts.PublishXHS && !opts.PrepareXHS {
return Options{}, fmt.Errorf("--declare-original requires --publish-xhs or --prepare-xhs\n\n%s", usageText())
}
if opts.XHSDeclarationTypeChanged && !opts.PublishXHS && !opts.PrepareXHS {
return Options{}, fmt.Errorf("--original-declaration-type requires --publish-xhs or --prepare-xhs\n\n%s", usageText())
}
if opts.XHSContentModeChanged && !opts.PublishXHS && !opts.PrepareXHS {
return Options{}, fmt.Errorf("--xhs-content-mode requires --publish-xhs or --prepare-xhs\n\n%s", usageText())
}
Expand Down Expand Up @@ -686,13 +696,9 @@ func validatePublishXHSOptions(opts app.PublishOptions) error {
if _, err := xhs.ValidateVisibility(opts.Visibility); err != nil {
return fmt.Errorf("--visibility: %w\n\n%s", err, publishXHSUsageText())
}
declarationType, err := xhs.ValidateOriginalDeclarationType(opts.OriginalDeclarationType)
if err != nil {
if _, err := xhs.ValidateOriginalDeclarationType(opts.OriginalDeclarationType); err != nil {
return fmt.Errorf("--original-declaration-type: %w\n\n%s", err, publishXHSUsageText())
}
if declarationType != "" && !opts.DeclareOriginal {
return fmt.Errorf("--original-declaration-type requires --declare-original=true\n\n%s", publishXHSUsageText())
}
if strings.TrimSpace(opts.Account) == "" {
return fmt.Errorf("--account is required\n\n%s", publishXHSUsageText())
}
Expand Down Expand Up @@ -945,10 +951,10 @@ func buildAutoPublishXHSOptions(renderOpts Options, renderResult app.Result) (ap
if renderOpts.XHSDeclareOriginalChanged {
cliOpts.DeclareOriginal = renderOpts.XHSDeclareOriginal
cliOpts.DeclareOriginalChanged = true
if !renderOpts.XHSDeclareOriginal {
cliOpts.OriginalDeclarationType = ""
cliOpts.OriginalDeclarationTypeChanged = true
}
}
if renderOpts.XHSDeclarationTypeChanged {
cliOpts.OriginalDeclarationType = strings.TrimSpace(renderOpts.XHSDeclarationType)
cliOpts.OriginalDeclarationTypeChanged = true
}
if renderOpts.StopBeforeSubmitChanged {
cliOpts.StopBeforeSubmit = renderOpts.StopBeforeSubmit
Expand Down
4 changes: 2 additions & 2 deletions cmd/mark2note/main_publish_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -481,8 +481,8 @@ func TestBuildAutoPublishXHSOptionsCanDisableElectronicPicklesOriginality(t *tes
if err != nil {
t.Fatalf("buildAutoPublishXHSOptions() error = %v", err)
}
if got.DeclareOriginal || got.OriginalDeclarationType != "" {
t.Fatalf("originality flags = declare:%v type:%q, want disabled without declaration type", got.DeclareOriginal, got.OriginalDeclarationType)
if got.DeclareOriginal || got.OriginalDeclarationType != "ai_generated" {
t.Fatalf("originality flags = declare:%v type:%q, want original disabled with AI content declaration", got.DeclareOriginal, got.OriginalDeclarationType)
}
}

Expand Down
6 changes: 3 additions & 3 deletions cmd/mark2note/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,12 +277,12 @@ func TestParseOptionsParsesAutoPublishXHSFlags(t *testing.T) {
}

func TestParseOptionsParsesAutoPublishXHSDeclareOriginalOverride(t *testing.T) {
opts, err := parseOptions([]string{"--input", "article.md", "--publish-xhs", "--declare-original=false"})
opts, err := parseOptions([]string{"--input", "article.md", "--publish-xhs", "--declare-original=false", "--original-declaration-type=ai_generated"})
if err != nil {
t.Fatalf("parseOptions() error = %v", err)
}
if !opts.PublishXHS || !opts.XHSDeclareOriginalChanged || opts.XHSDeclareOriginal {
t.Fatalf("opts = %#v, want publish-xhs declare original false override", opts)
if !opts.PublishXHS || !opts.XHSDeclareOriginalChanged || opts.XHSDeclareOriginal || !opts.XHSDeclarationTypeChanged || opts.XHSDeclarationType != "ai_generated" {
t.Fatalf("opts = %#v, want original disabled with AI content declaration", opts)
}
}

Expand Down
2 changes: 1 addition & 1 deletion configs/config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ xhs:
# 可选:加入已有小红书合集;留空则不选择合集。Optional existing Xiaohongshu collection name.
collection: ""
declare_original: true
# 可选:原创声明内容类型;留空则不选择。当前支持 ai_generated。
# 可选:内容类型声明;与 declare_original 独立。ai_generated 会选择“笔记含 AI 合成内容”
original_declaration_type: ""
allow_content_copy: false
topic_generation:
Expand Down
2 changes: 2 additions & 0 deletions internal/app/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ type Options struct {
XHSCollectionChanged bool
XHSDeclareOriginal bool
XHSDeclareOriginalChanged bool
XHSDeclarationType string
XHSDeclarationTypeChanged bool
XHSContentMode string
XHSContentModeChanged bool
Animated AnimatedOptions
Expand Down
5 changes: 1 addition & 4 deletions internal/xhs/publisher.go
Original file line number Diff line number Diff line change
Expand Up @@ -1485,10 +1485,7 @@ func isOriginalDeclarationNeedle(labelNeedle string) bool {
}

func (p *rodPage) applyOriginalDeclaration(enabled bool, declarationType string) error {
if !enabled {
return nil
}
if !p.isOriginalDeclared() {
if enabled && !p.isOriginalDeclared() {
if err := p.setCheckboxState("声明原创", true); err != nil {
if err := p.setCheckboxState("原创", true); err != nil {
if _, clickErr := p.clickVisibleNodeByText(".d-checkbox-main-label, .d-checkbox-label, .original-entry, div, span, label, button", "^声明原创$|^原创声明$|^原创$"); clickErr != nil {
Expand Down
20 changes: 17 additions & 3 deletions internal/xhs/publisher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,7 @@ func TestApplyCollectionSelectsMatchingCollection(t *testing.T) {
}
}

func TestApplyOriginalDeclarationSelectsAIGeneratedType(t *testing.T) {
func TestApplyContentDeclarationWithoutOriginalSelectsAIGeneratedType(t *testing.T) {
page := testPage(t)

html := `<!doctype html>
Expand All @@ -565,7 +565,7 @@ func TestApplyOriginalDeclarationSelectsAIGeneratedType(t *testing.T) {
<div class="custom-switch-wrapper">
<div class="custom-switch-card">
<span class="custom-switch-text-content">原创声明</span>
<input type="checkbox" checked>
<input type="checkbox">
</div>
</div>
<div class="wrapper">
Expand Down Expand Up @@ -608,14 +608,17 @@ func TestApplyOriginalDeclarationSelectsAIGeneratedType(t *testing.T) {
page.MustElement("body")

rodPage := &rodPage{page: page}
if err := rodPage.applyOriginalDeclaration(true, "ai_generated"); err != nil {
if err := rodPage.applyOriginalDeclaration(false, "ai_generated"); err != nil {
state := page.MustEval(`() => document.body.innerText.replace(/\s+/g, ' ').trim()`).String()
t.Fatalf("applyOriginalDeclaration() error = %v, state = %s", err, state)
}
selected := page.MustElement(".d-select-wrapper").MustAttribute("data-selected")
if selected == nil || *selected != "笔记含AI生成内容" {
t.Fatalf("selected declaration type = %v, want 笔记含AI生成内容", selected)
}
if page.MustElement(`input[type="checkbox"]`).MustProperty("checked").Bool() {
t.Fatal("original declaration checkbox = checked, want unchanged false")
}
if trusted := page.MustEval(`() => window.declarationTriggerTrustedClickSeen === true`).Bool(); !trusted {
t.Fatal("declaration type select should open by a trusted click")
}
Expand Down Expand Up @@ -923,6 +926,17 @@ func TestPublisherOnlySelfStopBeforeSubmitAppliesPreSubmitHooks(t *testing.T) {
}
}

func TestPublisherOnlySelfStopBeforeSubmitKeepsAITypeWhenOriginalDisabled(t *testing.T) {
page := &fakePublishPage{}
request := PublishRequest{Title: "标题", Content: "正文", ImagePaths: []string{"cover.jpg"}, StopBeforeSubmit: true, DeclareOriginal: false, OriginalDeclarationType: "ai_generated"}
if err := (Publisher{}).PublishStandardOnlySelf(context.Background(), page, request); err != nil {
t.Fatalf("PublishStandardOnlySelf() error = %v", err)
}
if page.originalApplied || page.originalDeclarationType != "ai_generated" {
t.Fatalf("original declaration = %v, content declaration type = %q", page.originalApplied, page.originalDeclarationType)
}
}

func TestPublisherSetsScheduleTimeBeforeSubmit(t *testing.T) {
page := &fakePublishPage{}
scheduledAt := time.Date(2026, 4, 11, 20, 30, 0, 0, time.FixedZone("CST", 8*60*60))
Expand Down
6 changes: 1 addition & 5 deletions internal/xhs/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,9 @@ func (r PublishRequest) Validate(now time.Time) error {
if _, err := ValidateVisibility(string(r.Visibility)); err != nil {
return err
}
declarationType, err := ValidateOriginalDeclarationType(r.OriginalDeclarationType)
if err != nil {
if _, err := ValidateOriginalDeclarationType(r.OriginalDeclarationType); err != nil {
return err
}
if declarationType != "" && !r.DeclareOriginal {
return fmt.Errorf("original declaration type requires declare_original")
}
switch r.Mode {
case PublishModeImmediate:
if r.ScheduleTime != nil {
Expand Down
17 changes: 17 additions & 0 deletions internal/xhs/request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,23 @@ func TestPublishRequestValidateAcceptsPublicVisibility(t *testing.T) {
}
}

func TestPublishRequestValidateAllowsContentDeclarationWithoutOriginal(t *testing.T) {
now := shanghaiTime(2026, 4, 10, 12, 0, 0)
request := PublishRequest{
Account: "creator-a",
Title: "标题",
Content: "正文",
Mode: PublishModeOnlySelf,
MediaKind: MediaKindStandard,
ImagePaths: []string{"cover.jpg"},
DeclareOriginal: false,
OriginalDeclarationType: OriginalDeclarationTypeAIGenerated,
}
if err := request.Validate(now); err != nil {
t.Fatalf("Validate() error = %v", err)
}
}

func TestPublishRequestValidateRejectsInvalidVisibility(t *testing.T) {
request := PublishRequest{Account: "creator-a", Title: "标题", Content: "正文", Mode: PublishModeOnlySelf, Visibility: PublishVisibility("friends"), MediaKind: MediaKindStandard, ImagePaths: []string{"cover.jpg"}}
if err := request.Validate(shanghaiTime(2026, 4, 10, 12, 0, 0)); err == nil || !strings.Contains(err.Error(), "visibility must be public or only-self") {
Expand Down