mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-05 21:06:38 +08:00
优化
This commit is contained in:
@@ -375,6 +375,12 @@ func (s *ReaderService) prepareBrowserPage(ctx context.Context, sourceURL, id st
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("打开页面失败: %w", err)
|
||||
}
|
||||
// 空响应没有可展示的内容:以前照样包一层空 <pre> 交给 iframe,面板就是一片白,
|
||||
// 用户看不出是地址有问题还是自己没登录。这里直接报错(如段评地址参数被改写、
|
||||
// 上游按缺参数返回空 body 的情形)。
|
||||
if strings.TrimSpace(body) == "" {
|
||||
return "", "", fmt.Errorf("目标地址没有返回任何内容: %s", truncateForLog(req.URL, 120))
|
||||
}
|
||||
cookie := s.browserCookieHeader(ctx, sourceURL, finalURL)
|
||||
if !strings.Contains(strings.ToLower(contentType), "html") {
|
||||
// 非 HTML(如 JSON 接口):包一层 <pre>,至少让用户看到内容
|
||||
|
||||
@@ -2,6 +2,7 @@ package rule
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/PaesslerAG/jsonpath"
|
||||
@@ -450,10 +451,7 @@ func (a *AnalyzeRule) getStringRules(ruleList []*SourceRule, mContent any, isUrl
|
||||
if result == nil {
|
||||
result = ""
|
||||
}
|
||||
str := resultString(result)
|
||||
if strings.Contains(str, "&") {
|
||||
str = html.UnescapeString(str)
|
||||
}
|
||||
str := unescapeRuleEntities(resultString(result))
|
||||
if isUrl {
|
||||
if strings.TrimSpace(str) == "" {
|
||||
// 对应 legado:取值为空时回退 baseUrl,让相对地址还能解析。
|
||||
@@ -597,6 +595,49 @@ func applyReplaceRegex(result string, r ResolvedSourceRule) string {
|
||||
return regexReplaceAll(r.ReplaceRegex, result, r.Replacement)
|
||||
}
|
||||
|
||||
// entityRefRe 匹配一个 HTML 字符引用:命名实体(&、 )、十进制(')、
|
||||
// 十六进制(')。分号可选——旧式简写( 、&)在 HTML 文本里同样成立。
|
||||
var entityRefRe = regexp.MustCompile(`&(?:[a-zA-Z][a-zA-Z0-9]{1,31}|#[0-9]{1,7}|#[xX][0-9a-fA-F]{1,6});?`)
|
||||
|
||||
// unescapeRuleEntities 还原规则结果里的 HTML 实体(`&` → `&`、`'` → `'`)。
|
||||
//
|
||||
// 唯一的例外是「不带分号的旧式简写后面紧跟 = 或字母数字」这一种:HTML5 里这类写法
|
||||
// 属于历史匹配,而规则结果并不是纯 HTML 文本——正文、地址、查询串都在同一份字符串里。
|
||||
// 按浏览器语义无差别还原会把查询串吃掉:光遇聚合的段评地址
|
||||
// `…?item_id=X¶=1&source=QQ阅读` 会变成 `…?item_id=X¶=1&source=QQ阅读`,
|
||||
// 上游按缺参数处理返回空页面,段评面板就只剩一片空白(见 reader/comment.go)。
|
||||
func unescapeRuleEntities(s string) string {
|
||||
if !strings.Contains(s, "&") {
|
||||
return s
|
||||
}
|
||||
var b strings.Builder
|
||||
last := 0
|
||||
for _, loc := range entityRefRe.FindAllStringIndex(s, -1) {
|
||||
match := s[loc[0]:loc[1]]
|
||||
if !strings.HasSuffix(match, ";") && loc[1] < len(s) && isEntityStopByte(s[loc[1]]) {
|
||||
continue
|
||||
}
|
||||
decoded := html.UnescapeString(match)
|
||||
if decoded == match {
|
||||
continue // 不是已知实体,原样保留
|
||||
}
|
||||
b.WriteString(s[last:loc[0]])
|
||||
b.WriteString(decoded)
|
||||
last = loc[1]
|
||||
}
|
||||
if last == 0 {
|
||||
return s
|
||||
}
|
||||
b.WriteString(s[last:])
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// isEntityStopByte 判断旧式简写实体后面这个字符是否让它失去实体语义
|
||||
// (HTML5 规定不带分号的引用后跟 = 或字母数字时按普通文本处理)。
|
||||
func isEntityStopByte(c byte) bool {
|
||||
return c == '=' || c >= '0' && c <= '9' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z'
|
||||
}
|
||||
|
||||
// resultString 对应 Kotlin 的 result.toString()。
|
||||
func resultString(result any) string {
|
||||
switch t := result.(type) {
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 规则结果里的地址带查询串。html.UnescapeString 会按 HTML5 旧式简写把 ¶ 吃成
|
||||
// 「¶」,参数名变成 ¶ 之后上游按缺参数返回空页面——光遇聚合的段评面板一片空白
|
||||
// 就是这个原因(见 reader/comment.go、reader/browser_panel.go)。
|
||||
func TestUnescapeRuleEntitiesKeepsQueryString(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "段评地址里的 ¶ 不能被当成实体",
|
||||
in: "https://v1.qingtian618.com/get_para_review?item_id=X¶=1&source=QQ%E9%98%85%E8%AF%BB",
|
||||
want: "https://v1.qingtian618.com/get_para_review?item_id=X¶=1&source=QQ%E9%98%85%E8%AF%BB",
|
||||
},
|
||||
{
|
||||
name: "¬ 开头的参数名同样不能被吃掉",
|
||||
in: "https://example.com/api?book=1¬ify=1",
|
||||
want: "https://example.com/api?book=1¬ify=1",
|
||||
},
|
||||
{
|
||||
name: "带分号的实体照旧还原",
|
||||
in: "url?a=1&b=2&c=3",
|
||||
want: "url?a=1&b=2&c=3",
|
||||
},
|
||||
{
|
||||
name: "数字实体照旧还原",
|
||||
in: "'引号' 'x' ©",
|
||||
want: "'引号' 'x' ©",
|
||||
},
|
||||
{
|
||||
name: "文字里的旧式简写(后面不是 = 或字母数字)照旧还原",
|
||||
in: "第一段 第二段© 2026×",
|
||||
want: "第一段\u00a0第二段© 2026×",
|
||||
},
|
||||
{
|
||||
name: "不是实体的 & 原样保留",
|
||||
in: "a & b &foo; c",
|
||||
want: "a & b &foo; c",
|
||||
},
|
||||
{
|
||||
name: "不含 & 的字符串直接放行",
|
||||
in: "魔法大陆。",
|
||||
want: "魔法大陆。",
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
if got := unescapeRuleEntities(c.in); got != c.want {
|
||||
t.Fatalf("unescapeRuleEntities(%q) = %q,期望 %q", c.in, got, c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 端到端:走 GetString 的规则结果同样不能把段评地址里的 ¶ 吃掉
|
||||
// (光遇聚合的正文规则是 <js>…</js>$.content 形态,最终取 $.content)。
|
||||
func TestGetStringCommentURLKeepsParaParam(t *testing.T) {
|
||||
const url = "https://v1.qingtian618.com/get_para_review?item_id=YmlkPTYxNDc4MiZjaWQ9MQ¶=11&source=QQ%E9%98%85%E8%AF%BB"
|
||||
payload := `{"content":"<p>正文<comment ident=\"` + url + `\" count=\"39\" /></p>"}`
|
||||
|
||||
a := NewAnalyzeRule()
|
||||
a.SetContent(payload, "https://v1.qingtian618.com")
|
||||
got, err := a.GetString("$.content", nil, false)
|
||||
if err != nil {
|
||||
t.Fatalf("GetString: %v", err)
|
||||
}
|
||||
if !strings.Contains(got, "¶=11") {
|
||||
t.Fatalf("段评地址里的 ¶=11 被改写了: %q", got)
|
||||
}
|
||||
if strings.Contains(got, "\u00b6") {
|
||||
t.Fatalf("段评地址里出现了 ¶(HTML 旧式实体还原的产物): %q", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user