mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-04 20:46:37 +08:00
优化登录,优化阅读
This commit is contained in:
@@ -53,7 +53,7 @@ func newJavaObject(vm *goja.Runtime, r *JSRunner, a *AnalyzeRule) *goja.Object {
|
||||
req.Headers[k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
body, _, _, err := r.cfg.Fetch(req)
|
||||
body, _, _, err := r.fetch(req)
|
||||
if err != nil {
|
||||
bridgeErr(name, err)
|
||||
}
|
||||
@@ -93,7 +93,7 @@ func newJavaObject(vm *goja.Runtime, r *JSRunner, a *AnalyzeRule) *goja.Object {
|
||||
if req.Unsupported != nil {
|
||||
bridgeErr(name, req.Unsupported)
|
||||
}
|
||||
respBody, finalURL, code, err := r.cfg.Fetch(req)
|
||||
respBody, finalURL, code, err := r.fetch(req)
|
||||
if err != nil {
|
||||
bridgeErr(name, err)
|
||||
}
|
||||
|
||||
@@ -98,6 +98,10 @@ func (r *JSRunner) openBrowser(req BrowserTask) error {
|
||||
return nil
|
||||
}
|
||||
if r.cfg.Browser != nil {
|
||||
// 宿主抓取待展示的页面同样是网络等待,不能吃掉 JS 执行预算
|
||||
// (与 awaitBrowser 同一套暂停机制,见 pauseTimeout 的说明)。
|
||||
resume := r.pauseTimeout()
|
||||
defer resume()
|
||||
return r.cfg.Browser.OpenBrowser(r.ctx(), req)
|
||||
}
|
||||
if r.state != nil {
|
||||
|
||||
@@ -177,6 +177,22 @@ func (r *JSRunner) pauseTimeout() func() {
|
||||
return g.Pause()
|
||||
}
|
||||
|
||||
// fetch 执行一次桥接网络请求,等待期间暂停 JS 超时看门狗。
|
||||
//
|
||||
// 书源会把「线路重试」写进规则 JS:光遇聚合的 request() 会串行试 7 条线路,单条
|
||||
// 线路最长可能等到客户端的 30s 超时。不暂停的话整条规则会被 10s 的 JS 超时打断,
|
||||
// 而这个中断是 goja 的 Go panic,书源自己写的 try/catch 接不住——表现就是「线路
|
||||
// 还在重试,接口已经 400」。java.startBrowserAwait 等待人工操作时用的是同一套暂停
|
||||
// 机制。暂停只覆盖网络等待,纯 CPU 死循环仍然受 Timeout 约束。
|
||||
func (r *JSRunner) fetch(req *Request) (string, string, int, error) {
|
||||
if r.cfg.Fetch == nil {
|
||||
return "", "", 0, ErrJsUnsupported
|
||||
}
|
||||
resume := r.pauseTimeout()
|
||||
defer resume()
|
||||
return r.cfg.Fetch(req)
|
||||
}
|
||||
|
||||
// NewJSRunner 创建运行时:注入全局对象 cookie / cache / source,并执行 jsLib。
|
||||
func NewJSRunner(cfg JSConfig) *JSRunner {
|
||||
vm := goja.New()
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ─── 基础执行与绑定 ─────────────────────────────────────────────────────────
|
||||
@@ -46,6 +47,39 @@ func TestJSTimeoutInterrupt(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestJSTimeoutPausedDuringFetch 网络等待期间不能被 JS 超时打断。
|
||||
//
|
||||
// 回归:书源把「线路重试」写在规则 JS 里(光遇聚合的 request() 串行试 7 条线路,
|
||||
// 单条最长等到 HTTP 客户端超时)。看门狗不暂停的话,整条规则会被 10s 的 JS 超时
|
||||
// 中断,而这个中断是 goja 的 Go panic,书源自己写的 try/catch 接不住——表现就是
|
||||
// 「线路还在重试,接口已经 400」。
|
||||
func TestJSTimeoutPausedDuringFetch(t *testing.T) {
|
||||
r := NewJSRunner(JSConfig{
|
||||
Timeout: 100 * time.Millisecond,
|
||||
Fetch: func(req *Request) (string, string, int, error) {
|
||||
time.Sleep(400 * time.Millisecond) // 远超过 JS 超时:模拟慢线路
|
||||
return `{"content":"正文"}`, req.URL, 200, nil
|
||||
},
|
||||
})
|
||||
ar := NewAnalyzeRule()
|
||||
v, err := r.Run(ar, `(function(){
|
||||
try { return java.ajax('https://slow.example.com/content'); }
|
||||
catch (e) { return 'caught:' + e; }
|
||||
})()`, nil, "")
|
||||
if err != nil {
|
||||
t.Fatalf("网络等待期间不应触发 JS 超时: %v", err)
|
||||
}
|
||||
if got := anyToString(v); !strings.Contains(got, "正文") {
|
||||
t.Fatalf("ajax 返回值 = %q,期望上游正文", got)
|
||||
}
|
||||
|
||||
// 暂停只覆盖网络等待:回到 JS 里的纯 CPU 死循环仍然要被超时打断。
|
||||
_, err = r.Run(ar, `java.ajax('https://slow.example.com/content'); while(true){}`, nil, "")
|
||||
if err == nil || !strings.Contains(err.Error(), "超时") {
|
||||
t.Fatalf("网络等待之后的 CPU 死循环应当仍然超时,实际: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 规则引擎中的 JS({{}} / @js: / <js>) ──────────────────────────────────
|
||||
|
||||
func TestAnalyzeRuleJSEval(t *testing.T) {
|
||||
|
||||
@@ -375,11 +375,20 @@ func initLoginInfoFromUI(props map[string]any) map[string]string {
|
||||
}
|
||||
|
||||
// ─── cache 对象(对应 legado CacheManager 注入的 `cache`) ──────────────────
|
||||
|
||||
//
|
||||
// legado 的 cache 有两套存储:put/get/delete 落持久缓存(ACache),
|
||||
// putMemory/getFromMemory 落进程内内存缓存。书源靠后者记录「这条段评点过几次」
|
||||
// 这类临时状态——光遇聚合的 paraForAndroid 每一段带段评的文字都会调
|
||||
// cache.putMemory(url, 0),缺了它整条正文规则会抛 TypeError 直接失败。
|
||||
// 两套存储分开,否则 getFromMemory 会读到 put 写进去的持久值。
|
||||
var jsCache = struct {
|
||||
mu sync.Mutex
|
||||
m map[string]string
|
||||
}{m: map[string]string{}}
|
||||
mu sync.Mutex
|
||||
m map[string]string
|
||||
mem map[string]string
|
||||
}{m: map[string]string{}, mem: map[string]string{}}
|
||||
|
||||
// jsCacheMaxEntries 单套存储的条目上限:超了整体清空,避免书源把内存吃满。
|
||||
const jsCacheMaxEntries = 4096
|
||||
|
||||
func newCacheObject(vm *goja.Runtime) *goja.Object {
|
||||
o := vm.NewObject()
|
||||
@@ -388,37 +397,52 @@ func newCacheObject(vm *goja.Runtime) *goja.Object {
|
||||
panic(vm.ToValue(err.Error()))
|
||||
}
|
||||
}
|
||||
set("put", func(call goja.FunctionCall) goja.Value {
|
||||
key := stringArg(call, 0)
|
||||
val := ""
|
||||
if len(call.Arguments) > 1 && !goja.IsUndefined(call.Arguments[1]) && !goja.IsNull(call.Arguments[1]) {
|
||||
val = call.Arguments[1].String()
|
||||
|
||||
// 两套存储共用同一份读写实现,只有落点不同。
|
||||
putTo := func(store *map[string]string) func(goja.FunctionCall) goja.Value {
|
||||
return func(call goja.FunctionCall) goja.Value {
|
||||
key := stringArg(call, 0)
|
||||
val := ""
|
||||
if len(call.Arguments) > 1 && !goja.IsUndefined(call.Arguments[1]) && !goja.IsNull(call.Arguments[1]) {
|
||||
val = call.Arguments[1].String()
|
||||
}
|
||||
jsCache.mu.Lock()
|
||||
if len(*store) >= jsCacheMaxEntries {
|
||||
*store = map[string]string{}
|
||||
}
|
||||
(*store)[key] = val
|
||||
jsCache.mu.Unlock()
|
||||
return vm.ToValue(val)
|
||||
}
|
||||
jsCache.mu.Lock()
|
||||
if len(jsCache.m) >= 4096 {
|
||||
jsCache.m = map[string]string{}
|
||||
}
|
||||
getFrom := func(store *map[string]string) func(goja.FunctionCall) goja.Value {
|
||||
return func(call goja.FunctionCall) goja.Value {
|
||||
key := stringArg(call, 0)
|
||||
jsCache.mu.Lock()
|
||||
v, ok := (*store)[key]
|
||||
jsCache.mu.Unlock()
|
||||
if !ok {
|
||||
return goja.Null()
|
||||
}
|
||||
return vm.ToValue(v)
|
||||
}
|
||||
jsCache.m[key] = val
|
||||
jsCache.mu.Unlock()
|
||||
return vm.ToValue(val)
|
||||
})
|
||||
set("get", func(call goja.FunctionCall) goja.Value {
|
||||
key := stringArg(call, 0)
|
||||
jsCache.mu.Lock()
|
||||
v, ok := jsCache.m[key]
|
||||
jsCache.mu.Unlock()
|
||||
if !ok {
|
||||
}
|
||||
deleteFrom := func(store *map[string]string) func(goja.FunctionCall) goja.Value {
|
||||
return func(call goja.FunctionCall) goja.Value {
|
||||
key := stringArg(call, 0)
|
||||
jsCache.mu.Lock()
|
||||
delete(*store, key)
|
||||
jsCache.mu.Unlock()
|
||||
return goja.Null()
|
||||
}
|
||||
return vm.ToValue(v)
|
||||
})
|
||||
set("delete", func(call goja.FunctionCall) goja.Value {
|
||||
key := stringArg(call, 0)
|
||||
jsCache.mu.Lock()
|
||||
delete(jsCache.m, key)
|
||||
jsCache.mu.Unlock()
|
||||
return goja.Null()
|
||||
})
|
||||
}
|
||||
|
||||
set("put", putTo(&jsCache.m))
|
||||
set("get", getFrom(&jsCache.m))
|
||||
set("delete", deleteFrom(&jsCache.m))
|
||||
// 内存缓存(legado Cache.getFromMemory / putMemory)
|
||||
set("putMemory", putTo(&jsCache.mem))
|
||||
set("getFromMemory", getFrom(&jsCache.mem))
|
||||
return o
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package rule
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestCacheMemoryRoundTrip 内存缓存(cache.putMemory / cache.getFromMemory)要能读写。
|
||||
//
|
||||
// 回归:「cache 对象只有 put/get/delete」曾让光遇聚合的正文规则整条失败——
|
||||
// paraForAndroid 每一段带段评的文字都会调 cache.putMemory(url, 0),缺了它就抛
|
||||
// TypeError: Object has no member 'putMemory',正文接口直接 400。
|
||||
func TestCacheMemoryRoundTrip(t *testing.T) {
|
||||
r := NewJSRunner(JSConfig{})
|
||||
ar := NewAnalyzeRule()
|
||||
|
||||
if _, err := r.Run(ar, `cache.putMemory('__test_mem', 3)`, nil, ""); err != nil {
|
||||
t.Fatalf("cache.putMemory 失败: %v", err)
|
||||
}
|
||||
v, err := r.Run(ar, `String(cache.getFromMemory('__test_mem'))`, nil, "")
|
||||
if err != nil {
|
||||
t.Fatalf("cache.getFromMemory 失败: %v", err)
|
||||
}
|
||||
if got := anyToString(v); got != "3" {
|
||||
t.Fatalf("getFromMemory = %q,期望 3", got)
|
||||
}
|
||||
|
||||
// 缺省返回 null(书源会写 `cache.getFromMemory(k) || ''` 这类兜底)。
|
||||
v, err = r.Run(ar, `String(cache.getFromMemory('__test_mem_missing'))`, nil, "")
|
||||
if err != nil {
|
||||
t.Fatalf("cache.getFromMemory 失败: %v", err)
|
||||
}
|
||||
if got := anyToString(v); got != "null" {
|
||||
t.Fatalf("缺失键 = %q,期望 null", got)
|
||||
}
|
||||
|
||||
// 持久缓存与内存缓存是两套存储(legado 的 put/get 走 ACache,putMemory 走内存),
|
||||
// 互不串门,否则书源会读到本该过期的值。
|
||||
if _, err := r.Run(ar, `cache.put('__test_persist', 'p')`, nil, ""); err != nil {
|
||||
t.Fatalf("cache.put 失败: %v", err)
|
||||
}
|
||||
v, err = r.Run(ar, `String(cache.getFromMemory('__test_persist'))`, nil, "")
|
||||
if err != nil {
|
||||
t.Fatalf("cache.getFromMemory 失败: %v", err)
|
||||
}
|
||||
if got := anyToString(v); got != "null" {
|
||||
t.Fatalf("getFromMemory 读到了持久缓存的值: %q", got)
|
||||
}
|
||||
v, err = r.Run(ar, `String(cache.get('__test_mem'))`, nil, "")
|
||||
if err != nil {
|
||||
t.Fatalf("cache.get 失败: %v", err)
|
||||
}
|
||||
if got := anyToString(v); got != "null" {
|
||||
t.Fatalf("cache.get 读到了内存缓存的值: %q", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user