Files
Cyrene_v2/internal/capability/browser/state.go
T
AskaEth e0a6bca501 feat(browser): B3 截图→上下文闭环 + B4 面板声明页(Y4)
- 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 已写盯梢用例)
2026-10-06 21:48:50 +08:00

150 lines
5.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}