feat(sites): site requests follow browser UA, smooth UA switch, CF cookies browser-only (BR-4)

- every outbound site request (adapter list/search/detail/download/health,
  RSS, login, user data) carries the account UA via SourcesRuntime.siteFetch;
  no more Deno default UA
- priority: account manual > pending > global setting > browser identity
  (fallback: same-shape Windows Firefox UA); settings UA default is empty
- migration 0056: existing cookie/password accounts keep their old UA as
  ua_pending until re-login or browser cookie write-back switches cookie+UA
  in one transaction; browser_provider.identity caches the browser identity
- cf_clearance/__cf_bm stripped on browser write-back (names kept only)
- browser sessions get per-site egress (same proxy as HTTP, or direct)
- site account page shows UA source; raw UA in tech info

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-30 03:17:13 +08:00
1 parent ebd07c9230
commit adc853aa64
21 files changed
+955 -72

No files matched your search

+7 -2
View File
@@ -28,6 +28,10 @@ export interface FetchFeedOptions {
signal?: AbortSignal;
/** 手动刷新:忽略退避窗口(不忽略熔断) */
ignoreBackoff?: boolean;
/** 本次请求的 UA(覆盖 RssFetcherOptions.userAgent;BR-4 按站点账号取) */
userAgent?: string;
/** 本次请求用的 fetch(覆盖 RssFetcherOptions.fetch;BR-4 站点代理 = 与站点其它请求同一出口) */
fetch?: typeof globalThis.fetch;
}
export function initialFeedState(): FeedFetchState {
@@ -158,7 +162,8 @@ export function createRssFetcher(options: RssFetcherOptions = {}): RssFetcher {
const headers: Record<string, string> = {
Accept: "application/rss+xml, application/atom+xml, application/xml;q=0.9, text/xml;q=0.8, */*;q=0.1",
};
if (options.userAgent) headers["User-Agent"] = options.userAgent;
const ua = opts.userAgent ?? options.userAgent;
if (ua) headers["User-Agent"] = ua;
if (state.etag) headers["If-None-Match"] = state.etag;
if (state.lastModified) headers["If-Modified-Since"] = state.lastModified;
@@ -166,7 +171,7 @@ export function createRssFetcher(options: RssFetcherOptions = {}): RssFetcher {
const signal = opts.signal ? AbortSignal.any([opts.signal, timeout]) : timeout;
let res: Response;
try {
res = await doFetch(url, { headers, signal, redirect: "follow" });
res = await (opts.fetch ?? doFetch)(url, { headers, signal, redirect: "follow" });
} catch (e) {
if (opts.signal?.aborted) throw e;
const err = e as Error;