优化阅读功能

This commit is contained in:
truewhile
2026-10-01 21:57:25 +08:00
parent c1ff55ffbf
commit ce4319ec25
56 changed files with 9511 additions and 354 deletions
+63 -1
View File
@@ -3,14 +3,66 @@
package helper
import (
"bytes"
"compress/flate"
"compress/gzip"
"compress/zlib"
"io"
"net/http"
"net/url"
"strings"
"time"
)
// defaultUserAgent 是默认浏览器 User-Agent(用于 HTTP 请求头)。
const defaultUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
// DecompressBody 兜底解压响应体(gzip / deflate)。
//
// 正常情况下用不到:只要不显式设置 Accept-Encoding,net/http 会自己带上 gzip
// 并透明解压。但书源 JSON 的 header 字段、或某些 CDN 的固定策略,都可能让响应
// 带着 Content-Encoding 回来,压缩字节一旦进入规则层,书源的 JSON.parse 就会
// 报 `invalid character '\x1f'`。这里做一次兜底,保证调用方拿到明文。
//
// brotli(br)无法在纯 Go 标准库里解,因此上面不再 advertise br;真遇到 br
// 响应则原样返回,由调用方按失败处理。
func DecompressBody(resp *http.Response, data []byte) []byte {
if resp == nil || len(data) == 0 {
return data
}
switch strings.ToLower(strings.TrimSpace(resp.Header.Get("Content-Encoding"))) {
case "gzip", "x-gzip":
r, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return data
}
defer r.Close()
if out, err := io.ReadAll(r); err == nil {
return out
}
case "deflate":
// deflate 有两种实际写法:zlib 包装与裸 DEFLATE,依次尝试。
if out, err := io.ReadAll(flate.NewReader(bytes.NewReader(data))); err == nil {
return out
}
if zr, err := zlib.NewReader(bytes.NewReader(data)); err == nil {
defer zr.Close()
if out, err := io.ReadAll(zr); err == nil {
return out
}
}
}
return data
}
// StripAcceptEncoding 移除显式设置的 Accept-Encoding,交回 net/http 管理。
//
// 只有「调用方没设置」时 net/http 才会自动解压,因此任何来源(预设头、书源
// header 字段)带来的 Accept-Encoding 都必须清掉。
func StripAcceptEncoding(h http.Header) {
h.Del("Accept-Encoding")
}
// NewSiteHTTPClient builds an http.Client honoring per-site policies:
// - timeout (seconds, defaults to 15)
// - proxy via HTTP(S)_PROXY environment variables when site.UseProxy is on
@@ -41,12 +93,22 @@ func NewSiteHTTPClient(timeoutSeconds int, useProxy bool) *http.Client {
// HTTPHeaderPresets returns a map of realistic browser HTTP headers.
// These mimic a real Chrome browser to avoid WAF/bot detection.
//
// 注意:这里刻意不设置 Accept-Encoding。
//
// net/http 只在「调用方没有显式设置 Accept-Encoding」时才会自己补上 gzip 并
// 透明解压;一旦我们显式写了这个头,它就原样把压缩字节交出来。之前这里写了
// "gzip, deflate, br",于是所有经历了压缩的响应(静态 JSON、CDN 上的页面等)
// 都会以原始压缩字节进入规则层,书源的 JSON.parse 直接报
// `SyntaxError: invalid character '\x1f'`,表现为「获取最新配置失败」。
//
// 交给 net/http 管理后:请求仍会带 `Accept-Encoding: gzip`(浏览器常见取值),
// 且响应被自动解压;另外也避免服务端挑选我们无法解码的 br。
func HTTPHeaderPresets() map[string]string {
return map[string]string{
"User-Agent": defaultUserAgent,
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Accept-Encoding": "gzip, deflate, br",
"Connection": "keep-alive",
"Upgrade-Insecure-Requests": "1",
"Sec-Fetch-Dest": "document",
+73
View File
@@ -0,0 +1,73 @@
package helper
import (
"bytes"
"compress/gzip"
"net/http"
"testing"
)
// 本文件:压缩相关的回归测试。
//
// 背景:预设头里曾显式写 `Accept-Encoding: gzip, deflate, br`,导致 net/http
// 不再自动解压(它只在调用方没设置该头时才解压),压缩字节直接进入规则层。
// TestHeaderPresetsDoNotSetAcceptEncoding 预设头不得设置 Accept-Encoding。
// 一旦设置,net/http 的透明解压就失效,压缩响应会以原始字节交给上层。
func TestHeaderPresetsDoNotSetAcceptEncoding(t *testing.T) {
for k := range HTTPHeaderPresets() {
if http.CanonicalHeaderKey(k) == "Accept-Encoding" {
t.Fatal("HTTPHeaderPresets 不应设置 Accept-Encoding(会让 net/http 放弃自动解压)")
}
}
}
// TestStripAcceptEncoding 任何来源带来的 Accept-Encoding 都应被清掉。
func TestStripAcceptEncoding(t *testing.T) {
h := http.Header{}
h.Set("Accept-Encoding", "br, gzip")
StripAcceptEncoding(h)
if got := h.Get("Accept-Encoding"); got != "" {
t.Fatalf("Accept-Encoding 未清理: %q", got)
}
}
// TestDecompressBody gzip / deflate 响应体应被还原成明文。
func TestDecompressBody(t *testing.T) {
payload := `{"version":"20260926"}`
t.Run("gzip", func(t *testing.T) {
var buf bytes.Buffer
gz := gzip.NewWriter(&buf)
_, _ = gz.Write([]byte(payload))
_ = gz.Close()
resp := &http.Response{Header: http.Header{}}
resp.Header.Set("Content-Encoding", "gzip")
if got := string(DecompressBody(resp, buf.Bytes())); got != payload {
t.Fatalf("gzip 解压 = %q", got)
}
})
t.Run("未压缩原样返回", func(t *testing.T) {
resp := &http.Response{Header: http.Header{}}
if got := string(DecompressBody(resp, []byte(payload))); got != payload {
t.Fatalf("未压缩响应被改动: %q", got)
}
})
t.Run("坏数据不 panic", func(t *testing.T) {
resp := &http.Response{Header: http.Header{}}
resp.Header.Set("Content-Encoding", "gzip")
raw := []byte("not-gzip")
if got := string(DecompressBody(resp, raw)); got != "not-gzip" {
t.Fatalf("解压失败时应原样返回,实际 %q", got)
}
})
t.Run("nil resp", func(t *testing.T) {
if got := string(DecompressBody(nil, []byte(payload))); got != payload {
t.Fatalf("nil resp 应原样返回,实际 %q", got)
}
})
}