Traducción automática
Esta página fue traducida automáticamente por IA y puede contener errores. Si algo no está claro, consulte la fuente en inglés.
Pipeline de exportación¶
1. Pipeline de exportación en detalle¶
El pipeline completo de exportación de un solo hilo (cmd_export, export_cmd.py:42-86; la ruta por lotes cmd_batch
reutiliza el mismo pipeline, batch_cmd.py:117-204). Funciones clave y números de línea entre corchetes:
flowchart TD
START(["thread URL / UUID"]) --> IDX
subgraph S1["① indexing (pre-step of index / batch)"]
IDX["GraphQLClient.list_threads<br/>(graphql.py:51)<br/>LibraryThreadsRelayQuery first page 25 items<br/>LibraryRecentThreadsPaginationQuery pagination<br/>stop when endCursor is empty (graphql.py:81-85)"]
IDX --> IDXO[("index/library_<account>.json<br/>cmd_index (index_cmd.py:17)")]
end
subgraph S2["② incremental planning (batch only)"]
PLAN["plan_incremental pure function<br/>(hooks/incremental.py:36)<br/>sorted by lastUpdated desc, four-state classification:<br/>new / updated / done / expired"]
PLAN --> ES{"neither full nor force?"}
ES -->|"yes"| TRIM["trim the longest trailing done/expired run<br/>(early stop, incremental.py:83-86)"]
ES -->|"no"| KEEP["return item by item (full-scan fallback)"]
end
subgraph S3["③ fetching (adapter.get_thread)"]
GT["PerplexityAdapter.get_thread<br/>(adapter.py:58)"]
GT --> PLAIN["ThreadFetcher.get_thread<br/>plain response (rest.py:59)<br/>GET /rest/thread/uuid<br/>entries + background_entries<br/>cursor pagination ≤20 pages, 3s between pages (rest.py:43-57)"]
DM{"detect_mode<br/>(normalize.py:66)<br/>mode detection after parse_turn, see §4"}
DM -->|"computer / deep-research / council / study"| BLK["ThreadFetcher.get_thread_blocks<br/>schematized response (rest.py:67)<br/>8 SCHEMATIZED_USE_CASES (rest.py:26)<br/>sleep blocks_delay=4s before fetching (adapter.py:28,88)"]
DM -->|"search (all signals present)"| NOBLK["skip blocks<br/>(search is simple query+answer)"]
DM -->|"all-signals-missing fallback (adapter.py:84-87)<br/>mode=search and no entry has display_model"| BLK
BLK --> ANOM["scan_wf_anomalies<br/>log.warning on any non-COMPLETED workflow<br/>(parsers.py:646; call site adapter.py:131-134)"]
end
subgraph S4["④ in-memory parsing and assembly (before persistence)"]
PT["parse_turn × N (parsers.py:173)<br/>steps / three-channel citation collection<br/>(entry.sources + FINAL.web_results<br/>+ WORKFLOW_ITEM_SOURCES)<br/>report_info / locked_reason into metadata"]
EA["extract_answer (parsers.py:80)<br/>FINAL.answer JSON → plan.goals fallback"]
BASE["Base Conversation assembled in memory<br/>raw responses retained on conv._plain / conv._blocks"]
ATT["attach_workflow_blocks (parsers.py:231)<br/>wf_block attached to turns by entry uuid<br/>wf_status into turn.metadata"]
STUB["attach_stub_workflows (parsers.py:470)<br/>stub-turn 10s time-window matching"]
UNC["collect_unconsumed_background<br/>(parsers.py:491) attribution waterfall ③"]
READY["Conversation ready for export<br/>(adapter.py:91-157)"]
BASE -->|"other modes"| READY
BASE -->|"computer/council only<br/>(adapter.py:150-157)"| ATT
ATT --> STUB
STUB -.-> UNC
UNC --> READY
end
subgraph S5["⑤ asset preparation (before writer)"]
ADL["adapter.get_assets (adapter.py:196)<br/>collect_downloadable_assets (parsers.py:696)<br/>same-name multi-version numbered v1..vN by created_at"]
CDN["CloudFront signed-URL direct download<br/>AssetDownloader.download_all (assets.py:80)<br/>resolve_ext three-source extension resolution (assets.py:122)"]
end
subgraph S6["⑥ persistence (fs_writer.write_thread)"]
WJ[("thread.json (fs_writer.py:224-253)<br/>+ interruptions registration collect_interruptions (parsers.py:535)<br/>+ answer_variants registration collect_answer_variants (parsers.py:589)")]
RAW1[("raw_entries.json<br/>retained plain response (fs_writer.py:257-261)")]
RAW2[("raw_blocks.json<br/>retained schematized response (fs_writer.py:262-266)<br/>absent when blocks were not fetched")]
WM[("conversation.md compact version<br/>render_conversation (render.py:641)")]
WT[("turns/turn_NNNN.md full version<br/>render_turn (render.py:596)")]
WS[("sources.json + sources.md<br/>(fs_writer.py:270-278)")]
WR[("report.md (fs_writer.py:308-316)<br/>adapter.get_report fallback when needed")]
MF[("assets/assets_manifest.json<br/>versioned manifest (fs_writer.py:320-330)")]
end
IDX --> S2
S2 -->|"per-thread new/updated"| S3
PLAIN --> PT
PT --> EA
EA --> DM
ANOM --> BASE
NOBLK --> BASE
READY --> ADL
ADL --> CDN
CDN --> WJ
WJ --> RAW1
WJ -. "when blocks fetched" .-> RAW2
RAW1 --> WS
RAW1 -. "offline re-runnable<br/>(re-render, see offline-operations.md §12)" .-> PT
WS --> WM
WM --> WT
WT --> WR
WR --> MF
MF --> DONE(["state.mark_ok<br/>BatchState checkpoint (state.py:123)"])
Diseños clave:
- Respuestas sin procesar retenidas para reproducción sin conexión:
adapter.get_threadanaliza y ensambla la conversación en memoria antes de queFilesystemWriter.write_threadse ejecute (adapter.py:58-157). Una escritura exitosa almacenathread.jsonprimero y luego las respuestas de texto simple/bloques disponibles comoraw_*.json(fs_writer.py:224-266); el análisis/renderizado puede volver a ejecutarse desde esos archivos sin acceso a la red. - Texto simple siempre obtenido; bloques por detección de modo: la búsqueda omite bloques (ahorra una solicitud);
cuando todas las señales de detección fallan, la alternativa también obtiene (adapter.py:74-85 comentario y decisión: mejor obtener de más que dejar que
raw_blocks.jsondesaparezca silenciosamente después de un cambio de campo en la plataforma). - Punto de análisis único: toda la extracción de campos se centraliza en
parsers.py(_ganalizadores de acceso seguro multinivel parsers.py:23,_loadstolerancia a fallos parsers.py:33,to_intclave de ordenación indulgente parsers.py:44) — las renovaciones del sitio solo necesitan cambiar un lugar. - El escritor es de solo lectura: el montaje de
wf_block/stub_wfs/unconsumed_bgsocurre enadapter.get_thread(adapter.py:150-157);FilesystemWriterno monta, solo consume (fs_writer.py:217 comentario).
2. Árbol de decisión de detección de modo (detect_mode)¶
La lógica de decisión completa de normalize.detect_mode (normalize.py:66-128). Cinco modos:
computer / council / deep-research / study / search (la búsqueda es el valor predeterminado).
La señal de mayor prioridad es el campo autoritativo de la plataforma entry.search_mode (SEARCH_MODE_MAP,
normalize.py:50-59): configuración de modelo oficial probada (GET /rest/models/config/v2)
default_models.search=pplx_pro (UI "Mejor"), default_models.research=pplx_alpha
(UI "Deep research"); los valores de search_mode se asignan uno a uno a los modos de conversación (verificación de archivo: 100+
hilos de entrada SEARCH + pplx_alpha son 100% search_mode=RESEARCH; 100+ hilos puros pplx_pro son todos
SEARCH). Solo cuando search_mode está completamente ausente, se recurre a la cadena original de nombre de paso + display_model.
flowchart TD
IN["Input: metadata + idx_thread + url<br/>+ turns + entries"] --> SMQ{"search_mode hit on any entry?<br/>(any over all entries, normalize.py:106-115)<br/>ASI→computer / AGENTIC_RESEARCH→council<br/>STUDY→study / RESEARCH→deep-research<br/>SEARCH/STUDIO→search"}
SMQ -->|"hit"| MULTI{"multi-value conflict within thread?<br/>(mode switching, normalize.py:116-119)"}
MULTI -->|"multiple values"| SPEC["take highest by specificity:<br/>computer > council > study<br/>> deep-research > search<br/>(_MODE_SPECIFICITY, normalize.py:63)<br/>+ log.warning"]
MULTI -->|"single value"| SMCF{"conflict with downstream signals (step names/<br/>display_model)?<br/>(normalize.py:120-123)"}
SPEC --> SMCF
SMCF -->|"conflict"| SMWARN["log.warning recorded<br/>search_mode wins"]
SMCF -->|"consistent or no downstream signal"| SMWIN["return the search_mode-mapped mode"]
SMWARN --> SMWIN
SMQ -->|"all absent"| Q1{"primary signal A: URL contains /computer/tasks/<br/>or metadata.mode == '4'<br/>or index mode ∈ ASI/COMPUTER?"}
Q1 -->|"yes"| SM1["step_mode = computer"]
Q1 -->|"no"| Q2{"primary signal B: a COUNCIL_RESEARCH step exists?"}
Q2 -->|"yes"| SM2["step_mode = council"]
Q2 -->|"no"| Q3{"primary signal C: a RESEARCH_ANSWER step exists?<br/>(content-based, not relying on Chinese labels)"}
Q3 -->|"yes"| SM3["step_mode = deep-research"]
Q3 -->|"no"| SM0["step_mode = (empty)"]
SM1 --> DMS
SM2 --> DMS
SM3 --> DMS
SM0 --> DMS
subgraph DMS["fallback-chain redundant signal: display_model (DISPLAY_MODEL_MODE, normalize.py:32-37)"]
DMQ{"display_model hit on any entry?<br/>(any over all entries, not just the first —<br/>the first entry of a mixed thread may not represent the whole)"}
MAP["mapping table:<br/>pplx_agentic_research → council<br/>pplx_asi_opus → computer<br/>pplx_asi_opus_thinking → computer<br/>pplx_study → study"]
end
DMQ -->|"hit"| CF{"conflicts with step_mode?"}
CF -->|"conflict"| WARN["log.warning records the conflict<br/>display_model wins"]
CF -->|"consistent or step_mode empty"| DMWIN["return the display_model-mapped mode"]
WARN --> DMWIN
DMQ -->|"no hit"| FB{"step_mode non-empty?"}
FB -->|"yes"| SMWIN2["return step_mode"]
FB -->|"no"| SEARCH["return search (default)"]
Disciplina de detección (base probada):
pplx_alphaestá deliberadamente ausente de la tabla de mapeo (normalize.py:15-31 comentario): es el modelo dedicado a RESEARCH (default_models.research; UI fija, sin selector) — es el objetivo que este clasificador debe detectar, no una señal de detección. La estadística anterior "121 hilos de búsqueda usaron pplx_alpha" eran en realidad muestras del propio error de juicio de este clasificador (sin la señal search_mode, las sesiones RESEARCH que carecían de un paso RESEARCH_ANSWER se juzgaron como búsqueda) — reevaluadas por search_mode y 124 hilos migrados.- search_mode gana en conflicto con señales posteriores: es el registro autoritativo de la plataforma del modo de conversación; los nombres de paso son los campos de superficie más propensos a cambios de la plataforma, display_model es solo una enumeración de la capa de modelo. Los conflictos siempre registran una advertencia (normalize.py:120-123).
- El cambio de modo dentro de un hilo toma el de mayor especificidad: computer > council > study > deep-research > search (normalize.py:63, 116-119), con log.warning.
- La ausencia total de señales no concluye búsqueda: recurre a la obtención de bloques a nivel de pipeline (adapter.py:84-87), asegurando que los datos esquematizados no desaparezcan silenciosamente debido a la deriva de campos.