Files
Cyrene_v2/internal/capability/browser/errtext.go
T
AskaEth 8058660537 fix(browser): M53 两条页面侧自由文本出口收敛脱敏
- Page.navigate 的 errorText 是**协议枚举**面(net error 码):先剥 `net::`
  命名空间再交给 trace.ErrorToken,像枚举就原样保留、散文一律丢弃只留字节数;
- Runtime.evaluate 的 exceptionDetails.text 是**页面自由文本**面:压平空白
  (防日志行伪造)+ trace.RedactTextLimited 按 200 rune 截断(档位理由见
  errtext.go:80 会截断真实异常消息,4096 等于允许页面塞 4KB 正文);
- 两类守卫不互换(口径见 trace/errortoken.go 的按字段类别判定),清洗实现
  仍只有 trace 那一份,本片只做「选哪一道 + 补形状前提」。

测试:出口 A/B 各一条经 RenderText 的真实路径断言(含 URL/凭据形状的假文本),
外加 net::ERR_* 正向保留、上限截断/换行压平、netErrorCode 形状表。三条泄露断言
已验证「退回原样直吐即红」。
2026-10-26 21:57:48 +08:00

99 lines
5.1 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
// 本文件是**页面侧自由文本出口**的收敛点:CDP 有两个字段会把页面/对端说的话原样
// 交给我们,它们出镜(进 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 截断。
//
// 🔴 两类守卫**不能互换**:拿 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 "页面异常(对端未给文本)"
}