onesvm-browser-server/server/internal/session/jar.go
chii 9b689b2476 feat: 落地节点指纹与 Cookie 罐,并按现网能力更新消费/接手文档
公开页抓取改为 Chrome 136 自洽身份 + 每节点 SQLite 养罐,Trafilatura 走 curl_cffi;MCP/README/接手说明与 09-02 现网复测对齐,避免消费方继续抄过期的站点三分表。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-02 17:04:25 +08:00

129 lines
3.2 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 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"
}