From 7e0b656df9a567e6596fe918c96aded43ff0e5a7 Mon Sep 17 00:00:00 2001 From: ventianima-lab <256240652+ventianima-lab@users.noreply.github.com> Date: Mon, 21 Sep 2026 04:19:58 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=AE=98=E7=BD=91=20prompt=20=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E6=AF=94=E8=BE=83=E7=BB=9F=E4=B8=80=E6=8D=A2=E8=A1=8C?= =?UTF-8?q?=E7=AC=A6=EF=BC=8C=E4=BF=AE=E5=A4=8D=20CRLF=20=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E8=AF=8D=20WAA=20proof=20=E8=B6=85=E6=97=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Worker.Proof 把写入 textarea 的返回值与原始 prompt 直接按字节比较。 官网 textarea 会把写入值中的 CRLF 规范化为 LF,因此当客户端提示词 含 CRLF 时该比较永远不成立,5s 轮询后返回“官网 prompt 状态未同步”, GenerateContent 随之失败并向上游返回 502。 Windows 客户端很容易触发:多行请求体、工具结果或粘贴内容常带 CRLF。 本地实测(Pro 账户,gemini-3.8-flash): 修复前 LF 换行 200 16827ms 修复前 CRLF 换行 502 37449ms 修复后 LF 换行 200 2439ms 修复后 CRLF 换行 200 1558ms 新增 normalizePromptNewlines,仅在同步判定处归一化 CRLF/CR 为 LF。 写入页面的内容与 proof digest 仍使用原始 prompt,协议行为不变。 bootstrap 的 filled 比较存在同样问题,一并修复。 补充 page_test.go 覆盖归一化与 CRLF/LF 同步判定。 go vet、go test ./internal/camoufoxnative/、go build ./... 均通过。 --- internal/camoufoxnative/page.go | 6 +++++ internal/camoufoxnative/page_test.go | 38 ++++++++++++++++++++++++++++ internal/camoufoxnative/worker.go | 4 +-- 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 internal/camoufoxnative/page_test.go diff --git a/internal/camoufoxnative/page.go b/internal/camoufoxnative/page.go index 37f2411..a50ec4b 100644 --- a/internal/camoufoxnative/page.go +++ b/internal/camoufoxnative/page.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "strings" ) // pageDOMHelpers 定义官网页面共用的可见目标与按钮状态判断 @@ -25,6 +26,11 @@ const promptReadyExpression = `(() => {` + pageDOMHelpers + ` // workerPageReadyExpression 等待输入框出现或页面跳转到登录入口 const workerPageReadyExpression = `(location.hostname === 'accounts.google.com' || ` + promptReadyExpression + `)` +// normalizePromptNewlines 把 CRLF/CR 统一成 LF,官网 textarea 会把写入值规范化为 LF +func normalizePromptNewlines(value string) string { + return strings.ReplaceAll(strings.ReplaceAll(value, "\r\n", "\n"), "\r", "\n") +} + // fillPromptExpression 向当前可见提示框写入文本并通知页面表单 func fillPromptExpression(prompt string) string { encoded, _ := json.Marshal(prompt) diff --git a/internal/camoufoxnative/page_test.go b/internal/camoufoxnative/page_test.go new file mode 100644 index 0000000..e8e2911 --- /dev/null +++ b/internal/camoufoxnative/page_test.go @@ -0,0 +1,38 @@ +package camoufoxnative + +import "testing" + +// TestNormalizePromptNewlines 覆盖官网 textarea 把写入值规范化为 LF 的比较场景 +func TestNormalizePromptNewlines(t *testing.T) { + cases := []struct { + name string + value string + want string + }{ + {name: "lf", value: "a\nb", want: "a\nb"}, + {name: "crlf", value: "a\r\nb", want: "a\nb"}, + {name: "cr", value: "a\rb", want: "a\nb"}, + {name: "mixed", value: "a\r\nb\nc\rd", want: "a\nb\nc\nd"}, + {name: "empty", value: "", want: ""}, + {name: "no newline", value: "abc", want: "abc"}, + } + for _, item := range cases { + t.Run(item.name, func(t *testing.T) { + if got := normalizePromptNewlines(item.value); got != item.want { + t.Fatalf("normalizePromptNewlines(%q) = %q, want %q", item.value, got, item.want) + } + }) + } +} + +// TestNormalizePromptNewlinesMatchesTextareaValue 确认 CRLF 提示词与官网返回的 LF 值判定为同步 +func TestNormalizePromptNewlinesMatchesTextareaValue(t *testing.T) { + prompt := "line1\r\nline2\r\nline3" + textarea := "line1\nline2\nline3" + if prompt == textarea { + t.Fatal("用例前提失效: CRLF 提示词不应与 textarea 值直接相等") + } + if normalizePromptNewlines(prompt) != normalizePromptNewlines(textarea) { + t.Fatal("CRLF 提示词与 textarea LF 值应判定为已同步") + } +} diff --git a/internal/camoufoxnative/worker.go b/internal/camoufoxnative/worker.go index 769cb79..db16e33 100644 --- a/internal/camoufoxnative/worker.go +++ b/internal/camoufoxnative/worker.go @@ -130,7 +130,7 @@ func (worker *Worker) Proof(ctx context.Context, digest string, prompt string) ( if err != nil { return "", fmt.Errorf("同步官网 prompt: %w", err) } - if value == prompt { + if normalizePromptNewlines(value) == normalizePromptNewlines(prompt) { break } if time.Now().After(deadline) { @@ -249,7 +249,7 @@ func (worker *Worker) bootstrap(ctx context.Context, options Options, storage st } options.reportStartup(StartupBootstrappingWAA) filled, err := client.evaluateString(ctx, contextID, fillPromptExpression(options.BootstrapPrompt)) - if err != nil || filled != options.BootstrapPrompt { + if err != nil || normalizePromptNewlines(filled) != normalizePromptNewlines(options.BootstrapPrompt) { return fmt.Errorf("填写 bootstrap 提示词失败 value=%q err=%v", filled, err) } if _, err := client.command(ctx, "session.subscribe", map[string]any{