Машинный перевод
Эта страница была автоматически переведена ИИ и может содержать ошибки. Если что-то неясно, обращайтесь к английскому источнику.
Обзор архитектуры¶
1. Обзор многоуровневой архитектуры¶
Структура пакета (pplx_export/, ~5,6 тыс. строк исходного кода и растёт с развитием, без учёта тестов;
точное количество строк по wc -l):
flowchart TD
subgraph CLI["CLI layer (entry points)"]
CL1["cli.py — pplx-export<br/>argparse definitions + dispatch (cli.py:57)"]
CL2["ask_cli.py — pplx-ask<br/>interactive query entry (ask_cli.py:221)"]
end
subgraph CMD["commands/ command layer (shared by both entries)"]
C0["common.py<br/>account mapping / make_transport assembly<br/>cookie validation and auto-switching (common.py:93)"]
C1["index_cmd / export_cmd / batch_cmd"]
C2["spaces_cmd / misc_cmd / rerender_cmd"]
C3["assets_backfill_cmd / usage_backfill_cmd<br/>search_mode_backfill_cmd / sync_deleted_cmd"]
end
subgraph SITES["sites/ site layer"]
SB["base.py — SiteAdapter ABC<br/>(pplx_export/sites/base.py:21)"]
subgraph PPLX["sites/perplexity/"]
AD["adapter.py<br/>PerplexityAdapter assembly (adapter.py:24)"]
GQ["graphql.py<br/>APQ list pagination (graphql.py:43)"]
RS["rest.py<br/>ThreadFetcher thread fetching (rest.py:38)"]
PA["parsers.py<br/>single point of schema parsing (parsers.py)"]
NM["normalize.py<br/>mode detection / math normalization (normalize.py:66,240)"]
RD["render.py<br/>markdown rendering (render.py)"]
AS["assets.py<br/>asset download and extension resolution (assets.py:27)"]
AK["ask_api.py<br/>envelope / SSE / telemetry (ask_api.py)"]
FW["fs_writer.py<br/>web_archive persistence (fs_writer.py:54)"]
VL["variant_log.py<br/>ANSWER_VARIANT_DETECTED detection log<br/>+ jsonl registry (variant_log.py:45)"]
end
end
subgraph CORE["core/ foundation layer (site-agnostic)"]
MO["models.py domain models (models.py)"]
ER["errors.py error types (errors.py)"]
TH["throttle.py rate-limit backoff (throttle.py:15)"]
ST["state.py BatchState checkpoint (state.py:63)"]
LG["logging.py central logging (logging.py:45)"]
CK["cookies/ + auth.py<br/>cookie sources and credentials (cookies/loaders.py:270)"]
RL["relations.py relations graph (relations.py:200)"]
RG["registry.py site registry (registry.py:9)"]
subgraph HTTP["core/http/ transports"]
TP["transport.py Transport ABC (transport.py:12)"]
CT["cookie_transport.py<br/>urllib direct (cookie_transport.py:44)"]
BT["bridge_transport.py<br/>WebBridge page context (bridge_transport.py:22)"]
FB["fallback.py fallback chain [reserved · not wired]"]
BA["browser_automation_transport.py<br/>heavyweight browser [reserved · not implemented]"]
end
end
subgraph HOOKS["hooks/ extension points"]
H1["incremental.py<br/>plan_incremental pure function (incremental.py:36)"]
H2["relations_hook.py relations rebuild (relations_hook.py:17)"]
H3["scheduler.py periodic schedule + cron (scheduler.py:22)"]
end
subgraph WRT["writers/ output abstraction"]
WB["base.py Writer ABC (writers/base.py:17)"]
end
CFG["config.py — single source of site constants<br/>+ user-level config loading (account table / BOT space externalized to config.toml)"]
CL1 --> C0
CL2 --> C0
C0 --> C1
C0 --> C2
C0 --> C3
C1 --> AD
C2 --> AD
C3 --> AD
AD --> GQ
AD --> RS
AD --> PA
AD --> AS
PA --> MO
RD --> PA
FW --> RD
FW --> VL
AD --> VL
FW --> WB
SB --> MO
AD --> SB
GQ --> TP
RS --> TP
AS --> TP
AK --> CT
CT --> TH
CT --> ER
C0 --> CK
C1 --> ST
H1 --> ST
H2 --> RL
RG -. "core's only import of sites:<br/>just the ABC of sites/base (registry.py:7)" .-> SB
CFG -. "imported by all layers, zero dependencies itself" .- MO
Основные направления зависимостей (проверено поиском всех импортов):
- Однонаправленно: CLI → commands → sites → core.
core/не импортирует ни одной конкретной реализации Perplexity (нет жёсткой привязки к сайту); единственное исключение —core/registry.py:7, импортирующий ABCSiteAdapterизsites/base.py— ссылка на интерфейс, а не на сайт; конкретные сайты внедряются черезregister()(встроенная регистрация Perplexity вpplx_export/__init__.py:34-43). config.pyявляется единственным источником констант сайта (домен,DEFAULT_ARCHIVE_ROOT) и загружает внешнюю конфигурацию на уровне пользователя: таблицы учётных записейACCOUNT_DISPLAY_NAMES/ACCOUNT_EMAIL/ACCOUNT_UIDи пространство BOT берутся из TOML (--config>PPLX_EXPORT_CONFIG>~/.config/pplx-export/config.toml; шаблонconfig.example.toml), словари обновляются на месте, корректная деградация при отсутствии;core/models.py:18также импортирует его (author_folder).ask_api.py— единственный модуль уровня сайта, который напрямую зависит от конкретной реализации транспорта (импортируетCookieTransportи повторно использует его внутренности_cookie_header/_openerдля потока SSE, ask_api.py:23, 114-130) — SSE находится за пределами абстракции Transport ABC.hooks/,writers/зависят только отcore/; единственная реализацияwriters/base.py,FilesystemWriter, находится на уровне сайта (fs_writer.py:54) — ABC и реализация разделены.
2. Граф зависимостей модулей (реальные отношения импорта)¶
Составлен на основе полной переписи grep '^from \.' (внутрипакетные ссылки опущены; все __init__.py пусты,
за исключением корня пакета pplx_export/__init__.py, который выполняет обязанности по регистрации):
flowchart LR
subgraph entry["entry points"]
E1["cli.py"]
E2["ask_cli.py"]
E0["__init__.py<br/>_register_builtin (pplx_export/__init__.py:34)"]
end
subgraph cmd["commands/"]
CM["common.py"]
CI["index_cmd.py"]
CE["export_cmd.py"]
CB["batch_cmd.py"]
CS["spaces_cmd.py"]
CR["rerender_cmd.py"]
CX["misc_cmd.py"]
CA["assets_backfill_cmd.py"]
CU["usage_backfill_cmd.py"]
CSM["search_mode_backfill_cmd.py"]
CSD["sync_deleted_cmd.py"]
end
subgraph site["sites/perplexity/"]
SA["adapter.py"]
SG["graphql.py"]
SR["rest.py"]
SP["parsers.py"]
SN["normalize.py"]
SE["render.py"]
SS["assets.py"]
SK["ask_api.py"]
SF["fs_writer.py"]
SV["variant_log.py"]
end
SBASE["sites/base.py"]
subgraph core["core/"]
KM["models.py"]
KE["errors.py"]
KT["throttle.py"]
KS["state.py"]
KL["logging.py"]
KC["cookies/ (profiles/loaders/cache)"]
KA["auth.py"]
KR["relations.py"]
KG["registry.py"]
KH["http/ (transport/cookie/bridge/fallback/browser_automation)"]
end
subgraph hooks["hooks/"]
HI["incremental.py"]
HR["relations_hook.py"]
HS["scheduler.py"]
end
WBS["writers/base.py"]
CFG2["config.py"]
TST["tests/<br/>pytest fully offline<br/>(case count per actual runs)"]
E1 --> CM
E1 --> CI
E1 --> CE
E1 --> CB
E1 --> CS
E1 --> CR
E1 --> CX
E1 --> CA
E1 --> CU
E1 --> CSM
E1 --> CSD
E1 --> KG
E1 --> KT
E1 --> SF
E2 --> CM
E2 --> CE
E2 --> SK
E2 --> KG
E2 --> SF
E0 --> KG
E0 --> SA
CM --> KC
CM --> KH
CM --> KM
CM --> CFG2
CB --> HI
CB --> KS
CB --> KT
CB --> KE
CB --> SF
CB --> SV
CE --> KS
CR --> SP
CR --> SE
CR --> SA
CR --> SV
CSM --> SN
CSD --> KS
CSD --> KH
CA --> SP
CA --> SS
CX --> HI
CX --> HR
CX --> HS
SA --> SG
SA --> SR
SA --> SP
SA --> SN
SA --> SS
SA --> SBASE
SP --> KM
SN --> KM
SE --> SP
SE --> SN
SF --> SP
SF --> SE
SF --> SV
SF --> WBS
SF --> KM
SA --> SV
SS --> SN
SK --> KH
SK --> CFG2
SG --> KH
SR --> KH
HI --> KS
HR --> KR
HS --> HI
WBS --> KM
WBS --> KS
KG --> SBASE
KH --> KT
KH --> KE
KM --> CFG2
KC --> CFG2
KA --> KH
TST -.-> SP
TST -.-> SE
TST -.-> KS
TST -.-> KT
TST -.-> HI
TST -.-> SS
TST -.-> SN
TST -.-> CR
Как читать граф:
- Цепочка ядра:
cli → commands → sites → core. Ни один модуль ядра не зависит от конкретных реализаций команд/сайтов;KG → SBASE(реестр → SiteAdapter ABC) — единственная обратная ссылка между уровнями, образующая цикл внедрения зависимостей с регистрацией вE0. - Агрегация внутри
sites/perplexity/:adapterявляется фасадом (объединяет graphql/rest/parsers/ normalize/assets);renderзависит отparsers(источник истины для классификации статуса wf);fs_writerзависит отrender + parsers + writers/base. - Тесты
tests/находятся вне пакета и напрямую импортируют чистые функции из каждого уровня (conftest.pyrender_fixtureповторно используетcommands.rerender_cmd.rerenderдля офлайн-перерендеринга).
3. Сводка принципов проектирования¶
- Сырые ответы сохраняются; отрендеренные артефакты регенерируемы офлайн:
adapter.get_threadразбирает и собирает разговор в памяти до того, как запускается писатель (adapter.py:58-157). Успешная запись сначала сохраняетthread.json, а затем доступные простые/схематизированные ответы какraw_*.json(fs_writer.py:224-266); разбор/рендеринг/регистрация прерываний могут быть повторно запущены из сырых данных без сети (§12), что отделяет эволюцию рендерера от исторических архивов. - Единая точка разбора, устойчивая к изменениям структуры данных: извлечение полей
сосредоточено в
parsers.py(трио отказоустойчивости_g/_loads/to_int); обнаружение режима имеет двойные избыточные сигналы плюс резервный запрос при отсутствии всех сигналов (§4) — радиус поражения при переработке платформы сжимается до одного модуля. - Детерминированный водопад атрибуции: «каждая фоновая полезная нагрузка попадает ровно в одно место, никогда не рендерится дважды» гарантируется структурами данных (набор закреплённых, однократное потребление used_cand, итерация только верхнего уровня для предотвращения двойного подсчёта) — никакого эвристического угадывания времени; семантика прерываний имеет пять классов с одним источником истины (classify_wf_status), общим для путей рендеринга/регистрации/оповещения (§5, §6).
- Дисциплина ограничения скорости — красная линия безопасности: случайные интервалы, без параллелизма, экспоненциальная задержка 3^N с пределом, быстрый отказ при ошибке аутентификации, терминальное состояние ENTRY_EXPIRED, 404 никогда не ошибочно считается истекшим (§11) — всё служит цели анти-бана «поведение экспорта ≈ просмотр человеком» (явное требование пользователя).
- Единый источник конфигурации + конфиденциальность вынесена вовне: константы сайта/пути по умолчанию
находятся только в
config.py; учётные записи/пространства — личная конфиденциальность, вынесены в пользовательский TOML (--config>PPLX_EXPORT_CONFIG>~/.config/pplx-export/config.toml); автоматическое переключение cookie для нескольких учётных записей — это цикл проверки, управляемый зарегистрированными email, без ручных операций в браузере (§9). - Многоуровневые однонаправленные зависимости: CLI → commands → sites → core, без жёсткой привязки к сайтам
в ядре, сайты внедряются через реестр — новый сайт реализует пять методов
SiteAdapterи использует все возможности ядра (sites/base.py:21-69).