- browsercfg 接进组合根:配置读 $DATA_DIR/config/plugins/browser.json(零 env)→ 探测(本机
Chromium → 已在跑的 CDP)→ 不可用就不注册(原因码入日志)
- 自研最小出站 CDP 客户端(不 Enabled Runtime、隔离世界求值、不发 --enable-automation)+
导航 + 截图 0600 落盘 + 指纹自检;工具 browser(open/screenshot/info,owner-only)
- 3 个变异自检(启动恢复 --enable-automation/无视可用性/空闲回收失效)
- 真机对照(两站各 1 次,**如实结论**):两侧都 pass ⇒ 「headless 被拦 / headful 通过」**未被证明**;
但测到两处硬差异:无头 UA 带 HeadlessChrome、screen 800 vs 2632;两模式 webdriver 均 false
(把 L6 的推断变成实测)。跑完 0 残留 Chrome、无对外监听端口
⚠️ 未做:MCP 自动接入(口径委派未落地)/自动注入上下文/面板页/搜索降级/docker 配方/Windows 用
pipe(Go ExtraFiles 不支持);SSRF 落了第三份同策实现待收口;浏览器会话未挂进程关闭链
82 lines
4.2 KiB
Go
82 lines
4.2 KiB
Go
package browser
|
||
|
||
import "errors"
|
||
|
||
// 本包对外哨兵错误,一律用 errors.Is 判定;文案由调用方补齐上下文(AGENTS §10:
|
||
// 错误一律 %w wrap,禁裸 return err)。**不设包级可变状态**(除这些 var ErrXxx)。
|
||
var (
|
||
// ErrCDP 表示 CDP 层失败:WebSocket 连不上 / 命令返回协议错误 / 连接被打断。
|
||
ErrCDP = errors.New("browser: CDP 调用失败")
|
||
// ErrLaunch 表示启动本机浏览器失败:可执行文件拉不起来、进程立刻退出、
|
||
// 或者等不到调试端口就绪。
|
||
ErrLaunch = errors.New("browser: 启动本机浏览器失败")
|
||
// ErrNavigate 表示导航失败(CDP 报 errorText,或等待页面就绪超时)。
|
||
ErrNavigate = errors.New("browser: 导航失败")
|
||
// ErrEvaluate 表示在页面里求值失败(表达式异常 / 拿不到执行上下文)。
|
||
ErrEvaluate = errors.New("browser: 页面求值失败")
|
||
// ErrBlockedURL 表示目标 URL 被 SSRF 守卫拒绝(回环/私网/链路本地等)。
|
||
ErrBlockedURL = errors.New("browser: URL 被安全守卫拒绝")
|
||
// ErrScreenshotTooLarge 表示截图在两次尝试(PNG → JPEG)之后仍超过配置的字节上限。
|
||
// 超限**不截断**:半个 PNG 是坏图,模型读到的会是噪声(design/28 §3「超限如实
|
||
// 截断/缩放并标注」的落地口径 —— 能降编码就降,降不下来就如实报错)。
|
||
ErrScreenshotTooLarge = errors.New("browser: 截图超过字节上限")
|
||
// ErrNoSession 表示管理器已被关闭,或当前没有可用会话(不可用时不假装可用)。
|
||
ErrNoSession = errors.New("browser: 没有可用的浏览器会话")
|
||
// ErrUnavailable 表示这台机器上没有任何可用来源(没装 Chrome、也没有在跑的
|
||
// CDP 端点)。它由组合根在探测阶段判定,此处只用于「手里没有 dialer」这一态。
|
||
ErrUnavailable = errors.New("browser: 本机没有可用的浏览器来源")
|
||
)
|
||
|
||
// Reason 是「这次为什么没成功」的**分类原因码**:上层据此如实降级与记日志,
|
||
// 不许把它含糊成「失败」两个字(design/28 §6 坑 5)。取值稳定、可断言;人类可读
|
||
// 文案见 Message()。
|
||
type Reason string
|
||
|
||
// 分类原因码。
|
||
const (
|
||
// ReasonLaunched 表示会话由本机 Chrome/Chromium 新起(默认有头、持久 profile)。
|
||
ReasonLaunched Reason = "launched-local-chrome"
|
||
// ReasonRemoteCDP 表示会话复用了配置里显式给出的 CDP 端点(容器/手动实例)。
|
||
ReasonRemoteCDP Reason = "remote-cdp"
|
||
// ReasonIdleRecycled 表示上一次会话因空闲超时被回收(下一个动作会重新起)。
|
||
ReasonIdleRecycled Reason = "idle-recycled"
|
||
// ReasonLaunchFailed 表示本机浏览器没拉起来(可执行文件 / 端口未就绪)。
|
||
ReasonLaunchFailed Reason = "launch-failed"
|
||
// ReasonNoPageTarget 表示连上了浏览器却找不到可用的 page target。
|
||
ReasonNoPageTarget Reason = "no-page-target"
|
||
// ReasonBlockedURL 表示 URL 没过 SSRF 守卫。
|
||
ReasonBlockedURL Reason = "blocked-url"
|
||
// ReasonNavigateFailed 表示导航失败或等待就绪超时。
|
||
ReasonNavigateFailed Reason = "navigate-failed"
|
||
// ReasonShotTooLarge 表示截图超过字节上限(如实报,不交坏图)。
|
||
ReasonShotTooLarge Reason = "screenshot-too-large"
|
||
// ReasonClosed 表示管理器已关闭。
|
||
ReasonClosed Reason = "closed"
|
||
)
|
||
|
||
// Message 返回原因码对应的中文文案;未知码返回空串(调用方回落到错误的 Error())。
|
||
func (r Reason) Message() string {
|
||
switch r {
|
||
case ReasonLaunched:
|
||
return "已启动本机浏览器(默认有头 + 持久 profile)"
|
||
case ReasonRemoteCDP:
|
||
return "已连接到配置指定的 CDP 端点"
|
||
case ReasonIdleRecycled:
|
||
return "空闲超时,上一次会话已回收"
|
||
case ReasonLaunchFailed:
|
||
return "本机浏览器未能启动"
|
||
case ReasonNoPageTarget:
|
||
return "连上了浏览器,但没有可用的 page target"
|
||
case ReasonBlockedURL:
|
||
return "目标 URL 未通过安全守卫"
|
||
case ReasonNavigateFailed:
|
||
return "导航失败或等待页面就绪超时"
|
||
case ReasonShotTooLarge:
|
||
return "截图超过配置的字节上限"
|
||
case ReasonClosed:
|
||
return "浏览器管理器已关闭"
|
||
default:
|
||
return ""
|
||
}
|
||
}
|