| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
feat(fetch): add ActionExecutor for sequential browser action execution | 5 个月前 | |
fix(fetch): harden solve-ladder — gate vision keychain read, delimit untrusted vision prompt, add ladder integration test | 2 个月前 | |
feat(fetch): implement CDP session discovery and auth fallback | 5 个月前 | |
feat(fetch): automated interactive-challenge pass via trusted CDP input | 2 个月前 | |
fix(fetch): enforce the humanize move sub-budget so the scroll is not starved humanizePage splits its time cap ~55% mouse traversal / the rest scroll, but the split was only ever used to size the per-step delay — the loop itself was bounded by the GLOBAL deadline. On a slow host each mouse.move costs real wall-clock, and sleep(perStep) with perStep <= 12ms rounds up to the platform timer granularity (~15.6ms on Windows), so the traversal ate the entire budget and the scroll never ran. The pass then silently delivered half of what it claims — mouse, no scroll — precisely when the machine is busy, with nothing reporting the degradation. Reproduced by charging wall-clock per move (default 1200ms cap): per-move 0ms -> scrolls=1 per-move 5ms -> scrolls=0 <-- starved per-move 16ms -> scrolls=0 <-- starved Give the traversal its own deadline. After: scrolls=1 in all three. This is what failed `lint + build + unit (windows-latest)` — the only red job once the lockfile desync was fixed and CI could install for the first time. behavior.test.ts and browser-pool.humanize.test.ts were already asserting the scroll; they just never ran on Windows. The new tests exercise the DEFAULT budget deliberately: a much tighter cap would fail for an honest reason (the two fixed settle sleeps can exceed it on their own), testing an unachievable contract instead of the starvation. | 2 个月前 | |
feat: opt-in human-like interaction layer on the browser tier Add a small, dependency-free behavioral-realism pass that runs on the browser tier after navigation settles and before content extraction, targeting 2026 anti-bot walls that score session behavior (mouse movement, scroll, timing). - src/fetch/behavior.ts: humanMousePath (Bezier + Fitts-law step count + eased sampling + micro-tremor, endpoints pinned) and humanizePage (curved traversal + randomized scroll + randomized delays), rng injectable for determinism, hard time-capped, no-ops on non-interactive pages, never throws. - config: humanize / WIGOLO_HUMANIZE (off|auto|on, default auto), normalized like stealth. - browser-pool: BrowserFetchOptions.humanize flag; engagement derived from config (on always; auto only on the stealth/escalation path) or forced by the caller; bounded by remaining budget. | 2 个月前 | |
feat(fetch): memoized lazy browser-engine acquirer (D3) Add BrowserAcquirer: joins a memoized install promise when the browser engine is missing, bounded by an in-call wait budget, with cross-process lockfile guard and failure memoization. Env-tunable WIGOLO_BROWSER_INSTALL_WAIT_MS / WIGOLO_BROWSER_INSTALL_RETRY_MS. | 2 个月前 | |
Merge remote-tracking branch 'origin/main' into feat/spa-settle-overhaul # Conflicts: # src/instructions.ts | 2 个月前 | |
fix(fetch): honor abort during post-goto hydration waits | 3 个月前 | |
feat(fetch): cdp-direct opt-in escalation rung (Phase 1, default off, graceful fallback) | 2 个月前 | |
fix(config): default humanize + autoPass to off, completing the opt-in posture WIGOLO_HUMANIZE auto -> off WIGOLO_AUTO_PASS auto -> off Both rungs are reactive — they only run once a fetch has already hit a challenge, so neither ever slowed a successful fetch. But both added time to a fetch that ends up blocked anyway, and neither has a measured win: behavioural interaction did not change a wall outcome in this project's own testing (passive render sufficed), and the auto-pass gesture has never converted a block into a pass on a live target. With these off, an existing install now behaves as it did before on the FAILURE path too, not just the happy path. Measured on glassdoor, cache-busted, same blocked_by_challenge verdict throughout: origin/main 15.5s this PR as submitted 226.0s after the widget-probe fix 19.3s now 2.7s Faster than the baseline, because the vendor-agnostic wall-shape rule recognises the wall at the HTTP tier and skips a browser escalation that was never going to clear it. Happy path unchanged: wikipedia 1.7s via method=http. Unknown values now normalize to 'off' rather than 'auto', so a typo can no longer silently enable a rung. WIGOLO_HARDCORE still flips both to 'on' — that preset is the right home for the aggressive posture. Tests that exercise either rung now enable it explicitly and clean the env up after, so no test inherits a default it depends on. | 2 个月前 | |
fix(config): default the behaviour-changing anti-bot knobs to opt-in Four defaults changed so an existing install behaves exactly as it does today on every path. The machinery is unchanged and one env var away. WIGOLO_PROXY_BYPASS_ON_CHALLENGE true -> false Silently retrying direct defeats a proxy the operator deliberately configured and leaks their real IP to the origin. That is a consent decision, not an optimization, and no escalation logic makes it safe to inherit. WIGOLO_CRAWL_COOLDOWN_MAX_MS 300000 -> 30000 A crawl hitting repeated 403/429 backed off to five minutes per request, which is indistinguishable from a hang. Raise it deliberately for a domain worth waiting on. WIGOLO_BROWSER_CHANNEL auto -> chromium Defaulting to the host's installed browser makes rendering vary per machine for no measured gain — the levers that decide a bot-wall outcome measured as classifier + IP, not browser identity. WIGOLO_STEALTH_DRIVER auto -> playwright The decisive one. launchDedicatedStealthBrowser resolves the launcher ONCE and every launch site uses it — the only try/catch falls back from channel:'chrome' to bundled through the SAME launcher. Nothing ever falls back to the standard driver, and resolveStealthLauncher only verifies the module imports, never that it can launch. The hardened driver resolves a Chromium revision it neither installs nor owns; today both packages want rev 1223 and are exact-pinned, but no test or CI check asserts that alignment. A playwright bump without a matching patchright bump would silently break every anti-bot fetch, with no fallback. All four remain reachable: WIGOLO_*=true/auto. WIGOLO_HARDCORE still flips channel and driver to their aggressive values, which is the right home for them. The missing launch-time fallback is a real latent bug and wants its own fix; defaulting off removes the exposure meanwhile. Tests that exercise these paths now opt in explicitly rather than inheriting, so each one states the behaviour it depends on. | 2 个月前 | |
harden(ssrf): wire the fetch-time resolved re-check into every fetch/serve seam Phase-0 (c7168ab) added guardResolvedHost and the http-client input hop. This wires the resolved re-check into the remaining seams so a public hostname whose DNS record points at cloud metadata / RFC-1918 / (in serve mode) loopback cannot slip past the literal-only guard anywhere: - http-client: thread allowPrivate from the call site's opts (was re-reading config directly) so the resolved policy tracks the literal one - router (defaultPdfProbe) + tls-tier redirect loops: resolved check at hop 0 and every redirect Location - escape-hatch: shared resolvedGuardOk helper at all 5 guardFetchUrl sites, each threading that site's own allowPrivate (sidecar true, target config); lookup injectable for tests - dispatch + firecrawl-compat serve seams: guardResolvedServeTarget with bindIsLoopback, so a name resolving to loopback is refused under a non-loopback bind (guardResolvedHost still allows loopback for plain fetch / local dev) - browser-pool: pre-goto check-only guard; comment notes it does NOT close DNS rebinding (Chromium re-resolves) — pinning tracked in #207 Also fixes a latent bug this surfaced: guardFetchUrl's docstring promised the IPv6 loopback (::1) exemption mirroring 127.0.0.0/8, but it was never implemented, so a dual-stack localhost (127.0.0.1 + ::1) was wrongly blocked once the resolved re-check feeds each IP back through guardFetchUrl. Added the narrow ::1 exemption; serve-mode ::1 refusal still applies via the serve guard. New tests (all via injected lookup): guardResolvedServeTarget matrix (metadata / RFC-1918 ±allowPrivate / loopback under serve non-loopback bind / loopback allowed under loopback bind / WIGOLO_SERVE_ALLOW_LOCAL_TARGETS / multi-record / non-resolving) plus the guardResolvedHost loopback-allowed case. | 2 个月前 | |
feat(fetch): content completeness classification on browser captures | 2 个月前 | |
fix(fetch): fall back to the standard driver when the hardened one cannot launch resolveStealthLauncher proves the optional hardened driver IMPORTS. It never proves it can LAUNCH — and the driver resolves a browser revision it neither installs nor owns. launchDedicatedStealthBrowser resolved the launcher once and used it at all four launch sites. The only try/catch fell back from channel:'chrome' to bundled through the SAME launcher, so nothing ever reached the standard driver. A version skew between the two packages therefore hard-failed every anti-bot fetch, with no fallback and no CI signal. Wrap each launch so a hardened-driver failure degrades to the standard launcher, and cache the demotion so a broken optional driver is probed once per process rather than once per fetch. A channel failure and a driver failure are now caught at different levels and cannot be confused. Errors from the standard launcher still propagate — nothing left to fall back to. Does NOT downgrade a hardened driver that works, which is the negative case the third test covers. | 2 个月前 | |
feat: opt-in human-like interaction layer on the browser tier Add a small, dependency-free behavioral-realism pass that runs on the browser tier after navigation settles and before content extraction, targeting 2026 anti-bot walls that score session behavior (mouse movement, scroll, timing). - src/fetch/behavior.ts: humanMousePath (Bezier + Fitts-law step count + eased sampling + micro-tremor, endpoints pinned) and humanizePage (curved traversal + randomized scroll + randomized delays), rng injectable for determinism, hard time-capped, no-ops on non-interactive pages, never throws. - config: humanize / WIGOLO_HUMANIZE (off|auto|on, default auto), normalized like stealth. - browser-pool: BrowserFetchOptions.humanize flag; engagement derived from config (on always; auto only on the stealth/escalation path) or forced by the caller; bounded by remaining budget. | 2 个月前 | |
feat(fetch): content completeness classification on browser captures | 2 个月前 | |
feat(fetch): ai-solve image sub-type detection + hosted scraping-browser wiring (opt-in) | 2 个月前 | |
refactor(fetch): both browser paths settle via shared settlePage | 2 个月前 | |
feat(fetch): thread anti-bot status through challenge blocks + proxy-bypass direct-retry Part 1: ChallengeBlockedError now carries the triggering anti-bot HTTP status (403/429/503), threaded onto the router's blocked_by_challenge stage error as http_status so a hard challenge-block reaches the crawl adaptive-cooldown (previously only bare 403/429 did). Status is only surfaced when a real one exists — a goto-timeout or 2xx interstitial shell stays unset, never invented. Part 2: when a proxy is in use and the browser tier hits a managed challenge, attempt one direct (no-proxy) browser fetch before returning blocked_by_challenge. A datacenter proxy converts many managed-challenge passes into blocks; direct residential-grade egress often clears, and wigolo cannot know the proxy's ASN type. Gated behind proxyBypassOnChallenge (WIGOLO_PROXY_BYPASS_ON_CHALLENGE, default true); no-op when no proxy is configured. | 2 个月前 | |
feat(fetch): ai-solve image sub-type detection + hosted scraping-browser wiring (opt-in) | 2 个月前 | |
test: pin the suite to the standard browser driver patchright ships as an installed optionalDependency and stealthDriver defaults to 'auto', so the dedicated stealth path preferred it over the standard launcher. That escaped every vi.mock('playwright') in the suite: the pool launched a REAL browser, navigated to a fake host, and 22 tests failed on ERR_NAME_NOT_RESOLVED instead of exercising their mocks. The same tests pass at origin/main, so this was a regression, not the environmental flake it looked like. Pin WIGOLO_STEALTH_DRIVER=playwright suite-wide — resolveStealthLauncher short-circuits on that mode and never probes the optional package. An env default, deliberately: a static import of the stealth module in setup.ts hoists above that file's env assignments (config caches the wrong search backend), and a dynamic import inside a hook executes the src graph inside each test file's own mocked fs/os context. Both were measured breaking unrelated suites. The one test that genuinely exercises the hardened driver now opts in explicitly, and the file's hooks restore the pin rather than deleting the var (deleting falls back to the production default and re-arms the hazard). | 2 个月前 | |
refactor(fetch): both browser paths settle via shared settlePage | 2 个月前 | |
fix(fetch): bound the challenge-widget probe so a miss costs ms, not minutes locateChallengeWidget probed seven selectors with boundingBox() and no timeout, so every miss inherited the browser engine's 30s actionability default. A challenge with no locatable widget therefore burned ~210s before the auto-pass rung could give up — on the DEFAULT config, since autoPass engages on the escalation path out of the box. Measured live against glassdoor, cache-busted: origin/main 15.5s blocked_by_challenge before this fix 226.0s blocked_by_challenge (same outcome) WIGOLO_AUTO_PASS=off 16.3s (isolates the cause) after this fix 19.3s auto-pass still enabled A crawl over a protected domain was minutes per page. Give every probe an explicit 400ms budget and hoist the selector lists to named constants so the worst-case cost is a stated, assertable contract rather than an emergent property of an inline array. Also publishes the launched Chrome major recorded in the previous commit, which is what re-enables clearance reuse. | 2 个月前 | |
feat: install browser system deps on Linux + launch smoke-test (#116) warmup historically reported a browser "ok" once the binary was on disk, but on bare Linux the binary lands without the OS shared libs (libnss3, libgbm, ...) so chromium.launch() fails at runtime. existsSync only proved the file was present, not that it launches. - Add src/fetch/browser-probe.ts: a single shared probeBrowser() helper (executablePath + existsSync + real headless launch smoke-test under a timeout) used by BOTH warmup and doctor, so they can never disagree. - warmup now installs OS system libs on Linux via install-deps: directly when root, via passwordless `sudo -n` when available, else SKIP (never prompts for a password / never hangs CI or the TUI). macOS/Windows skip deps entirely. - A browser is only reported ok when it actually launches. On Linux when launch fails and deps were skipped, the error carries the exact remediation: `sudo npx playwright install-deps <browser>`. - doctor uses the same probe; surfaces "on disk but will not launch" and the install-deps hint on Linux. | 3 个月前 | |
feat(fetch): add BrowserSelector with round-robin, hash, and random strategies | 5 个月前 | |
test(sp1): real migration DROP-path coverage + parser guard; rm dead stub Address code review (REQUEST-CHANGES): 1. Add real DROP-path tests in migrations-runner.test.ts — seed one DB with legacy lightpanda_routing, run applyMigrations, assert table gone + migration recorded; plus a fresh-DB no-op case. Remove the misleading test in no-lightpanda.test.ts (it opened a separate :memory: DB so the guarded postStep DROP never executed). 2. browser-types.test.ts: assert parseBrowserTypes('lightpanda') → ['chromium']. 3. Delete dead stub src/cache/migrations/007-drop-lightpanda-routing.sql; runner references only 007-drop-lp-routing. | 4 个月前 | |
feat(fetch): add CDP client for Chrome DevTools Protocol session discovery | 5 个月前 | |
fix(fetch): make cdp-direct rung functional — drop --no-startup-window (Chrome 129+ suppressed the page target) + poll for target | 2 个月前 | |
fix(fetch): make cdp-direct honour proxy + redirect egress policy This rung spawns its own browser, so it inherits none of the egress controls the Playwright tiers get for free. Proxy: it launched with a proxy-stripped env and no --proxy-server, so with a proxy configured it egressed DIRECT on the operator's real IP, unlogged — silently defeating a boundary they deliberately set up. Now it passes --proxy-server. Chrome accepts no inline credentials there, so an authenticated proxy cannot be honoured at all; rather than fall back to direct (the exact leak) the rung REFUSES and the fetch degrades to the normal browser tier, which does honour it. Redirects: the SSRF guard ran on the REQUESTED url only, but Page.navigate follows redirects inside the browser — a public host redirecting to cloud metadata / RFC-1918 had its content read and returned. finalUrl was also hardcoded to the requested url, so the hop was invisible to the cache key and every downstream consumer. Now the landing url is read from the isolated world, reported honestly, and re-guarded. The landing guard deliberately does NOT skip IP literals the way the pre-navigation guard does: that skip is only justified because the caller's literal guard already vetted the requested url, and a redirect target was never seen by it — http://169.254.169.254/ is precisely what a rebind lands on. Only an http(s) url is trusted as a landing value; an unreadable one falls back to the requested url rather than failing the fetch. Honest limit, stated in the code: this blocks the RESULT, not the request. The redirect hop has already left the machine by the time location is readable. Guarding every hop needs request interception, which would mean enabling another CDP domain on the one rung that exists to keep its protocol surface minimal. | 2 个月前 | |
fix(fetch): detect PerimeterX denied-variant blocks + poll cdp-direct challenges to clear Live-found on real stubborn sites (residential IP, 2026-07-28): - PerimeterX 'denied' pages (zillow) carry neither id="px-captcha" nor the 'Robot or human?' title, so isChallengeResponse missed them entirely and a 403 block body leaked as a 38-char 'successful' page. Adds the vendor px-captcha-error class marker + the block title to the title pattern. - cdp-direct read once at a fixed 1.2s settle, catching interstitials that auto-clear ~3s later. Replaces the one-shot read with a bounded challenge-clear poll (mirrors the browser tier's pollUntilCleared). zillow.com and indeed.com now return real content through cdp-direct; walmart and g2 fall back honestly. | 2 个月前 | |
feat(fetch): coherent headless identity — no display required Headless Chrome differs from headful in exactly three server-visible ways: the UA carries a HeadlessChrome token, the UA leaks the full build where headful sends a reduced major.0.0.0 (and sec-ch-ua names HeadlessChrome), and devicePixelRatio reports 1 even on a HiDPI host — implausible beside a real GPU renderer string. Everything else (header order, casing, Accept-Encoding, Sec-Fetch-*, HTTP/2 SETTINGS and pseudo-header order) is already byte-identical, so it is left untouched. Fixes all three together in one CDP call each, derived from the binary's OWN reported user agent so UA major, brand major and fullVersion can never disagree — detectors score cross-layer contradictions, and a partial fix is worse than none. Adds --accept-lang at launch because headless omits Accept-Language entirely; setting it via the UA override would relocate the header out of its natural last position and create a fresh ordering anomaly. Applied only when headless: headful's identity is already correct and overriding it could only introduce a contradiction. Unparseable UA returns null and the browser's own identity is left alone. Measured: a headless Chrome hardened this way returned the same 3,170 chars of real content as headful on a PerimeterX-protected target, with no window. | 2 个月前 | |
feat(fetch): leak-free raw-CDP connect (Phase 0, dark/unwired) | 2 个月前 | |
fix(fetch): stop the slider heuristic firing on ordinary page furniture hasSliderMarker required a corroborating hint, but the hint could be satisfied by the token that triggered the check: `'slider'.includes('slide')` is true, so `sliderish && (puzzle || slide)` was vacuous for every page carrying a carousel, a range input, or `class="slider"`. Those pages classified as a drag puzzle, and the vision rung would attempt a drag gesture on them. The corroboration must be a SEPARATE signal, so it is now puzzle/verify/ captcha. GeeTest and slidebg still short-circuit ahead of it, and a slider co-present with a real puzzle hint still resolves. Found by CodeRabbit. | 2 个月前 | |
feat(fetch): pollUntilCleared — poll a challenged page until it clears or a bounded deadline | 2 个月前 | |
fix(fetch): judge wall density over the whole document, not a leading slice isLowContentDensity measured the text ratio on html.slice(0, 32768). Any modern page opens with 32KB+ of <head> scripts and stylesheets carrying almost no text, so a genuinely substantive page read as empty and a real 403 was relabelled a bot wall: bytes total 69279 visible WHOLE doc 34799 <- clearly a real page visible first 32KB 379 <- the slice lies isChallengeShell(403) true <- relabelled challenge-classify.ts already documents this exact trap, in this same PR: walmart's 405KB page carries <600 visible chars in its first 32KB but 2,777 overall, "so slicing here would defeat the guard entirely". The density rule made the mistake that comment warns about. approxVisibleTextLength keeps its 32KB bound by DEFAULT — that is correct for the interstitial detectors, where a challenge shell is tiny and a leading slice sees all of it. Only the density rule opts into the whole document, and only behind an anti-bot status, so no hot path pays for the full scan. My earlier test asserted the opposite and passed only because its fixture was ~1.6KB — smaller than the slice, so the slice WAS the whole document. A test that could not fail on the real case. Replaced with a large head-heavy page, plus the mirror case (a large page that genuinely is all scaffolding must still be caught, so the fix cannot degenerate into "ignore large pages"). Found by CodeRabbit. | 2 个月前 | |
fix(fetch): gate clearance reuse on the launched browser major, not the pin T1-C made the stealth path advertise the UA of the browser it ACTUALLY launched (read from browser.version()), and clearances are minted under that UA. uaMatchesTier kept comparing against the static STEALTH_CHROME_MAJOR, so on any machine whose installed Chrome differs from the pin every clearance the tier just minted was refused on reuse and the whole feature silently no-opped. Measured here: pin 142, installed Chrome 151, minted UA Chrome/151.0.0.0, uaMatchesTier -> false. Track the launched major in the stealth module and compare against that, falling back to the shared pin before any launch (and for the header tiers, whose behaviour is unchanged). The pool publishes the value in the next commit. | 2 个月前 | |
feat(fetch): route-identity gate on clearance reuse + migration 010 A cf_clearance is bound to the {IP + UA + TLS} of the egress it was solved on, so a clearance harvested on one route (proxy-or-direct) is invalid from another (FlareSolverr #871). Capture the egress route at harvest and hard-refuse reuse on a route mismatch. - migration 010-clearance-route: postStep-guarded ADD COLUMN solved_route on domain_routing (mirrors 008); grep-mirror .sql - DomainClearance gains solvedRoute; record/get round-trip it, legacy NULL rows read back as 'direct' - clearance-reuse: pure routeMatchesClearance + composed isClearanceReusable (fresh AND UA-ok AND route-matches AND cookie) - browser-pool harvest: persist proxyUrl ?? 'direct' | 2 个月前 | |
fix(fetch): pre-mark known SPA domains and detect non-empty shells without semantic content | 4 个月前 | |
feat(fetch): map browser-tier challenge to blocked_by_challenge stage error | 2 个月前 | |
test(ssrf): cover a 302 whose target RESOLVES to metadata (not a literal IP) The existing redirect tests refuse a 302 into a literal 169.254.169.254 — that's the literal guard. This adds the resolved counterpart: a 302 to an ordinary public-looking hostname that DNS-resolves to metadata, which only the fetch-time resolved re-check on the redirect hop can catch. Host-aware injected lookup so only the redirect target resolves to metadata; hop 0 stays public. Asserts the hop is refused before the second fetch fires. | 2 个月前 | |
feat(fetch): router injects reused clearance per-tier + purges on re-challenge + cross-host cookie drop | 2 个月前 | |
feat(fetch): human-solve last-resort rung (consent + visible-surface gated) | 2 个月前 | |
feat(fetch): thin frameless pages are partial/thin_content, not shell | 2 个月前 | |
refactor(fetch): both browser paths settle via shared settlePage | 2 个月前 | |
feat(fetch): politeness helpers — parse Retry-After + clamp backoff window | 2 个月前 | |
feat(fetch): wire escape-hatch ladder into router + thread proxy to browser On a terminal browser challenge-block, try the opt-in solver then hosted-reader rungs (lazy import — never loaded on a default install). Thread the resolved proxy into the browser via Playwright's structured proxy launch option (credentials in username/password fields, never inline in server). Redact CDP URLs in logs. | 2 个月前 | |
fix(fetch): bound the reddit OAuth requests Neither the token mint nor the data request passed a signal, and the default fetch has no request timeout. A stalled call held the router's fetch path open indefinitely rather than falling through to the normal ladder. Both now carry a 15s ceiling, and the data request combines it with the caller's signal so cancellation still wins. anySignal's cleanup runs in a finally — it attaches a listener to the caller's long-lived shared signal, and skipping it would accumulate one listener per fetch. Found by CodeRabbit. | 2 个月前 | |
fix(fetch): reject reddit path segments instead of rewriting them sanitizeSegment stripped anything outside [A-Za-z0-9_-] so the fixed-host endpoint could never be escaped. That kept the URL safe but silently changed WHICH resource was fetched: /r/foo.bar -> /r/foobar /r/AskReddit%2F..%2Fpolitics -> /r/AskReddit2F2Fpolitics Both are real subreddits. The caller asked for one community and got another back with no signal that a substitution had happened. Reddit names are [A-Za-z0-9_-]+, so a segment needing rewrite is one we cannot serve. Return null and let the router fall through to the normal ladder against the URL as written. Strictly stronger than the old behaviour, so the two existing SSRF tests are updated to assert the stronger property rather than relaxed: a hostile path now performs NO egress at all — not the data endpoint, not even a token — where before it fetched a sanitized, wrong resource. The fixed-host assertion is retained on a legal path so that guarantee stays covered. | 2 个月前 | |
fix(security): re-guard SSRF on every resolved redirect target The manual 3xx redirect loop resolved Location and followed it with no SSRF re-check, letting a public URL redirect a fetch onto a private LAN host or a cloud-metadata endpoint. Re-run the fetch guard on every resolved redirect in the http-client loop, the TLS-impersonation tier, and the PDF content probe (converted from redirect:follow to a bounded guarded loop). Loopback stays exempt to match the input-URL guard; link-local/metadata always refused. Applies to MCP/CLI fetch too (spec-surfaced). | 2 个月前 | |
fix(fetch): close a hosted browser that connects after timeout or abort withTimeoutAndAbort rejects on whichever fires first, but the underlying connect(wss) keeps running. When it later resolved, the fulfilment handler saw settled === true and returned without closing it. Nobody was waiting for that browser and nobody would ever close it — and it is a HOSTED one, so it kept running (and billing) on the provider's side until their own idle timeout. Close it instead, best-effort and detached, since by then there is no caller a rejection could reach. | 2 个月前 | |
refactor(fetch): capability-named fetch_method 'browser' (was 'playwright') across value/docs/openapi/SDK/tests The fetch_method returned value leaked the browser library name in user-facing output. Rename the RETURNED contract value 'playwright' -> 'browser' — the tier name, its type members, its producers, and its documentation. The internal browser library (imports, browser pool, warmup/doctor component names, the dep) is untouched. - FetchMethod union + doc comment (src/types.ts): 'playwright' -> 'browser'. - RawFetchResult.method union + doc comment (the value that flows straight to FetchOutput.fetch_method): 'playwright' -> 'browser'. - CachedContent.fetchMethod DB-mirror union: 'playwright' -> 'browser'. - The three value producers (router.ts browser-escalation return, browser-pool.ts download + main return): method: 'playwright' -> 'browser'. - fetch tool description (src/instructions.ts): fetch_method (cache/http/tls-impersonation/playwright) -> (.../browser). - Tests: every fetch_method / RawFetchResult.method assertion and mock literal across the fetch + router + browser-pool + cache-store lanes -> 'browser'. OpenAPI types fetch_method as a generic string (no tier enum) and the SDKs type it as string too, so no OpenAPI/SDK value change and no drift is introduced. Left untouched: vi.mock('playwright'), 'playwright' library imports, warmup reporter keys, doctor's npx playwright, and the OpenAPI playwright->'browser engine' sanitizer + its forbidden-term tests. Prior research/agent free-key guidance, source_span restore, and the competitor-routing guard tests are unchanged. | 2 个月前 | |
refactor(fetch): capability-named fetch_method 'browser' (was 'playwright') across value/docs/openapi/SDK/tests The fetch_method returned value leaked the browser library name in user-facing output. Rename the RETURNED contract value 'playwright' -> 'browser' — the tier name, its type members, its producers, and its documentation. The internal browser library (imports, browser pool, warmup/doctor component names, the dep) is untouched. - FetchMethod union + doc comment (src/types.ts): 'playwright' -> 'browser'. - RawFetchResult.method union + doc comment (the value that flows straight to FetchOutput.fetch_method): 'playwright' -> 'browser'. - CachedContent.fetchMethod DB-mirror union: 'playwright' -> 'browser'. - The three value producers (router.ts browser-escalation return, browser-pool.ts download + main return): method: 'playwright' -> 'browser'. - fetch tool description (src/instructions.ts): fetch_method (cache/http/tls-impersonation/playwright) -> (.../browser). - Tests: every fetch_method / RawFetchResult.method assertion and mock literal across the fetch + router + browser-pool + cache-store lanes -> 'browser'. OpenAPI types fetch_method as a generic string (no tier enum) and the SDKs type it as string too, so no OpenAPI/SDK value change and no drift is introduced. Left untouched: vi.mock('playwright'), 'playwright' library imports, warmup reporter keys, doctor's npx playwright, and the OpenAPI playwright->'browser engine' sanitizer + its forbidden-term tests. Prior research/agent free-key guidance, source_span restore, and the competitor-routing guard tests are unchanged. | 2 个月前 | |
fix(config): default the behaviour-changing anti-bot knobs to opt-in Four defaults changed so an existing install behaves exactly as it does today on every path. The machinery is unchanged and one env var away. WIGOLO_PROXY_BYPASS_ON_CHALLENGE true -> false Silently retrying direct defeats a proxy the operator deliberately configured and leaks their real IP to the origin. That is a consent decision, not an optimization, and no escalation logic makes it safe to inherit. WIGOLO_CRAWL_COOLDOWN_MAX_MS 300000 -> 30000 A crawl hitting repeated 403/429 backed off to five minutes per request, which is indistinguishable from a hang. Raise it deliberately for a domain worth waiting on. WIGOLO_BROWSER_CHANNEL auto -> chromium Defaulting to the host's installed browser makes rendering vary per machine for no measured gain — the levers that decide a bot-wall outcome measured as classifier + IP, not browser identity. WIGOLO_STEALTH_DRIVER auto -> playwright The decisive one. launchDedicatedStealthBrowser resolves the launcher ONCE and every launch site uses it — the only try/catch falls back from channel:'chrome' to bundled through the SAME launcher. Nothing ever falls back to the standard driver, and resolveStealthLauncher only verifies the module imports, never that it can launch. The hardened driver resolves a Chromium revision it neither installs nor owns; today both packages want rev 1223 and are exact-pinned, but no test or CI check asserts that alignment. A playwright bump without a matching patchright bump would silently break every anti-bot fetch, with no fallback. All four remain reachable: WIGOLO_*=true/auto. WIGOLO_HARDCORE still flips channel and driver to their aggressive values, which is the right home for them. The missing launch-time fallback is a real latent bug and wants its own fix; defaulting off removes the exposure meanwhile. Tests that exercise these paths now opt in explicitly rather than inheriting, so each one states the behaviour it depends on. | 2 个月前 | |
feat(fetch): solve-ladder orchestrator — wire classify/auto-pass/ai-solve/human + enforce clearance route-gate + solve provenance Add src/fetch/solve-ladder.ts, a pure injected orchestrator that sequences the in-band solve rungs per challenge class (behavioral/none run nothing; interactive → auto-pass then human; image → ai-vision then human). Wire it into the browser tier's challenge-fail point with concrete injected callbacks (trusted CDP input for auto-pass, clipped screenshot + vision LLM for ai-solve, headful window + consent for human), harvesting clearance on a solve and falling through to normal hydration. Enforce the P6 clearance route-identity gate in router.clearanceFor (a proxy-solved clearance is refused on the direct route and vice-versa). Thread challenge_class/solve_method provenance onto ChallengeBlockedError, the browser RawFetchResult returns, the router's blocked_by_challenge mapping, and the fetch tool result. cdp-direct and scraping-browser remain dark (CEO GO-gated / opt-in). | 2 个月前 | |
refactor(fetch): capability-named fetch_method 'browser' (was 'playwright') across value/docs/openapi/SDK/tests The fetch_method returned value leaked the browser library name in user-facing output. Rename the RETURNED contract value 'playwright' -> 'browser' — the tier name, its type members, its producers, and its documentation. The internal browser library (imports, browser pool, warmup/doctor component names, the dep) is untouched. - FetchMethod union + doc comment (src/types.ts): 'playwright' -> 'browser'. - RawFetchResult.method union + doc comment (the value that flows straight to FetchOutput.fetch_method): 'playwright' -> 'browser'. - CachedContent.fetchMethod DB-mirror union: 'playwright' -> 'browser'. - The three value producers (router.ts browser-escalation return, browser-pool.ts download + main return): method: 'playwright' -> 'browser'. - fetch tool description (src/instructions.ts): fetch_method (cache/http/tls-impersonation/playwright) -> (.../browser). - Tests: every fetch_method / RawFetchResult.method assertion and mock literal across the fetch + router + browser-pool + cache-store lanes -> 'browser'. OpenAPI types fetch_method as a generic string (no tier enum) and the SDKs type it as string too, so no OpenAPI/SDK value change and no drift is introduced. Left untouched: vi.mock('playwright'), 'playwright' library imports, warmup reporter keys, doctor's npx playwright, and the OpenAPI playwright->'browser engine' sanitizer + its forbidden-term tests. Prior research/agent free-key guidance, source_span restore, and the competitor-routing guard tests are unchanged. | 2 个月前 | |
refactor(fetch): capability-named fetch_method 'browser' (was 'playwright') across value/docs/openapi/SDK/tests The fetch_method returned value leaked the browser library name in user-facing output. Rename the RETURNED contract value 'playwright' -> 'browser' — the tier name, its type members, its producers, and its documentation. The internal browser library (imports, browser pool, warmup/doctor component names, the dep) is untouched. - FetchMethod union + doc comment (src/types.ts): 'playwright' -> 'browser'. - RawFetchResult.method union + doc comment (the value that flows straight to FetchOutput.fetch_method): 'playwright' -> 'browser'. - CachedContent.fetchMethod DB-mirror union: 'playwright' -> 'browser'. - The three value producers (router.ts browser-escalation return, browser-pool.ts download + main return): method: 'playwright' -> 'browser'. - fetch tool description (src/instructions.ts): fetch_method (cache/http/tls-impersonation/playwright) -> (.../browser). - Tests: every fetch_method / RawFetchResult.method assertion and mock literal across the fetch + router + browser-pool + cache-store lanes -> 'browser'. OpenAPI types fetch_method as a generic string (no tier enum) and the SDKs type it as string too, so no OpenAPI/SDK value change and no drift is introduced. Left untouched: vi.mock('playwright'), 'playwright' library imports, warmup reporter keys, doctor's npx playwright, and the OpenAPI playwright->'browser engine' sanitizer + its forbidden-term tests. Prior research/agent free-key guidance, source_span restore, and the competitor-routing guard tests are unchanged. | 2 个月前 | |
test(router): mock BrowserAcquirer in remaining router unit tests (CI browserless timeout fix) | 2 个月前 | |
refactor(fetch): capability-named fetch_method 'browser' (was 'playwright') across value/docs/openapi/SDK/tests The fetch_method returned value leaked the browser library name in user-facing output. Rename the RETURNED contract value 'playwright' -> 'browser' — the tier name, its type members, its producers, and its documentation. The internal browser library (imports, browser pool, warmup/doctor component names, the dep) is untouched. - FetchMethod union + doc comment (src/types.ts): 'playwright' -> 'browser'. - RawFetchResult.method union + doc comment (the value that flows straight to FetchOutput.fetch_method): 'playwright' -> 'browser'. - CachedContent.fetchMethod DB-mirror union: 'playwright' -> 'browser'. - The three value producers (router.ts browser-escalation return, browser-pool.ts download + main return): method: 'playwright' -> 'browser'. - fetch tool description (src/instructions.ts): fetch_method (cache/http/tls-impersonation/playwright) -> (.../browser). - Tests: every fetch_method / RawFetchResult.method assertion and mock literal across the fetch + router + browser-pool + cache-store lanes -> 'browser'. OpenAPI types fetch_method as a generic string (no tier enum) and the SDKs type it as string too, so no OpenAPI/SDK value change and no drift is introduced. Left untouched: vi.mock('playwright'), 'playwright' library imports, warmup reporter keys, doctor's npx playwright, and the OpenAPI playwright->'browser engine' sanitizer + its forbidden-term tests. Prior research/agent free-key guidance, source_span restore, and the competitor-routing guard tests are unchanged. | 2 个月前 | |
refactor(fetch): capability-named fetch_method 'browser' (was 'playwright') across value/docs/openapi/SDK/tests The fetch_method returned value leaked the browser library name in user-facing output. Rename the RETURNED contract value 'playwright' -> 'browser' — the tier name, its type members, its producers, and its documentation. The internal browser library (imports, browser pool, warmup/doctor component names, the dep) is untouched. - FetchMethod union + doc comment (src/types.ts): 'playwright' -> 'browser'. - RawFetchResult.method union + doc comment (the value that flows straight to FetchOutput.fetch_method): 'playwright' -> 'browser'. - CachedContent.fetchMethod DB-mirror union: 'playwright' -> 'browser'. - The three value producers (router.ts browser-escalation return, browser-pool.ts download + main return): method: 'playwright' -> 'browser'. - fetch tool description (src/instructions.ts): fetch_method (cache/http/tls-impersonation/playwright) -> (.../browser). - Tests: every fetch_method / RawFetchResult.method assertion and mock literal across the fetch + router + browser-pool + cache-store lanes -> 'browser'. OpenAPI types fetch_method as a generic string (no tier enum) and the SDKs type it as string too, so no OpenAPI/SDK value change and no drift is introduced. Left untouched: vi.mock('playwright'), 'playwright' library imports, warmup reporter keys, doctor's npx playwright, and the OpenAPI playwright->'browser engine' sanitizer + its forbidden-term tests. Prior research/agent free-key guidance, source_span restore, and the competitor-routing guard tests are unchanged. | 2 个月前 | |
fix(fetch): reject 3xx on reddit OAuth fetches + close test gaps Reddit OAuth token-mint and data fetches used the default redirect:'follow', so guardFetchUrl only ran on hop 0 — a 3xx to an internal address would be auto-followed with credentials attached. Both fetches now use redirect:'manual' and reject any 3xx (the fixed OAuth/data hosts never legitimately redirect); a data-fetch 3xx surfaces an error so the router falls through the normal ladder. Tests: make the secret-leak assertion non-vacuous (force debug-level logging so the mint's debug line is actually emitted; also assert the bearer token is absent); add token-refresh inflight-dedup concurrency test; add 3xx-redirect SSRF tests for both mint and data fetch; add RedditRateLimitError router arm. | 2 个月前 | |
refactor(fetch): capability-named fetch_method 'browser' (was 'playwright') across value/docs/openapi/SDK/tests The fetch_method returned value leaked the browser library name in user-facing output. Rename the RETURNED contract value 'playwright' -> 'browser' — the tier name, its type members, its producers, and its documentation. The internal browser library (imports, browser pool, warmup/doctor component names, the dep) is untouched. - FetchMethod union + doc comment (src/types.ts): 'playwright' -> 'browser'. - RawFetchResult.method union + doc comment (the value that flows straight to FetchOutput.fetch_method): 'playwright' -> 'browser'. - CachedContent.fetchMethod DB-mirror union: 'playwright' -> 'browser'. - The three value producers (router.ts browser-escalation return, browser-pool.ts download + main return): method: 'playwright' -> 'browser'. - fetch tool description (src/instructions.ts): fetch_method (cache/http/tls-impersonation/playwright) -> (.../browser). - Tests: every fetch_method / RawFetchResult.method assertion and mock literal across the fetch + router + browser-pool + cache-store lanes -> 'browser'. OpenAPI types fetch_method as a generic string (no tier enum) and the SDKs type it as string too, so no OpenAPI/SDK value change and no drift is introduced. Left untouched: vi.mock('playwright'), 'playwright' library imports, warmup reporter keys, doctor's npx playwright, and the OpenAPI playwright->'browser engine' sanitizer + its forbidden-term tests. Prior research/agent free-key guidance, source_span restore, and the competitor-routing guard tests are unchanged. | 2 个月前 | |
refactor(fetch): capability-named fetch_method 'browser' (was 'playwright') across value/docs/openapi/SDK/tests The fetch_method returned value leaked the browser library name in user-facing output. Rename the RETURNED contract value 'playwright' -> 'browser' — the tier name, its type members, its producers, and its documentation. The internal browser library (imports, browser pool, warmup/doctor component names, the dep) is untouched. - FetchMethod union + doc comment (src/types.ts): 'playwright' -> 'browser'. - RawFetchResult.method union + doc comment (the value that flows straight to FetchOutput.fetch_method): 'playwright' -> 'browser'. - CachedContent.fetchMethod DB-mirror union: 'playwright' -> 'browser'. - The three value producers (router.ts browser-escalation return, browser-pool.ts download + main return): method: 'playwright' -> 'browser'. - fetch tool description (src/instructions.ts): fetch_method (cache/http/tls-impersonation/playwright) -> (.../browser). - Tests: every fetch_method / RawFetchResult.method assertion and mock literal across the fetch + router + browser-pool + cache-store lanes -> 'browser'. OpenAPI types fetch_method as a generic string (no tier enum) and the SDKs type it as string too, so no OpenAPI/SDK value change and no drift is introduced. Left untouched: vi.mock('playwright'), 'playwright' library imports, warmup reporter keys, doctor's npx playwright, and the OpenAPI playwright->'browser engine' sanitizer + its forbidden-term tests. Prior research/agent free-key guidance, source_span restore, and the competitor-routing guard tests are unchanged. | 2 个月前 | |
refactor(fetch): capability-named fetch_method 'browser' (was 'playwright') across value/docs/openapi/SDK/tests The fetch_method returned value leaked the browser library name in user-facing output. Rename the RETURNED contract value 'playwright' -> 'browser' — the tier name, its type members, its producers, and its documentation. The internal browser library (imports, browser pool, warmup/doctor component names, the dep) is untouched. - FetchMethod union + doc comment (src/types.ts): 'playwright' -> 'browser'. - RawFetchResult.method union + doc comment (the value that flows straight to FetchOutput.fetch_method): 'playwright' -> 'browser'. - CachedContent.fetchMethod DB-mirror union: 'playwright' -> 'browser'. - The three value producers (router.ts browser-escalation return, browser-pool.ts download + main return): method: 'playwright' -> 'browser'. - fetch tool description (src/instructions.ts): fetch_method (cache/http/tls-impersonation/playwright) -> (.../browser). - Tests: every fetch_method / RawFetchResult.method assertion and mock literal across the fetch + router + browser-pool + cache-store lanes -> 'browser'. OpenAPI types fetch_method as a generic string (no tier enum) and the SDKs type it as string too, so no OpenAPI/SDK value change and no drift is introduced. Left untouched: vi.mock('playwright'), 'playwright' library imports, warmup reporter keys, doctor's npx playwright, and the OpenAPI playwright->'browser engine' sanitizer + its forbidden-term tests. Prior research/agent free-key guidance, source_span restore, and the competitor-routing guard tests are unchanged. | 2 个月前 | |
test: repair driver-override isolation and Vitest 4 mock generics Two test defects, both mine. stealth-driver-test-isolation reset the override inside the `try`, so a failed assertion would leak it into every later test — defeating the suite-wide pin that file exists to hold. Moved to `finally`. Its env save/restore was also dead (the test never writes the var) and unsafe (restoring an `undefined` stores the STRING "undefined", which would break the pin); removed. scraping-browser.test used the Vitest 2 `vi.fn<Args, Return>()` form. Vitest 4 takes a single function type, so the file carried 14 type errors — invisible because tsconfig.json sets `include: ["src"]` and excludes tests, so `tsc --noEmit` never looks at them. Both found by CodeRabbit. | 2 个月前 | |
feat(fetch): thin frameless pages are partial/thin_content, not shell | 2 个月前 | |
feat(fetch): solve-ladder orchestrator — wire classify/auto-pass/ai-solve/human + enforce clearance route-gate + solve provenance Add src/fetch/solve-ladder.ts, a pure injected orchestrator that sequences the in-band solve rungs per challenge class (behavioral/none run nothing; interactive → auto-pass then human; image → ai-vision then human). Wire it into the browser tier's challenge-fail point with concrete injected callbacks (trusted CDP input for auto-pass, clipped screenshot + vision LLM for ai-solve, headful window + consent for human), harvesting clearance on a solve and falling through to normal hydration. Enforce the P6 clearance route-identity gate in router.clearanceFor (a proxy-solved clearance is refused on the direct route and vice-versa). Thread challenge_class/solve_method provenance onto ChallengeBlockedError, the browser RawFetchResult returns, the router's blocked_by_challenge mapping, and the fetch tool result. cdp-direct and scraping-browser remain dark (CEO GO-gated / opt-in). | 2 个月前 | |
test: repair driver-override isolation and Vitest 4 mock generics Two test defects, both mine. stealth-driver-test-isolation reset the override inside the `try`, so a failed assertion would leak it into every later test — defeating the suite-wide pin that file exists to hold. Moved to `finally`. Its env save/restore was also dead (the test never writes the var) and unsafe (restoring an `undefined` stores the STRING "undefined", which would break the pin); removed. scraping-browser.test used the Vitest 2 `vi.fn<Args, Return>()` form. Vitest 4 takes a single function type, so the file carried 14 type errors — invisible because tsconfig.json sets `include: ["src"]` and excludes tests, so `tsc --noEmit` never looks at them. Both found by CodeRabbit. | 2 个月前 | |
feat(fetch): tier-2 anti-bot driver hardening (WebGL coherence + optional patched driver) T2-F: route the dedicated stealth chromium launch through the ANGLE GL backend (--use-gl=angle --enable-webgl --ignore-gpu-blocklist) so the unmasked WebGL renderer is the host's real GPU instead of the obvious- headless SwiftShader software rasterizer. No JS canvas/WebGL spoofing (de-coheres from the rest of the fingerprint). Verified byte-identical layout/canvas/screenshot output with the flags on/off. Adds isSwiftShaderRenderer() coherence self-check helper. T2-E: add optional patchright driver (self-contained fork tracking Playwright 1.60.x, pinned 1.60.2 — no playwright bump) that patches the CDP Runtime.enable-class automation leak at the driver level. Declared in optionalDependencies (never required); lazy-imported ONLY on the dedicated stealth launch, mirroring the wreq-js pattern; resolves to null when absent. New WIGOLO_STEALTH_DRIVER knob (auto|patchright|playwright, default auto). Pooled fast path and firefox/webkit are unaffected. | 2 个月前 | |
feat(fetch): tier-2 anti-bot driver hardening (WebGL coherence + optional patched driver) T2-F: route the dedicated stealth chromium launch through the ANGLE GL backend (--use-gl=angle --enable-webgl --ignore-gpu-blocklist) so the unmasked WebGL renderer is the host's real GPU instead of the obvious- headless SwiftShader software rasterizer. No JS canvas/WebGL spoofing (de-coheres from the rest of the fingerprint). Verified byte-identical layout/canvas/screenshot output with the flags on/off. Adds isSwiftShaderRenderer() coherence self-check helper. T2-E: add optional patchright driver (self-contained fork tracking Playwright 1.60.x, pinned 1.60.2 — no playwright bump) that patches the CDP Runtime.enable-class automation leak at the driver level. Declared in optionalDependencies (never required); lazy-imported ONLY on the dedicated stealth launch, mirroring the wreq-js pattern; resolves to null when absent. New WIGOLO_STEALTH_DRIVER knob (auto|patchright|playwright, default auto). Pooled fast path and firefox/webkit are unaffected. | 2 个月前 | |
refactor(fetch): detect bot walls by SHAPE, not by vendor marker catalog Replaces the site-specific PerimeterX strings added earlier with a general, vendor-agnostic rule: at an anti-bot STATUS, a large document that is almost entirely markup with no human-readable text is a wall, whoever served it. The CHALLENGE_MARKERS list only ever recognises walls already met, so each new vendor/variant leaks its block page as content until a string is added. Live-measured visible-text density separates the classes by two orders of magnitude: genuine/substantive pages 0.28-0.93, vendor walls ~0.006. Both halves of the gate are required: status alone would swallow substantive 403s (admin pages must pass through); low density alone would swallow un-hydrated SPA shells at 2xx. Existing must-not-fire guards stay green. | 2 个月前 |
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 5 个月前 | ||
| 2 个月前 | ||
| 5 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 3 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 3 个月前 | ||
| 5 个月前 | ||
| 4 个月前 | ||
| 5 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 4 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 |