package browser import ( "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/browsercfg" ) // 本文件是 design/28 §8 的 **B4(M3 面板入口)在驱动层的事实面**:面板要显示与 // 操作的那一组只读事实、以及「最近截图」这类**非内容**统计。 // // 三条口径: // // 1. **面板取数不启动浏览器**:`State` 只读内存里已有的事实(配置、可用性、会话是否活着、 // 上次指纹自检、最近截图统计)。真正的自检(要起浏览器)是**一条显式动作**由主人点, // 不是「打开面板就顺手起一个 Chrome」—— 面板的只读区块连 viewer 都能看(design/09 // §5.4.1),让「看一眼」产生进程外副作用是明确的越界; // 2. **只存非内容事实**:这里存的是字节数/尺寸/格式/时间,**绝不存字节本身、绝不存页面 // 内容或 URL**。截图的正文只以文件形式存在(0600),面板只报「拍到了几张、多大」; // 3. **有界**:最近截图统计最多留 `maxShotRecords` 条(环),指纹只留最后一次。 // maxShotRecords 是「最近截图」保留的条数。面板是一张只读表(排障用),20 条足够看出 // 「刚刚那一下到底拍成功没有」,再多只是把页面拉长。 const maxShotRecords = 20 // ShotRecord 是一次截图的**统计事实**(不含路径、不含内容)。 type ShotRecord struct { // At 是截图落盘时间。 At time.Time // Bytes 是落盘字节数。 Bytes int // Width / Height 是真实像素尺寸;DimensionsKnown=false 时这两个数不可信。 Width int Height int // DimensionsKnown 报告宽高是否从字节里解出来。 DimensionsKnown bool // Format 是实际编码("png" / "jpeg")。 Format string // Degraded 报告这张图是否是「PNG 超上限、降级成 JPEG」的产物。 Degraded bool } // StateSnapshot 是面板与自查工具要的一组只读事实。 // // 字段全部是**当前进程里的事实**(配置取自生效配置、可用性取自构造时的探测结论), // 因此它不会自己发现新装了一个 Chrome 或刚改过的配置文件 —— 那需要重启或面板保存 // 触发的热重载,两种情形都会重建 Manager,快照随之更新。 type StateSnapshot struct { // Source 是来源描述(本机有头 / 容器端点),不含凭据。 Source string // Available 报告当前有没有可用来源(不可用时工具根本不会被注册)。 Available bool // Closed 报告会话管理器是否已关闭(`Close` 之后)。 Closed bool // Headless 是生效档(false = 有头,design/28 §2 的默认档)。 Headless bool // ViewportWidth / ViewportHeight 是生效视口。 ViewportWidth int ViewportHeight int // ProfileDir 是持久化 profile 目录(含登录态,0700;**不进上下文**)。 ProfileDir string // ShotsDir 是截图落点目录。 ShotsDir string // Concurrency 是生效并发上限:**恒为 1**(同一 profile 起两个实例会互锁, // design/28 §3)。面板如实显示它,而不是把它当可调项。 Concurrency int // TimeoutSeconds / IdleTimeoutSeconds 是动作超时与空闲回收秒数。 TimeoutSeconds int IdleTimeoutSeconds int // ScreenshotMaxBytes 是单张截图字节上限。 ScreenshotMaxBytes int // SessionLive 报告此刻是否有一个活着的浏览器会话。 SessionLive bool // SessionOpenedAt 是当前会话的建立时间(SessionLive=false 时为零值)。 SessionOpenedAt time.Time // Shots 是最近截图统计,**最新在前**。 Shots []ShotRecord // Fingerprint / FingerprintAt 是最后一次指纹自检的读数与时间; // 从未自检过时 Fingerprint 为 nil(面板如实显示「未自检」,不编造)。 Fingerprint *Fingerprint FingerprintAt time.Time } // State 返回当前只读事实快照。它不启动浏览器、不发网络请求(见文件头口径 1)。 func (m *Manager) State() StateSnapshot { if m == nil { return StateSnapshot{} } m.mu.Lock() defer m.mu.Unlock() out := StateSnapshot{ Source: m.source, Available: m.d != nil, Closed: m.closed, Headless: m.cfg.headless, ViewportWidth: m.cfg.viewportW, ViewportHeight: m.cfg.viewportH, ProfileDir: m.cfg.profileDir, ShotsDir: m.cfg.shotsDir, Concurrency: browsercfg.DefaultConcurrency, TimeoutSeconds: int(m.cfg.timeout / time.Second), IdleTimeoutSeconds: int(m.cfg.idle / time.Second), ScreenshotMaxBytes: m.cfg.shotMaxBytes, SessionLive: m.sess != nil, SessionOpenedAt: m.sessionOpenedAt, Fingerprint: cloneFingerprint(m.fp), FingerprintAt: m.fpAt, } out.Shots = append([]ShotRecord(nil), m.shots...) return out } // recordShotLocked 记一条截图统计(最新在前、最多 maxShotRecords 条)。调用方必须持有 m.mu。 func (m *Manager) recordShotLocked(shot Shot, at time.Time) { rec := ShotRecord{ At: at, Bytes: shot.Bytes, Width: shot.Width, Height: shot.Height, DimensionsKnown: shot.DimensionsKnown, Format: shot.Format, Degraded: shot.Degraded, } m.shots = append([]ShotRecord{rec}, m.shots...) if len(m.shots) > maxShotRecords { m.shots = m.shots[:maxShotRecords] } } // recordFingerprint 记下最后一次指纹自检的读数(面板据此显示「上次自检」的时间与读数)。 func (m *Manager) recordFingerprint(fp Fingerprint) { if m == nil { return } m.mu.Lock() defer m.mu.Unlock() m.fp = &fp m.fpAt = time.Now() } // cloneFingerprint 深拷一份指纹读数(含切片),使 `State` 的返回值与内部状态互不影响。 func cloneFingerprint(fp *Fingerprint) *Fingerprint { if fp == nil { return nil } out := *fp out.Languages = append([]string(nil), fp.Languages...) return &out }