feat(browser): 方案 A 落地 —— 显式端口 + 去掉 stealth flag(主人 2026-10-07 拍板)
依据 `DevDocs/reference/chromium-cdp/`(9 份实测+官方源码):旧姿势传 `--remote-debugging-port=0`
会被 Chromium 当成 ChromeDriver 式自动化启动 ⇒ 主动打开 `AutomationControlled`
⇒ `navigator.webdriver=true`;那个 bad-flag 是承重覆盖,但代价是**每次有头弹横幅**。
改用显式端口后 `webdriver` **本来就是 false** ⇒ flag 变纯冗余 ⇒ 去掉它,**两者兼得**。
- `launch.go`:`--remote-debugging-port=<我们选的端口>` 替掉 `=0`;**删掉**
`--disable-blink-features=AutomationControlled`(注释写明理由)。
- 新增 `portrecord.go`:端口来源与落点的**唯一出处** —— 取空闲端口(`net.Listen`
取号后立刻关闭,**本进程不常驻监听**)、写/读/清**我们自己的记录**
`<profile>/cyrene-debug-port`(选它与 Chrome 的 `DevToolsActivePort` 同目录:
天然按 profile 隔离、profile 换掉记录自动消失、内容只有端口号不含凭据);
**兼容老记录**(换二进制时上一版起的浏览器可能还在跑 ⇒ 认不出会把"自己的浏览器"
误报成"profile 被别人占了");就绪判据换成「我们选的那个端口答得上 `/json/version`」;
TOCTOU 处理 = 端口被占**换号重试一次**,重试仍失败则如实报错且不留记录。
- 三条认领路径(复用/清陈记录/如实报错保留现场)与三条失败分类(进程立刻退出 /
等端口超时 / 预算耗尽)**全部保留**;`ErrLaunch` + `errors.Is` 口径未变;
**绝不杀进程**红线未变。
真机自证(子代理跑,默认 Skip 的 `realchrome_test.go`,`CYRENE_BROWSER_CDP_E2E=1` 开闸):
`webdriver=false` · 命令行**无** flag/`--enable-automation`/`--headless` · UA 无
`HeadlessChrome`、`plugins=5`、`languages=zh-CN,zh`、`window.chrome` 在 ·
**UI Automation 控件树 17 个命名节点里没有「信息栏容器/信息栏」⇒ 无横幅** ·
第二次 Dial **复用同端口、浏览器主进程仍是同一个 PID** · 收尾按 profile 路径筛残留 = 0。
验收(主会话复跑 + 亲自变异):整树 `go build ./...` + 两包 vet 绿;browser 包测试绿;
我用 -overlay 把该 flag 加回参数表 ⇒ `TestLaunchArgs_HeadfulCarriesNoAutomationTraces`
变红(逐字报"不许传的 flag")。
⚠️ 未验(如实):无头档的横幅(无窗口判不了)、`chrome-headless-shell`、风控侧是否把
"端口写法"本身当信号、生产 profile 整链路(未碰)。
This commit is contained in:
1 parent
74e171bc04
commit
6fe68ecfac
11 files changed
+1316
-320
No files matched your search
@@ -15,11 +15,9 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -124,11 +122,15 @@ func TestAppClose_RealChromeStopsOnShutdown(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// ① 起一个真浏览器(指纹自检走 CDP,无需外网),并从 profile 读回它的调试端点。
|
||||
// ① 起一个真浏览器(指纹自检走 CDP,无需外网),并取回它的调试端点。
|
||||
if _, err := tool.Execute(ctx, map[string]any{"action": "info"}); err != nil {
|
||||
t.Fatalf("action=info(起真浏览器): %v", err)
|
||||
}
|
||||
endpoint := devToolsEndpoint(t, browsercfg.DefaultProfileDir(cfg.DataDir))
|
||||
endpoint, ok := browsercap.DebugEndpoint(browsercfg.DefaultProfileDir(cfg.DataDir))
|
||||
if !ok {
|
||||
t.Fatalf("会话已建立,但 profile %s 下没有端口记录(显式端口由我们自己记,见 portrecord.go)",
|
||||
browsercfg.DefaultProfileDir(cfg.DataDir))
|
||||
}
|
||||
if !endpointAlive(endpoint) {
|
||||
t.Fatalf("会话已建立但调试端点 %s 不应答:本用例的前提不成立", endpoint)
|
||||
}
|
||||
@@ -151,22 +153,6 @@ func TestAppClose_RealChromeStopsOnShutdown(t *testing.T) {
|
||||
endpoint, tailOfLog(out.String()))
|
||||
}
|
||||
|
||||
// devToolsEndpoint 从 profile 目录的 DevToolsActivePort 读出这次会话的调试端点
|
||||
// (格式与 launch.go 的 waitDevToolsPort 同源:首行是端口号)。
|
||||
func devToolsEndpoint(t *testing.T, profileDir string) string {
|
||||
t.Helper()
|
||||
raw, err := os.ReadFile(filepath.Join(profileDir, "DevToolsActivePort"))
|
||||
if err != nil {
|
||||
t.Fatalf("读 DevToolsActivePort: %v(profile=%s)", err, profileDir)
|
||||
}
|
||||
line, _, _ := strings.Cut(strings.TrimSpace(string(raw)), "\n")
|
||||
port, err := strconv.Atoi(strings.TrimSpace(line))
|
||||
if err != nil || port <= 0 {
|
||||
t.Fatalf("DevToolsActivePort 内容不合法: %q", raw)
|
||||
}
|
||||
return fmt.Sprintf("http://127.0.0.1:%d", port)
|
||||
}
|
||||
|
||||
// endpointAlive 报告调试端点是否在应答(一次 /json/version)。
|
||||
func endpointAlive(endpoint string) bool {
|
||||
client := &http.Client{Timeout: 2 * time.Second}
|
||||
|
||||
Reference in new issue
Block a user