diff --git a/README.md b/README.md index 0f5760f..af4e64e 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ The `capture-html` command is also a public CLI subcommand that converts existin ## Requirements - Go 1.25+ -- An AI CLI that can be invoked with `-p ` +- An authenticated Codex CLI; the default integration uses `codex exec` and sends prompts through stdin - Google Chrome, or a compatible browser binary that supports `--headless=new` - `img2webp` (required only when Animated WebP export is enabled) - `ffmpeg` (required when MP4 or Live export is enabled) @@ -242,26 +242,31 @@ Additional notes: ## AI CLI examples -Using `ccs`: +Using `codex`: ```yaml ai: - command: ccs + command: codex args: - - codex - - --bare + - exec + - --ignore-user-config + - --disable + - apps + - --disable + - plugins + - --disable + - remote_plugin + - --ephemeral + - --skip-git-repo-check + - --sandbox + - read-only + - --color + - never + - --model + - gpt-5.6-sol ``` -Using `claude`: - -```yaml -ai: - command: claude - args: - - -p -``` - -Note: adjust the arguments to match your local AI CLI setup, as long as `mark2note` can use it to generate deck JSON. +For direct `codex` calls, `mark2note` sends the prompt through stdin. Custom AI CLIs retain the legacy `-p ` invocation, so adjust their arguments to match your local setup. ## Common commands diff --git a/README.zh-CN.md b/README.zh-CN.md index 91bf0ce..3506c03 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -22,7 +22,7 @@ ## 环境要求 - Go 1.25+ -- 可通过 `-p ` 调用的 AI CLI +- 已完成登录的 Codex CLI;默认通过 `codex exec` 调用,并使用 stdin 传入 prompt - Google Chrome,或兼容 `--headless=new` 的浏览器可执行文件 - `img2webp`(仅在启用 Animated WebP 增强导出时需要) - `ffmpeg`(启用 MP4 或 Live 导出时需要) @@ -242,26 +242,31 @@ posters: ## AI CLI 示例 -使用 `ccs`: +使用 `codex`: ```yaml ai: - command: ccs + command: codex args: - - codex - - --bare + - exec + - --ignore-user-config + - --disable + - apps + - --disable + - plugins + - --disable + - remote_plugin + - --ephemeral + - --skip-git-repo-check + - --sandbox + - read-only + - --color + - never + - --model + - gpt-5.6-sol ``` -使用 `claude`: - -```yaml -ai: - command: claude - args: - - -p -``` - -说明:请根据你本地 AI CLI 的实际调用方式调整参数,只要能被 `mark2note` 用来生成 deck JSON 即可。 +直连 `codex` 时,`mark2note` 会通过 stdin 传入 prompt。自定义 AI CLI 仍沿用原有的 `-p ` 调用方式,可按本机环境调整其他参数。 ## 常用命令 diff --git a/configs/config.example.yaml b/configs/config.example.yaml index d5934d1..d6a9628 100644 --- a/configs/config.example.yaml +++ b/configs/config.example.yaml @@ -2,10 +2,24 @@ output: dir: output ai: - command: claude + command: codex args: - - --bare - - --disable-slash-commands + - exec + - --ignore-user-config + - --disable + - apps + - --disable + - plugins + - --disable + - remote_plugin + - --ephemeral + - --skip-git-repo-check + - --sandbox + - read-only + - --color + - never + - --model + - gpt-5.6-sol retry: delays: - 1s diff --git a/internal/ai/deck_builder.go b/internal/ai/deck_builder.go index b4bdc33..9de166a 100644 --- a/internal/ai/deck_builder.go +++ b/internal/ai/deck_builder.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "os/exec" + "path/filepath" "slices" "strings" "time" @@ -56,7 +57,7 @@ var ( ) type CommandRunner interface { - Run(name string, args ...string) (string, string, error) + Run(name, stdin string, args ...string) (string, string, error) } type Builder struct { @@ -91,8 +92,11 @@ func buildDeckPromptWithMaxPages(markdown, promptExtra string, maxPages int) str type execRunner struct{} -func (execRunner) Run(name string, args ...string) (string, string, error) { +func (execRunner) Run(name, stdin string, args ...string) (string, string, error) { cmd := exec.Command(name, args...) + if stdin != "" { + cmd.Stdin = strings.NewReader(stdin) + } out, err := cmd.Output() if err == nil { return strings.TrimSpace(string(out)), "", nil @@ -117,13 +121,13 @@ func effectiveAICommandRetryDelays(delays []time.Duration) []time.Duration { return cloneDurations(delays) } -func runAICommand(runner CommandRunner, name string, retryDelays []time.Duration, args ...string) (string, string, error) { +func runAICommand(runner CommandRunner, name, stdin string, retryDelays []time.Duration, args ...string) (string, string, error) { var stdout string var stderr string var err error delays := effectiveAICommandRetryDelays(retryDelays) for attempt := 0; attempt <= len(delays); attempt++ { - stdout, stderr, err = runner.Run(name, args...) + stdout, stderr, err = runner.Run(name, stdin, args...) if err == nil { return stdout, stderr, nil } @@ -178,12 +182,12 @@ func (b Builder) effectiveRunner() CommandRunner { } func (b Builder) BuildDeckJSON(markdown string) (string, error) { - args := append([]string{}, b.Args...) - if shouldUseBareOutput(b.Command, b.Args) && !containsArg(args, "--bare") { - args = append(args, "--bare") - } - args = append(args, "-p", buildDeckPromptWithMaxPages(markdown, b.PromptExtra, b.MaxPages)) - stdout, stderr, err := runAICommand(b.effectiveRunner(), b.Command, b.RetryDelays, args...) + args, stdin := buildAICommandInvocation( + b.Command, + b.Args, + buildDeckPromptWithMaxPages(markdown, b.PromptExtra, b.MaxPages), + ) + stdout, stderr, err := runAICommand(b.effectiveRunner(), b.Command, stdin, b.RetryDelays, args...) if err != nil { return "", fmt.Errorf("%w: %v\nstderr: %s", ErrAICommandFailed, err, stderr) } @@ -268,6 +272,23 @@ func containsArg(args []string, target string) bool { return slices.Contains(args, target) } +func buildAICommandInvocation(command string, configuredArgs []string, prompt string) ([]string, string) { + args := append([]string(nil), configuredArgs...) + if isDirectCodexCommand(command) { + return args, prompt + } + if shouldUseBareOutput(command, configuredArgs) && !containsArg(args, "--bare") { + args = append(args, "--bare") + } + return append(args, "-p", prompt), "" +} + +func isDirectCodexCommand(command string) bool { + base := strings.ToLower(filepath.Base(strings.TrimSpace(command))) + base = strings.TrimSuffix(base, ".exe") + return base == "codex" +} + func shouldUseBareOutput(command string, args []string) bool { if command != "ccs" { return false diff --git a/internal/ai/deck_builder_test.go b/internal/ai/deck_builder_test.go index c2897d7..f4021dd 100644 --- a/internal/ai/deck_builder_test.go +++ b/internal/ai/deck_builder_test.go @@ -10,14 +10,16 @@ import ( type fakeRunner struct { name string + stdin string args []string stdout string stderr string err error } -func (r *fakeRunner) Run(name string, args ...string) (string, string, error) { +func (r *fakeRunner) Run(name, stdin string, args ...string) (string, string, error) { r.name = name + r.stdin = stdin r.args = append([]string(nil), args...) return r.stdout, r.stderr, r.err } @@ -33,7 +35,7 @@ type sequenceRunner struct { count int } -func (r *sequenceRunner) Run(_ string, _ ...string) (string, string, error) { +func (r *sequenceRunner) Run(_, _ string, _ ...string) (string, string, error) { if r.count >= len(r.calls) { return "", "", errors.New("unexpected runner call") } @@ -85,13 +87,13 @@ func TestBuildDeckPromptIgnoresWhitespaceOnlyPromptExtra(t *testing.T) { func TestBuildDeckJSONUsesConfiguredMaxPages(t *testing.T) { runner := &fakeRunner{stdout: `{"pages":[]}`} b := Builder{Runner: runner, MaxPages: 18} - b.SetCommand("ccs", []string{"codex"}) + b.SetCommand("codex", []string{"exec"}) _, err := b.BuildDeckJSON("# title") if err != nil { t.Fatalf("BuildDeckJSON() error = %v", err) } - prompt := runner.args[len(runner.args)-1] + prompt := runner.stdin if !strings.Contains(prompt, "3-18 页") || !strings.Contains(prompt, "3 到 18 页之间") { t.Fatalf("prompt = %q, want configured max page range", prompt) } @@ -100,28 +102,22 @@ func TestBuildDeckJSONUsesConfiguredMaxPages(t *testing.T) { func TestBuildDeckJSONUsesConfiguredCommand(t *testing.T) { runner := &fakeRunner{stdout: `{"pages":[]}`} b := Builder{Runner: runner} - b.SetCommand("ccs", []string{"codex"}) + b.SetCommand("codex", []string{"exec", "--ephemeral"}) _, err := b.BuildDeckJSON("# title") if err != nil { t.Fatalf("BuildDeckJSON() error = %v", err) } - if runner.name != "ccs" { - t.Fatalf("command = %q, want %q", runner.name, "ccs") + if runner.name != "codex" { + t.Fatalf("command = %q, want %q", runner.name, "codex") } - if len(runner.args) < 4 { - t.Fatalf("args = %v, want command args plus --bare and -p prompt", runner.args) - } - if runner.args[0] != "codex" { - t.Fatalf("first arg = %q, want %q", runner.args[0], "codex") - } - if runner.args[1] != "--bare" { - t.Fatalf("args[1] = %q, want %q", runner.args[1], "--bare") + if !reflect.DeepEqual(runner.args, []string{"exec", "--ephemeral"}) { + t.Fatalf("args = %v, want configured Codex args only", runner.args) } - if runner.args[2] != "-p" { - t.Fatalf("args[2] = %q, want %q", runner.args[2], "-p") + if containsArg(runner.args, "-p") || containsArg(runner.args, "--bare") { + t.Fatalf("args = %v, direct Codex must not receive Claude flags", runner.args) } - prompt := runner.args[3] + prompt := runner.stdin if !strings.Contains(prompt, "# title") { t.Fatalf("prompt missing markdown input: %q", prompt) } @@ -235,6 +231,47 @@ func TestBuildDeckJSONUsesConfiguredCommand(t *testing.T) { } } +func TestBuildAICommandInvocationRecognizesCodexExecutablePath(t *testing.T) { + args, stdin := buildAICommandInvocation( + "/opt/homebrew/bin/codex", + []string{"exec", "--ephemeral"}, + "long prompt", + ) + + if !reflect.DeepEqual(args, []string{"exec", "--ephemeral"}) { + t.Fatalf("args = %#v, want configured Codex args only", args) + } + if stdin != "long prompt" { + t.Fatalf("stdin = %q, want prompt", stdin) + } +} + +func TestBuildAICommandInvocationRecognizesWindowsCodexExecutable(t *testing.T) { + args, stdin := buildAICommandInvocation( + `C:/tools/CODEX.EXE`, + []string{"exec", "--ephemeral"}, + "long prompt", + ) + + if !reflect.DeepEqual(args, []string{"exec", "--ephemeral"}) { + t.Fatalf("args = %#v, want configured Codex args only", args) + } + if stdin != "long prompt" { + t.Fatalf("stdin = %q, want prompt", stdin) + } +} + +func TestBuildAICommandInvocationKeepsLegacyPromptFlag(t *testing.T) { + args, stdin := buildAICommandInvocation("custom-ai", []string{"--json"}, "prompt") + + if !reflect.DeepEqual(args, []string{"--json", "-p", "prompt"}) { + t.Fatalf("args = %#v, want legacy -p prompt invocation", args) + } + if stdin != "" { + t.Fatalf("stdin = %q, want empty", stdin) + } +} + func TestBuildDeckJSONPromptLocksMarkdownSemanticPreservation(t *testing.T) { runner := &fakeRunner{stdout: `{"pages":[]}`} b := Builder{Runner: runner} @@ -330,7 +367,7 @@ func TestBuildDeckJSONPromptSplitsOversizedFencedCodeBlocks(t *testing.T) { func TestBuildPublishTopicsUsesConfiguredCommand(t *testing.T) { runner := &fakeRunner{stdout: `{"topics":["AI编程","开源项目","工程实践"]}`} b := TopicBuilder{Runner: runner} - b.SetCommand("ccs", []string{"codex"}) + b.SetCommand("codex", []string{"exec", "--ephemeral"}) got, err := b.BuildPublishTopics("# 标题\n\n正文", "标题") if err != nil { @@ -340,13 +377,13 @@ func TestBuildPublishTopicsUsesConfiguredCommand(t *testing.T) { if !reflect.DeepEqual(got, want) { t.Fatalf("BuildPublishTopics() = %#v, want %#v", got, want) } - if runner.name != "ccs" { - t.Fatalf("command = %q, want ccs", runner.name) + if runner.name != "codex" { + t.Fatalf("command = %q, want codex", runner.name) } - if len(runner.args) < 4 || runner.args[0] != "codex" || runner.args[1] != "--bare" || runner.args[2] != "-p" { - t.Fatalf("args = %#v, want codex --bare -p prompt", runner.args) + if !reflect.DeepEqual(runner.args, []string{"exec", "--ephemeral"}) { + t.Fatalf("args = %#v, want configured Codex args only", runner.args) } - prompt := runner.args[3] + prompt := runner.stdin 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) @@ -390,7 +427,7 @@ func TestBuildPublishTopicsRetriesTransientAILockError(t *testing.T) { func TestBuildPublishTitleUsesConfiguredCommand(t *testing.T) { runner := &fakeRunner{stdout: `{"title":"把代码合进真实开源"}`} b := TitleBuilder{Runner: runner} - b.SetCommand("ccs", []string{"codex"}) + b.SetCommand("codex", []string{"exec", "--ephemeral"}) got, err := b.BuildPublishTitle("# 别只用 AI 写 Demo:把代码合进真实开源项目,那会是新的开始", "别只用 AI 写 Demo:把代码合进真实开源项目,那会是新的开始", 20) if err != nil { @@ -399,13 +436,13 @@ func TestBuildPublishTitleUsesConfiguredCommand(t *testing.T) { if got != "把代码合进真实开源" { t.Fatalf("BuildPublishTitle() = %q", got) } - if runner.name != "ccs" { - t.Fatalf("command = %q, want ccs", runner.name) + if runner.name != "codex" { + t.Fatalf("command = %q, want codex", runner.name) } - if len(runner.args) < 4 || runner.args[0] != "codex" || runner.args[1] != "--bare" || runner.args[2] != "-p" { - t.Fatalf("args = %#v, want codex --bare -p prompt", runner.args) + if !reflect.DeepEqual(runner.args, []string{"exec", "--ephemeral"}) { + t.Fatalf("args = %#v, want configured Codex args only", runner.args) } - prompt := runner.args[3] + prompt := runner.stdin for _, want := range []string{"只能输出 JSON", `{"title":"改写后的标题"}`, "不超过 20 个字符", "保留原始标题的核心意思", "不要把标题压缩成电报式短语", "保持自然中文语序", "原始标题:别只用 AI 写 Demo", "Markdown 如下:\n# 别只用 AI 写 Demo"} { if !strings.Contains(prompt, want) { t.Fatalf("title prompt missing %q: %q", want, prompt) diff --git a/internal/ai/title_builder.go b/internal/ai/title_builder.go index e1903ab..9601598 100644 --- a/internal/ai/title_builder.go +++ b/internal/ai/title_builder.go @@ -56,12 +56,8 @@ func (b TitleBuilder) effectiveRunner() CommandRunner { } func (b TitleBuilder) BuildPublishTitle(markdown, title string, maxRunes int) (string, error) { - args := append([]string{}, b.Args...) - if shouldUseBareOutput(b.Command, b.Args) && !containsArg(args, "--bare") { - args = append(args, "--bare") - } - args = append(args, "-p", buildPublishTitlePrompt(markdown, title, maxRunes)) - stdout, stderr, err := runAICommand(b.effectiveRunner(), b.Command, b.RetryDelays, args...) + args, stdin := buildAICommandInvocation(b.Command, b.Args, buildPublishTitlePrompt(markdown, title, maxRunes)) + stdout, stderr, err := runAICommand(b.effectiveRunner(), b.Command, stdin, b.RetryDelays, args...) if err != nil { return "", fmt.Errorf("%w: %v\nstderr: %s", ErrAICommandFailed, err, stderr) } diff --git a/internal/ai/topic_builder.go b/internal/ai/topic_builder.go index 36a8ef8..0b1fdae 100644 --- a/internal/ai/topic_builder.go +++ b/internal/ai/topic_builder.go @@ -60,12 +60,8 @@ func (b TopicBuilder) effectiveRunner() CommandRunner { } func (b TopicBuilder) BuildPublishTopics(markdown, title string) ([]string, error) { - args := append([]string{}, b.Args...) - if shouldUseBareOutput(b.Command, b.Args) && !containsArg(args, "--bare") { - args = append(args, "--bare") - } - args = append(args, "-p", buildTopicPrompt(markdown, title)) - stdout, stderr, err := runAICommand(b.effectiveRunner(), b.Command, b.RetryDelays, args...) + args, stdin := buildAICommandInvocation(b.Command, b.Args, buildTopicPrompt(markdown, title)) + stdout, stderr, err := runAICommand(b.effectiveRunner(), b.Command, stdin, b.RetryDelays, args...) if err != nil { return nil, fmt.Errorf("%w: %v\nstderr: %s", ErrAICommandFailed, err, stderr) } diff --git a/internal/app/service_test.go b/internal/app/service_test.go index 25f08cc..56fa717 100644 --- a/internal/app/service_test.go +++ b/internal/app/service_test.go @@ -23,14 +23,16 @@ import ( type fakeAICommandRunner struct { name string + stdin string args []string stdout string stderr string err error } -func (r *fakeAICommandRunner) Run(name string, args ...string) (string, string, error) { +func (r *fakeAICommandRunner) Run(name, stdin string, args ...string) (string, string, error) { r.name = name + r.stdin = stdin r.args = append([]string(nil), args...) return r.stdout, r.stderr, r.err } @@ -59,7 +61,7 @@ func deckJSONWithPageCount(count int) string { } func TestServiceGeneratePreviewPassesPromptExtraToAIBuilder(t *testing.T) { - cfg := &config.Config{Output: config.OutputCfg{Dir: t.TempDir()}, AI: config.AICfg{Command: "ccs", Args: []string{"codex"}}} + cfg := &config.Config{Output: config.OutputCfg{Dir: t.TempDir()}, AI: config.AICfg{Command: "codex", Args: []string{"exec", "--ephemeral"}}} runner := &fakeAICommandRunner{stdout: `{"pages":[{"name":"p1-cover","variant":"cover","meta":{"badge":"第 1 页","counter":"1/3","theme":"orange","cta":"cta1"},"content":{"title":"封面"}},{"name":"p2-bullets","variant":"bullets","meta":{"badge":"第 2 页","counter":"2/3","theme":"orange","cta":"cta2"},"content":{"title":"中间","items":["要点"]}},{"name":"p3-ending","variant":"ending","meta":{"badge":"第 3 页","counter":"3/3","theme":"green","cta":"cta3"},"content":{"title":"结尾","body":"正文"}}]}`} svc := Service{ @@ -73,10 +75,10 @@ func TestServiceGeneratePreviewPassesPromptExtraToAIBuilder(t *testing.T) { if err != nil { t.Fatalf("GeneratePreview() error = %v", err) } - if len(runner.args) < 4 { - t.Fatalf("runner args = %v, want command args plus --bare and -p prompt", runner.args) + if !reflect.DeepEqual(runner.args, []string{"exec", "--ephemeral"}) { + t.Fatalf("runner args = %v, want configured Codex args only", runner.args) } - prompt := runner.args[3] + prompt := runner.stdin if !strings.Contains(prompt, "以下是本次生成的额外约束") || !strings.Contains(prompt, "不得原文复制") { t.Fatalf("prompt = %q, want hidden extra constraint wrapper", prompt) } @@ -86,7 +88,7 @@ func TestServiceGeneratePreviewPassesPromptExtraToAIBuilder(t *testing.T) { } func TestServiceGeneratePreviewPassesMaxPagesToAIBuilder(t *testing.T) { - cfg := &config.Config{Output: config.OutputCfg{Dir: t.TempDir()}, AI: config.AICfg{Command: "ccs", Args: []string{"codex"}}, Deck: config.DeckCfg{MaxPages: 18}} + cfg := &config.Config{Output: config.OutputCfg{Dir: t.TempDir()}, AI: config.AICfg{Command: "codex", Args: []string{"exec", "--ephemeral"}}, Deck: config.DeckCfg{MaxPages: 18}} runner := &fakeAICommandRunner{stdout: `{"pages":[{"name":"p1-cover","variant":"cover","meta":{"badge":"第 1 页","counter":"1/3","theme":"orange","cta":"cta1"},"content":{"title":"封面"}},{"name":"p2-bullets","variant":"bullets","meta":{"badge":"第 2 页","counter":"2/3","theme":"orange","cta":"cta2"},"content":{"title":"中间","items":["要点"]}},{"name":"p3-ending","variant":"ending","meta":{"badge":"第 3 页","counter":"3/3","theme":"green","cta":"cta3"},"content":{"title":"结尾","body":"正文"}}]}`} _svcRenderer := &fakeRenderer{} svc := Service{ @@ -100,7 +102,7 @@ func TestServiceGeneratePreviewPassesMaxPagesToAIBuilder(t *testing.T) { if err != nil { t.Fatalf("GeneratePreview() error = %v", err) } - prompt := runner.args[len(runner.args)-1] + prompt := runner.stdin if !strings.Contains(prompt, "3-18 页") { t.Fatalf("prompt = %q, want configured max pages", prompt) } diff --git a/internal/config/config.go b/internal/config/config.go index 6dc5987..688c7b3 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -294,10 +294,21 @@ func Load(configPath string) (*Config, error) { cfg.Output.Dir = "output" } if cfg.AI.Command == "" { - cfg.AI.Command = "claude" + cfg.AI.Command = "codex" } if len(cfg.AI.Args) == 0 { - cfg.AI.Args = []string{"--bare", "--disable-slash-commands"} + cfg.AI.Args = []string{ + "exec", + "--ignore-user-config", + "--disable", "apps", + "--disable", "plugins", + "--disable", "remote_plugin", + "--ephemeral", + "--skip-git-repo-check", + "--sandbox", "read-only", + "--color", "never", + "--model", "gpt-5.6-sol", + } } if cfg.AI.Retry.Delays == nil { cfg.AI.Retry.Delays = cloneDurations(defaultAIRetryDelays) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index c186f06..758f67b 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -24,11 +24,23 @@ func TestLoadAppliesDefaultAIConfig(t *testing.T) { if cfg.Output.Dir != "custom-output" { t.Fatalf("Output.Dir = %q, want %q", cfg.Output.Dir, "custom-output") } - if cfg.AI.Command != "claude" { - t.Fatalf("AI.Command = %q, want %q", cfg.AI.Command, "claude") - } - if !reflect.DeepEqual(cfg.AI.Args, []string{"--bare", "--disable-slash-commands"}) { - t.Fatalf("AI.Args = %v, want %v", cfg.AI.Args, []string{"--bare", "--disable-slash-commands"}) + if cfg.AI.Command != "codex" { + t.Fatalf("AI.Command = %q, want %q", cfg.AI.Command, "codex") + } + wantAIArgs := []string{ + "exec", + "--ignore-user-config", + "--disable", "apps", + "--disable", "plugins", + "--disable", "remote_plugin", + "--ephemeral", + "--skip-git-repo-check", + "--sandbox", "read-only", + "--color", "never", + "--model", "gpt-5.6-sol", + } + if !reflect.DeepEqual(cfg.AI.Args, wantAIArgs) { + t.Fatalf("AI.Args = %v, want %v", cfg.AI.Args, wantAIArgs) } wantRetryDelays := []time.Duration{time.Second, 2 * time.Second, 5 * time.Second, 9 * time.Second, 17 * time.Second} if !reflect.DeepEqual(cfg.AI.Retry.Delays, wantRetryDelays) { diff --git a/internal/xhs/browser_session.go b/internal/xhs/browser_session.go index 8a11a9c..f8c1c10 100644 --- a/internal/xhs/browser_session.go +++ b/internal/xhs/browser_session.go @@ -850,7 +850,7 @@ func defaultRodPageTimeouts() rodPageTimeouts { permissionDropdown: 2 * time.Second, originalConfirm: 2 * time.Second, topicSuggestion: 2 * time.Second, - topicConfirmation: 1200 * time.Millisecond, + topicConfirmation: 5 * time.Second, topicFallbackSuggestion: 300 * time.Millisecond, } } diff --git a/internal/xhs/publisher_test.go b/internal/xhs/publisher_test.go index cb95537..9c6ccb8 100644 --- a/internal/xhs/publisher_test.go +++ b/internal/xhs/publisher_test.go @@ -348,7 +348,7 @@ func TestRodPageTimeoutsUseDefaultsAndAllowPartialOverrides(t *testing.T) { permissionDropdown: 2 * time.Second, originalConfirm: 2 * time.Second, topicSuggestion: 2 * time.Second, - topicConfirmation: 1200 * time.Millisecond, + topicConfirmation: 5 * time.Second, topicFallbackSuggestion: 300 * time.Millisecond, } if defaults != wantDefaults { @@ -368,7 +368,7 @@ func TestRodPageTimeoutsUseDefaultsAndAllowPartialOverrides(t *testing.T) { if overrides.topicSuggestion != 30*time.Millisecond { t.Fatalf("topicSuggestion = %v, want override", overrides.topicSuggestion) } - if overrides.topicConfirmation != 1200*time.Millisecond { + if overrides.topicConfirmation != 5*time.Second { t.Fatalf("topicConfirmation = %v, want default", overrides.topicConfirmation) } } @@ -441,6 +441,33 @@ func TestWaitForTopicConfirmationRejectsPlainTextTopic(t *testing.T) { } } +func TestWaitForTopicConfirmationAcceptsDelayedHighlightedTopic(t *testing.T) { + page := testPage(t) + + html := ` + + + +
#AI科技
+ + +` + page.MustNavigate("data:text/html;charset=utf-8," + url.PathEscape(html)) + page.MustWaitLoad() + page.MustElement("body") + + rodPage := &rodPage{page: page} + if err := rodPage.waitForTopicConfirmation("AI科技", rodPage.effectiveTimeouts().topicConfirmation); err != nil { + t.Fatalf("waitForTopicConfirmation() error = %v, want delayed highlighted topic accepted", err) + } +} + func TestWaitForTopicSuggestionAcceptsSuggestionNode(t *testing.T) { page := testPage(t)