Files
MeBox/internal/service/reader/rule/state.go
T
2026-10-01 21:57:25 +08:00

293 lines
7.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package rule
import (
"encoding/json"
"net"
"net/url"
"strings"
"sync"
"golang.org/x/net/publicsuffix"
)
// 本文件:书源会话状态(变量 / 登录信息 / 登录请求头 / Cookie)的抽象。
// 对应 legado BaseSource 的 getVariable/putVariable/getLoginInfo/putLoginInfo/
// getLoginHeader/putLoginHeader 与 CookieStore。实现由服务层落库提供;
// 未注入时退回进程内 MemoryState(单测、冒烟 CLI 用)。
// SourceState 书源会话状态读写接口。
type SourceState interface {
// GetVariable / SetVariable 对应 source.getVariable / source.setVariable。
GetVariable() string
SetVariable(v string)
// GetLoginInfo / SetLoginInfo 对应 source.getLoginInfo / source.putLoginInfo。
// 内容为登录表单的 JSON(键为字段名)。
GetLoginInfo() string
SetLoginInfo(v string)
// GetLoginHeader / SetLoginHeader 对应 source 的登录请求头(JSON),
// 其中 Cookie 键会在请求时合并进 Cookie 头。
GetLoginHeader() string
SetLoginHeader(v string)
// Cookie 读写:rawURL 可为完整 URL 或裸域名,按有效顶级域+1 归并。
GetCookie(rawURL string) string
GetCookieKey(rawURL, key string) string
SetCookie(rawURL, cookie string)
RemoveCookie(rawURL string)
// Toast 收集宿主提示(java.toast / java.longToast),登录反馈靠它回传前端。
Toast(msg string)
// OpenBrowser 记录需要浏览器完成的地址(java.startBrowser)——
// 服务端无法弹窗,前端据此提供「在新标签打开」。
OpenBrowser(url, title string)
}
// UIState 是可选实现的登录界面信号接口。
//
// java.reLoginView / java.refreshExplore / java.upLoginData 在 legado 中直接
// 操作登录对话框的控件。服务端不能碰 DOM,因此把意图显式交给宿主处理,
// 而不是静默丢弃——静默丢弃会让书源以为表单已经按它的预期更新了。
type UIState interface {
// RequestUIRefresh 请求宿主重新渲染登录表单
// (对应 legado SourceLoginJsExtensions.reLoginView / refreshExplore)。
RequestUIRefresh()
// ApplyLoginData 把书源给出的值合并进登录表单
// (对应 legado SourceLoginJsExtensions.upLoginData)。
ApplyLoginData(data map[string]string)
}
// CookieDomain 取 URL 的有效顶级域 +1(对应 legado NetworkUtils.getSubDomain)。
// 裸域名(无 scheme)按 http 处理;IP 原样返回;解析失败回退 host 本身。
func CookieDomain(rawURL string) string {
s := strings.TrimSpace(rawURL)
if s == "" {
return ""
}
if !strings.Contains(s, "://") {
if strings.HasPrefix(s, "//") {
s = "http:" + s
} else {
s = "http://" + s
}
}
u, err := url.Parse(s)
if err != nil {
return ""
}
host := u.Hostname()
if host == "" {
return ""
}
if net.ParseIP(host) != nil {
return host
}
etld1, err := publicsuffix.EffectiveTLDPlusOne(host)
if err != nil {
return host
}
return etld1
}
// ParseCookie 把 "a=1; b=2" 拆为键值对(值不 Trim 内部空白,仅去首尾)。
func ParseCookie(cookie string) map[string]string {
out := map[string]string{}
for _, part := range strings.Split(cookie, ";") {
kv := strings.SplitN(strings.TrimSpace(part), "=", 2)
if len(kv) != 2 || kv[0] == "" {
continue
}
out[kv[0]] = kv[1]
}
return out
}
// MergeCookie 将新 cookie 覆盖式合并进旧 cookie(对应 CookieStore.replaceCookie)。
func MergeCookie(old, newCookie string) string {
merged := ParseCookie(old)
for k, v := range ParseCookie(newCookie) {
merged[k] = v
}
return formatCookie(merged)
}
func formatCookie(kv map[string]string) string {
parts := make([]string, 0, len(kv))
for k, v := range kv {
parts = append(parts, k+"="+v)
}
return strings.Join(parts, "; ")
}
// MemoryState 进程内会话状态(未注入持久化实现时的回退)。
type MemoryState struct {
mu sync.Mutex
variable string
loginInfo string
loginHeader string
cookies map[string]map[string]string // domain → name → value
toasts []string
browsers []BrowserRequest
uiRefresh bool
}
// BrowserRequest 前端可代为打开的浏览器地址(java.startBrowser 收集)。
type BrowserRequest struct {
URL string `json:"url"`
Title string `json:"title"`
}
// NewMemoryState 创建进程内状态。
func NewMemoryState() *MemoryState {
return &MemoryState{cookies: map[string]map[string]string{}}
}
func (m *MemoryState) GetVariable() string { m.mu.Lock(); defer m.mu.Unlock(); return m.variable }
func (m *MemoryState) SetVariable(v string) {
m.mu.Lock()
defer m.mu.Unlock()
m.variable = v
}
func (m *MemoryState) GetLoginInfo() string { m.mu.Lock(); defer m.mu.Unlock(); return m.loginInfo }
func (m *MemoryState) SetLoginInfo(v string) {
m.mu.Lock()
defer m.mu.Unlock()
m.loginInfo = v
}
func (m *MemoryState) GetLoginHeader() string { m.mu.Lock(); defer m.mu.Unlock(); return m.loginHeader }
func (m *MemoryState) SetLoginHeader(v string) {
m.mu.Lock()
defer m.mu.Unlock()
m.loginHeader = v
}
func (m *MemoryState) GetCookie(rawURL string) string {
return m.GetCookieKey(rawURL, "")
}
func (m *MemoryState) GetCookieKey(rawURL, key string) string {
domain := CookieDomain(rawURL)
if domain == "" {
return ""
}
m.mu.Lock()
defer m.mu.Unlock()
jar, ok := m.cookies[domain]
if !ok {
return ""
}
if key != "" {
return jar[key]
}
return formatCookie(jar)
}
func (m *MemoryState) SetCookie(rawURL, cookie string) {
domain := CookieDomain(rawURL)
if domain == "" || strings.TrimSpace(cookie) == "" {
return
}
m.mu.Lock()
defer m.mu.Unlock()
jar, ok := m.cookies[domain]
if !ok {
jar = map[string]string{}
m.cookies[domain] = jar
}
for k, v := range ParseCookie(cookie) {
jar[k] = v
}
}
func (m *MemoryState) RemoveCookie(rawURL string) {
domain := CookieDomain(rawURL)
if domain == "" {
return
}
m.mu.Lock()
defer m.mu.Unlock()
delete(m.cookies, domain)
}
func (m *MemoryState) Toast(msg string) {
m.mu.Lock()
defer m.mu.Unlock()
m.toasts = append(m.toasts, msg)
}
func (m *MemoryState) OpenBrowser(url, title string) {
m.mu.Lock()
defer m.mu.Unlock()
m.browsers = append(m.browsers, BrowserRequest{URL: url, Title: title})
}
// RequestUIRefresh 实现 UIState(进程内实现,供单测)。
func (m *MemoryState) RequestUIRefresh() {
m.mu.Lock()
defer m.mu.Unlock()
m.uiRefresh = true
}
// ApplyLoginData 实现 UIState:合并进 loginInfo。
func (m *MemoryState) ApplyLoginData(data map[string]string) {
if len(data) == 0 {
return
}
m.mu.Lock()
defer m.mu.Unlock()
cur := map[string]string{}
if m.loginInfo != "" {
_ = json.Unmarshal([]byte(m.loginInfo), &cur)
}
for k, v := range data {
cur[k] = v
}
if b, err := json.Marshal(cur); err == nil {
m.loginInfo = string(b)
}
m.uiRefresh = true
}
// UIRefreshRequested 返回并清空「重画登录表单」标记。
func (m *MemoryState) UIRefreshRequested() bool {
m.mu.Lock()
defer m.mu.Unlock()
out := m.uiRefresh
m.uiRefresh = false
return out
}
// Toasts 返回并清空已收集的宿主提示。
func (m *MemoryState) Toasts() []string {
m.mu.Lock()
defer m.mu.Unlock()
out := m.toasts
m.toasts = nil
return out
}
// Browsers 返回并清空已收集的浏览器地址。
func (m *MemoryState) Browsers() []BrowserRequest {
m.mu.Lock()
defer m.mu.Unlock()
out := m.browsers
m.browsers = nil
return out
}
// Snapshot 返回当前 Cookie(domain → cookie 串)副本。
func (m *MemoryState) Snapshot() map[string]string {
m.mu.Lock()
defer m.mu.Unlock()
out := make(map[string]string, len(m.cookies))
for d, jar := range m.cookies {
out[d] = formatCookie(jar)
}
return out
}
var _ SourceState = (*MemoryState)(nil)