Files
AskaEth b040f588b2 fix(browser): M53 追加 Chrome 输出预览打码
preview() 出镜前过 trace.RedactTextLimited(600 rune),覆盖失败日志的 chrome_output 与 withChromeOutput 追加给调用方的错误串;补凭据形状/原话可见/有界三条测试,删实现可打红。
2026-10-26 22:19:31 +08:00

125 lines
6.9 KiB
Go
Raw Permalink 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
// 本文件是**对端自由文本出口**的收敛点:有三处会把页面/浏览器自己说的话原样交给我们,
// 它们出镜(进 error ⇒ 日志 + 模型可见视图)之前必须在这里过一道。
//
// 三处**不是同一类**,因此守卫也不同(口径见 internal/runtime/trace/errortoken.go
// 的「按字段类别判定」):
//
// - `Page.navigate` 的 `errorText` 是**协议枚举**:值域是 Chrome 的 net error 码
// (`net::ERR_NAME_NOT_RESOLVED` 之类),短、标识符形状 ⇒ 用 trace.ErrorToken 守:
// 像枚举就保留,散文一律丢弃;
// - `Runtime.evaluate` 的 `exceptionDetails.text` 是**页面自由文本**:页面的 JS 可以
// throw 任何东西 —— 包括它读到的 DOM 正文、地址栏里带 token 的 URL。⇒ 默认不带
// 全文,按「带上限地带」处理:压平空白 + trace.RedactTextLimited 截断;
// - 浏览器自己的 stdout/stderr 预览(launchoutput.go 的 `preview()`)也是自由文本:
// 参数被拒时 Chromium 会把**本次 argv** 与地址原话打出来(`chromeOutputPreview`)。
//
// 🔴 两类守卫**不能互换**:拿 ErrorToken 去「清洗」自由文本是错的口径(它只会把整段
// 话删掉,不是清洗);拿 RedactTextLimited 去守枚举字段也不对(它不判形状,挡不住
// 散文)。清洗实现仍然只有一份,本文件只做「选哪一道 + 补形状前提」。
import (
"fmt"
"strings"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace"
)
// maxExceptionTextRunes 是 `exceptionDetails.text` 允许出镜的 rune 上限:**200**。
//
// 为什么不复用既有的两个档位:
// - **80**(`trace.RedactText` 的摘要档)会把真实的异常消息拦腰截断 ——
// `TypeError: Cannot read properties of undefined (reading 'addEventListener')`
// 已经 73 rune,再带上半句调用点就过 80,而调用点是唯一的定位线索;
// - **4096**(网关日志页 `maxLogMsgRunes` 那一档)是给「运维要读全文」的日志行用的;
// 这个字段是**页面自己说的话**,4096 等于允许页面把 4KB 正文塞进一条错误串 ——
// 而这条错误串同时进日志与模型可见视图。
//
// 200 容得下「异常消息 + 一段调用点」,又把页面可控文本压在「一句话」量级。超限时
// trace.RedactTextLimited 会补 `…`,因此截断本身是看得见的,不会被误读成全文。
const maxExceptionTextRunes = 200
// netErrorNamespace 是 Chrome net error 码的命名空间前缀(`net::ERR_...`)。
//
// 为什么要先剥掉它再交给 ErrorToken:ErrorToken 的字符集是 `[A-Za-z0-9_.-]`,**不含
// `:`**,整串直接送进去会把合法的网络错误码一起判掉 —— 而「合法网络错误码形态保留」
// 是这条出口的硬要求(上层与工具都在断言它,见 render_test ③)。这个前缀是**协议
// 常量**,不是从对端抄来的文本,因此剥它是安全的。
const netErrorNamespace = "net::"
// netErrorCodePrefix 是 net error 枚举的名字前缀(`ERR_`):所有真实取值都以它开头。
// 于是这一条额外把「恰好是单个令牌的自由文本」(`hunter2` 之类)也挡掉 —— 形状判定
// 仍由 trace.ErrorToken 做,本文件只补这一个前提,不复制第二份守卫实现。
const netErrorCodePrefix = "ERR_"
// netErrorCode 只放行「像 net error 枚举」的 errorText:`[net::]ERR_<标识符>`。
// 其余(散文、被回显的 URL、带凭据的文本)返回空串,由调用方按「已隐去」处理。
func netErrorCode(raw string) string {
text := strings.TrimSpace(raw)
namespace := ""
if rest, ok := strings.CutPrefix(text, netErrorNamespace); ok {
namespace, text = netErrorNamespace, rest
}
if !strings.HasPrefix(text, netErrorCodePrefix) {
return ""
}
token := trace.ErrorToken(text)
if token == "" {
return ""
}
return namespace + token
}
// navigateCause 把 `Page.navigate` 的 errorText 变成可出镜的原因子句。
// 调用前提:raw 非空(navigate 只在 errorText 非空时判定为失败)。
//
// 不是枚举时一个字符的自由文本都不带,只留**字节数** —— 够判断「对端说了句话」,
// 不带那句话(与 cdpError.Error 同一口径)。
func navigateCause(raw string) string {
if code := netErrorCode(raw); code != "" {
return code
}
return fmt.Sprintf("网络错误文本已隐去(%d 字节)", len(raw))
}
// pageExceptionText 是 `exceptionDetails.text` 出镜前的唯一处理:压平空白(一个换行
// 就能在按行聚合的日志里伪造出「另一条记录」,与 sdklog / judgeReason 同一做法)+
// 凭据形状打码 + 截断(清洗实现只有一份:trace.RedactTextLimited)。
func pageExceptionText(raw string) string {
return trace.RedactTextLimited(strings.Join(strings.Fields(raw), " "), maxExceptionTextRunes)
}
// evaluateCause 把 `Runtime.evaluate` 的 exceptionDetails.text 变成可出镜的原因子句。
func evaluateCause(raw string) string {
if text := pageExceptionText(raw); text != "" {
return text
}
// 压平之后什么都不剩(对端只给了空白):如实说「没给文本」,不假装异常无因。
return "页面异常(对端未给文本)"
}
// chromeOutputPreviewRunes 是浏览器 stdout/stderr 预览出镜时的 rune 上限:**600**
// (= 12 行 × 50 rune)。
//
// 为什么是这个量级:预览的**行**预算已经是 12 行(chromeOutputMaxLines),50 rune 大致
// 是一个短分句 —— Chromium 的抱怨句式是「原因在前」(`ERROR:...] 参数被拒:...`),
// 所以 600 保得住原因那一句,又把「一整块转储」挡在模型可见的错误串之外。
//
// 为什么不沿用另外两档:80(`trace.RedactText` 摘要档)连**一行**浏览器日志都放不下;
// 4096(`chromeOutputMaxBytes` 缓存档、网关日志页 maxLogMsgRunes)是「运维读全文」的
// 量级,对浏览器可控的字段等于允许把 4KB 正文塞进工具结果。
//
// 为什么 600 不会把「到底说了多少」藏起来:本上限只压在**预览正文**上,调用方
// (launchoutput.go)随后仍会追加「已截断,共 N 字节」——读者看到的永远是「这里有 600
// rune 的原话 + 它其实说了 N 字节」,截断与量级都看得见。
const chromeOutputPreviewRunes = 600
// chromeOutputPreview 是浏览器 stdout/stderr 预览出镜前的唯一处理:凭据形状打码 +
// 截断(清洗实现只有一份:trace.RedactTextLimited)。它服务**两个**读面 —— 失败日志的
// `chrome_output` 字段与返回给调用方的错误串(launch.go 的 launchOnce),因此收敛在
// preview() 里,两个读面不会判得不一致。
func chromeOutputPreview(text string) string {
return trace.RedactTextLimited(text, chromeOutputPreviewRunes)
}