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 已写盯梢用例)
This commit is contained in:
1 parent
4213b268fe
commit
e0a6bca501
18 files changed
+3698
-72
No files matched your search
@@ -0,0 +1,150 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user