feat(reader): P2 收尾——冒烟 CLI、书架正文链路、替换净化规则

- SmokeChain 结构化全链路冒烟(搜索→详情→目录→正文,分阶段失败定位),
  SmokeSource 支持不落库直测;Debug 接口改为结构化日志输出
- cmd/reader-smoke:批量书源冒烟 CLI(-file/-url 导入、并发、
  各阶段通过率汇总、-json 完整报告)
- GetContentForBook:书架维度正文(章节缓存 + 书源 replaceRegex +
  用户替换净化规则),前端阅读器切换到该接口
- 替换净化规则 CRUD(后端 + /reader/replace 前端页),用户规则
  按 scope/excludeScope 书名过滤、正则带匹配超时防回溯挂死
- rule.ApplyUserReplace 导出(regexp2 MatchTimeout)
This commit is contained in:
truewhile
2026-09-30 16:55:52 +08:00
parent 5976a8d310
commit 9cc21a47c5
11 changed files with 828 additions and 65 deletions
+27
View File
@@ -2,6 +2,7 @@ package rule
import (
"strings"
"time"
"github.com/dlclark/regexp2"
)
@@ -105,6 +106,32 @@ func regexReplaceAll(pattern, result, replacement string) string {
return out
}
// ApplyUserReplace 应用一条用户替换净化规则(对应 legado ReplaceRule)。
// isRegex=false 按字面替换;isRegex=true 用 Java 正则语义并带匹配超时
// (防灾难性回溯挂死服务),编译失败回退字面替换。
func ApplyUserReplace(content, pattern, replacement string, isRegex bool, timeoutMS int64) string {
if pattern == "" {
return content
}
if !isRegex {
return strings.ReplaceAll(content, pattern, replacement)
}
re, err := regexp2.Compile(pattern, regexp2.None)
if err != nil {
return strings.ReplaceAll(content, pattern, replacement)
}
if timeoutMS > 0 {
re.MatchTimeout = time.Duration(timeoutMS) * time.Millisecond
} else {
re.MatchTimeout = 3 * time.Second
}
out, err := re.Replace(content, replacement, 0, -1)
if err != nil {
return content
}
return out
}
// ApplyReplaceRegexString 应用书源 replaceRegex 字符串("##pattern##replace[##x]" 格式),
// 对应 ContentRule.replaceRegex 的处理。
func ApplyReplaceRegexString(content, replaceRegex string) string {