按 design/28 §2/§3 的「配置不进 .env」口径:读 $DATA_DIR/config/plugins/browser.json (0600 + temp+rename 范式,**不读 env、不加新键**);有头/无头、CDP 端点、profile 目录、 视口/超时/并发(1)/截图上限全部类型化 + 常量默认值;**探测优先级**(本机 Chrome/Chromium 可执行文件 → 已在跑的容器)并返回可分类的「为什么不可用」(供上层如实降级,不假装可用); profile 目录含凭据 ⇒ 0600 + 不进 git/不进上下文的注释与校验。 本片零既有文件改动(不注册工具、不碰 cmd/),接线留 B2。
186 lines
8.6 KiB
Go
186 lines
8.6 KiB
Go
package browsercfg
|
||
|
||
import (
|
||
"fmt"
|
||
"net/url"
|
||
"path/filepath"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// ---- 默认值与硬上限(design/28 §3;一切具名,本包不存在裸数字)----
|
||
//
|
||
// 口径(design/28 §8 B1):`.env` 已冻结为「最小化功能可跑」的引导面 ⇒ B1 的一切可调项
|
||
// **只**落在 `$DATA_DIR/config/plugins/browser.json` + 插件面板上,代码只提供默认值与
|
||
// 自动探测。因此下面的常量是「配置文件缺失 / 字段 ≤0」时的回落值。
|
||
const (
|
||
// DefaultHeadless 默认档 = **有头**(design/28 §2:「默认档 = 有头 + 持久化 profile」)。
|
||
// 有头的目的是过风控;无头只在「对反爬不敏感 / 要省资源」时由面板显式降级。
|
||
DefaultHeadless = false
|
||
|
||
// DefaultViewportWidth / DefaultViewportHeight 是视口默认 1280×800:桌面常见分辨率的
|
||
// 下界,且与 design/28 §2「指纹一致性」对齐 —— 刻意避开 800×600 那种一眼自动化的尺寸。
|
||
DefaultViewportWidth = 1280
|
||
DefaultViewportHeight = 800
|
||
|
||
// MaxViewportWidth / MaxViewportHeight 是视口硬上限 7680×4320(8K,design/28 §3
|
||
// 「视口尺寸上限」)。超过即判配置非法:不设上限等于允许一次渲染把整机内存交出去。
|
||
MaxViewportWidth = 7680
|
||
MaxViewportHeight = 4320
|
||
|
||
// DefaultTimeoutSeconds 是单次浏览器动作超时 30s:够一次冷启动导航 + 渲染完成。
|
||
// 它是**正常路径**的预算,不是重试预算(design/28 §3)。
|
||
DefaultTimeoutSeconds = 30
|
||
|
||
// DefaultIdleTimeoutSeconds 是空闲回收 300s(design/28 §3「空闲 N 秒回收」的常量):
|
||
// 5 分钟无动作即回收会话,避免一个 profile 被长期占着。
|
||
DefaultIdleTimeoutSeconds = 300
|
||
|
||
// DefaultConcurrency 并发上限恒为 1,**不是可调项**:同一 user-data-dir 起两个实例
|
||
// 会互锁(design/28 §3、§6 坑 2),且 profile 含凭据,串行最省心。Normalize 把任何
|
||
// >1 的取值夹回 1,而不是照单收下。
|
||
DefaultConcurrency = 1
|
||
|
||
// DefaultScreenshotMaxBytes 是单张截图字节上限 8 MiB:1280×800 的 PNG/JPEG 远小于它,
|
||
// 而 vision 管线也只该吃这么大的图(design/28 §3「超限如实截断/缩放并标注」)。
|
||
DefaultScreenshotMaxBytes = 8 << 20
|
||
|
||
// MaxScreenshotMaxBytes 是单张截图字节上限的**硬上限** 32 MiB(design/28 §3
|
||
// 「单张字节上限」):在默认 8 MiB 之上给面板留「显式放大」的余量,但绝不让单张截图无界。
|
||
MaxScreenshotMaxBytes = 32 << 20
|
||
|
||
// DefaultCDPEndpoint 是「已在跑的容器」的候选端点:CDP 默认调试端口 9222 绑回环。
|
||
// 它只是**出站探测目标**,不新开我们的监听端口(AGENTS §9 红线、design/28 §3)。
|
||
DefaultCDPEndpoint = "http://127.0.0.1:9222"
|
||
)
|
||
|
||
// Config 是 browser 插件的落盘配置(`$DATA_DIR/config/plugins/browser.json`,
|
||
// design/28 §1 M2、§8 B1)。零值经 Normalize 即为「全默认」,所以 `Config{}` 是安全的。
|
||
type Config struct {
|
||
// Headless 为 true 用无头;默认 false(有头,见 DefaultHeadless)。
|
||
// 用 plain bool 而不是 *bool:默认值本身就是 false,「缺失」与「显式 false」同义,无歧义。
|
||
Headless bool `json:"headless"`
|
||
|
||
// Executable 显式指定本机 Chrome/Chromium 可执行文件;空 ⇒ 交给自动探测。
|
||
Executable string `json:"executable,omitempty"`
|
||
|
||
// CDPEndpoint 显式指定已在跑的 CDP 端点(如容器)。空 ⇒ 交给自动探测;**显式给出时
|
||
// 它取代自动探测**:连不上就如实报 cdp_unreachable,不偷偷回退本机(design/28 §6 坑 5)。
|
||
CDPEndpoint string `json:"cdpEndpoint,omitempty"`
|
||
|
||
// ProfileDir 是持久化 profile(user-data-dir)目录,**含 cookie / 登录态等凭据**
|
||
// (design/28 §2 三件套之 2):空 ⇒ 默认 `$DATA_DIR/browser/profile`。使用前须过
|
||
// CheckProfileLocation(不得进 git 工作树),目录 0700、内容绝不进上下文。
|
||
ProfileDir string `json:"profileDir,omitempty"`
|
||
|
||
// ViewportWidth / ViewportHeight 是视口尺寸;≤0 ⇒ 用默认(见 DefaultViewport*),
|
||
// 超过上限 ⇒ Validate 判非法(见 MaxViewport*)。
|
||
ViewportWidth int `json:"viewportWidth"`
|
||
ViewportHeight int `json:"viewportHeight"`
|
||
|
||
// TimeoutSeconds 是单次动作超时;≤0 ⇒ 用默认(见 DefaultTimeoutSeconds)。
|
||
TimeoutSeconds int `json:"timeoutSeconds"`
|
||
|
||
// IdleTimeoutSeconds 是空闲回收秒数;≤0 ⇒ 用默认(见 DefaultIdleTimeoutSeconds)。
|
||
IdleTimeoutSeconds int `json:"idleTimeoutSeconds"`
|
||
|
||
// Concurrency 是并发上限;≤0 ⇒ 用默认、>1 ⇒ 夹回 1(见 DefaultConcurrency:profile 锁)。
|
||
Concurrency int `json:"concurrency"`
|
||
|
||
// ScreenshotMaxBytes 是单张截图字节上限;≤0 ⇒ 用默认、超过硬上限 ⇒ Validate 判非法。
|
||
ScreenshotMaxBytes int `json:"screenshotMaxBytes"`
|
||
}
|
||
|
||
// Defaults 返回一组全默认的配置(ProfileDir 落在 dataDir 下)。
|
||
func Defaults(dataDir string) Config {
|
||
return Config{}.Normalize(dataDir)
|
||
}
|
||
|
||
// Normalize 用常量默认补足「≤0 / 缺失」的字段,并把并发夹到 1。它**只填不改**:
|
||
// 越界的值原样保留,交给 Validate 拒绝 —— 悄悄改小会让面板显示的与生效的不是一回事。
|
||
func (c Config) Normalize(dataDir string) Config {
|
||
if c.ViewportWidth <= 0 {
|
||
c.ViewportWidth = DefaultViewportWidth
|
||
}
|
||
if c.ViewportHeight <= 0 {
|
||
c.ViewportHeight = DefaultViewportHeight
|
||
}
|
||
if c.TimeoutSeconds <= 0 {
|
||
c.TimeoutSeconds = DefaultTimeoutSeconds
|
||
}
|
||
if c.IdleTimeoutSeconds <= 0 {
|
||
c.IdleTimeoutSeconds = DefaultIdleTimeoutSeconds
|
||
}
|
||
if c.Concurrency <= 0 {
|
||
c.Concurrency = DefaultConcurrency
|
||
}
|
||
if c.Concurrency > DefaultConcurrency {
|
||
c.Concurrency = DefaultConcurrency
|
||
}
|
||
if c.ScreenshotMaxBytes <= 0 {
|
||
c.ScreenshotMaxBytes = DefaultScreenshotMaxBytes
|
||
}
|
||
if strings.TrimSpace(c.ProfileDir) == "" {
|
||
c.ProfileDir = DefaultProfileDir(dataDir)
|
||
}
|
||
return c
|
||
}
|
||
|
||
// Validate 检查配置是否在硬上限内(design/28 §3)。它判的是**原始值**(未 Normalize 也可用):
|
||
// 负值/零按「用默认」放行(≤0 的语义由 Normalize 承担),只有**越上限**与**端点非法**算错。
|
||
// 错误一律 %w 包住 ErrInvalidConfig;配置错误里不含任何凭据(本类型的字段就没有凭据)。
|
||
func (c Config) Validate() error {
|
||
if c.ViewportWidth > MaxViewportWidth {
|
||
return fmt.Errorf("%w: viewportWidth=%d 超过上限 %d", ErrInvalidConfig, c.ViewportWidth, MaxViewportWidth)
|
||
}
|
||
if c.ViewportHeight > MaxViewportHeight {
|
||
return fmt.Errorf("%w: viewportHeight=%d 超过上限 %d", ErrInvalidConfig, c.ViewportHeight, MaxViewportHeight)
|
||
}
|
||
if c.ScreenshotMaxBytes > MaxScreenshotMaxBytes {
|
||
return fmt.Errorf("%w: screenshotMaxBytes=%d 超过上限 %d", ErrInvalidConfig, c.ScreenshotMaxBytes, MaxScreenshotMaxBytes)
|
||
}
|
||
if raw := strings.TrimSpace(c.CDPEndpoint); raw != "" && !isHTTPAbsolute(raw) {
|
||
return fmt.Errorf("%w: cdpEndpoint %q 不是可用的 http(s) 绝对地址", ErrInvalidConfig, raw)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// Timeout 返回单次动作超时;调用前应已 Normalize(否则可能为 0)。
|
||
func (c Config) Timeout() time.Duration {
|
||
return time.Duration(c.TimeoutSeconds) * time.Second
|
||
}
|
||
|
||
// IdleTimeout 返回空闲回收时长;调用前应已 Normalize(否则可能为 0)。
|
||
func (c Config) IdleTimeout() time.Duration {
|
||
return time.Duration(c.IdleTimeoutSeconds) * time.Second
|
||
}
|
||
|
||
// isHTTPAbsolute 报告 raw 是否为带主机的 http(s) 绝对地址(照 config.ResolveEndpoint 口径:
|
||
// `ftp://x`、`127.0.0.1:9222` 这类一律不算)。
|
||
func isHTTPAbsolute(raw string) bool {
|
||
u, err := url.Parse(raw)
|
||
if err != nil || u.Host == "" {
|
||
return false
|
||
}
|
||
return u.Scheme == "http" || u.Scheme == "https"
|
||
}
|
||
|
||
// DefaultConfigPath 返回 browser 插件配置文件的落点:
|
||
// `$DATA_DIR/config/plugins/browser.json`(design/13 §2.1「一个插件一个文件,文件名 = 插件 id」
|
||
// + design/28 §1 M2)。
|
||
func DefaultConfigPath(dataDir string) string {
|
||
return filepath.Join(dataDir, "config", "plugins", "browser.json")
|
||
}
|
||
|
||
// DefaultProfileDir 返回持久化 profile 目录的默认落点:`$DATA_DIR/browser/profile`。
|
||
// **它含凭据**:0700、不进 git、内容不进上下文(见 EnsureProfileDir / CheckProfileLocation)。
|
||
func DefaultProfileDir(dataDir string) string {
|
||
return filepath.Join(dataDir, "browser", "profile")
|
||
}
|
||
|
||
// DefaultShotsDir 返回截图落盘目录的默认落点(design/28 §4:「截图存
|
||
// `$DATA_DIR/browser/shots/`,0600」)。本片不落盘,只给 B3 一个一致的默认位置。
|
||
func DefaultShotsDir(dataDir string) string {
|
||
return filepath.Join(dataDir, "browser", "shots")
|
||
}
|