公开页抓取改为 Chrome 136 自洽身份 + 每节点 SQLite 养罐,Trafilatura 走 curl_cffi;MCP/README/接手说明与 09-02 现网复测对齐,避免消费方继续抄过期的站点三分表。 Co-authored-by: Cursor <cursoragent@cursor.com>
129 lines
3.2 KiB
Go
129 lines
3.2 KiB
Go
// Package session 每节点共享 Cookie 罐(P1,落 SQLite)。
|
||
//
|
||
// 事件:
|
||
//
|
||
// Bind 调度器 Execute 前:读 active 模版 + 罐;失败则空罐继续(不挡抓取)
|
||
// Save 200 且非投毒:Sanitize 后覆盖该域
|
||
// Drop 403/验证页/换出口:整域删除
|
||
// Reap 每 60s:过期 + 域数/字节硬顶
|
||
//
|
||
// Cookie 不进 MCP;jobs.payload 不含 Session(json:"-")。
|
||
package session
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"sync"
|
||
|
||
"onesvm.com/onesvm/browser-server/internal/contract"
|
||
"onesvm.com/onesvm/browser-server/internal/fingerprint"
|
||
"onesvm.com/onesvm/browser-server/internal/store"
|
||
)
|
||
|
||
// Jar 进程内只缓存 active 模版;Cookie 全在 SQLite。
|
||
type Jar struct {
|
||
db *store.DB
|
||
nodeID string
|
||
|
||
mu sync.RWMutex
|
||
active fingerprint.Profile
|
||
}
|
||
|
||
// Open 确保本节点模版落库并选定 active。
|
||
func Open(ctx context.Context, db *store.DB, nodeID string) (*Jar, error) {
|
||
if nodeID == "" {
|
||
nodeID = fingerprint.NodeID()
|
||
}
|
||
p, err := db.EnsureNodeTemplates(ctx, nodeID)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("session: 初始化模版: %w", err)
|
||
}
|
||
return &Jar{db: db, nodeID: nodeID, active: p}, nil
|
||
}
|
||
|
||
// Active 当前模版(内存副本,重启后从库恢复同一张脸)。
|
||
func (j *Jar) Active() fingerprint.Profile {
|
||
j.mu.RLock()
|
||
defer j.mu.RUnlock()
|
||
return j.active
|
||
}
|
||
|
||
// Attach 为一次 read 装配会话。adapter 非 http/cdp 返回 nil。
|
||
func (j *Jar) Attach(ctx context.Context, adapter, egress, pageURL string) *contract.SessionAttach {
|
||
if j == nil {
|
||
return nil
|
||
}
|
||
cls := AdapterClassOf(adapter)
|
||
if cls == "" {
|
||
return nil
|
||
}
|
||
p := j.Active()
|
||
etld := ETLD1(pageURL)
|
||
if etld == "" {
|
||
return nil
|
||
}
|
||
if egress == "" {
|
||
egress = "direct"
|
||
}
|
||
cookies, err := j.db.LoadCookies(ctx, store.CookieKey{
|
||
TemplateID: p.ID, AdapterClass: cls, Egress: egress, ETLD: etld,
|
||
})
|
||
if err != nil {
|
||
cookies = nil
|
||
}
|
||
return &contract.SessionAttach{
|
||
TemplateID: p.ID,
|
||
Impersonate: p.Impersonate,
|
||
UserAgent: p.UserAgent,
|
||
Headers: fingerprint.Headers(p),
|
||
Cookies: cookies,
|
||
AdapterClass: cls,
|
||
Egress: egress,
|
||
ETLD: etld,
|
||
}
|
||
}
|
||
|
||
// Commit 按结果写罐或丢罐。存储失败不失败任务。
|
||
func (j *Jar) Commit(ctx context.Context, sess *contract.SessionAttach, raw *contract.RawResult, eb *contract.ErrBody) {
|
||
if j == nil || sess == nil {
|
||
return
|
||
}
|
||
k := store.CookieKey{
|
||
TemplateID: sess.TemplateID, AdapterClass: sess.AdapterClass,
|
||
Egress: sess.Egress, ETLD: sess.ETLD,
|
||
}
|
||
if IsPoison(eb, raw) {
|
||
_ = j.db.DropCookies(ctx, k)
|
||
return
|
||
}
|
||
if eb != nil || raw == nil {
|
||
return
|
||
}
|
||
cleaned := Sanitize(raw.SetCookies)
|
||
if len(cleaned) == 0 {
|
||
return
|
||
}
|
||
_ = j.db.ReplaceCookies(ctx, k, cleaned)
|
||
}
|
||
|
||
// Reap 过期 + 硬顶。返回删除行数。
|
||
func (j *Jar) Reap(ctx context.Context) (int, error) {
|
||
if j == nil {
|
||
return 0, nil
|
||
}
|
||
return j.db.ReapCookies(ctx, MaxETLDPerKey, MaxJarBytes)
|
||
}
|
||
|
||
// EgressKey 罐用的出口键:国内 direct;海外钉节点名。
|
||
func EgressKey(region, node, proxy string) string {
|
||
if region != contract.RegionOverseas {
|
||
return "direct"
|
||
}
|
||
if node != "" {
|
||
return "node:" + node
|
||
}
|
||
if proxy != "" {
|
||
return "proxy:" + proxy
|
||
}
|
||
return "overseas"
|
||
}
|