bug处理

This commit is contained in:
truewhile
2026-10-02 15:14:36 +08:00
parent 83ec494a36
commit 2c6646beea
23 changed files with 1900 additions and 108 deletions
+127 -23
View File
@@ -3,7 +3,9 @@
package reader
import (
"bytes"
"encoding/json"
"strconv"
"strings"
)
@@ -51,6 +53,23 @@ func (b *BookSource) HasLogin() bool {
return strings.TrimSpace(SPtr(b.LoginURL)) != "" || strings.TrimSpace(SPtr(b.LoginUI)) != ""
}
// SearchCheckKeyWord 书源在搜索规则里声明的校验关键字(legado SearchRule.checkKeyWord)。
//
// 源的作者用它证明「这个地址返回的确实是正常搜索结果」:校验时搜这个关键字,
// 响应里应该出现它;被风控/换域名后响应里就没有了。legado 的 getCheckKeyword
// 对含 http / :: / ++ / -- 的值不认(那是地址或扩展标记,不是关键字),这里保持一致。
func (b *BookSource) SearchCheckKeyWord() string {
if b == nil || b.RuleSearch == nil {
return ""
}
kw := strings.TrimSpace(SPtr(b.RuleSearch.CheckKeyWord))
if kw == "" || strings.Contains(kw, "http") || strings.Contains(kw, "::") ||
strings.Contains(kw, "++") || strings.Contains(kw, "--") {
return ""
}
return kw
}
// LoginJS 返回 loginUrl 的纯 JS 体(剥掉 @js: / <js>…< 包裹)。
// 对应 legado BaseSource.getLoginJs():loginUi 的按钮 action 会拼在其后执行,
// 因此 loginUrl 同时充当登录交互的函数库。
@@ -60,17 +79,26 @@ func (b *BookSource) LoginJS() string {
// stripJSWrapper 去掉 JS 规则的 @js: / <js>…</js> 包裹。
func stripJSWrapper(s string) string {
s = strings.TrimSpace(s)
if strings.HasPrefix(s, "@js:") {
return s[len("@js:"):]
out, _ := stripJSWrapperOK(s)
return out
}
// stripJSWrapperOK 与 stripJSWrapper 相同,但额外告知是否命中 JS 包装。
// 「可能是 JSON 也可能是 JS 规则」的字段(如书源 header)需要区分这两者。
func stripJSWrapperOK(s string) (string, bool) {
trimmed := strings.TrimSpace(s)
lower := strings.ToLower(trimmed)
switch {
case strings.HasPrefix(lower, "@js:"):
return trimmed[len("@js:"):], true
case strings.HasPrefix(lower, "<js>"):
body := trimmed[len("<js>"):]
body = strings.TrimSuffix(strings.TrimSpace(body), "</js>")
body = strings.TrimSuffix(strings.TrimSpace(body), "<")
return body, true
default:
return trimmed, false
}
if strings.HasPrefix(s, "<js>") {
s = s[len("<js>"):]
s = strings.TrimSuffix(strings.TrimSpace(s), "</js>")
s = strings.TrimSuffix(strings.TrimSpace(s), "<")
return s
}
return s
}
// SearchRule 搜索规则。
@@ -110,6 +138,9 @@ type TocRule struct {
ChapterURL *string `json:"chapterUrl"`
IsVolume *string `json:"isVolume"`
UpdateTime *string `json:"updateTime"`
// NextTocURL 下一页目录规则(可为多值)。长书目录按 offset/limit 分页时靠它
// 取全,缺了就只能拿到第一页(如 399 章只出现 100 章)。
NextTocURL *string `json:"nextTocUrl"`
}
// ContentRule 正文规则。
@@ -125,25 +156,33 @@ type ContentRule struct {
// ExploreRule 发现规则。
type ExploreRule struct {
BookList *string `json:"bookList"`
Name *string `json:"name"`
Author *string `json:"author"`
Kind *string `json:"kind"`
WordCount *string `json:"wordCount"`
LastChapter *string `json:"lastChapter"`
Intro *string `json:"intro"`
CoverURL *string `json:"coverUrl"`
BookURL *string `json:"bookUrl"`
ExploreURL *string `json:"exploreUrl"`
ExploreKinds *string `json:"exploreKinds"`
CheckKeyWord *string `json:"checkKeyWord"`
BookList *string `json:"bookList"`
Name *string `json:"name"`
Author *string `json:"author"`
Kind *string `json:"kind"`
WordCount *string `json:"wordCount"`
LastChapter *string `json:"lastChapter"`
Intro *string `json:"intro"`
CoverURL *string `json:"coverUrl"`
BookURL *string `json:"bookUrl"`
ExploreURL *string `json:"exploreUrl"`
ExploreKinds *string `json:"exploreKinds"`
CheckKeyWord *string `json:"checkKeyWord"`
}
// ParseBookSource 将书源 JSON 解析为结构体。
//
// 先按标准 JSON 解一次;失败时用「宽容版」再解一次(见 normalizeSourceJSON)。
// 阅读生态里的导出工具写法五花八门,同一份书源在别处能用、在这里却整体导入失败,
// 只因为某个字段的类型变了形状 —— 这类差异不值得让用户改 JSON。
func ParseBookSource(raw string) (*BookSource, error) {
var bs BookSource
if err := json.Unmarshal([]byte(raw), &bs); err != nil {
return nil, err
var relaxed BookSource
if err2 := json.Unmarshal([]byte(normalizeSourceJSON(raw)), &relaxed); err2 != nil {
return nil, err // 报原始错误:字段位置更贴近用户看到的 JSON
}
bs = relaxed
}
bs.RawJSON = raw
if bs.RawVariables != nil && strings.TrimSpace(*bs.RawVariables) != "" {
@@ -152,6 +191,71 @@ func ParseBookSource(raw string) (*BookSource, error) {
return &bs, nil
}
// sourceNumericFields 是书源里可能被写成字符串的数字字段。
// 例:{"lastUpdateTime":"1788449879889"}(阅读本体导出/第三方源站的常见写法)。
var sourceNumericFields = []string{
"customOrder", "weight", "bookSourceType", "lastUpdateTime", "respondTime",
}
// sourceRuleObjectFields 在 legado 里是对象,但部分导出工具把空规则写成 []。
// 例:{"ruleExplore":[]}(空发现规则)。
var sourceRuleObjectFields = []string{
"ruleExplore", "ruleSearch", "ruleBookInfo", "ruleToc", "ruleContent",
}
// normalizeSourceJSON 消化书源 JSON 与 legado 实体之间的形状差异:
// - 数字字段被写成字符串 → 转成数字;
// - 规则对象被写成空数组 [] → 转成空对象 {}。
//
// 只动顶层已知字段,规则 JS 字符串原样保留。解析不出来就原样返回,交给调用方报错。
func normalizeSourceJSON(raw string) string {
var fields map[string]json.RawMessage
if err := json.Unmarshal([]byte(raw), &fields); err != nil {
return raw
}
changed := false
for _, key := range sourceNumericFields {
value, ok := fields[key]
if !ok {
continue
}
var text string
if json.Unmarshal(value, &text) != nil {
continue // 本来就是数字(或其它类型):不掺和
}
text = strings.TrimSpace(text)
if text == "" {
// 空字符串当成「没有这个字段」,否则会报类型错误
delete(fields, key)
changed = true
continue
}
if _, err := strconv.ParseInt(text, 10, 64); err != nil {
continue // 不是整数(如 "1.0"):不猜,让标准解析去报错
}
fields[key] = json.RawMessage(text)
changed = true
}
for _, key := range sourceRuleObjectFields {
value, ok := fields[key]
if !ok {
continue
}
if bytes.Equal(bytes.TrimSpace(value), []byte("[]")) {
fields[key] = json.RawMessage("{}")
changed = true
}
}
if !changed {
return raw
}
out, err := json.Marshal(fields)
if err != nil {
return raw
}
return string(out)
}
// SourceProps 书源 JSON 原样转为 map(注入 JS 的 `source` 对象)。
func (b *BookSource) SourceProps() map[string]any {
if b.RawJSON == "" {