// 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" }