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 }