- 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 已写盯梢用例)
151 lines
5.0 KiB
Go
151 lines
5.0 KiB
Go
package browser
|
||
|
||
// 本文件是 `browser` 面板三块只读表格的**行形状**(design/09 §5.2.1 的 section.table):
|
||
// 把驱动层的事实面(`internal/capability/browser` 的 `StateSnapshot`)翻成表格行。
|
||
//
|
||
// 两条硬口径:
|
||
//
|
||
// 1. **行 key 与声明里的列 key 同源**:两边都引用 panel.go 的 `col*` 常量 —— 声明一个
|
||
// key、行里写另一个,表现就是「表头在、数据空」,而且不会有任何报错;
|
||
// 2. **不含内容、不含路径、不含 URL**:截图只报字节/尺寸/格式,页面只报「有没有会话」。
|
||
// 面板是只读投影(viewer 也能看到表格行),把主人的浏览内容投到那里是越界。
|
||
|
||
import (
|
||
"fmt"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
browsercap "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/browser"
|
||
)
|
||
|
||
// 未自检 / 未知的占位文案。**不编造数字**:没读到的读数就写「未自检」,
|
||
// 让面板与真读数一眼可分(design/28 §6 坑 5 的同一条精神:失败如实报)。
|
||
const (
|
||
notCheckedText = "未自检"
|
||
unknownText = "未知"
|
||
enabledText = "开"
|
||
disabledText = "关"
|
||
availableText = "可用"
|
||
unavailableText = "不可用"
|
||
closedText = "已关闭"
|
||
degradedYes = "是(PNG 超限降级 JPEG)"
|
||
degradedNo = "否"
|
||
)
|
||
|
||
// StateRows 把运行状态摊成一行(表格是一行事实:这个浏览器现在长什么样)。
|
||
//
|
||
// 只有一行,因为事实只有一份 —— 多行会诱导读者以为「有多个浏览器在跑」,而并发上限
|
||
// 恒为 1(design/28 §3)是这套设计的硬约束。
|
||
func StateRows(s browsercap.StateSnapshot) []map[string]any {
|
||
availability := availableText
|
||
switch {
|
||
case s.Closed:
|
||
availability = closedText
|
||
case !s.Available:
|
||
availability = unavailableText
|
||
}
|
||
source := strings.TrimSpace(s.Source)
|
||
if source == "" {
|
||
source = unknownText
|
||
}
|
||
session := "未打开"
|
||
if s.SessionLive {
|
||
session = "已打开"
|
||
if !s.SessionOpenedAt.IsZero() {
|
||
session += "(" + formatTime(s.SessionOpenedAt) + ")"
|
||
}
|
||
}
|
||
return []map[string]any{{
|
||
colSource: source + "(会话:" + session + ")",
|
||
colAvailable: availability,
|
||
colHeadless: yesNo(s.Headless, enabledText, disabledText),
|
||
colViewport: fmt.Sprintf("%d×%d", s.ViewportWidth, s.ViewportHeight),
|
||
colProfileDir: s.ProfileDir,
|
||
colConcurrent: strconv.Itoa(s.Concurrency),
|
||
colIdleReco: strconv.Itoa(s.IdleTimeoutSeconds),
|
||
colShotCap: strconv.Itoa(s.ScreenshotMaxBytes),
|
||
}}
|
||
}
|
||
|
||
// FingerprintRows 把最后一次指纹自检摊成一行;从未自检过时给一行**如实**的「未自检」。
|
||
func FingerprintRows(fp *browsercap.Fingerprint, at time.Time) []map[string]any {
|
||
if fp == nil {
|
||
return []map[string]any{{
|
||
colWebdriver: notCheckedText,
|
||
colScreen: notCheckedText,
|
||
colTimezone: notCheckedText,
|
||
colLanguage: notCheckedText,
|
||
colUserAgent: notCheckedText,
|
||
colCheckedAt: notCheckedText,
|
||
}}
|
||
}
|
||
return []map[string]any{{
|
||
// webdriver 是**布尔事实**:false 才是正常(我们不传 --enable-automation)。
|
||
colWebdriver: yesNo(fp.Webdriver, "true(自动化痕迹没关掉!)", "false(正常)"),
|
||
colScreen: fmt.Sprintf("%d×%d @%dbit dpr=%g", fp.ScreenWidth, fp.ScreenHeight, fp.ColorDepth, fp.DevicePixelRatio),
|
||
colTimezone: orUnknown(fp.Timezone),
|
||
colLanguage: languageText(fp),
|
||
colUserAgent: orUnknown(fp.UserAgent),
|
||
colCheckedAt: formatTime(at),
|
||
}}
|
||
}
|
||
|
||
// ShotRows 把最近截图统计摊成多行(**最新在前**,与驱动的环同序)。
|
||
func ShotRows(recs []browsercap.ShotRecord) []map[string]any {
|
||
out := make([]map[string]any, 0, len(recs))
|
||
for _, r := range recs {
|
||
out = append(out, map[string]any{
|
||
colShotAt: formatTime(r.At),
|
||
colShotBytes: strconv.Itoa(r.Bytes),
|
||
colShotSize: sizeText(r),
|
||
colShotFormat: orUnknown(r.Format),
|
||
colShotDegraded: yesNo(r.Degraded, degradedYes, degradedNo),
|
||
})
|
||
}
|
||
return out
|
||
}
|
||
|
||
// sizeText 渲染像素尺寸。尺寸未知时**不拿视口冒充**(那是「我们以为的」,不是「图里的」)。
|
||
func sizeText(r browsercap.ShotRecord) string {
|
||
if !r.DimensionsKnown || r.Width <= 0 || r.Height <= 0 {
|
||
return unknownText
|
||
}
|
||
return fmt.Sprintf("%d×%d", r.Width, r.Height)
|
||
}
|
||
|
||
// languageText 渲染语言列表(首项 + 总数,避免长串把单元格撑爆)。
|
||
func languageText(fp *browsercap.Fingerprint) string {
|
||
if fp.Language == "" && len(fp.Languages) == 0 {
|
||
return unknownText
|
||
}
|
||
if len(fp.Languages) <= 1 {
|
||
return orUnknown(fp.Language)
|
||
}
|
||
return fmt.Sprintf("%s 等 %d 种", orUnknown(fp.Language), len(fp.Languages))
|
||
}
|
||
|
||
// formatTime 渲染时间(秒级可读;零值给「未知」)。
|
||
func formatTime(t time.Time) string {
|
||
if t.IsZero() {
|
||
return unknownText
|
||
}
|
||
return t.Local().Format("2006-01-02 15:04:05")
|
||
}
|
||
|
||
// orUnknown 把空串渲染成「未知」。
|
||
func orUnknown(s string) string {
|
||
if strings.TrimSpace(s) == "" {
|
||
return unknownText
|
||
}
|
||
return s
|
||
}
|
||
|
||
// yesNo 按真值返回一对文案。
|
||
func yesNo(v bool, yes, no string) string {
|
||
if v {
|
||
return yes
|
||
}
|
||
return no
|
||
}
|