feat(browser): B1 接线 + B2 薄层工具面(V5)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 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 落了第三份同策实现待收口;浏览器会话未挂进程关闭链
This commit is contained in:
AskaEth committed 2026-10-06 20:25:16 +08:00
1 parent ea150ceea9
commit f1fbc249fd
21 files changed
+3579

No files matched your search

+98
View File
@@ -0,0 +1,98 @@
package browser
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"image"
_ "image/jpeg" // 注册 JPEG 解码器:截图的宽高要如实报,不能靠猜
_ "image/png" // 注册 PNG 解码器
"os"
"path/filepath"
"strconv"
"time"
)
// 截图落盘的具名常量。
const (
// shotFileMode 是截图文件的权限:0600。截图里有页面内容,可能是登录态、二维码、
// 订单号 —— 它按凭据对待(design/28 §4 第 3 条)。
shotFileMode os.FileMode = 0o600
// shotDirMode 是截图目录的权限:0700(只有本用户可进)。
shotDirMode os.FileMode = 0o700
// shotPrefix 是截图文件名前缀,便于人工在 `$DATA_DIR/browser/shots/` 里辨认。
shotPrefix = "shot-"
// shotExtPNG 与 shotExtJPEG 是两种落盘扩展名,也是「实际用了哪种编码」的凭证。
shotExtPNG = "png"
shotExtJPEG = "jpeg"
)
// Shot 是一次截图的落盘结论:**路径 + 指纹 + 真实尺寸**,供 vision/image-crop
// 循既有路径进上下文,也供面板按路径展示(B4)。
type Shot struct {
// Path 是落盘的绝对路径(0600)。它是闭环入口:vision 工具按本地路径读它。
Path string `json:"path"`
// Bytes 是落盘字节数(如实,不含任何缩放)。
Bytes int `json:"bytes"`
// SHA256 是内容指纹的十六进制全串(对齐 reference/browser-stealth/07 §6 的字段口径)。
SHA256 string `json:"sha256"`
// Format 是实际使用的编码("png",或超上限降级后的 "jpeg")。
Format string `json:"format"`
// Width / Height 是从字节里解出来的**真实**像素尺寸;解不出来时为 0 且
// DimensionsKnown=false(不编造数字)。
Width int `json:"width"`
Height int `json:"height"`
// DimensionsKnown 报告宽高是否可信(false ⇒ 字节不是本函数认识的图像格式)。
DimensionsKnown bool `json:"dimensionsKnown"`
// Degraded 报告这张图是否是「PNG 超上限、降级成 JPEG」的产物(如实标注)。
Degraded bool `json:"degraded"`
}
// saveShot 把截图字节写进 dir(0700),文件 0600,返回结论。
//
// 文件名 = `shot-<36 进制纳秒时间戳>.<ext>`:时间戳是的的确确的(便于按时间排序),
// 而**不含任何 URL/站点信息** —— 截图路径会进上下文与日志,站点名本身就是信息
// (对齐 design/28 §4「审计只记必要项」)。
func saveShot(dir string, data []byte, format string) (Shot, error) {
if len(data) == 0 {
return Shot{}, fmt.Errorf("browser: 截图字节为空")
}
if err := os.MkdirAll(dir, shotDirMode); err != nil {
return Shot{}, fmt.Errorf("browser: 建截图目录 %s: %w", dir, err)
}
ext := shotExtPNG
if format == shotExtJPEG {
ext = shotExtJPEG
}
name := shotPrefix + strconv.FormatInt(time.Now().UnixNano(), 36) + "." + ext
path := filepath.Join(dir, name)
if err := os.WriteFile(path, data, shotFileMode); err != nil {
return Shot{}, fmt.Errorf("browser: 写截图 %s: %w", path, err)
}
sum := sha256.Sum256(data)
width, height, known := imageSize(data)
return Shot{
Path: path,
Bytes: len(data),
SHA256: hex.EncodeToString(sum[:]),
Format: ext,
Width: width,
Height: height,
DimensionsKnown: known,
Degraded: ext == shotExtJPEG,
}, nil
}
// imageSize 从字节里解出像素宽高。解不出来返回 (0, 0, false) —— 如实标注未知,
// 而不是拿配置里的视口尺寸冒充(那是「我们以为的」,不是「图里的」)。
func imageSize(data []byte) (int, int, bool) {
cfg, _, err := image.DecodeConfig(bytes.NewReader(data))
if err != nil || cfg.Width <= 0 || cfg.Height <= 0 {
return 0, 0, false
}
return cfg.Width, cfg.Height, true
}