plugin-architecture-patterns
GitHub指导 Xberg 插件系统的架构设计、类型定义、注册机制、优先级冲突处理及生命周期管理,涵盖 Rust 原生提取器与 Python 桥接实现。
Trigger Scenarios
Install
npx skills add xberg-io/xberg --skill plugin-architecture-patterns -g -y
SKILL.md
Frontmatter
{
"name": "plugin-architecture-patterns",
"priority": "critical",
"description": "Design, implement, or diagnose Xberg plugin traits, typed registries, priority collisions, lifecycle, native extractors, and Alef-generated Python plugin bridges. Load for plugin-system work, not ordinary extractor parsing."
}
Plugin Architecture & Registration
Plugin Types
| Type | Trait | Location |
|---|---|---|
| Document extractor (binding-facing) | DocumentExtractor: Plugin |
plugins/extractor/trait.rs |
| Document extractor (in-crate) | InternalDocumentExtractor: Plugin |
plugins/extractor/trait.rs |
| OCR backend | OcrBackend: Plugin |
plugins/ocr.rs (a file, not a directory) |
| Post processor | PostProcessor: Plugin |
plugins/processor/trait.rs |
| Validator | Validator: Plugin |
plugins/validator/trait.rs |
| Embedding backend | EmbeddingBackend: Plugin |
plugins/embedding.rs |
| Reranker backend | RerankerBackend: Plugin |
plugins/reranker.rs |
| Tokenizer backend | TokenizerBackend: Plugin |
plugins/tokenizer.rs |
| Renderer | Renderer: Plugin |
plugins/renderer.rs |
Plugin (plugins/traits.rs) is Send + Sync and requires name(); version(),
initialize(), shutdown(), description(), and author() have defaults. There is no
'static trait bound; registry-owned Arc<dyn Trait> supplies the necessary lifetime.
Native Rust extractors implement InternalDocumentExtractor
DocumentExtractor is the binding-facing surface. In-crate extractors implement
InternalDocumentExtractor and get DocumentExtractor from a blanket impl. Implementing
DocumentExtractor directly in this crate is the wrong layer.
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
impl InternalDocumentExtractor for MyExtractor {
async fn extract_content(&self, content: &[u8], mime_type: &str, config: &ExtractionConfig)
-> Result<InternalDocument> { /* ... */ }
fn supported_mime_types(&self) -> &[&str] { &["application/x-custom"] }
fn priority(&self) -> i32 { 50 }
}
extract_path has a default that reads the file and delegates to extract_content (and
errors without tokio-runtime).
Always use the two-arm cfg_attr form for async_trait. A bare #[async_trait] does not
match the trait declaration on wasm32.
The public trait has exactly four items — extract, supported_mime_types, priority,
can_handle. There is no as_sync_extractor; writing one is a compile error. WASM sync
support is the separate SyncExtractor trait — see wasm-constraints.
Priority System
| Range | Use |
|---|---|
| 0-25 | Fallback/low-quality |
| 26-49 | Alternative extractors |
| 50 | Default (built-in) |
| 51-75 | Premium/enhanced |
| 76-100 | Specialized/high-priority |
The registry selects the highest priority extractor for each MIME type. The ranges are
conventions over an unclamped i32; negative and values above 100 are representable. Equal
MIME and priority is a collision: the later registration replaces the earlier entry and
warns. Give competing plugins distinct priorities.
Registration
// crates/xberg/src/extractors/mod.rs -> register_default_extractors()
let registry = get_document_extractor_registry();
let mut registry = registry.write();
registry.register(Arc::new(MyExtractor::new()))?;
Feature-gate optional formats:
#[cfg(feature = "office")]
{
registry.register(Arc::new(DocxExtractor::new()))?;
registry.register(Arc::new(PptxExtractor::new()))?;
}
PostProcessor Pattern
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
impl PostProcessor for MyProcessor {
async fn process(&self, result: &mut ExtractedDocument, config: &ExtractionConfig)
-> Result<()> {
result.content = process_content(&result.content);
Ok(())
}
fn processing_stage(&self) -> ProcessingStage { ProcessingStage::Middle }
}
The enum is ProcessingStage and the accessor is processing_stage(). Stages:
Early (default) → Middle → Late. process takes &mut ExtractedDocument, not an owned
result.
Critical Rules
- All plugins MUST be
Send + Sync—Pluginrequires it. - In-crate extractors implement
InternalDocumentExtractor, neverDocumentExtractor. - Use the two-arm
cfg_attrasync_traitform on every plugin trait impl. - Feature-gate optional formats with
#[cfg(feature = "...")]at the registration site. - Initialization is lazy via
ensure_initialized()(extractors/mod.rs), called before first extraction. - Plugin names are kebab-case (e.g.
"pdf-extractor"). - A new extractor struct needs
#[cfg_attr(alef, alef(skip))]or the binding regen aborts — seealef-generated-bindings.
Registry and lifecycle invariants
- Eight plugin types have eight process-global typed registries in
plugins/registry/mod.rs. There is no universalPluginRegistry. - Registries use
Arc<parking_lot::RwLock<_>>. Their guards are not poisoned and.read()/.write()return guards directly. - Extractor lookup is
HashMap<mime, BTreeMap<priority, entry>>: exact MIME lookup is constant-time on the outer map; wildcard-family lookup scans registered MIME keys. - Registration calls
initialize()and rejects a plugin whose initialization fails. Registries support register, remove, clear, andshutdown_all; there is no hot reload. - All eight plugin types can be registered from language bindings. Plugin interfaces are public APIs, so breaking changes follow the public compatibility policy.
- Return errors rather than panicking. Test lifecycle, collision/replacement, concurrent access, and failure paths with test doubles; use real backends for integration coverage. No dispatch-overhead benchmark exists unless one is explicitly added.
Alef-generated Python bridge
The Python bridge is generated into crates/xberg-py/src/lib.rs; there is no hand-written
plugins.rs. Change Alef/configuration and regenerate rather than editing the bridge.
- PyO3 0.29 uses
Python::attach. Async host calls enter Python fromtokio::task::spawn_blockingand propagate the caller'scontextvarscontext. - Cache frequently accessed host data such as plugin names in Rust fields so infallible
methods do not need repeated GIL acquisition. Do not assume
allow_threadsis in use. - Every trait method return crosses the bridge through native extraction or JSON fallback.
Crossing types therefore need
Serialize + Deserialize + Default, including unit enums. - Host exceptions become
XbergError::Otherwith plugin and method context; the original Python exception type and traceback are not retained. Infallible methods can only warn and returnDefault::default(), so a default may indicate bridge failure rather than real data. - Rust-side extractor plugin failures may use
XbergError::Plugin, which is fallback-eligible; do not assume Python bridge errors have the same fallback behavior. - Validate the Python protocol at registration. Do not quote GIL overhead without a current benchmark.
Version History
-
d8e4815
Current 2026-08-28 18:31
更新插件类型表格以匹配实际代码结构;移除已废弃的 as_sync_extractor 方法说明;修正 WASM async_trait 使用规范;新增 OCR、嵌入、分词等后端插件描述。
- 531e0f7 2026-08-20 07:47


