- B3:截图落盘后**自动**并入本轮视图(能收图 ⇒ 真 image 部件挂 user 消息;收不了 ⇒ vision/OCR 文字;
大图先经 image-crop 裁切;取走即消费、每会话 ≤4 张、TTL 10min、跨会话隔离)
- B4:browser 面板**声明**(配置表单 + 运行状态/指纹自检/最近截图三张只读表 + 拍一张/重新自检)
+ 六个动作走 PLG-2 的通用动作端点(**不开新路由**)+ 保存事务(不可用则回滚 409)+ 热重载
- 读面**不启动浏览器**(计数型假 dialer 证明 0 次拨号);行数据不含路径/URL/内容
- 3 个变异(短路主路/行与列 key 脱钩/保存不回滚)
⚠️ **两行接线未落**(不在我文件面):wire_runtime.go 的 TransformContext + wire_frontend.go 的
wireBrowserPanel —— 落地前 B3/B4 只是「组件就绪」,跑着的后端里不存在(Y4 已写盯梢用例)
150 lines
5.9 KiB
Go
150 lines
5.9 KiB
Go
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
|
||
}
|