browser4-plugin · git:20260818.523cffe · 2026-08-18 · sha256 595a4606eeb66d8a
browser4-plugin git:20260818.523cffeA
Immutable. This exact content never changes and is served at /api/v1/blob/595a4606eeb66d8a.
---
name: browser4-plugin
title: "Browser4 Plugin Development — AI Agent Skill"
description: "Guides the creation of Browser4 plugins from requirements gathering through deployment. Use when the user wants to create, build, scaffold, or extend Browser4 with a new plugin — whether for CAPTCHA solving, media processing, content conversion, page category detection, custom RPA actions, or new LLM agent tools."
tier: procedure
---
# Browser4 Plugin Development
Step-by-step guide to creating a Browser4 plugin — from scaffolding via the Maven archetype through implementing `PluginMount` interfaces, services, event handlers, tool executors, and tests.
## Description
This skill covers the complete plugin development lifecycle: clarifying requirements, choosing the right `PluginMount` interfaces, scaffolding a project from the PDK archetype, implementing business logic (event handlers, services, LLM agent tools), wiring Spring auto-configuration, writing tests, building, and deploying. It includes decision trees to help identify which extension points to use and concrete code patterns drawn from the five built-in first-party plugins.
## Dependencies
- JDK 17+
- Maven 3.9+
- Access to the Browser4 PDK parent POM (`ai.platon.pulsar:browser4-pdk`) — published to Maven Central
- For building the archetype locally: `mvn -pl browser4-pdk install` from this repository
## Parameters
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `groupId` | String | Yes | — | Maven groupId for the new plugin project (e.g., `com.example`) |
| `artifactId` | String | Yes | — | Maven artifactId, conventionally `browser4-<feature-name>` |
| `version` | String | No | `1.0.0-SNAPSHOT` | Plugin version |
| `pluginName` | String | Yes | — | Human-readable plugin name (e.g., `"My Feature Plugin"`) |
| `pluginDescription` | String | No | `"A Browser4 plugin that provides custom functionality"` | One-line description |
| `mountPoints` | String[] | No | `["BrowseEventMount"]` | Which `PluginMount` interfaces to implement. Options: `BrowseEventMount`, `LoadEventMount`, `CrawlEventMount`, `ToolMount`, `PageSnifferMount` |
| `hasCustomTools` | Boolean | No | `false` | Whether the plugin registers LLM agent tool executors |
| `hasLifecycle` | Boolean | No | `false` | Whether the plugin implements the `Browser4Plugin` lifecycle interface |
| `features` | String[] | No | — | List of concrete capabilities to implement (e.g., `"detect media on page"`, `"download files"`, `"expose LLM tool"`) |
## Return Value
A new Maven project directory at the path specified by `artifactId`. The project builds to a thin JAR (`target/<artifactId>-<version>.jar`) deployable to Browser4's `plugins/` directory. The plugin is auto-discovered on restart and logs:
```
PluginManager: Found X PluginMount bean(s)
PluginManager: ✓ Configured browse event handlers
PluginManager: Found X Browser4Plugin bean(s)
- <plugin-name> v<version>
```
## When to Create a Plugin
Create a plugin when you need to:
- **Hook into the browse lifecycle** — execute custom logic on page-navigation events (before navigation, after DOM is steady, before tab close). Examples: CAPTCHA detection and solving, ad blocking, custom RPA workflows.
- **Hook into the load lifecycle** — intercept or transform page content during loading. Examples: URL normalization and stripping tracking parameters, content extraction, HTML post-processing.
- **Hook into the crawl lifecycle** — accept or reject URLs during crawling. Examples: domain allowlists, duplicate URL filtering, paywall detection and skip.
- **Register custom tools for LLM agents** — expose new capabilities as callable functions. Examples: image download, PPTX generation, database queries, API integrations.
- **Add page category sniffers** — teach Browser4 to recognize new page types so it can adapt its behavior. Examples: CAPTCHA pages, login pages, paywalls, shopping carts.
**Do NOT create a plugin for:**
- Simple data extraction from a known site — use the browser4-cli HTML snapshot / X-SQL instead.
- One-off browser automation — the CLI or a quick script is faster.
- Modifying core Browser4 behavior — that belongs in the main source tree, not a plugin.
## Usage Examples
### Example 1: Scaffold a New Plugin (Quick Start)
```bash
mvn archetype:generate \
-DarchetypeGroupId=ai.platon.pulsar \
-DarchetypeArtifactId=browser4-plugin-archetype \
-DarchetypeVersion=4.12.0 \
-DgroupId=com.example \
-DartifactId=browser4-myfeature \
-Dversion=1.0.0-SNAPSHOT \
-DpluginName="My Feature" \
-DpluginDescription="A Browser4 plugin that performs custom page processing"
```
### Example 2: Build and Deploy
```bash
cd browser4-myfeature
mvn package -DskipTests
cp target/browser4-myfeature-1.0.0-SNAPSHOT.jar /path/to/browser4/plugins/
# Restart Browser4 — plugin is auto-discovered
```
### Example 3: Install via REST API
```bash
curl -X POST http://localhost:8182/api/plugins/install \
-F "file=@target/browser4-myfeature-1.0.0-SNAPSHOT.jar"
curl http://localhost:8182/api/plugins
```
### Example 4: Complete Auto-Configuration with BrowseEventMount + Config + Service
```kotlin
@AutoConfiguration
@ConditionalOnProperty(name = ["myfeature.enabled"], havingValue = "true", matchIfMissing = true)
@Lazy
open class MyFeatureAutoConfiguration(
private val applicationContext: ApplicationContext,
) : BrowseEventMount {
override fun configureBrowseHandlers(handlers: BrowseEventHandlers) {
handlers.onDocumentSteady.addLast { page, driver ->
val service = applicationContext.getBean(MyFeatureService::class.java)
service.process(page, driver)
}
}
@Bean
@ConditionalOnMissingBean
open fun myFeatureConfig(conf: Config): MyFeatureConfig =
MyFeatureConfig.fromConfig(conf)
@Bean
@ConditionalOnMissingBean
open fun myFeatureService(config: MyFeatureConfig): MyFeatureService =
MyFeatureService(config)
}
```
---
## Step-by-Step Workflow
### Step 1: Clarify Requirements
Before writing any code, ask the user these questions. The answers determine which `PluginMount` interfaces and project structure to use.
| Question | Why it matters |
|----------|----------------|
| What is the plugin's core purpose? | Determines which `PluginMount` interfaces to implement and what services are needed |
| Should the plugin act automatically on every page, or only when explicitly invoked? | Automatic action → `BrowseEventMount` on `onDocumentSteady`. Explicit invocation → `ToolMount`. Both → implement both. |
| Does the plugin need to transform or extract data during page loading? | Yes → `LoadEventMount` or `CrawlEventMount`. No → skip these. |
| Should LLM agents be able to invoke the plugin's features as tools? | Yes → `ToolMount`. No → skip. |
| Does the plugin recognize a new category of page (CAPTCHA, paywall, etc.)? | Yes → `PageSnifferMount`. No → skip. |
| What external services or APIs does the plugin use? | Determines additional dependencies (OkHttp client, AWS SDK, etc.) and configuration properties. |
| Should the plugin be configurable (enabled/disabled, timeouts, endpoints)? | Yes → create a `Config` data class and `@ConditionalOnProperty` annotations. |
| Does the plugin need explicit startup/shutdown lifecycle hooks? | Yes → implement `Browser4Plugin`. No → auto-configuration only is sufficient. |
### Step 2: Choose PluginMount Interfaces
Map the plugin's capabilities to one or more mount points. The auto-configuration class implements all chosen interfaces.
| If the plugin needs to... | Implement | Primary hook |
|---------------------------|-----------|-------------|
| Execute code when a page finishes loading and the DOM is stable | `BrowseEventMount` | `onDocumentSteady` — the recommended RPA hook |
| Intercept or block resources before navigation | `BrowseEventMount` | `onWillNavigate` — call `driver.addBlockedURLs()` |
| Take screenshots or extract data before tab closes | `BrowseEventMount` | `onWillStopTab` — last chance before tab teardown |
| Normalize or modify URLs before fetching | `LoadEventMount` | `onNormalize` |
| Extract data right after HTML is parsed | `LoadEventMount` | `onHTMLDocumentParsed` |
| Filter which URLs enter the crawl pipeline | `CrawlEventMount` | `onWillLoad` — return `null` to reject |
| Expose new LLM-callable functions | `ToolMount` | `getToolExecutors()` — must return a `List<ToolExecutor>` |
| Detect a page category (e.g., "this is a CAPTCHA page") | `PageSnifferMount` | `getPageSniffers()` — must return a `List<PageCategorySniffer>` |
| Have startup/shutdown lifecycle hooks | `Browser4Plugin` | Override `onStartup()` / `onShutdown()` |
**Common combinations from built-in plugins:**
| Plugin | Mount points used | Pattern |
|--------|------------------|---------|
| CAPTCHA | `BrowseEventMount` + `ToolMount` + `PageSnifferMount` | Auto-detect + manual solve + page classification |
| Images | `BrowseEventMount` + `ToolMount` + `Browser4Plugin` | Auto-detect + agent tool + explicit lifecycle |
| Media | `BrowseEventMount` + `ToolMount` | Auto-detect + agent tool |
| PPTX | `BrowseEventMount` + `ToolMount` | Agent-invoked conversion |
| Markdown | `BrowseEventMount` + `ToolMount` | Agent-invoked conversion |
| PDK Test | `BrowseEventMount` + `LoadEventMount` + `CrawlEventMount` | All event phases (reference/canary) |
### Step 3: Scaffold via the Archetype
Run the Maven archetype command from the Quick Start. The generated project contains:
```
browser4-<feature>/
├── pom.xml # Maven build (parent: browser4-pdk)
├── .gitignore
├── README.md
└── src/main/
├── kotlin/<package>/
│ ├── MyPlugin.kt # Optional: Browser4Plugin lifecycle
│ ├── config/
│ │ └── PluginAutoConfiguration.kt # Required: @AutoConfiguration + mounts
│ ├── integration/
│ │ ├── MyBrowseEventHandler.kt # Optional: browse event handler
│ │ └── MyLoadEventHandler.kt # Optional: load event handler
│ └── tools/
│ └── MyToolExecutor.kt # Optional: LLM agent tool
└── resources/
└── META-INF/
├── browser4-plugin.json # Required: plugin manifest
└── spring/
└── org.springframework.boot.autoconfigure.AutoConfiguration.imports # Required
```
**Immediately after scaffolding, do these renames:**
1. Rename the generated `PluginAutoConfiguration` class to `<Feature>AutoConfiguration` (e.g., `CaptchaAutoConfiguration`)
2. Rename `MyPlugin` to `<Feature>Plugin` (if keeping the lifecycle class)
3. Rename handler classes: `MyBrowseEventHandler` → `<Feature>BrowseEventHandler`
4. Update `browser4-plugin.json` — change the `name`, `description`, and `autoConfigurationClasses` to match
5. Update `AutoConfiguration.imports` — replace the FQN with the renamed class
6. Delete stub files you won't use (removing unused files is better than leaving dead code)
### Step 4: Implement Mount Points
#### 4a. BrowseEventMount — Custom RPA on Every Page
For plugins that execute code automatically when a page loads. The key hook is `onDocumentSteady`, which fires when the DOM is fully rendered and the page is stable.
```kotlin
@AutoConfiguration
@ConditionalOnProperty(name = ["myfeature.enabled"], havingValue = "true", matchIfMissing = true)
@Lazy
open class MyFeatureAutoConfiguration : BrowseEventMount {
override fun configureBrowseHandlers(handlers: BrowseEventHandlers) {
// Primary RPA hook — fires when DOM is fully rendered
handlers.onDocumentSteady.addLast { page, driver ->
// Access page.url, page.content, driver.*
// Custom logic here
}
// Optional: block unwanted resources before navigation
handlers.onWillNavigate.addLast { page, driver ->
driver.addBlockedURLs(listOf("*.png", "*.jpg", "*analytics*"))
}
}
}
```
**Browse event hooks in execution order:**
| Position | Hook | Best for |
|----------|------|----------|
| 1 | `onWillLaunchBrowser` | Pre-launch setup |
| 2 | `onBrowserLaunched` | First access to `WebDriver` |
| 3 | `onWillFetch` | Pre-fetch configuration |
| 4 | `onWillNavigate` | **Block resources, set headers** |
| 5 | `onNavigated` | Post-navigation checks |
| 6 | `onWillInteract` | Pre-interaction setup |
| 7 | `onWillCheckDocumentState` | Check readyState |
| 8 | `onDocumentFullyLoaded` | DOM ready |
| 9 | `onWillScroll` | Pre-scroll |
| 10 | `onDidScroll` | Post-scroll |
| 11 | `onDocumentSteady` | **★ Best for custom RPA** |
| 12 | `onWillComputeFeature` | Pre-feature computation |
| 13 | `onFeatureComputed` | Features computed |
| 14 | `onDidInteract` | All interactions complete |
| 15 | `onWillStopTab` | **Last chance before tab close** |
| 16 | `onTabStopped` | Tab stopped |
| 17 | `onFetched` | Fetch complete |
#### 4b. LoadEventMount — Content Interception During Loading
For plugins that normalize URLs, extract data during parsing, or transform content.
```kotlin
override fun configureLoadHandlers(handlers: LoadEventHandlers) {
// Strip tracking parameters from all URLs before loading
handlers.onNormalize.addLast { url ->
url.replace(Regex("\\?utm_.*"), "")
}
// Extract data right after HTML parsing (doc is a Jsoup Document)
handlers.onHTMLDocumentParsed.addLast { page, doc ->
// Parse and extract structured data from doc
}
}
```
**Load event hooks:** `onNormalize` → `onWillLoad` → `onWillFetch` → `onFetched` → `onWillParse` → `onWillParseHTMLDocument` → `onHTMLDocumentParsed` (★ best for data extraction) → `onParsed` → `onLoaded`
#### 4c. CrawlEventMount — URL Pipeline Filtering
For plugins that accept/reject URLs in the crawl pipeline.
```kotlin
override fun configureCrawlHandlers(handlers: CrawlEventHandlers) {
handlers.onWillLoad.addLast { url ->
if (isBlacklisted(url.url)) null else url // null = reject
}
}
```
**Crawl hooks:** `onWillLoad` (return `null` to reject URL) → `onLoaded` (results are available)
#### 4d. ToolMount — LLM Agent Tools
For plugins whose features should be invocable by AI agents. Extend `AbstractToolExecutor` from `browser4-agentic`.
```kotlin
// In the auto-configuration class:
class MyFeatureAutoConfiguration : ToolMount {
override fun getToolExecutors(): List<ToolExecutor> =
listOf(applicationContext.getBean("myFeatureToolExecutor") as ToolExecutor)
}
// In tools/MyFeatureToolExecutor.kt:
open class MyFeatureToolExecutor(
private val service: MyFeatureService,
) : AbstractToolExecutor() {
override val domain = "myfeature"
init {
toolSpec["doSomething"] = ToolSpec(
domain = domain,
method = "doSomething",
arguments = listOf(
ToolSpec.Arg("param1", "String"),
ToolSpec.Arg("param2", "Int?", "0"),
),
returnType = "MyResult",
description = "Does something useful with the current page"
)
}
override suspend fun callFunctionOn(
domain: String,
functionName: String,
args: Map<String, Any?>,
receiver: Any,
): Any? {
val driver = receiver as? WebDriver
return when (functionName) {
"doSomething" -> service.execute(driver, args)
else -> throw IllegalArgumentException("Unknown method: $functionName")
}
}
}
```
#### 4e. PageSnifferMount — Page Category Detection
For plugins that recognize new page types. Implement `PageCategorySniffer` and return it.
#### 4f. Browser4Plugin — Lifecycle Hooks (Optional)
Implement only if you need explicit startup/shutdown callbacks:
```kotlin
open class MyFeaturePlugin(
override val manifest: PluginManifest = PluginManifest(
name = "browser4-myfeature",
version = "4.12.0-rc.1",
description = "My plugin description",
dependsOn = listOf("browser4-protocol", "browser4-agentic"),
autoConfigurationClasses = listOf(
"ai.platon.pulsar.myfeature.config.MyFeatureAutoConfiguration"
)
)
) : Browser4Plugin {
override fun onStartup() { /* initialize resources */ }
override fun onShutdown() { /* release resources */ }
}
```
### Step 5: Implement Services and Config
Separate business logic from mount-point wiring. The auto-configuration class wires dependencies; the service class contains business logic; the config data class holds tunable settings.
```kotlin
// config/MyFeatureConfig.kt
data class MyFeatureConfig(
val enabled: Boolean = true,
val timeoutSeconds: Long = 30,
val endpoint: String = "https://default.example.com",
) {
companion object {
private const val PREFIX = "myfeature."
fun fromConfig(conf: Config): MyFeatureConfig = MyFeatureConfig(
enabled = conf.getBoolean("${PREFIX}enabled", true),
timeoutSeconds = conf.getLong("${PREFIX}timeout.seconds", 30),
endpoint = conf.get("${PREFIX}endpoint", "https://default.example.com"),
)
}
}
// service/MyFeatureService.kt
open class MyFeatureService(
private val config: MyFeatureConfig,
) {
suspend fun process(page: WebPage, driver: WebDriver): Result {
// Business logic — use driver.evaluate() for JavaScript,
// OkHttpClient for HTTP calls, etc.
}
}
```
### Step 6: Create Plugin Manifest and Auto-Configuration Imports
Two mandatory resource files in every plugin JAR:
**`src/main/resources/META-INF/browser4-plugin.json`:**
```json
{
"name": "browser4-myfeature",
"version": "4.12.0-rc.1",
"description": "A Browser4 plugin that provides custom page processing functionality",
"dependsOn": ["browser4-protocol", "browser4-agentic"],
"defaultEnabled": true,
"autoConfigurationClasses": [
"ai.platon.pulsar.myfeature.config.MyFeatureAutoConfiguration"
]
}
```
`defaultEnabled` decides which loading category the plugin belongs to:
- `true` (default) — **default-loaded**: the plugin activates automatically.
- `false` — **opt-in / default-disabled**: the plugin is skipped unless explicitly
enabled with `browser4.plugins.enable=<name>` or `browser4.plugins.enable-all=true`.
**`src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`** (single line):
```
ai.platon.pulsar.myfeature.config.MyFeatureAutoConfiguration
```
### Step 7: Write Tests
Place tests in `src/test/kotlin/` mirroring the source package. Use JUnit 5 + `kotlin-test-junit5` + `spring-boot-test` (all `test` scope).
**Config test pattern:**
```kotlin
@Test
fun `test config defaults`() {
val config = MyFeatureConfig()
assertTrue(config.enabled)
assertEquals(30, config.timeoutSeconds)
}
@Test
fun `test fromConfig reads properties`() {
val conf = Config.of(mapOf("myfeature.enabled" to "false"))
val config = MyFeatureConfig.fromConfig(conf)
assertFalse(config.enabled)
}
```
**Service test pattern — use `java.lang.reflect.Proxy` for lightweight mocks:**
```kotlin
@Suppress("UNCHECKED_CAST")
private fun webPageProxy(): WebPage {
return Proxy.newProxyInstance(
WebPage::class.java.classLoader,
arrayOf(WebPage::class.java)
) { _, _, _ -> null } as WebPage
}
```
**Event handler test pattern — use `runBlocking` for coroutine testing:**
```kotlin
@Test
fun `test browse event handler processes page`() = runBlocking {
val handler = MyFeatureService(mockConfig)
val result = handler.process(webPageProxy(), mockDriver)
assertNotNull(result)
}
```
### Step 8: Build, Verify, and Deploy
```bash
# Build the thin JAR
mvn package -DskipTests
# Verify the JAR structure (optional but recommended)
# bin/verify-plugin.ps1 target/browser4-myfeature-1.0.0-SNAPSHOT.jar
# Deploy — copy to Browser4's plugins/ directory and restart
cp target/browser4-myfeature-1.0.0-SNAPSHOT.jar /path/to/browser4/plugins/
# Or install via REST API
curl -X POST http://localhost:8182/api/plugins/install \
-F "file=@target/browser4-myfeature-1.0.0-SNAPSHOT.jar"
```
After restart, check application logs for:
```
PluginManager: Found X PluginMount bean(s)
PluginManager: ✓ Configured browse event handlers
PluginManager: Found X Browser4Plugin bean(s)
- browser4-myfeature v4.12.0-rc.1
```
---
## Plugin Loading: Default vs Opt-in
Every plugin belongs to one of two loading categories, declared in the manifest:
| Category | `defaultEnabled` | Behavior |
|---|---|---|
| Default-loaded | `true` (default) | Activated automatically at startup, unless explicitly disabled |
| Opt-in (default-disabled) | `false` | **Not** activated at startup, unless explicitly enabled |
The effective decision is made by `PluginLoadPolicy` at two levels:
1. **Classpath** — `PluginClasspathEnhancer` only adds enabled plugins to the
classloader before Spring starts (applies to the standalone `plugins/`
directory; this is the hard gate).
2. **Runtime** — `PluginManager` skips mount wiring / `onStartup` for plugins
whose beans reach the Spring context via the JVM classpath (e.g. the bundle's
`plugins/*` wildcard). Tools, event handlers, and swarm facades of disabled
plugins are not registered.
Explicit overrides (system property, env var, or `application.properties`):
| Property / env var | Effect |
|---|---|
| `browser4.plugins.enable` / `BROWSER4_PLUGINS_ENABLE` | Comma-separated plugin names to force-enable |
| `browser4.plugins.disable` / `BROWSER4_PLUGINS_DISABLE` | Comma-separated plugin names to force-disable |
| `browser4.plugins.enable-all` / `BROWSER4_PLUGINS_ENABLE_ALL` | `true` → activate every plugin unless explicitly disabled |
`disable` always wins over `enable`. Opt-in plugins still ship in the `plugins/`
directory and show up in `plugin list` / `GET /api/plugins` with
`defaultEnabled: false, enabled: false` until enabled.
```bash
# Activate a specific opt-in plugin
browser4-rest --browser4.plugins.enable=browser4-myfeature
# Or enable everything
browser4-rest --browser4.plugins.enable-all=true
```
---
## File Reference
| File | Required | Purpose |
|------|----------|---------|
| `pom.xml` | Yes | Maven build with `browser4-pdk` parent; all Browser4 deps in `provided` scope |
| `src/main/resources/META-INF/browser4-plugin.json` | Yes | Plugin manifest: name, version, description, dependsOn, defaultEnabled, autoConfigurationClasses |
| `src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports` | Yes | Single line: FQN of the `@AutoConfiguration` class |
| `config/<Feature>AutoConfiguration.kt` | Yes | Spring `@AutoConfiguration` — implements `PluginMount` sub-interfaces and defines beans |
| `config/<Feature>Config.kt` | Common | Configuration data class read from Properties/Config |
| `integration/<Feature>BrowseEventHandler.kt` | Common | Browse-phase event handler with business logic for each hook |
| `integration/<Feature>LoadEventHandler.kt` | Optional | Load-phase event handler for URL/parsing hooks |
| `service/<Feature>Service.kt` | Common | Business logic service (injected into event handlers and tool executors) |
| `tools/<Feature>ToolExecutor.kt` | Optional | LLM agent tool extending `AbstractToolExecutor` |
| `<Feature>Plugin.kt` | Optional | `Browser4Plugin` lifecycle (manifest + onStartup/onShutdown) |
| `README.md` | Common | Plugin documentation |
---
## Reference Map
Key source files to read for patterns and examples:
| Resource | Path | What it demonstrates |
|----------|------|---------------------|
| Canonical test plugin | `browser4-pdk/browser4-pdk-test-plugin/` | All three event-phase mount points (BrowseEventMount, LoadEventMount, CrawlEventMount) — the compatibility canary |
| Plugin archetype | `browser4-pdk/browser4-plugin-archetype/src/main/resources/archetype-resources/` | Scaffolded project structure, templates for all required files |
| PluginMount interfaces | `browser4-core/browser4-skeleton/src/main/kotlin/ai/platon/pulsar/skeleton/plugin/MountPoints.kt` | `PluginMount`, `BrowseEventMount`, `LoadEventMount`, `CrawlEventMount` interface definitions |
| Event lifecycle | `browser4-core/browser4-skeleton/src/main/kotlin/ai/platon/pulsar/skeleton/event/PageEvents.kt` | `LoadEventHandlers`, `BrowseEventHandlers`, `CrawlEventHandlers` — all 28 hooks |
| Event handler types | `browser4-core/browser4-skeleton/src/main/kotlin/ai/platon/pulsar/skeleton/event/EventHandlers.kt` | Chainable handler function types (`WebPageWebDriverEventHandler`, etc.) |
| Plugin manifest | `browser4-core/browser4-skeleton/src/main/kotlin/ai/platon/pulsar/skeleton/plugin/PluginManifest.kt` | `PluginManifest` data class schema |
| ToolMount + registry | `browser4-agentic/src/main/kotlin/ai/platon/pulsar/agentic/tools/ToolMount.kt` | `ToolMount` interface + `CustomToolRegistry` singleton |
| ToolExecutor base | `browser4-agentic/src/main/kotlin/ai/platon/pulsar/agentic/tools/builtin/AbstractToolExecutor.kt` | `ToolExecutor` interface and `AbstractToolExecutor` base class |
| PDK parent POM | `browser4-pdk/pom.xml` | Parent POM for plugin projects (standalone — inherits from `pulsar-parent` on Maven Central) |
| Plugin dev docs | `docs/plugin-development.md` | Full plugin development guide with API reference |
| **Most complete reference plugin** | `browser4-plugins/browser4-images/` | Implements BrowseEventMount + ToolMount + Browser4Plugin + Config + Service + BrowseEventHandler + ToolExecutor — all major patterns |
| CAPTCHA plugin | `browser4-plugins/browser4-captcha/` | Reference for PageSnifferMount + multi-tool executor |
| PDK test plugin source | `browser4-pdk/browser4-pdk-test-plugin/src/main/kotlin/ai/platon/pulsar/pdk/testplugin/config/TestPluginAutoConfiguration.kt` | Minimal mount-point wiring for all three event phases |
---
## Error Handling
| Symptom | Likely cause | Fix |
|---------|-------------|-----|
| Plugin not loaded at startup; no "registered plugin" log | `browser4-plugin.json` missing, malformed, or JAR not in the plugins directory | Verify JAR is in the configured plugins directory; verify JSON is valid; verify `autoConfigurationClasses` FQN matches |
| Plugin skipped as "default-disabled (opt-in)" | Manifest has `defaultEnabled: false` and no explicit enable override | Enable with `browser4.plugins.enable=<name>` or `browser4.plugins.enable-all=true`, or set `defaultEnabled: true` |
| `ClassNotFoundException` for Browser4 API classes | Dependency scope is `compile` instead of `provided` | Change all `browser4-*` and Spring Boot deps to `<scope>provided</scope>` |
| Mount point handlers never fire | Auto-configuration class doesn't implement the correct `PluginMount` interface, or `AutoConfiguration.imports` file is missing/wrong | Verify `AutoConfiguration.imports` contains the exact FQN; verify the auto-config class implements the mount interface |
| `BeanCreationException` at startup | A bean dependency is missing or circular | Check bean constructor args; ensure `@Lazy` on the auto-config class |
| Tool executor not available to agents | `getToolExecutors()` returns empty list, or executor not exposed as a Spring bean | Verify the list contains beans from `applicationContext`; verify executor has `@Bean` in auto-config |
| Archetype generation fails | Maven can't resolve `browser4-plugin-archetype` | Build the PDK locally first: `mvn -pl browser4-pdk install` |
| `onDocumentSteady` handler throws silently | Missing try-catch in handler body | Always wrap handler body in try-catch; log errors via a logger |
| Config properties have no effect | Config class not reading from `Config` or property prefix mismatch | Verify `fromConfig()` method reads all keys with correct prefix; verify property names in application config |
| `NoSuchMethodError` at runtime | Plugin compiled against a different Browser4 version than the host | Rebuild with matching `browser4-pdk` version; or reinstall the matching Browser4 version |
| JAR contains embedded dependencies | Fat JAR instead of thin JAR — `spring-boot-maven-plugin` with `repackage` goal | Remove or skip the `repackage` goal; ensure only `maven-jar-plugin` is active |
| `NoClassDefFoundError` for third-party libraries | Third-party dependency not bundled in the plugin JAR | Use `compile` scope for third-party deps (not `provided`) so they are included in the JAR |
---
## Critical Warnings
> **Warning:** All `browser4-*` and Spring Boot dependencies must use `<scope>provided</scope>`. Using `compile` scope causes `ClassNotFoundException` at runtime because the host application provides these classes. The plugin JAR should only bundle your own code and true third-party libraries (use `compile` scope for those).
> **Warning:** Always annotate the auto-configuration class with `@Lazy`. Plugin beans depend on services that may not be available until the application context is fully initialized. Without `@Lazy`, startup may fail with `BeanCreationException`.
> **Warning:** The `browser4-plugin.json` manifest and the `Browser4Plugin.manifest` property must stay in sync. The JSON file is always required for JAR discovery; the `Browser4Plugin` interface is optional. If both are present, the JSON manifest is authoritative for the plugin registry.
> **Warning:** Event handlers run on the browser event loop. Never perform blocking I/O (HTTP calls, file writes) directly in a handler — use coroutines. For `ToolMount` executors, the framework handles coroutine dispatch automatically.
> **Warning:** An uncaught exception in one event handler can break the entire handler chain. Always wrap handler bodies in try-catch and log failures.
> **Note:** For lightweight plugin development without cloning the full Browser4 repo, the `browser4-pdk` parent POM extends `pulsar-parent` from Maven Central. Third-party developers only need the archetype and a Maven installation.
> **Tip:** Build and test incrementally. After scaffolding, immediately run `mvn package` to verify the build works before writing any custom code. Then implement one mount point at a time, rebuilding and testing after each.
> **Tip:** Read the `browser4-pdk-test-plugin` source before writing complex handlers. It demonstrates every mount point and serves as the compatibility canary.
> **Tip:** When using `@ConditionalOnProperty`, prefer `matchIfMissing = true` so the plugin is enabled by default unless explicitly disabled. This matches the convention used by all first-party plugins.
> **Tip:** The `browser4-plugins/browser4-images/` plugin is the best all-around reference — it implements `BrowseEventMount` + `ToolMount` + `Browser4Plugin` with a config data class, service layer, browse event handler, and tool executor.
---
## See Also
- [Plugin Development Guide](../docs/plugin-development.md) — Official plugin development documentation
- [AGENTS.md](../AGENTS.md) — Project architecture, build commands, code style, and testing conventions
- [PDK Test Plugin](browser4-pdk/browser4-pdk-test-plugin/) — Minimal reference plugin implementing all mount points
- [Built-in Plugins](browser4-plugins/) — Five first-party plugin implementations (captcha, images, media, pptx, markdown)