Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d46640364 | ||
|
|
77a422b9bf | ||
|
|
a078a00f45 |
Binary file not shown.
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 8.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 7.0 KiB |
+21
-8
@@ -48,6 +48,10 @@ var (
|
|||||||
reIllegal = regexp.MustCompile(`[\\/:*?"<>|]`)
|
reIllegal = regexp.MustCompile(`[\\/:*?"<>|]`)
|
||||||
reControl = regexp.MustCompile(`[\x00-\x1f\x7f]`)
|
reControl = regexp.MustCompile(`[\x00-\x1f\x7f]`)
|
||||||
reSpace = regexp.MustCompile(`\s+`)
|
reSpace = regexp.MustCompile(`\s+`)
|
||||||
|
// 目录分隔符 —— 只用来切「模板文本」本身。
|
||||||
|
// ⚠️ 千万别拿它去切占位符替换后的结果:那会把「值里的 / | : 」
|
||||||
|
// 变成真的目录层级。EH 标题惯例是 "原文 | 译名",一个竖线就多一层文件夹。
|
||||||
|
reSep = regexp.MustCompile(`[\\/]+`)
|
||||||
// 模板占位符:{key} 或 {tags:N}
|
// 模板占位符:{key} 或 {tags:N}
|
||||||
rePlaceholder = regexp.MustCompile(`\{([a-zA-Z0-9_]+(?::\d+)?)\}`)
|
rePlaceholder = regexp.MustCompile(`\{([a-zA-Z0-9_]+(?::\d+)?)\}`)
|
||||||
reTagsKey = regexp.MustCompile(`(?i)^tags(?::(\d+))?$`)
|
reTagsKey = regexp.MustCompile(`(?i)^tags(?::(\d+))?$`)
|
||||||
@@ -132,6 +136,13 @@ func FallbackIfEmpty(s, fb string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BuildDir 用模板算出画廊的相对目录(相对 downloadRoot,逐段清洗)
|
// BuildDir 用模板算出画廊的相对目录(相对 downloadRoot,逐段清洗)
|
||||||
|
//
|
||||||
|
// ⚠️ 分层只认「模板文本里写的分隔符」。
|
||||||
|
// 占位符替换出来的值一律当普通名字:里面的 \ / : * ? " < > |
|
||||||
|
// 全部清洗成 _(SanitizeSegment 干的活),绝不凭空多出一层目录。
|
||||||
|
//
|
||||||
|
// 反例(2026-10 修):先替换再 reIllegal.Split,导致标题里的
|
||||||
|
// "[Warashibe] Enkou | 牝穴…" 被竖线切成两层文件夹。
|
||||||
func BuildDir(tpl string, g *eh.Gallery) string {
|
func BuildDir(tpl string, g *eh.Gallery) string {
|
||||||
tpl = strings.TrimSpace(tpl)
|
tpl = strings.TrimSpace(tpl)
|
||||||
if tpl == "" {
|
if tpl == "" {
|
||||||
@@ -140,7 +151,7 @@ func BuildDir(tpl string, g *eh.Gallery) string {
|
|||||||
|
|
||||||
vars := buildDirVars(tpl, g)
|
vars := buildDirVars(tpl, g)
|
||||||
|
|
||||||
out := rePlaceholder.ReplaceAllStringFunc(tpl, func(m string) string {
|
expand := func(m string) string {
|
||||||
key := strings.ToLower(m[1 : len(m)-1])
|
key := strings.ToLower(m[1 : len(m)-1])
|
||||||
// {tags} / {tags:N} —— 冒号后数字表示取前几个
|
// {tags} / {tags:N} —— 冒号后数字表示取前几个
|
||||||
if mt := reTagsKey.FindStringSubmatch(key); mt != nil {
|
if mt := reTagsKey.FindStringSubmatch(key); mt != nil {
|
||||||
@@ -154,20 +165,22 @@ func BuildDir(tpl string, g *eh.Gallery) string {
|
|||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
return m // 未知占位符原样保留,避免用户以为拼错了却静默消失
|
return m // 未知占位符原样保留,避免用户以为拼错了却静默消失
|
||||||
})
|
}
|
||||||
|
|
||||||
// 逐段清洗(兼容 / 和 \ 两种分隔符输入)
|
// 先按模板里的分隔符切段,再逐段展开占位符
|
||||||
segs := []string{}
|
segs := []string{}
|
||||||
for _, s := range reIllegal.Split(out, -1) {
|
for _, raw := range reSep.Split(tpl, -1) {
|
||||||
if strings.TrimSpace(s) == "" {
|
if strings.TrimSpace(raw) == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
idx := len(segs)
|
|
||||||
fb := "未命名"
|
fb := "未命名"
|
||||||
if idx == 0 {
|
if len(segs) == 0 {
|
||||||
fb = g.GID
|
fb = g.GID
|
||||||
}
|
}
|
||||||
segs = append(segs, SanitizeSegment(s, fb))
|
s := SanitizeSegment(rePlaceholder.ReplaceAllStringFunc(raw, expand), fb)
|
||||||
|
if s != "" {
|
||||||
|
segs = append(segs, s)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(segs) == 0 {
|
if len(segs) == 0 {
|
||||||
|
|||||||
@@ -66,6 +66,75 @@ func TestBuildDirDefaults(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 占位符「值」里的非法字符只能清洗成 _,绝不能变成目录层级。
|
||||||
|
//
|
||||||
|
// 回归 2026-10 的线上问题:EH 标题惯例是「原文 | 译名」,
|
||||||
|
// 旧实现先替换占位符再按 [\\/:*?"<>|] 切分,于是标题里一个竖线
|
||||||
|
// 就凭空多出一层文件夹(6 个里 2 个中招)。
|
||||||
|
func TestBuildDirValueIllegalCharsDoNotNest(t *testing.T) {
|
||||||
|
sep := string(filepath.Separator)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
tpl string
|
||||||
|
title string
|
||||||
|
want string // 用 / 书写,断言前会换成本机分隔符
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "竖线(EH 标题惯例:原文 | 译名)",
|
||||||
|
tpl: "{title}",
|
||||||
|
title: "[Warashibe] Mesuana Hojiri Enkou | 牝穴開發援助交尾 [Chinese]",
|
||||||
|
want: "[Warashibe] Mesuana Hojiri Enkou _ 牝穴開發援助交尾 [Chinese]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "斜杠",
|
||||||
|
tpl: "{title}",
|
||||||
|
title: "[Circle] AAA/BBB Collection",
|
||||||
|
want: "[Circle] AAA_BBB Collection",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "反斜杠",
|
||||||
|
tpl: "{title}",
|
||||||
|
title: `[Circle] AAA\BBB Collection`,
|
||||||
|
want: "[Circle] AAA_BBB Collection",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "冒号",
|
||||||
|
tpl: "{title}",
|
||||||
|
title: "Dai Ni Miyasaka Byouin 3 karte: Tsuyo Tanba",
|
||||||
|
want: "Dai Ni Miyasaka Byouin 3 karte_ Tsuyo Tanba",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "问号星号双引号尖括号",
|
||||||
|
tpl: "{title}",
|
||||||
|
title: `a?b*c"d<e>f|g`,
|
||||||
|
want: `a_b_c_d_e_f_g`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "模板里的分隔符仍然生效(多级目录要保留)",
|
||||||
|
tpl: "{artist}/{title}",
|
||||||
|
title: "Enkou | 牝穴",
|
||||||
|
want: "Noxe/Enkou _ 牝穴",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
g := &eh.Gallery{GID: "3492157", Title: tt.title}
|
||||||
|
g.Meta.Artist = "Noxe"
|
||||||
|
got := BuildDir(tt.tpl, g)
|
||||||
|
want := strings.ReplaceAll(tt.want, "/", sep)
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("\n got: %q\nwant: %q", got, want)
|
||||||
|
}
|
||||||
|
// 值里的非法字符不该产生额外层级:段数必须和模板段数一致
|
||||||
|
if n := strings.Count(got, sep); n != strings.Count(tt.tpl, "/") {
|
||||||
|
t.Errorf("层级数不对: got %q (%d 层)", got, n)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 占位符兜底值
|
// 占位符兜底值
|
||||||
func TestBuildDirFallbacks(t *testing.T) {
|
func TestBuildDirFallbacks(t *testing.T) {
|
||||||
g := &eh.Gallery{GID: "12345678", Title: "Plain Title"}
|
g := &eh.Gallery{GID: "12345678", Title: "Plain Title"}
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package eh
|
||||||
|
|
||||||
|
// 回归测试:EH「Content Warning」拦截页(2026-10 线上问题)
|
||||||
|
//
|
||||||
|
// 现象:部分画廊(多被标成 Offensive For Everyone)第一次访问只返回
|
||||||
|
// 一个 3~4KB 的黄框页 —— <title> 和 meta 都在,但一个 /s/ 链接都没有,
|
||||||
|
// 旧代码因此报「页面结构无法解析(EH 可能又改版)」,纯属误导。
|
||||||
|
//
|
||||||
|
// 放行方式:请求一次 ?nw=always,EH 会种下 nw=1 Cookie。
|
||||||
|
// 关键是这个 Cookie 必须被记住,所以 Client 必须带 cookie jar。
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// warnPage 复刻 EH 的内容警告页(关键特征:没有 /s/ 链接、没有 taglist)
|
||||||
|
func warnPage(gid, token string) string {
|
||||||
|
return fmt.Sprintf(`<!DOCTYPE html><html><head>
|
||||||
|
<title>[Some Circle] Flagged Gallery - E-Hentai Galleries</title>
|
||||||
|
<meta name="description" content="Free Hentai Doujinshi Gallery: [Some Circle] Flagged Gallery" />
|
||||||
|
</head><body>
|
||||||
|
<div style="background:#FF6; width:800px; margin:100px auto">
|
||||||
|
<h1>Content Warning</h1>
|
||||||
|
<p>This gallery has been flagged as <strong>Offensive For Everyone</strong>.</p>
|
||||||
|
<p>[<a href="%s/g/%s/%s/?nw=session">View Gallery</a>]</p>
|
||||||
|
<p>[<a href="%s/g/%s/%s/?nw=always">Never Warn Me Again</a>]</p>
|
||||||
|
</div></body></html>`, EHBase, gid, token, EHBase, gid, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拦截页 + nw=always 放行(真实行为:带 nw=always 的请求会被种下 nw=1)
|
||||||
|
func TestFetchGalleryPageContentWarningAutoBypass(t *testing.T) {
|
||||||
|
const (
|
||||||
|
gid = "4166903"
|
||||||
|
tok = "97d35eebb2"
|
||||||
|
)
|
||||||
|
|
||||||
|
real := mockGalleryHTML(t, gid, tok, 25)[0]
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/g/"+gid+"/"+tok+"/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// 真实 EH:看到 nw=always 就种下 nw=1 Cookie,然后进画廊
|
||||||
|
if r.URL.Query().Get("nw") == "always" {
|
||||||
|
http.SetCookie(w, &http.Cookie{Name: "nw", Value: "1", Path: "/"})
|
||||||
|
w.Write([]byte(real))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c, err := r.Cookie("nw")
|
||||||
|
if err != nil || c.Value != "1" {
|
||||||
|
w.Write([]byte(warnPage(gid, tok))) // 还没放行 -> 拦截页
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Write([]byte(real)) // 已放行 -> 真正的画廊页
|
||||||
|
})
|
||||||
|
|
||||||
|
srv := httptest.NewServer(mux)
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
oldBase := EHBase
|
||||||
|
EHBase = srv.URL
|
||||||
|
defer func() { EHBase = oldBase }()
|
||||||
|
|
||||||
|
c := NewClient(5*time.Second, "", "off", "auto")
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
g, err := c.FetchGalleryPage(context.Background(), gid, tok, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("撞到内容警告页应该自动放行,不该失败: %v", err)
|
||||||
|
}
|
||||||
|
if len(g.Pages) == 0 {
|
||||||
|
t.Fatal("自动放行后应至少解析出 1 页")
|
||||||
|
}
|
||||||
|
if len(g.Pages) != 20 {
|
||||||
|
t.Errorf("第 1 屏应解析出 20 页,实际 %d", len(g.Pages))
|
||||||
|
}
|
||||||
|
t.Logf("自动放行成功,解析出 %d 页(总 %d)", len(g.Pages), g.TotalPages)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只在 jar 缺失时才会失败——所以单独锁住「Client 必须带 jar」
|
||||||
|
func TestClientHasCookieJar(t *testing.T) {
|
||||||
|
c := NewClient(5*time.Second, "", "off", "auto")
|
||||||
|
defer c.Close()
|
||||||
|
if c.httpClient(0).Jar == nil {
|
||||||
|
t.Fatal("http.Client 没有 cookie jar —— EH 的 nw=1 记不住,内容警告页永远过不去")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拦截页里识别不出来时的报错文案,不能再说「EH 可能又改版」了
|
||||||
|
func TestContentWarningDetection(t *testing.T) {
|
||||||
|
if !isContentWarning(warnPage("1", "aa")) {
|
||||||
|
t.Error("应识别出内容警告页")
|
||||||
|
}
|
||||||
|
if !isContentWarning(`<h1>Content Warning</h1>`) {
|
||||||
|
t.Error("大小写/空白差异也应识别")
|
||||||
|
}
|
||||||
|
if isContentWarning(`<html><body><a href="/s/k/1-1">thumb</a></body></html>`) {
|
||||||
|
t.Error("正常画廊页不该被误判成警告页")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContentWarningErrorMessage(t *testing.T) {
|
||||||
|
const (
|
||||||
|
gid = "4999999"
|
||||||
|
tok = "deadbeef01"
|
||||||
|
)
|
||||||
|
// 永远只返回拦截页:即使放行也拿不到内容
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/g/"+gid+"/"+tok+"/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write([]byte(warnPage(gid, tok)))
|
||||||
|
})
|
||||||
|
srv := httptest.NewServer(mux)
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
oldBase := EHBase
|
||||||
|
EHBase = srv.URL
|
||||||
|
defer func() { EHBase = oldBase }()
|
||||||
|
|
||||||
|
c := NewClient(5*time.Second, "", "off", "auto")
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
_, err := c.FetchGalleryPage(context.Background(), gid, tok, nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("应当失败")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "Offensive For Everyone") {
|
||||||
|
t.Errorf("报错应说明是内容警告,实际: %v", err)
|
||||||
|
}
|
||||||
|
if strings.Contains(err.Error(), "改版") {
|
||||||
|
t.Errorf("报错不该再提「改版」,那会误导排查: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
+48
-2
@@ -20,6 +20,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/cookiejar"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -280,6 +281,11 @@ type Client struct {
|
|||||||
|
|
||||||
transport *http.Transport // 全局共用,只有代理配置变了才重建
|
transport *http.Transport // 全局共用,只有代理配置变了才重建
|
||||||
limiter *Limiter
|
limiter *Limiter
|
||||||
|
|
||||||
|
// jar 必须有:EH 对部分画廊先弹「Content Warning」拦截页,
|
||||||
|
// 放行靠的是它下发的 nw=1 Cookie。没有 jar 的话每次请求都是
|
||||||
|
// 无状态连接,Cookie 下发即丢,于是永远过不去(2026-10 修)。
|
||||||
|
jar http.CookieJar
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient 建客户端
|
// NewClient 建客户端
|
||||||
@@ -290,12 +296,14 @@ func NewClient(timeout time.Duration, proxyURL, proxyMode, xres string) *Client
|
|||||||
if xres == "" {
|
if xres == "" {
|
||||||
xres = "auto"
|
xres = "auto"
|
||||||
}
|
}
|
||||||
|
jar, _ := cookiejar.New(nil) // cookiejar.New 只在 nil options 时出错
|
||||||
return &Client{
|
return &Client{
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
proxy: proxyURL,
|
proxy: proxyURL,
|
||||||
proxyMode: proxyMode,
|
proxyMode: proxyMode,
|
||||||
xres: xres,
|
xres: xres,
|
||||||
limiter: NewLimiter(0),
|
limiter: NewLimiter(0),
|
||||||
|
jar: jar,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,7 +371,9 @@ func (c *Client) httpClient(timeout time.Duration) *http.Client {
|
|||||||
if timeout <= 0 {
|
if timeout <= 0 {
|
||||||
timeout = defTimeout
|
timeout = defTimeout
|
||||||
}
|
}
|
||||||
return &http.Client{Transport: tr, Timeout: timeout}
|
// Jar 挂在 Client 上(Go 1.26 起 http.Transport 已经没有 Jar 字段了),
|
||||||
|
// 少一个 jar 就会退回无状态模式,EH 的 nw=1 记不住。
|
||||||
|
return &http.Client{Transport: tr, Timeout: timeout, Jar: c.jar}
|
||||||
}
|
}
|
||||||
|
|
||||||
// rebuildTransport 按当前代理配置重建 Transport,并关掉旧连接的空闲池
|
// rebuildTransport 按当前代理配置重建 Transport,并关掉旧连接的空闲池
|
||||||
@@ -457,6 +467,37 @@ func (c *Client) getText(ctx context.Context, u string) (string, error) {
|
|||||||
return string(b), nil
|
return string(b), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reContentWarning EH 的「Content Warning」拦截页特征。
|
||||||
|
//
|
||||||
|
// 这类画廊(多被标成 Offensive For Everyone)第一次访问只会拿到一个
|
||||||
|
// 3~4KB 的黄框页,标题和 meta 都在,但**一个 /s/ 链接都没有**,
|
||||||
|
// 于是旧代码直接报「页面结构无法解析(EH 可能又改版)」——纯误导。
|
||||||
|
var reContentWarning = regexp.MustCompile(`(?i)<h1>\s*Content Warning\s*</h1>|nw=always`)
|
||||||
|
|
||||||
|
func isContentWarning(html string) bool {
|
||||||
|
return reContentWarning.MatchString(html)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTextAutoWarn 抓文本;撞上内容警告页就自动放行一次再重试。
|
||||||
|
//
|
||||||
|
// 放行方式是请求一次 ?nw=always —— EH 会种下 nw=1 Cookie,
|
||||||
|
// 而 jar 会把它记住,之后所有画廊页、/s/ 页都不用再警告。
|
||||||
|
func (c *Client) getTextAutoWarn(ctx context.Context, u string) (string, error) {
|
||||||
|
html, err := c.getText(ctx, u)
|
||||||
|
if err != nil || !isContentWarning(html) {
|
||||||
|
return html, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sep := "?"
|
||||||
|
if strings.Contains(u, "?") {
|
||||||
|
sep = "&"
|
||||||
|
}
|
||||||
|
if _, werr := c.getText(ctx, u+sep+"nw=always"); werr != nil {
|
||||||
|
return html, nil // 放行失败就原样返回,交给调用方判断
|
||||||
|
}
|
||||||
|
return c.getText(ctx, u)
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------- 1) 画廊页
|
// ---------------------------------------------------------------- 1) 画廊页
|
||||||
|
|
||||||
// ParseProgress 解析进度回调
|
// ParseProgress 解析进度回调
|
||||||
@@ -489,7 +530,7 @@ func (c *Client) FetchGalleryPage(ctx context.Context, gid, token string, onProg
|
|||||||
if p > 0 {
|
if p > 0 {
|
||||||
u += "?p=" + strconv.Itoa(p)
|
u += "?p=" + strconv.Itoa(p)
|
||||||
}
|
}
|
||||||
html, err := c.getText(ctx, u)
|
html, err := c.getTextAutoWarn(ctx, u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("解析画廊页失败: %w", err)
|
return nil, fmt.Errorf("解析画廊页失败: %w", err)
|
||||||
}
|
}
|
||||||
@@ -552,6 +593,11 @@ func (c *Client) FetchGalleryPage(ctx context.Context, gid, token string, onProg
|
|||||||
if reRestricted.MatchString(lastHTML) {
|
if reRestricted.MatchString(lastHTML) {
|
||||||
return nil, fmt.Errorf("画廊受限或不存在(无 cookie 访问不了): %s", gid)
|
return nil, fmt.Errorf("画廊受限或不存在(无 cookie 访问不了): %s", gid)
|
||||||
}
|
}
|
||||||
|
if isContentWarning(lastHTML) {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"画廊被 EH 标记为「Offensive For Everyone」,自动放行没成功"+
|
||||||
|
"(请在浏览器里打开一次并点 Never Warn Me Again): %s", gid)
|
||||||
|
}
|
||||||
return nil, fmt.Errorf("页面结构无法解析(EH 可能又改版): %s", gid)
|
return nil, fmt.Errorf("页面结构无法解析(EH 可能又改版): %s", gid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user