Problem: Meow upstream uses Cloudflare, which sets cf_clearance + session
cookies that must all be sent together. The old code only captured a single
session-named cookie via a whitelist, discarding cf_clearance entirely, and
wrote back only 'name=value' instead of the full cookie string.
Changes:
auth_capture_service.py:
- Add _cookie_matches_hostname(): hostname suffix matching supporting
dot-prefixed domains (.saki.lat matches api.saki.lat)
- Add _build_cookie_bundle(): collects ALL cookies matching the current
page's hostname, returns complete 'name1=v1; name2=v2' string
- _curate_candidates(): new 'cookie_bundle' candidate type (type=0 in sort,
highest priority), carries cookie_count + cookie_names in extra fields
- extract_all(): obtain real-time page URL from session.page.url and pass
to _curate_candidates so cookie domain filtering is accurate
- Sort order: cookie_bundle > cookie > bearer_token/api_key > credential
- Fix bug in original JWT dedup check (was assigning instead of checking)
custom_pages.py:
- Add logging import + logger
- _pick_best_candidate(): cookie preferred_auth_type now tries cookie_bundle
first, then single cookie; bearer/api_key use existing type_map logic
- RefreshAuthResponse: add optional 'warning' field
- refresh_auth(): handle ctype='cookie_bundle' same as 'cookie'; always
write full candidate.value as cookie_string (works for both types)
- Post-write validation: attempt get_available_groups with new credentials;
on failure, still commit (lenient mode) but set warning message explaining
cf_clearance IP-binding as the likely cause; success logs at INFO level
Tests (test_auth_capture.py, 19 cases):
- _cookie_matches_hostname: exact, dot-prefix subdomain, empty domain,
different domain, evil-subdomain partial match rejection
- _build_cookie_bundle: cf_clearance included, cross-domain excluded,
single cookie, empty value excluded, no cookies
- _curate_candidates: bundle ranks first, value is full string, bundle
beats single session cookie, bearer wins when no cookies, empty case,
cookie_count/cookie_names in extra, session fallback preserved,
new_api_user propagation to bundle
All 46 tests pass.
P1 - Missing rate data now skips account instead of falling back to 1.0:
In sync_account_priorities_for_upstream(), the rated list now filters
out accounts whose upstream snapshot has no rate entry for their group_id.
If after filtering a competitive bucket has fewer than 2 accounts with
valid rate data, the entire bucket is silently skipped (no update_account
call, no webhook) rather than treating missing rates as 1.0 and
potentially triggering spurious notifications.
P2 - Re-importing an existing account now backfills imported_target_group_id:
In the exists-is-True idempotency branch of import_upstream_keys_as_accounts(),
if the current request supplies a target_group_id for the account's source group
and it differs from what is stored, the field is written back and committed.
This lets operators fix old data by simply re-running the import dialog.
Tests added:
- test_missing_rate_skips_entire_competitive_group: all accounts in
competitive group lack snapshot → bucket skipped, no update called
- test_partial_missing_rate_sufficient_accounts_still_updates: 3 accounts
in same bucket, 1 missing rate → the 2 with rates still compete normally
All 27 tests pass.
Root cause: sync_account_priorities_for_upstream() was doing a global
priority re-rank across ALL imported accounts on a website whenever any
upstream rate changed, triggering spurious account_priority_changed
notifications for accounts in different target groups with no competition.
Fix:
- Add imported_target_group_id / imported_target_group_name to
UpstreamGeneratedKey (nullable; old data falls back to group_id)
- Writ imported_target_group_id on account import in websites.py
- Rewrite sync_account_priorities_for_upstream():
* bucket accounts by competition_group = imported_target_group_id or group_id
* only process buckets with count > 1 (genuine competition)
* each competitive bucket independently sorted by rate; priority starts at 1
* single-account groups: completely skipped (no update_account, no notification)
* no competitive groups at all: early return, no log, no notification
- Remove auto priority update in re-import idempotency path (was also
incorrect; now fully delegated to sync_account_priorities_for_upstream)
- Fix Sub2ApiWebsiteClient local import in sync fn → use module-level name
so monkeypatch works correctly in tests
Tests: rewrite test_priority_sync.py
- REMOVED: test_priority_sync_full_website_update (was asserting the buggy behavior)
- NEW: test_no_update_when_different_groups_single_account_each
- NEW: test_same_target_group_two_accounts_updated
- NEW: test_two_target_groups_independent_priority
- NEW: test_old_data_null_target_group_fallback
- NEW: test_single_account_in_mixed_website
- UPDATED: test_priority_sync_log_structure (now requires competitive group)
- KEPT: test_priority_sync_cross_upstream_group, test_import_auto_priority_by_rate
All 25 tests pass (8 priority_sync + 17 existing upstream tests).
- POST /api/upstreams/test-all: batch connection test for all enabled
upstreams (no snapshot, no webhook); updates last_status, balance
- POST /api/upstreams/check-now-all: full batch sync (snapshot, diff,
webhook, key sync, priority sync); mirrors single check-now behavior
- Both routes are registered before /{uid} to avoid path capture
- Skips disabled upstreams (status=skipped); single failure does not
abort subsequent upstreams (serial execution)
- Returns UpstreamBatchActionResponse with per-item detail and summary
Refactor: extract _test_upstream_core(db, u) and _check_now_core(db, u)
- All four routes (single + batch × 2) now share the same core helpers
- Eliminates duplicate logic and future divergence risk
Frontend:
- Add UpstreamBatchActionResponse / Item / Summary TS types
- Add upstreamsApi.testAll() and upstreamsApi.checkNowAll()
- Add '一键测试' and '一键同步' buttons in Upstreams.vue toolbar
(order: 一键测试 → 一键同步 → 刷新 → 新增上游)
- Buttons disabled when list is empty or another batch op is running
- On completion: refresh list + ElMessageBox with per-item failure detail
- Add tini (init: true in compose) to reap orphan Chromium processes
- Reduce screenshot push frequency (active 0.12→0.20s, idle 0.35→1.00s,
deep idle 1.00→5.00s, backoff 0.60→2.00s)
- Add 5s timeout to screenshot in WebSocket push loop
- close() now wraps context.close() in asyncio.wait_for(10s)
with browser.close() fallback on timeout
- Two-phase close logging (closing → closed / close_failed)
- Auth-capture sessions evicted after 10min TTL
- shutdown() with timeout protection and logging
- close_ok correctly tracks success through browser fallback path
- list_generated_keys now fetches live keys from upstream API, merges with
local DB: remote keys with plaintext values are auto-upserted (by
group_id+managed_prefix), remote-only keys shown as unimportable
- Use _fetch_remote_managed_prefixes to support custom key prefixes
- Group remote keys by (group_id, prefix), pick latest by key_id
- Extract _remote_group_name helper for safe group name parsing
(handles dict group field from Meow upstream)
- Frontend excludes orphaned keys from importable list
- Backend import endpoint reconciles upstream before importing
- Add linked_upstream_id to CustomPage model with DB migration
- New POST /api/custom-pages/{pid}/refresh-auth endpoint extracts
credentials from active remote browser and updates linked upstream
- PageViewer toolbar shows key icon button when page has linked upstream
- CustomPages form adds upstream dropdown for remote_browser pages
- Auth capture extracts New-Api-User from localStorage uid/user/self API
- Upstream client sends New-Api-User header in cookie auth mode
- Fix auth capture dialog: transparent background, field persistence,
login URL defaults to base_url/login, focus on click for keyboard input
- Fix upstream test ASCII encoding with non-header characters validation
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- AuthCaptureDialog: revokeObjectURL on each frame to prevent memory leak
- CDP Network capture starts before initial page.goto, not after
- /extract defaults to candidates-only; pass ?include_raw=true for full data
- AuthCaptureDialog: real WebSocket for binary JPEG frame stream (no polling)
- Pointer drag: mousedown/mousemove/mouseup events for slider-captcha
- CDP capture starts at session creation, caches headers in session.captured_headers
- Ephemeral profile dir deleted on session close (shutil.rmtree)
- Candidate types unified: bearer_token / cookie / api_key / credential
- Frontend handleAuthCaptureSelect maps all 4 types to correct form fields
- _init_admin now re-hashes password if .env password differs from DB
- docker-compose: add extra_hosts host.docker.internal:host-gateway
so containers can reach host services via host.docker.internal
- UpstreamClient & Sub2ApiWebsiteClient: add __enter__/__exit__
- Convert all call sites to `with Client(...) as c:` pattern
- Remove unused `upstream_name`/`upstream_base_url` locals in scheduler
- Fix stale _decimal_str→decimal_string in _rate_from_group
Enable managed remote browser custom pages with login autofill and add website sync workflows so external admin surfaces can be handled inside SmartUp.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>