diff --git a/README.md b/README.md
index 6750e409c5..adf1ff999c 100644
--- a/README.md
+++ b/README.md
@@ -2532,6 +2532,9 @@ knowledge service; artifacts do not automatically enable citations. It also appe
in the extension catalog with localized installation-guide metadata.
Reopening a bookmark resolves the conversation's current agent through the host, so
custom-agent conversations retain their original chat entry point, including older bookmarks.
+In **Capabilities → Extensions**, an enabled extension’s detail page links directly to its registered workspace pages. The page also includes usage guidance and keeps package details under **Technical details**.
+Extension packages can provide localized purpose and usage steps in `deerflow-extension.json`, including optional prerequisites. The same detail page supports background extensions without a browser module. All bundled examples include this description; see the [authoring contract](examples/README.md#describe-the-extension).
+
The independent [Agent teams example](examples/deerflow-extension-agent-teams/README.md)
lets full Custom Agents collaborate through native `@` mentions, shared messages and
asynchronous peer requests, with a separate team page and persistent member conversations.
diff --git a/backend/AGENTS.md b/backend/AGENTS.md
index 260ea508ef..8da479ff4a 100644
--- a/backend/AGENTS.md
+++ b/backend/AGENTS.md
@@ -295,9 +295,8 @@ Interaction/HITL rules: [policy](docs/RUN_INTERACTION_POLICY.md).
### Context Summarization
-Automatic conversation summarization when approaching token limits, configured
-under the `summarization` key in `config.yaml` (triggers: tokens, messages, or
-fraction of max input); recent messages are kept while older ones are summarized.
+`summarization` in `config.yaml` triggers on tokens, messages or max-input fraction.
+Keep recent messages; summarize older ones.
- Manual compaction uses `POST /api/threads/{id}/compact`, reuses the same `DeerFlowSummarizationMiddleware`, writes a new checkpoint with updated `messages`/`summary_text`, and bumps only those channel versions. The route uses the shared `reserve_checkpoint_write()` boundary (also used by manual state updates); its short-lived `checkpoint_write` operation shares the durable active-thread uniqueness constraint with run admission, preventing worker-local and cross-worker checkpoint-write races.
- Cache only first-candidate no-ops; never suppress primary retries with fallback results. See [reuse and telemetry](docs/summarization.md#reuse-and-telemetry) for cache and counter contracts.
diff --git a/backend/app/gateway/routers/plugins.py b/backend/app/gateway/routers/plugins.py
index 61994903c3..f7ed1e5c52 100644
--- a/backend/app/gateway/routers/plugins.py
+++ b/backend/app/gateway/routers/plugins.py
@@ -70,6 +70,7 @@ async def list_plugins(request: Request, response: Response):
principal = _principal(request)
response.headers["Cache-Control"] = "private, no-store"
entries = []
+ documentation = dict(request.app.state.extensions.documentation)
for source, plugin in request.app.state.extensions.plugins:
settings = plugin_settings(source, plugin)
module = plugin.frontend
@@ -93,8 +94,36 @@ async def list_plugins(request: Request, response: Response):
"transport": transport,
"settings": {key: settings[key] for key in public},
"backend_actions": [action.name for action in plugin.backend],
+ "model_tools": [tool.name for tool in plugin.tools],
+ "documentation": documentation.get(source) if documentation.get(source, {}).get("id") == plugin.namespace else None,
}
)
+ registered_sources = {source for source, _ in request.app.state.extensions.plugins}
+ known_ids = {entry["namespace"] for entry in entries}
+ for source, page in request.app.state.extensions.documentation:
+ if source in registered_sources or page["id"] in known_ids:
+ continue
+ locales = page["locales"]
+ copy = next((value for tag, value in locales.items() if tag.lower() == "en-us"), None)
+ if copy is None:
+ copy = next((value for tag, value in locales.items() if tag.partition("-")[0] == "en"), next(iter(locales.values())))
+ entries.append(
+ {
+ "namespace": page["id"],
+ "title": copy["title"],
+ "description": copy["description"],
+ "viewer_id": principal.user_id,
+ "module": None,
+ "entry": None,
+ "transport": None,
+ "settings": {"enabled": True},
+ "backend_actions": [],
+ "model_tools": [],
+ "documentation": page,
+ "runtime_only": True,
+ }
+ )
+ known_ids.add(page["id"])
return entries
diff --git a/backend/packages/harness/deerflow/extensions/AGENTS.md b/backend/packages/harness/deerflow/extensions/AGENTS.md
index 79ae3a903e..3857bd910f 100644
--- a/backend/packages/harness/deerflow/extensions/AGENTS.md
+++ b/backend/packages/harness/deerflow/extensions/AGENTS.md
@@ -1,5 +1,14 @@
### Python Extension System (Runtime and Distribution)
+Optional `deerflow-extension.json` is public package documentation, read beside an
+already-resolved entrypoint only after successful registration. Validate and project
+known fields at startup; never inspect private config or import disabled plugins for
+copy. Missing files preserve legacy behavior; invalid files add a safe warning.
+`LoadedExtensions.documentation` carries the snapshot into discovery, including
+registered backend extensions without browser/full-stack contributions. Requirements
+are optional. The package checker and bundled-catalog generator share the validator.
+Tests: `test_extension_documentation.py`; authoring: `examples/README.md`.
+
`registry.knowledge_provider(name, provider)` (extension-api 0.2.7) adds an
operator-installed read-only `deerflow.knowledge.RetrievalProvider`. The host
validates advertised async operations, attributes entries and rolls them back
diff --git a/backend/packages/harness/deerflow/extensions/documentation.py b/backend/packages/harness/deerflow/extensions/documentation.py
new file mode 100644
index 0000000000..46fcb71b34
--- /dev/null
+++ b/backend/packages/harness/deerflow/extensions/documentation.py
@@ -0,0 +1,78 @@
+"""Public, package-owned documentation. Never inspect deployment config."""
+
+from __future__ import annotations
+
+import json
+import re
+import sys
+from pathlib import Path
+from urllib.parse import urlsplit
+
+FILENAME = "deerflow-extension.json"
+MAX_BYTES = 65536
+
+
+def validate_documentation(value: object) -> dict:
+ """Validate v1; requirements and documentation links are optional."""
+
+ def text(value: object) -> bool:
+ return isinstance(value, str) and bool(value.strip()) and len(value) <= 4000
+
+ if not isinstance(value, dict) or type(value.get("schema_version")) is not int or value["schema_version"] != 1:
+ raise ValueError("documentation requires schema_version 1")
+ if not isinstance(value.get("id"), str) or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._/-]{0,127}", value["id"]):
+ raise ValueError("documentation requires a stable id")
+ locales = value.get("locales")
+ if not isinstance(locales, dict) or not locales or len(locales) > 20:
+ raise ValueError("documentation requires at least one locale")
+ result = {"schema_version": 1, "id": value["id"], "locales": {}}
+ for locale, copy in locales.items():
+ if not re.fullmatch(r"[a-z]{2}(?:-[A-Za-z]{2,8})?", locale) or not isinstance(copy, dict):
+ raise ValueError("invalid documentation locale")
+ if not text(copy.get("title")) or not text(copy.get("description")):
+ raise ValueError("documentation title and description are required")
+ steps = copy.get("usage")
+ if not isinstance(steps, list) or not 1 <= len(steps) <= 12:
+ raise ValueError("documentation requires usage steps")
+ if any(not isinstance(step, dict) or not text(step.get("title")) or not text(step.get("description")) for step in steps):
+ raise ValueError("usage steps require a title and description")
+ public = {"title": copy["title"], "description": copy["description"], "usage": [{"title": s["title"], "description": s["description"]} for s in steps]}
+ requirements = copy.get("requirements")
+ if requirements is not None:
+ if not isinstance(requirements, list) or len(requirements) > 12 or any(not text(item) for item in requirements):
+ raise ValueError("requirements must be a list of nonempty descriptions")
+ if requirements:
+ public["requirements"] = requirements
+ result["locales"][locale] = public
+ if "docs_url" in value:
+ url = value["docs_url"]
+ parsed = urlsplit(url) if isinstance(url, str) else None
+ if not parsed or parsed.scheme != "https" or not parsed.hostname or parsed.username or parsed.password:
+ raise ValueError("docs_url must be a public HTTPS URL")
+ # urlsplit defers numeric/range validation until port is accessed.
+ _ = parsed.port
+ result["docs_url"] = url
+ # Validate only the public projection; unknown/private package fields are
+ # deliberately excluded and must not affect discovery.
+ try:
+ json.dumps(result, ensure_ascii=False).encode("utf-8")
+ except UnicodeEncodeError as exc:
+ raise ValueError("documentation must contain valid UTF-8 text") from exc
+ return result
+
+
+def read_documentation(source: str) -> dict | None:
+ """Read beside the already imported entrypoint; never import a disabled plugin."""
+ module = sys.modules.get(source.partition(":")[0])
+ filename = getattr(module, "__file__", None)
+ if not filename:
+ return None
+ path = Path(filename).parent / FILENAME
+ try:
+ with path.open("rb") as stream:
+ raw = stream.read(MAX_BYTES + 1)
+ except FileNotFoundError:
+ return None
+ if len(raw) > MAX_BYTES:
+ raise ValueError("extension documentation exceeds 64 KiB")
+ return validate_documentation(json.loads(raw))
diff --git a/backend/packages/harness/deerflow/extensions/loader.py b/backend/packages/harness/deerflow/extensions/loader.py
index 9586f67ddf..1d7817884e 100644
--- a/backend/packages/harness/deerflow/extensions/loader.py
+++ b/backend/packages/harness/deerflow/extensions/loader.py
@@ -10,12 +10,13 @@ from __future__ import annotations
import logging
from collections.abc import Mapping, Sequence
-from dataclasses import dataclass
+from dataclasses import dataclass, replace
from typing import Any, Literal
from deerflow_extension_api import API_VERSION
from pydantic import BaseModel, ConfigDict, Field, model_validator
+from deerflow.extensions.documentation import read_documentation
from deerflow.extensions.host_access import HostAccess, bind_host_access
from deerflow.extensions.model_access import ModelInvocationScope
from deerflow.extensions.registry import ExtensionRegistry, LoadedExtensions
@@ -169,6 +170,7 @@ def load_extensions(specs: Sequence[ExtensionSpec]) -> tuple[LoadedExtensions, l
registry = ExtensionRegistry()
diagnostics: list[Diagnostic] = []
loaded_sources: list[str] = []
+ documentation: list[tuple[str, dict]] = []
for spec in specs:
if spec.table_prefix:
@@ -263,6 +265,15 @@ def load_extensions(specs: Sequence[ExtensionSpec]) -> tuple[LoadedExtensions, l
continue
loaded_sources.append(spec.use)
+ if registry.mark() != mark:
+ try:
+ page = read_documentation(spec.use)
+ if page is not None:
+ documentation.append((spec.use, page))
+ except (ValueError, OSError, TypeError, RecursionError):
+ # Bad optional copy must not disable a working legacy extension.
+ diagnostics.append(Diagnostic.warning(spec.use, "Extension documentation is invalid."))
+ logger.warning("Extension %s: invalid documentation", spec.use)
# Loading third-party code is exactly the event an operator needs positive
# confirmation of, and every other branch here is failure-only — so without
@@ -277,7 +288,7 @@ def load_extensions(specs: Sequence[ExtensionSpec]) -> tuple[LoadedExtensions, l
# every deployment, and an unconditional line would be pure boot noise.
logger.debug("No extensions configured")
- return registry.build(), diagnostics
+ return replace(registry.build(), documentation=tuple(documentation)), diagnostics
def _frozen_config(config: dict[str, Any]) -> Mapping[str, Any]:
diff --git a/backend/packages/harness/deerflow/extensions/registry.py b/backend/packages/harness/deerflow/extensions/registry.py
index 8ddf7cc0d6..3749c4970d 100644
--- a/backend/packages/harness/deerflow/extensions/registry.py
+++ b/backend/packages/harness/deerflow/extensions/registry.py
@@ -57,6 +57,7 @@ class LoadedExtensions:
routers: tuple[tuple[str, Any], ...] = ()
plugins: tuple[tuple[str, PluginContribution], ...] = ()
knowledge_providers: tuple[tuple[str, tuple[str, Any]], ...] = ()
+ documentation: tuple[tuple[str, dict], ...] = ()
# Precomputed attributes, not methods: hook sites read one attribute to
# short-circuit, so the zero-extension path constructs nothing.
diff --git a/backend/tests/test_extension_check_script.py b/backend/tests/test_extension_check_script.py
index 7291b0101e..0476093192 100644
--- a/backend/tests/test_extension_check_script.py
+++ b/backend/tests/test_extension_check_script.py
@@ -43,6 +43,22 @@ def test_packaged_entry_is_checked_without_importing_plugin(tmp_path):
checker.check_wheel(tmp_path, wheel(tmp_path))
+def test_documentation_must_survive_packaging(tmp_path):
+ package = tmp_path / "src/example"
+ package.mkdir(parents=True)
+ (package / "deerflow-extension.json").write_text("{}")
+ with pytest.raises(ValueError, match="documentation is not packaged"):
+ checker.check_wheel(tmp_path, wheel(tmp_path))
+
+
+def test_packaged_documentation_requires_usage(tmp_path):
+ page = {"schema_version": 1, "id": "example.helper", "locales": {"en-US": {"title": "Helper", "description": "Does useful work."}}}
+ with pytest.raises(ValueError, match="usage"):
+ checker.check_wheel(tmp_path, wheel(tmp_path, files={"example/deerflow-extension.json": json.dumps(page)}))
+ page["locales"]["en-US"]["usage"] = [{"title": "Start a task", "description": "Works automatically."}]
+ checker.check_wheel(tmp_path, wheel(tmp_path, files={"example/deerflow-extension.json": json.dumps(page)}))
+
+
def test_backend_only_wheel_skips_extraction(tmp_path, monkeypatch):
def fail_if_extracted(*args, **kwargs):
pytest.fail("Backend-only wheels must not be extracted")
diff --git a/backend/tests/test_extension_documentation.py b/backend/tests/test_extension_documentation.py
new file mode 100644
index 0000000000..291b3dfa60
--- /dev/null
+++ b/backend/tests/test_extension_documentation.py
@@ -0,0 +1,224 @@
+"""Package-owned copy survives backend-only discovery without importing disabled code."""
+
+import copy
+import json
+import runpy
+from dataclasses import replace
+from pathlib import Path
+from types import SimpleNamespace
+
+import pytest
+from deerflow_extension_api.auth import EXTENSION_PRINCIPAL_RESOLVER_KEY, ExtensionPrincipal
+from deerflow_extension_api.plugins import PluginContribution
+from fastapi import FastAPI
+from fastapi.testclient import TestClient
+
+from app.gateway.routers.plugins import router
+from deerflow.extensions.documentation import FILENAME, validate_documentation
+from deerflow.extensions.loader import ExtensionSpec, load_extensions
+from extension_test_fixtures import demo_extensions
+
+SOURCE = "extension_test_fixtures.demo_extensions:install_ok"
+PAGE = {
+ "schema_version": 1,
+ "id": "example.background",
+ "locales": {"en-US": {"title": "Background helper", "description": "Keeps tool statistics.", "usage": [{"title": "Run a task", "description": "Statistics are collected automatically."}]}},
+}
+
+
+@pytest.fixture
+def package(tmp_path, monkeypatch):
+ monkeypatch.setattr(demo_extensions, "__file__", str(tmp_path / "__init__.py"))
+ path = tmp_path / FILENAME
+ path.write_text(json.dumps(PAGE))
+ return path
+
+
+def test_requirements_are_optional_and_empty_requirements_are_omitted():
+ assert "requirements" not in validate_documentation(PAGE)["locales"]["en-US"]
+ value = copy.deepcopy(PAGE)
+ value["locales"]["en-US"]["requirements"] = []
+ assert "requirements" not in validate_documentation(value)["locales"]["en-US"]
+
+
+@pytest.mark.parametrize("field", ["title", "description", "usage"])
+def test_new_documentation_requires_meaningful_copy(field):
+ value = copy.deepcopy(PAGE)
+ del value["locales"]["en-US"][field]
+ with pytest.raises(ValueError):
+ validate_documentation(value)
+
+
+@pytest.mark.parametrize("url", ["javascript:alert(1)", "http://example.org", "https://secret@example.org", "https://example.org:bad/docs", "https://example.org:65536/docs", "https://["])
+def test_documentation_links_are_public_https(url):
+ with pytest.raises(ValueError):
+ validate_documentation({**PAGE, "docs_url": url})
+
+
+def test_loader_projects_only_public_documentation(package):
+ value = copy.deepcopy(PAGE)
+ value["private_config"] = {"key": "synthetic-secret"}
+ package.write_text(json.dumps(value))
+ loaded, diagnostics = load_extensions([ExtensionSpec(use=SOURCE, config={"token": "private"})])
+ assert diagnostics == []
+ assert loaded.documentation == ((SOURCE, PAGE),)
+ # Reads use the loaded snapshot, not mutable files or config on request threads.
+ package.unlink()
+ app = FastAPI()
+ app.state.extensions = loaded
+ setattr(app.state, EXTENSION_PRINCIPAL_RESOLVER_KEY, lambda request: ExtensionPrincipal("viewer"))
+ app.include_router(router)
+ with TestClient(app) as client:
+ response = client.get("/api/plugins")
+ assert response.status_code == 200
+ assert response.headers["cache-control"] == "private, no-store"
+ (entry,) = response.json()
+ assert entry["namespace"] == PAGE["id"]
+ assert entry["documentation"] == PAGE
+ assert entry["runtime_only"] is True
+ assert entry["module"] is None
+ assert entry["settings"] == {"enabled": True}
+ assert "private" not in response.text
+
+
+@pytest.mark.parametrize(
+ "tags,expected",
+ [
+ (["zh-CN", "en"], "English"),
+ (["zh-CN", "en-us"], "English"),
+ (["zh-CN", "en-GB"], "English"),
+ (["zh-CN", "en", "en-us"], "US English"),
+ (["zh-CN", "fr"], "中文"),
+ ],
+)
+def test_backend_only_discovery_prefers_english_language_variants(package, tags, expected):
+ page = copy.deepcopy(PAGE)
+ page["locales"] = {}
+ titles = {"zh-CN": "中文", "fr": "Français"}
+ if len(tags) == 3:
+ titles["en-us"] = "US English"
+ for tag in tags:
+ localized = copy.deepcopy(PAGE["locales"]["en-US"])
+ localized["title"] = titles.get(tag, "English")
+ page["locales"][tag] = localized
+ package.write_text(json.dumps(page))
+ loaded, diagnostics = load_extensions([ExtensionSpec(use=SOURCE)])
+ assert not diagnostics
+ app = FastAPI()
+ app.state.extensions = loaded
+ setattr(app.state, EXTENSION_PRINCIPAL_RESOLVER_KEY, lambda request: ExtensionPrincipal("viewer"))
+ app.include_router(router)
+ with TestClient(app) as client:
+ response = client.get("/api/plugins")
+ assert response.status_code == 200
+ (entry,) = response.json()
+ assert entry["title"] == expected
+ assert entry["documentation"] == page
+
+
+def test_bad_documentation_does_not_disable_working_extension(package):
+ package.write_text('{"secret":"synthetic-secret"}')
+ loaded, diagnostics = load_extensions([ExtensionSpec(use=SOURCE)])
+ assert loaded.has_middleware_contributors
+ assert loaded.documentation == ()
+ assert [(item.level, item.source, item.message) for item in diagnostics] == [("warning", SOURCE, "Extension documentation is invalid.")]
+
+
+@pytest.mark.parametrize("field", ["id", "locale", "title", "description", "usage_title", "usage_description", "requirements", "docs_url"])
+@pytest.mark.parametrize("surrogate", ["\ud800", "\udfff"])
+def test_unencodable_public_documentation_cannot_break_discovery(package, field, surrogate):
+ page = copy.deepcopy(PAGE)
+ localized = page["locales"]["en-US"]
+ if field == "id":
+ page["id"] = surrogate
+ elif field == "locale":
+ page["locales"] = {surrogate: localized}
+ elif field.startswith("usage_"):
+ localized["usage"][0][field.removeprefix("usage_")] = surrogate
+ elif field == "requirements":
+ localized["requirements"] = [surrogate]
+ elif field == "docs_url":
+ page["docs_url"] = "https://example.org/" + surrogate
+ else:
+ localized[field] = surrogate
+ package.write_text(json.dumps(page))
+ loaded, diagnostics = load_extensions([ExtensionSpec(use=SOURCE)])
+ assert loaded.has_middleware_contributors
+ healthy = PluginContribution(namespace="community.bookmarks", title="Bookmarks", enabled=True)
+ app = FastAPI()
+ app.state.extensions = replace(loaded, plugins=(("healthy:install", healthy),))
+ setattr(app.state, EXTENSION_PRINCIPAL_RESOLVER_KEY, lambda request: ExtensionPrincipal("viewer"))
+ app.include_router(router)
+ with TestClient(app) as client:
+ response = client.get("/api/plugins")
+ assert response.status_code == 200
+ assert [entry["namespace"] for entry in response.json()] == [healthy.namespace]
+ assert loaded.documentation == ()
+ assert [(item.level, item.source, item.message) for item in diagnostics] == [("warning", SOURCE, "Extension documentation is invalid.")]
+
+
+def test_unicode_documentation_and_ignored_private_fields_remain_valid():
+ page = copy.deepcopy(PAGE)
+ page["locales"]["en-US"]["title"] = "指南 🦌"
+ page["docs_url"] = "https://example.org:8443/docs"
+ expected = copy.deepcopy(page)
+ page["private_config"] = {"ignored": "\ud800"}
+ assert validate_documentation(page) == expected
+
+
+def test_missing_documentation_keeps_legacy_behavior(package):
+ package.unlink()
+ loaded, diagnostics = load_extensions([ExtensionSpec(use=SOURCE)])
+ assert loaded.has_middleware_contributors
+ assert not loaded.documentation and not diagnostics
+
+
+def test_disabled_or_failed_install_cannot_publish_documentation(package):
+ for spec in [ExtensionSpec(use=SOURCE, enabled=False), ExtensionSpec(use=SOURCE.replace("install_ok", "install_partial_then_raise"))]:
+ loaded, _ = load_extensions([spec])
+ assert not loaded.documentation
+ assert not loaded.has_middleware_contributors
+
+
+def test_bundled_documentation_snapshot_is_current():
+ root = Path(__file__).resolve().parents[2]
+ script = runpy.run_path(str(root / "scripts/sync_extension_documentation.py"))
+ assert script["DESTINATION"].read_text(encoding="utf-8") == script["render"]()
+ assert len(json.loads(script["render"]())) == 15
+
+
+@pytest.mark.parametrize("collision", [False, True])
+def test_plugin_discovery_fences_documentation_identity(collision):
+ plugin = PluginContribution(namespace="example.background", title="Plugin", description="Plugin copy")
+ page = copy.deepcopy(PAGE)
+ page["id"] = plugin.namespace if collision else "another.plugin"
+ source = "backend.only:install" if collision else SOURCE
+ app = FastAPI()
+ app.state.extensions = SimpleNamespace(plugins=((SOURCE, plugin),), documentation=((source, page),))
+ setattr(app.state, EXTENSION_PRINCIPAL_RESOLVER_KEY, lambda request: ExtensionPrincipal("viewer"))
+ app.include_router(router)
+ with TestClient(app) as client:
+ response = client.get("/api/plugins")
+ assert response.status_code == 200
+ (entry,) = response.json()
+ assert entry["namespace"] == plugin.namespace
+ assert entry["documentation"] is None
+
+
+def test_bundled_generator_rejects_missing_language_and_shared_size_limit(tmp_path, monkeypatch):
+ root = Path(__file__).resolve().parents[2]
+ script = runpy.run_path(str(root / "scripts/sync_extension_documentation.py"))
+ namespace = script["render"].__globals__
+ monkeypatch.setitem(namespace, "ROOT", tmp_path)
+ path = tmp_path / "examples/deerflow-extension-test/package/deerflow-extension.json"
+ path.parent.mkdir(parents=True)
+ path.write_text(json.dumps(PAGE))
+ with pytest.raises(ValueError, match="English and Chinese"):
+ script["render"]()
+ value = copy.deepcopy(PAGE)
+ value["locales"]["zh"] = copy.deepcopy(value["locales"]["en-US"])
+ path.write_text(json.dumps(value))
+ assert "deerflow-extension-test" in script["render"]()
+ monkeypatch.setitem(namespace, "MAX_BYTES", 1)
+ with pytest.raises(ValueError, match="too large"):
+ script["render"]()
diff --git a/docs/full-stack-plugins.md b/docs/full-stack-plugins.md
index f388577c57..57785d4247 100644
--- a/docs/full-stack-plugins.md
+++ b/docs/full-stack-plugins.md
@@ -435,15 +435,20 @@ visiting an unavailable page directly does not mount a plugin.
Inline modules use authenticated fetch plus a released Blob URL. Manifest assets use
native credentialed module scripts, preserving relative imports and resource URLs.
Both honor the backend base and prefixes; transport and cache semantics are documented in
-`docs/full-stack-plugins.md`. Host copy belongs in the typed locale dictionaries.
+`docs/full-stack-plugins.md`. Host UI labels belong in the typed locale dictionaries;
+package purpose and usage come from `deerflow-extension.json` (see the
+[authoring contract](../examples/README.md#describe-the-extension)). Installed copy
+takes precedence over the generated bundled snapshot; omit empty prerequisites.
Conversation action factories, shapes and availability callbacks are guarded per plugin;
only validated value snapshots reach the toolbar/sidebar render paths.
`PluginNavigation` and the dynamic workspace extension route consume page declarations;
Capability Center defaults to the repository examples in `core/extensions/catalog.ts`,
merged by explicit namespace with runtime descriptors. Catalog-only entries are discovery
-metadata, never module-loader inputs or proof of installation. Backend-only examples may
-have no plugin descriptor; keep their runtime status unasserted. Details link to package
-installation instructions. Keep the catalog aligned with `examples/deerflow-extension-*`.
+metadata, never module-loader inputs or proof of installation. Documented backend
+extensions with successful registrations have runtime-only descriptors without browser
+modules. Other catalog-only entries retain an unknown runtime status. Details show usage
+steps and optional documentation links; launch links require actual enabled page
+registrations through `pluginPages`. Keep the catalog aligned with `examples/deerflow-extension-*`.
Agent teams uses `community.agent-teams`; merge its installed descriptor into the
localized catalog entry without asserting runtime status for a catalog-only row.
Conversation action slots augment normal/custom-agent toolbars and sidebar menus without replacing native export or notification.
diff --git a/examples/README.md b/examples/README.md
index 22d3e45296..3dbd7ca928 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -14,6 +14,47 @@ Additional examples cover [text-list classification](deerflow-extension-jev-clas
[advisory screening of fetched content](deerflow-extension-jev-screening/README.md), and
[sectioned long-form report writing](deerflow-extension-report-sections/README.md).
+## Describe the extension
+
+Package `deerflow-extension.json` beside the entrypoint's Python module (beside
+`__init__.py` for a package). Capability Center renders this public description
+without requiring a browser module. Use the full-stack plugin namespace as `id`,
+or a unique stable ID for a backend-only extension. A v1 document requires an ID,
+at least one locale, and a nonempty **title**, **description** (purpose) and
+**usage** list in every supplied locale. `requirements` and `docs_url` are optional;
+omit requirements when none apply. Keep secrets and deployment configuration out
+of this file. Names, purposes and instructions are authored by the package;
+loaded/enabled state and registered tools come from the host.
+
+```json
+{
+ "schema_version": 1,
+ "id": "company.helper",
+ "locales": {
+ "en-US": {
+ "title": "Task statistics",
+ "description": "Collect tool-call statistics during tasks.",
+ "usage": [
+ {"title": "Run a task", "description": "Statistics are collected automatically after activation."}
+ ]
+ }
+ }
+}
+```
+
+The packaging checker validates this file when present and detects omitted wheel
+resources. Older packages without it remain compatible. Invalid optional copy
+produces a startup diagnostic while the working extension remains available.
+Gateway reads copy at startup only, after successful registration. Disabled and
+failed packages are not imported for documentation; a catalog entry alone does
+not prove activation. A documented backend extension with actual registrations
+appears in discovery even if it supplies no full-stack plugin.
+
+For bundled examples, update their package files and run
+`python scripts/sync_extension_documentation.py` from the repository root. The
+generated frontend snapshot supplies descriptions before installation; installed
+package copy takes precedence. Backend tests check that the snapshot is current.
+
## Edit, test, check, then install
1. Copy the selected example outside the DeerFlow checkout. Rename its Python
diff --git a/examples/deerflow-extension-agent-teams/deerflow_extension_agent_teams/deerflow-extension.json b/examples/deerflow-extension-agent-teams/deerflow_extension_agent_teams/deerflow-extension.json
new file mode 100644
index 0000000000..facdc21e51
--- /dev/null
+++ b/examples/deerflow-extension-agent-teams/deerflow_extension_agent_teams/deerflow-extension.json
@@ -0,0 +1,49 @@
+{
+ "schema_version": 1,
+ "id": "community.agent-teams",
+ "locales": {
+ "zh-CN": {
+ "title": "Agent 团队",
+ "description": "让多个 Agent 围绕同一个目标协作,共享进展,交接任务。",
+ "usage": [
+ {
+ "title": "组建你的团队",
+ "description": "选择 2–8 个已有的自定义 Agent,给团队命名,并设定共同目标。"
+ },
+ {
+ "title": "用 @ 分配任务",
+ "description": "在团队对话中输入 @ 并选择成员,再发送任务;成员会在各自的会话中处理。"
+ },
+ {
+ "title": "查看进展与成果",
+ "description": "在活动记录中查看成员回复和交接;展开任务可打开成员会话、处理提问或下载成果。"
+ }
+ ],
+ "requirements": [
+ "至少准备 2 个自定义 Agent;如限制了工具分组,需要允许 extensions。"
+ ]
+ },
+ "en-US": {
+ "title": "Agent teams",
+ "description": "Bring Agents together around a shared goal, share progress and hand off tasks.",
+ "usage": [
+ {
+ "title": "Build your team",
+ "description": "Choose 2–8 existing Custom Agents, name your team and set a shared goal."
+ },
+ {
+ "title": "Delegate with @mentions",
+ "description": "Type @ and select members in the team composer, then send a task to their own conversations."
+ },
+ {
+ "title": "Review progress and results",
+ "description": "Read replies and handoffs in the activity feed. Expand a task to open its conversation, answer questions or download results."
+ }
+ ],
+ "requirements": [
+ "Prepare at least two Custom Agents; allow the extensions tool group if using a tool-group allowlist."
+ ]
+ }
+ },
+ "docs_url": "https://github.com/bytedance/deer-flow/tree/main/examples/deerflow-extension-agent-teams#readme"
+}
diff --git a/examples/deerflow-extension-bookmarks/deerflow_extension_bookmarks/deerflow-extension.json b/examples/deerflow-extension-bookmarks/deerflow_extension_bookmarks/deerflow-extension.json
new file mode 100644
index 0000000000..97f62dd345
--- /dev/null
+++ b/examples/deerflow-extension-bookmarks/deerflow_extension_bookmarks/deerflow-extension.json
@@ -0,0 +1,35 @@
+{
+ "schema_version": 1,
+ "id": "community.bookmarks",
+ "locales": {
+ "zh-CN": {
+ "title": "会话书签",
+ "description": "收藏有用的回答,在独立页面查找与整理自己的书签。",
+ "usage": [
+ {
+ "title": "收藏有用的回答",
+ "description": "在已有回答的会话菜单中,选择“书签 → 保存最后一条回答”。"
+ },
+ {
+ "title": "查找与整理",
+ "description": "打开“我的书签”,按文字搜索、修改名称,或回到原会话查看上下文。"
+ }
+ ]
+ },
+ "en-US": {
+ "title": "Bookmarks",
+ "description": "Save useful answers and find and organize your own bookmarks on a dedicated page.",
+ "usage": [
+ {
+ "title": "Save a useful answer",
+ "description": "In a conversation with an answer, choose Bookmarks → Save last answer from its menu."
+ },
+ {
+ "title": "Find and organize",
+ "description": "Open My bookmarks to search saved text, rename entries or return to their source conversations."
+ }
+ ]
+ }
+ },
+ "docs_url": "https://github.com/bytedance/deer-flow/tree/main/examples/deerflow-extension-bookmarks#readme"
+}
diff --git a/examples/deerflow-extension-example/deerflow_extension_example/deerflow-extension.json b/examples/deerflow-extension-example/deerflow_extension_example/deerflow-extension.json
new file mode 100644
index 0000000000..1cc477e41d
--- /dev/null
+++ b/examples/deerflow-extension-example/deerflow_extension_example/deerflow-extension.json
@@ -0,0 +1,35 @@
+{
+ "schema_version": 1,
+ "id": "catalog/example",
+ "locales": {
+ "zh-CN": {
+ "title": "扩展开发示例",
+ "description": "演示中间件、任务生命周期、模型观察器、服务和 HTTP 路由的扩展方式。",
+ "usage": [
+ {
+ "title": "安装到开发环境",
+ "description": "参考包内文档安装示例,配置并重启 Gateway。它用于演示扩展开发接口。"
+ },
+ {
+ "title": "运行任务并检查统计",
+ "description": "执行包含工具调用的任务,再通过示例统计 API 查看调用次数、结果类型与耗时。"
+ }
+ ]
+ },
+ "en-US": {
+ "title": "Extension development example",
+ "description": "Explore middleware, task lifecycle, model observers, services and HTTP route contributions.",
+ "usage": [
+ {
+ "title": "Install in a development environment",
+ "description": "Follow the package guide to install, configure and restart Gateway. This package demonstrates extension development contracts."
+ },
+ {
+ "title": "Run a task and inspect statistics",
+ "description": "Run a task that uses tools, then inspect call counts, outcome types and durations through the example statistics API."
+ }
+ ]
+ }
+ },
+ "docs_url": "https://github.com/bytedance/deer-flow/tree/main/examples/deerflow-extension-example#readme"
+}
diff --git a/examples/deerflow-extension-jev-classify/deerflow_extension_jev_classify/deerflow-extension.json b/examples/deerflow-extension-jev-classify/deerflow_extension_jev_classify/deerflow-extension.json
new file mode 100644
index 0000000000..30e5ac1236
--- /dev/null
+++ b/examples/deerflow-extension-jev-classify/deerflow_extension_jev_classify/deerflow-extension.json
@@ -0,0 +1,41 @@
+{
+ "schema_version": 1,
+ "id": "community.jev-classify",
+ "locales": {
+ "zh-CN": {
+ "title": "文本分类",
+ "description": "按给定类别为文本列表打标签,使用部署方配置的分类服务。",
+ "usage": [
+ {
+ "title": "提供文本和分类标准",
+ "description": "在对话中提供待分类的文本列表,说明各类别的含义和边界;也可上传表格并指出文本列。"
+ },
+ {
+ "title": "让 Agent 执行分类",
+ "description": "要求使用文本分类工具,检查返回标签;如需表格,让 Agent 将标签写回输出文件。"
+ }
+ ],
+ "requirements": [
+ "管理员已配置 Jev 或兼容聊天接口的分类服务;待分类文本会发送给该服务。"
+ ]
+ },
+ "en-US": {
+ "title": "Text classification",
+ "description": "Label a list of texts with supplied categories using the deployment-configured classification service.",
+ "usage": [
+ {
+ "title": "Provide texts and categories",
+ "description": "Give the Agent a list of texts and explain the categories, or upload a table and identify its text column."
+ },
+ {
+ "title": "Ask the Agent to classify",
+ "description": "Ask it to use the text classification tool and inspect the labels. For a table, ask it to write labels into an output file."
+ }
+ ],
+ "requirements": [
+ "An administrator must configure Jev or a compatible chat endpoint. Texts to classify are sent to that service."
+ ]
+ }
+ },
+ "docs_url": "https://github.com/bytedance/deer-flow/tree/main/examples/deerflow-extension-jev-classify#readme"
+}
diff --git a/examples/deerflow-extension-jev-context/deerflow_extension_jev_context/deerflow-extension.json b/examples/deerflow-extension-jev-context/deerflow_extension_jev_context/deerflow-extension.json
new file mode 100644
index 0000000000..5f869f8297
--- /dev/null
+++ b/examples/deerflow-extension-jev-context/deerflow_extension_jev_context/deerflow-extension.json
@@ -0,0 +1,41 @@
+{
+ "schema_version": 1,
+ "id": "community.jev-context",
+ "locales": {
+ "zh-CN": {
+ "title": "上下文裁剪",
+ "description": "按需缩短旧的只读工具结果,保留近期消息与原生摘要。",
+ "usage": [
+ {
+ "title": "正常进行对话",
+ "description": "启用后自动检查长对话中的旧工具输出,不需要打开独立页面或输入额外命令。"
+ },
+ {
+ "title": "达到阈值时自动处理",
+ "description": "满足配置的上下文阈值和调用间隔后,裁剪已过时的只读工具结果,保留近期消息和受保护内容。"
+ }
+ ],
+ "requirements": [
+ "管理员已配置 TypeSafe Jev 服务凭据及触发阈值;处理时会向该服务发送限定范围的对话内容。"
+ ]
+ },
+ "en-US": {
+ "title": "Context pruning",
+ "description": "Shorten older read-only tool results while preserving recent messages and native summaries.",
+ "usage": [
+ {
+ "title": "Continue your conversation",
+ "description": "Once enabled, it automatically checks old tool output in long conversations. No separate page or command is needed."
+ },
+ {
+ "title": "Prune when the threshold is reached",
+ "description": "After the configured token threshold and call interval, obsolete read-only results are shortened while recent and protected content is retained."
+ }
+ ],
+ "requirements": [
+ "An administrator must configure TypeSafe Jev credentials and thresholds. Processing sends bounded conversation content to that service."
+ ]
+ }
+ },
+ "docs_url": "https://github.com/bytedance/deer-flow/tree/main/examples/deerflow-extension-jev-context#readme"
+}
diff --git a/examples/deerflow-extension-jev-screening/deerflow_extension_jev_screening/deerflow-extension.json b/examples/deerflow-extension-jev-screening/deerflow_extension_jev_screening/deerflow-extension.json
new file mode 100644
index 0000000000..86eb1427a1
--- /dev/null
+++ b/examples/deerflow-extension-jev-screening/deerflow_extension_jev_screening/deerflow-extension.json
@@ -0,0 +1,41 @@
+{
+ "schema_version": 1,
+ "id": "catalog/screening",
+ "locales": {
+ "zh-CN": {
+ "title": "内容风险提示",
+ "description": "检查获取的外部内容,为可疑工具结果添加提示,不阻断工具执行。",
+ "usage": [
+ {
+ "title": "按平常方式搜索与读取",
+ "description": "启用后,在 Agent 获取网页、搜索或 MCP 工具结果时自动检查部分内容。"
+ },
+ {
+ "title": "结合提示判断内容",
+ "description": "检测到可疑指令时,会在工具结果中增加风险提示。它提供提醒,不阻断工具,也不保证识别所有风险。"
+ }
+ ],
+ "requirements": [
+ "管理员已配置 TypeSafe Jev 服务;待检查的内容片段会发送给该服务。"
+ ]
+ },
+ "en-US": {
+ "title": "Content risk warnings",
+ "description": "Screen fetched content and add advisory warnings to suspicious tool results without blocking tools.",
+ "usage": [
+ {
+ "title": "Search and read as usual",
+ "description": "Once enabled, the middleware checks bounded excerpts of fetched pages, search results and MCP tool output."
+ },
+ {
+ "title": "Review advisory warnings",
+ "description": "Suspicious instructions add a warning to the tool result. Screening is advisory; it does not block tools or guarantee detection."
+ }
+ ],
+ "requirements": [
+ "An administrator must configure TypeSafe Jev. Content excerpts are sent to that service for screening."
+ ]
+ }
+ },
+ "docs_url": "https://github.com/bytedance/deer-flow/tree/main/examples/deerflow-extension-jev-screening#readme"
+}
diff --git a/examples/deerflow-extension-knowledge-center/deerflow_extension_knowledge_center/deerflow-extension.json b/examples/deerflow-extension-knowledge-center/deerflow_extension_knowledge_center/deerflow-extension.json
new file mode 100644
index 0000000000..d45f33a23b
--- /dev/null
+++ b/examples/deerflow-extension-knowledge-center/deerflow_extension_knowledge_center/deerflow-extension.json
@@ -0,0 +1,49 @@
+{
+ "schema_version": 1,
+ "id": "knowledge-center",
+ "locales": {
+ "zh-CN": {
+ "title": "知识中心",
+ "description": "在独立页面浏览知识空间、导入资料,并查看文档处理状态。",
+ "usage": [
+ {
+ "title": "选择空间与知识库",
+ "description": "打开知识中心,选择你有权限访问的空间及其中的知识库。"
+ },
+ {
+ "title": "导入资料",
+ "description": "按页面提供的操作导入文件、网址或文本。可用按钮取决于你的权限和服务支持。"
+ },
+ {
+ "title": "查看处理状态",
+ "description": "检查返回的文档记录或按文档标识查询状态,必要时手动刷新。"
+ }
+ ],
+ "requirements": [
+ "已安装并启用知识空间与连接、知识入库与处理扩展,并为你分配可访问的空间和资源。"
+ ]
+ },
+ "en-US": {
+ "title": "Knowledge center",
+ "description": "Browse knowledge spaces, import content and inspect document processing on a dedicated page.",
+ "usage": [
+ {
+ "title": "Choose a space and knowledge base",
+ "description": "Open Knowledge center and select an accessible space and mapped knowledge base."
+ },
+ {
+ "title": "Import content",
+ "description": "Use the available controls to import a file, URL or text. Controls depend on your permissions and provider support."
+ },
+ {
+ "title": "Inspect processing",
+ "description": "Inspect the returned document record or look up a document by ID, refreshing its status when needed."
+ }
+ ],
+ "requirements": [
+ "Enable the knowledge platform and management extensions and grant you access to spaces and resources."
+ ]
+ }
+ },
+ "docs_url": "https://github.com/bytedance/deer-flow/tree/main/examples/deerflow-extension-knowledge-center#readme"
+}
diff --git a/examples/deerflow-extension-knowledge-management/deerflow_extension_knowledge_management/deerflow-extension.json b/examples/deerflow-extension-knowledge-management/deerflow_extension_knowledge_management/deerflow-extension.json
new file mode 100644
index 0000000000..0d82f5e9ab
--- /dev/null
+++ b/examples/deerflow-extension-knowledge-management/deerflow_extension_knowledge_management/deerflow-extension.json
@@ -0,0 +1,41 @@
+{
+ "schema_version": 1,
+ "id": "knowledge-management",
+ "locales": {
+ "zh-CN": {
+ "title": "知识入库与处理",
+ "description": "导入文件、网址和文本,并查询、管理文档处理任务。",
+ "usage": [
+ {
+ "title": "连接已有管理入口",
+ "description": "由管理员通过知识中心或管理 API,选择已授权的知识库与操作。此扩展本身提供后台接口。"
+ },
+ {
+ "title": "导入并查询处理结果",
+ "description": "提交文件、网址或文本,保留返回的文档标识,再查询处理状态;可用操作由上游能力和权限共同决定。"
+ }
+ ],
+ "requirements": [
+ "管理员已配置 WeKnora 管理连接及明确的用户、知识库和操作授权。"
+ ]
+ },
+ "en-US": {
+ "title": "Knowledge ingestion and processing",
+ "description": "Import files, URLs and text, then inspect and manage document processing.",
+ "usage": [
+ {
+ "title": "Use an existing management entry",
+ "description": "Use Knowledge center or the management API to select an authorized knowledge base and operation. This extension provides backend endpoints."
+ },
+ {
+ "title": "Ingest and inspect processing",
+ "description": "Submit a file, URL or text, retain the returned document ID and query its status. Available operations depend on upstream capabilities and permissions."
+ }
+ ],
+ "requirements": [
+ "The administrator must configure a WeKnora management connection and explicit user, knowledge-base and operation grants."
+ ]
+ }
+ },
+ "docs_url": "https://github.com/bytedance/deer-flow/tree/main/examples/deerflow-extension-knowledge-management#readme"
+}
diff --git a/examples/deerflow-extension-knowledge-platform/deerflow_extension_knowledge_platform/deerflow-extension.json b/examples/deerflow-extension-knowledge-platform/deerflow_extension_knowledge_platform/deerflow-extension.json
new file mode 100644
index 0000000000..7a6625e5e8
--- /dev/null
+++ b/examples/deerflow-extension-knowledge-platform/deerflow_extension_knowledge_platform/deerflow-extension.json
@@ -0,0 +1,41 @@
+{
+ "schema_version": 1,
+ "id": "knowledge-platform",
+ "locales": {
+ "zh-CN": {
+ "title": "知识空间与连接",
+ "description": "组织知识空间、连接与成员权限,让团队按授权范围使用资料。",
+ "usage": [
+ {
+ "title": "由管理员配置空间与资源",
+ "description": "通过平台管理接口配置连接、空间成员和可访问资源,明确操作权限。"
+ },
+ {
+ "title": "在授权范围内使用",
+ "description": "通过知识中心或平台接口选择可访问的空间和知识库,再进行导入与状态查询。"
+ }
+ ],
+ "requirements": [
+ "已安装知识入库与处理扩展,并配置连接、持久化存储和空间策略。"
+ ]
+ },
+ "en-US": {
+ "title": "Knowledge spaces and connections",
+ "description": "Organize knowledge spaces, connections and membership for authorized access.",
+ "usage": [
+ {
+ "title": "Configure spaces and resources",
+ "description": "An administrator configures connections, space membership, accessible resources and permitted operations through the platform API."
+ },
+ {
+ "title": "Work within an authorized space",
+ "description": "Use Knowledge center or platform APIs to select an accessible space and knowledge base, then ingest content or inspect processing."
+ }
+ ],
+ "requirements": [
+ "Install the knowledge management extension and configure connections, persistent storage and space policies."
+ ]
+ }
+ },
+ "docs_url": "https://github.com/bytedance/deer-flow/tree/main/examples/deerflow-extension-knowledge-platform#readme"
+}
diff --git a/examples/deerflow-extension-knowledge/deerflow_extension_knowledge/deerflow-extension.json b/examples/deerflow-extension-knowledge/deerflow_extension_knowledge/deerflow-extension.json
new file mode 100644
index 0000000000..1c7f69f312
--- /dev/null
+++ b/examples/deerflow-extension-knowledge/deerflow_extension_knowledge/deerflow-extension.json
@@ -0,0 +1,41 @@
+{
+ "schema_version": 1,
+ "id": "catalog/knowledge",
+ "locales": {
+ "zh-CN": {
+ "title": "知识范围选择示例",
+ "description": "用示例文档演示知识库和文件范围选择、检索及来源引用。",
+ "usage": [
+ {
+ "title": "选择示例知识范围",
+ "description": "在对话的知识范围选择器中选择 Synthetic guides,可进一步选择 Synthetic knowledge note。"
+ },
+ {
+ "title": "提问并查看引用",
+ "description": "让 Agent 搜索 Cedar,检查回答中的来源引用。此插件使用公开的虚构演示文档。"
+ }
+ ],
+ "requirements": [
+ "管理员已启用知识检索工具和范围选择,并将工具连接到此示例 Provider。"
+ ]
+ },
+ "en-US": {
+ "title": "Knowledge scope example",
+ "description": "Search sample documents within selected knowledge bases or files and inspect source citations.",
+ "usage": [
+ {
+ "title": "Select the example scope",
+ "description": "Choose Synthetic guides in the chat scope selector, optionally narrowing it to Synthetic knowledge note."
+ },
+ {
+ "title": "Ask and inspect citations",
+ "description": "Ask the Agent to search for Cedar and inspect its source citations. This example uses a public synthetic document."
+ }
+ ],
+ "requirements": [
+ "The administrator must enable knowledge retrieval and scope selection and connect the tool to this example provider."
+ ]
+ }
+ },
+ "docs_url": "https://github.com/bytedance/deer-flow/tree/main/examples/deerflow-extension-knowledge#readme"
+}
diff --git a/examples/deerflow-extension-lightrag/deerflow_extension_lightrag/deerflow-extension.json b/examples/deerflow-extension-lightrag/deerflow_extension_lightrag/deerflow-extension.json
new file mode 100644
index 0000000000..ccd7faca96
--- /dev/null
+++ b/examples/deerflow-extension-lightrag/deerflow_extension_lightrag/deerflow-extension.json
@@ -0,0 +1,41 @@
+{
+ "schema_version": 1,
+ "id": "catalog/lightrag",
+ "locales": {
+ "zh-CN": {
+ "title": "LightRAG",
+ "description": "从 LightRAG 检索文档与实体关系,并使用统一的来源引用。",
+ "usage": [
+ {
+ "title": "在对话中提出检索问题",
+ "description": "让 Agent 查询已连接知识库中的文档、实体或实体间关系。"
+ },
+ {
+ "title": "查看来源依据",
+ "description": "检查结果的文档引用;实体关系只展示服务返回的边,有文档片段支持时才提供对应引用。"
+ }
+ ],
+ "requirements": [
+ "管理员已配置 LightRAG 连接、凭据、用户绑定和相关检索工具。"
+ ]
+ },
+ "en-US": {
+ "title": "LightRAG",
+ "description": "Retrieve documents and entity relations from LightRAG with shared source citations.",
+ "usage": [
+ {
+ "title": "Ask a retrieval question",
+ "description": "Ask the Agent to search the connected knowledge base for documents, entities or relationships."
+ },
+ {
+ "title": "Inspect supporting sources",
+ "description": "Check document citations. Relations reflect returned edges and receive document citations only when supporting excerpts are available."
+ }
+ ],
+ "requirements": [
+ "The administrator must configure the LightRAG connection, credentials, user bindings and retrieval tools."
+ ]
+ }
+ },
+ "docs_url": "https://github.com/bytedance/deer-flow/tree/main/examples/deerflow-extension-lightrag#readme"
+}
diff --git a/examples/deerflow-extension-report-sections/deerflow_extension_report_sections/deerflow-extension.json b/examples/deerflow-extension-report-sections/deerflow_extension_report_sections/deerflow-extension.json
new file mode 100644
index 0000000000..a78e442b90
--- /dev/null
+++ b/examples/deerflow-extension-report-sections/deerflow_extension_report_sections/deerflow-extension.json
@@ -0,0 +1,43 @@
+{
+ "schema_version": 1,
+ "id": "community.report-sections",
+ "locales": {
+ "zh-CN": {
+ "title": "分段长文写作",
+ "description": "逐段撰写长报告,并将各章节汇总为可下载的 Markdown 文档。",
+ "usage": [
+ {
+ "title": "在对话里提出写作任务",
+ "description": "告诉 Agent 报告主题、目标读者和章节要求,并要求使用分段长文写作工具。"
+ },
+ {
+ "title": "先定大纲,再逐章完成",
+ "description": "让 Agent 先保存大纲,再逐章撰写和保存。需要修改时,在同一对话中指出要修改的章节。"
+ },
+ {
+ "title": "汇总并下载",
+ "description": "全部章节完成后,让 Agent 合并报告;在对话的文件成果中下载 Markdown 文档。"
+ }
+ ]
+ },
+ "en-US": {
+ "title": "Sectioned long-form writing",
+ "description": "Write long reports section by section and assemble them into a downloadable Markdown document.",
+ "usage": [
+ {
+ "title": "Describe the report in chat",
+ "description": "Tell the Agent the topic, audience and chapter requirements, and ask it to use the sectioned writing tool."
+ },
+ {
+ "title": "Outline, then write each chapter",
+ "description": "Ask the Agent to save an outline, then write and save chapters one at a time. Request chapter revisions in the same conversation."
+ },
+ {
+ "title": "Assemble and download",
+ "description": "Ask the Agent to assemble the finished chapters, then download the Markdown document from the conversation artifacts."
+ }
+ ]
+ }
+ },
+ "docs_url": "https://github.com/bytedance/deer-flow/tree/main/examples/deerflow-extension-report-sections#readme"
+}
diff --git a/examples/deerflow-extension-retrieval/deerflow_extension_retrieval/deerflow-extension.json b/examples/deerflow-extension-retrieval/deerflow_extension_retrieval/deerflow-extension.json
new file mode 100644
index 0000000000..2427c318c9
--- /dev/null
+++ b/examples/deerflow-extension-retrieval/deerflow_extension_retrieval/deerflow-extension.json
@@ -0,0 +1,35 @@
+{
+ "schema_version": 1,
+ "id": "example.retrieval",
+ "locales": {
+ "zh-CN": {
+ "title": "本地检索示例",
+ "description": "使用固定示例文档演示只读检索与结构化工具结果,不连接外部知识库。",
+ "usage": [
+ {
+ "title": "在对话里试一次检索",
+ "description": "让 Agent 使用本地检索示例搜索 release,查看匹配到的固定演示文档。"
+ },
+ {
+ "title": "检查返回结果",
+ "description": "观察文字摘要与结构化工具成果;示例数据是虚构的,不代表真实知识库内容。"
+ }
+ ]
+ },
+ "en-US": {
+ "title": "Local retrieval example",
+ "description": "Demonstrate read-only retrieval and structured tool results using fixed sample documents, without an external knowledge service.",
+ "usage": [
+ {
+ "title": "Try a search in chat",
+ "description": "Ask the Agent to use the local retrieval example to search for release and inspect the matching demonstration document."
+ },
+ {
+ "title": "Inspect the result",
+ "description": "Review its text summary and structured tool artifact. The fixed corpus is synthetic, not a real knowledge base."
+ }
+ ]
+ }
+ },
+ "docs_url": "https://github.com/bytedance/deer-flow/tree/main/examples/deerflow-extension-retrieval#readme"
+}
diff --git a/examples/deerflow-extension-weknora/deerflow_extension_weknora/deerflow-extension.json b/examples/deerflow-extension-weknora/deerflow_extension_weknora/deerflow-extension.json
new file mode 100644
index 0000000000..5ac307dbc9
--- /dev/null
+++ b/examples/deerflow-extension-weknora/deerflow_extension_weknora/deerflow-extension.json
@@ -0,0 +1,41 @@
+{
+ "schema_version": 1,
+ "id": "catalog/weknora",
+ "locales": {
+ "zh-CN": {
+ "title": "WeKnora 知识检索",
+ "description": "检索 WeKnora 知识库和所选文档,并展示来源引用。",
+ "usage": [
+ {
+ "title": "选择检索范围",
+ "description": "在对话中选择已授权的 WeKnora 知识库,按需缩小到具体文档。"
+ },
+ {
+ "title": "提问并核对来源",
+ "description": "提出与已入库资料相关的问题,查看返回片段和来源引用。"
+ }
+ ],
+ "requirements": [
+ "已有完成索引的 WeKnora 知识库;管理员已配置连接、凭据、用户授权和检索工具。"
+ ]
+ },
+ "en-US": {
+ "title": "WeKnora knowledge retrieval",
+ "description": "Search WeKnora knowledge bases and selected documents with source citations.",
+ "usage": [
+ {
+ "title": "Select a search scope",
+ "description": "Choose an authorized WeKnora knowledge base in chat and narrow it to specific documents when needed."
+ },
+ {
+ "title": "Ask and verify sources",
+ "description": "Ask a question about indexed content and inspect the retrieved excerpts and source citations."
+ }
+ ],
+ "requirements": [
+ "An indexed WeKnora knowledge base is required, with an administrator-configured connection, credentials, user grants and retrieval tool."
+ ]
+ }
+ },
+ "docs_url": "https://github.com/bytedance/deer-flow/tree/main/examples/deerflow-extension-weknora#readme"
+}
diff --git a/examples/deerflow-extension-workflows/deerflow_extension_workflows/deerflow-extension.json b/examples/deerflow-extension-workflows/deerflow_extension_workflows/deerflow-extension.json
new file mode 100644
index 0000000000..31d1e3bbb6
--- /dev/null
+++ b/examples/deerflow-extension-workflows/deerflow_extension_workflows/deerflow-extension.json
@@ -0,0 +1,49 @@
+{
+ "schema_version": 1,
+ "id": "community.workflows",
+ "locales": {
+ "zh-CN": {
+ "title": "脚本工作流",
+ "description": "运行可检查的 Agent 脚本工作流,跟踪步骤进度,并按保留版本确认成果。",
+ "usage": [
+ {
+ "title": "打开脚本工作流",
+ "description": "进入工作流页面,准备一个受支持的 JavaScript 工作流及其输入参数。"
+ },
+ {
+ "title": "提交并查看执行过程",
+ "description": "启动后查看步骤进度、Agent 子会话和执行结果;需要人工确认的步骤会等待你的处理。"
+ },
+ {
+ "title": "按保留版本确认成果",
+ "description": "检查实际保留的输出版本,再确认或继续工作流。重启恢复取决于工作流和部署是否支持。"
+ }
+ ],
+ "requirements": [
+ "管理员已配置可调用的 Agent 和持久化存储;Gateway 运行在 Linux 或 macOS。"
+ ]
+ },
+ "en-US": {
+ "title": "Script workflows",
+ "description": "Run inspectable Agent workflows, track steps and review retained output versions.",
+ "usage": [
+ {
+ "title": "Open Script workflows",
+ "description": "Open the workflow page and prepare a supported JavaScript workflow and its input arguments."
+ },
+ {
+ "title": "Run and inspect",
+ "description": "Start the workflow and inspect steps, Agent conversations and results. Human checkpoints wait for your response."
+ },
+ {
+ "title": "Review retained output",
+ "description": "Inspect the retained output version before approving or continuing. Restart recovery depends on workflow and deployment compatibility."
+ }
+ ],
+ "requirements": [
+ "The administrator must configure allowed Agents and persistent storage on a Linux or macOS Gateway."
+ ]
+ }
+ },
+ "docs_url": "https://github.com/bytedance/deer-flow/tree/main/examples/deerflow-extension-workflows#readme"
+}
diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md
index d29d582224..ef02220140 100644
--- a/frontend/AGENTS.md
+++ b/frontend/AGENTS.md
@@ -423,11 +423,10 @@ and require a known model to save a model switch.
## Full-stack plugin UI
-Follow [plugin invariants](../docs/full-stack-plugins.md#frontend-implementation-invariants):
-viewer-bound auth/CSRF, native actions, i18n, mount/dispose/abort fences.
-Shadow DOM is not a sandbox. Bundled catalogs need typed en/zh entries, namespaces;
-entries do not prove activation. Optional `requestBackend` binds viewers,
-rejects redirects, never retries; routes retain authorization.
+Follow [plugin invariants](../docs/full-stack-plugins.md#frontend-implementation-invariants).
+Use `pluginPages` only for enabled runtime entries; catalogs never imply activation.
+Package documentation and bundled-copy sync: `examples/README.md`. Hide empty requirements.
+Optional `requestBackend` binds viewers, rejects redirects, never retries; routes retain authorization.
### Composer references
diff --git a/frontend/src/components/workspace/capabilities/extension-detail.tsx b/frontend/src/components/workspace/capabilities/extension-detail.tsx
new file mode 100644
index 0000000000..f7a7b93d39
--- /dev/null
+++ b/frontend/src/components/workspace/capabilities/extension-detail.tsx
@@ -0,0 +1,239 @@
+"use client";
+
+import {
+ ArrowRightIcon,
+ BookOpenIcon,
+ ChevronDownIcon,
+ CircleAlertIcon,
+ CircleCheckIcon,
+ ExternalLinkIcon,
+ RefreshCwIcon,
+} from "lucide-react";
+import Link from "next/link";
+
+import { Button } from "@/components/ui/button";
+import type { ExtensionCatalogEntry } from "@/core/extensions/catalog";
+import { pluginPages, pluginPageTitle } from "@/core/extensions/pages";
+import { extensionIcon } from "@/core/extensions/registry";
+import { useI18n } from "@/core/i18n/hooks";
+
+export function ExtensionDetail({
+ entry,
+ discovery,
+}: {
+ entry: ExtensionCatalogEntry;
+ discovery: "pending" | "error" | "ready";
+}) {
+ const { t, locale } = useI18n();
+ const copy = t.extensions;
+ const { loaded } = entry;
+ const available = discovery === "ready";
+ const failed = available && !!loaded?.error;
+ const enabled = available && !failed && loaded?.settings.enabled === true;
+ const pages = enabled && loaded ? pluginPages([loaded]) : [];
+ const primaryPage = pages[0];
+ const Icon = extensionIcon(loaded?.extension?.icon ?? entry.icon);
+ const label = !available
+ ? discovery === "pending"
+ ? copy.loading
+ : copy.unavailable
+ : failed
+ ? copy.moduleUnavailable
+ : !loaded
+ ? copy.catalogEntry
+ : enabled
+ ? loaded?.runtime_only
+ ? copy.detail.loaded
+ : t.capabilities.enabled
+ : t.capabilities.disabled;
+ const hint = !available
+ ? discovery === "pending"
+ ? copy.loading
+ : copy.unavailable
+ : failed
+ ? copy.detail.failedHint
+ : !loaded
+ ? copy.catalogHint
+ : enabled
+ ? loaded?.runtime_only
+ ? copy.detail.loadedHint
+ : copy.detail.readyHint
+ : copy.detail.disabledHint;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {entry.title}
+
+
+ {enabled ? (
+
+ ) : failed ? (
+
+ ) : null}
+ {label}
+
+
+
+ {entry.description}
+
+
+
+ {primaryPage && (
+
+ )}
+
+
+
+
+
+
+ {copy.detail.gettingStarted}
+
+ {entry.gettingStarted?.length ? (
+
+ {entry.gettingStarted.map((step, index) => (
+ -
+
+ {String(index + 1).padStart(2, "0")}
+
+
+
{step.title}
+
+ {step.description}
+
+
+
+ ))}
+
+ ) : !pages.length ? (
+
+ {enabled ? copy.detail.noPageHint : copy.detail.guideHint}
+
+ ) : null}
+ {pages.length > (entry.gettingStarted?.length ? 1 : 0) && (
+
+ {pages.map(({ surface, href }) => (
+
+ {pluginPageTitle(surface, locale)}
+
+
+ ))}
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/workspace/capabilities/extension-gallery.tsx b/frontend/src/components/workspace/capabilities/extension-gallery.tsx
index 48370b63c1..74591b89ce 100644
--- a/frontend/src/components/workspace/capabilities/extension-gallery.tsx
+++ b/frontend/src/components/workspace/capabilities/extension-gallery.tsx
@@ -1,18 +1,8 @@
"use client";
-import {
- ArrowLeftIcon,
- ChevronRightIcon,
- CircleAlertIcon,
- CircleCheckIcon,
- CircleMinusIcon,
- ExternalLinkIcon,
- InfoIcon,
- RefreshCwIcon,
-} from "lucide-react";
+import { ArrowLeftIcon, ChevronRightIcon, RefreshCwIcon } from "lucide-react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
-import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import {
extensionDirectory,
@@ -21,8 +11,8 @@ import {
import { useFrontendExtensions } from "@/core/extensions/hooks";
import { extensionIcon } from "@/core/extensions/registry";
import { useI18n } from "@/core/i18n/hooks";
-import type { Translations } from "@/core/i18n/locales/types";
+import { ExtensionDetail } from "./extension-detail";
import { PluginRow } from "./plugin-directory";
function ExtensionIconTile({ entry }: { entry: ExtensionCatalogEntry }) {
@@ -34,68 +24,8 @@ function ExtensionIconTile({ entry }: { entry: ExtensionCatalogEntry }) {
);
}
-function ExtensionDetail({
- entry,
- copy,
-}: {
- entry: ExtensionCatalogEntry;
- copy: Translations["extensions"];
-}) {
- const { loaded } = entry;
- const notice = loaded?.error
- ? { icon: , text: copy.moduleUnavailable }
- : !loaded
- ? { icon: , text: copy.catalogHint }
- : loaded.settings.enabled === true
- ? { icon: , text: copy.enabledManaged }
- : { icon: , text: copy.disabledManaged };
- return (
-
-
-
-
-
- {entry.title}
-
-
- {entry.description}
-
- {entry.package && (
-
- {entry.package}
-
- )}
-
-
-
- {notice.icon}
-
- {notice.text}
- {entry.guide && (
-
- )}
-
-
-
- );
-}
-
export function ExtensionGallery({ query = "" }: { query?: string }) {
- const { t } = useI18n();
+ const { t, locale } = useI18n();
const publicQuery = useFrontendExtensions();
const params = useSearchParams();
const router = useRouter();
@@ -104,6 +34,7 @@ export function ExtensionGallery({ query = "" }: { query?: string }) {
const entries = extensionDirectory(
publicQuery.data ?? [],
t.extensions.catalog,
+ locale,
);
const source = publicQuery;
function select(namespace?: string) {
@@ -140,7 +71,7 @@ export function ExtensionGallery({ query = "" }: { query?: string }) {
if (selected) {
const entry = entries.find((item) => item.id === selected);
return (
-
+
{t.extensions.all}
- {reload}
{status}
{!entry ? (
@@ -159,7 +89,12 @@ export function ExtensionGallery({ query = "" }: { query?: string }) {
{t.extensions.notInstalled}
) : (
-
+
)}
);
diff --git a/frontend/src/core/extensions/api.ts b/frontend/src/core/extensions/api.ts
index b8ce37a1b9..ab77beab8d 100644
--- a/frontend/src/core/extensions/api.ts
+++ b/frontend/src/core/extensions/api.ts
@@ -3,12 +3,17 @@ import { z } from "zod";
import { fetch } from "@/core/api/fetcher";
import { getBackendBaseURL } from "@/core/config";
+import { documentationSchema } from "./documentation";
+
const contributions = z.array(
z.object({
namespace: z.string(),
viewer_id: z.string().nullable().optional(),
module: z.string().nullable(),
backend_actions: z.array(z.string()).optional(),
+ model_tools: z.array(z.string()).optional(),
+ documentation: documentationSchema.optional().catch(undefined),
+ runtime_only: z.boolean().optional(),
entry: z.string().nullable(),
// Keep unknown transports so the loader can reject only that contribution.
transport: z.string().nullable().optional(),
diff --git a/frontend/src/core/extensions/bundled-documentation.json b/frontend/src/core/extensions/bundled-documentation.json
new file mode 100644
index 0000000000..7f8165242d
--- /dev/null
+++ b/frontend/src/core/extensions/bundled-documentation.json
@@ -0,0 +1,625 @@
+{
+ "deerflow-extension-agent-teams": {
+ "schema_version": 1,
+ "id": "community.agent-teams",
+ "locales": {
+ "zh-CN": {
+ "title": "Agent 团队",
+ "description": "让多个 Agent 围绕同一个目标协作,共享进展,交接任务。",
+ "usage": [
+ {
+ "title": "组建你的团队",
+ "description": "选择 2–8 个已有的自定义 Agent,给团队命名,并设定共同目标。"
+ },
+ {
+ "title": "用 @ 分配任务",
+ "description": "在团队对话中输入 @ 并选择成员,再发送任务;成员会在各自的会话中处理。"
+ },
+ {
+ "title": "查看进展与成果",
+ "description": "在活动记录中查看成员回复和交接;展开任务可打开成员会话、处理提问或下载成果。"
+ }
+ ],
+ "requirements": [
+ "至少准备 2 个自定义 Agent;如限制了工具分组,需要允许 extensions。"
+ ]
+ },
+ "en-US": {
+ "title": "Agent teams",
+ "description": "Bring Agents together around a shared goal, share progress and hand off tasks.",
+ "usage": [
+ {
+ "title": "Build your team",
+ "description": "Choose 2–8 existing Custom Agents, name your team and set a shared goal."
+ },
+ {
+ "title": "Delegate with @mentions",
+ "description": "Type @ and select members in the team composer, then send a task to their own conversations."
+ },
+ {
+ "title": "Review progress and results",
+ "description": "Read replies and handoffs in the activity feed. Expand a task to open its conversation, answer questions or download results."
+ }
+ ],
+ "requirements": [
+ "Prepare at least two Custom Agents; allow the extensions tool group if using a tool-group allowlist."
+ ]
+ }
+ },
+ "docs_url": "https://github.com/bytedance/deer-flow/tree/main/examples/deerflow-extension-agent-teams#readme"
+ },
+ "deerflow-extension-bookmarks": {
+ "schema_version": 1,
+ "id": "community.bookmarks",
+ "locales": {
+ "zh-CN": {
+ "title": "会话书签",
+ "description": "收藏有用的回答,在独立页面查找与整理自己的书签。",
+ "usage": [
+ {
+ "title": "收藏有用的回答",
+ "description": "在已有回答的会话菜单中,选择“书签 → 保存最后一条回答”。"
+ },
+ {
+ "title": "查找与整理",
+ "description": "打开“我的书签”,按文字搜索、修改名称,或回到原会话查看上下文。"
+ }
+ ]
+ },
+ "en-US": {
+ "title": "Bookmarks",
+ "description": "Save useful answers and find and organize your own bookmarks on a dedicated page.",
+ "usage": [
+ {
+ "title": "Save a useful answer",
+ "description": "In a conversation with an answer, choose Bookmarks → Save last answer from its menu."
+ },
+ {
+ "title": "Find and organize",
+ "description": "Open My bookmarks to search saved text, rename entries or return to their source conversations."
+ }
+ ]
+ }
+ },
+ "docs_url": "https://github.com/bytedance/deer-flow/tree/main/examples/deerflow-extension-bookmarks#readme"
+ },
+ "deerflow-extension-example": {
+ "schema_version": 1,
+ "id": "catalog/example",
+ "locales": {
+ "zh-CN": {
+ "title": "扩展开发示例",
+ "description": "演示中间件、任务生命周期、模型观察器、服务和 HTTP 路由的扩展方式。",
+ "usage": [
+ {
+ "title": "安装到开发环境",
+ "description": "参考包内文档安装示例,配置并重启 Gateway。它用于演示扩展开发接口。"
+ },
+ {
+ "title": "运行任务并检查统计",
+ "description": "执行包含工具调用的任务,再通过示例统计 API 查看调用次数、结果类型与耗时。"
+ }
+ ]
+ },
+ "en-US": {
+ "title": "Extension development example",
+ "description": "Explore middleware, task lifecycle, model observers, services and HTTP route contributions.",
+ "usage": [
+ {
+ "title": "Install in a development environment",
+ "description": "Follow the package guide to install, configure and restart Gateway. This package demonstrates extension development contracts."
+ },
+ {
+ "title": "Run a task and inspect statistics",
+ "description": "Run a task that uses tools, then inspect call counts, outcome types and durations through the example statistics API."
+ }
+ ]
+ }
+ },
+ "docs_url": "https://github.com/bytedance/deer-flow/tree/main/examples/deerflow-extension-example#readme"
+ },
+ "deerflow-extension-jev-classify": {
+ "schema_version": 1,
+ "id": "community.jev-classify",
+ "locales": {
+ "zh-CN": {
+ "title": "文本分类",
+ "description": "按给定类别为文本列表打标签,使用部署方配置的分类服务。",
+ "usage": [
+ {
+ "title": "提供文本和分类标准",
+ "description": "在对话中提供待分类的文本列表,说明各类别的含义和边界;也可上传表格并指出文本列。"
+ },
+ {
+ "title": "让 Agent 执行分类",
+ "description": "要求使用文本分类工具,检查返回标签;如需表格,让 Agent 将标签写回输出文件。"
+ }
+ ],
+ "requirements": [
+ "管理员已配置 Jev 或兼容聊天接口的分类服务;待分类文本会发送给该服务。"
+ ]
+ },
+ "en-US": {
+ "title": "Text classification",
+ "description": "Label a list of texts with supplied categories using the deployment-configured classification service.",
+ "usage": [
+ {
+ "title": "Provide texts and categories",
+ "description": "Give the Agent a list of texts and explain the categories, or upload a table and identify its text column."
+ },
+ {
+ "title": "Ask the Agent to classify",
+ "description": "Ask it to use the text classification tool and inspect the labels. For a table, ask it to write labels into an output file."
+ }
+ ],
+ "requirements": [
+ "An administrator must configure Jev or a compatible chat endpoint. Texts to classify are sent to that service."
+ ]
+ }
+ },
+ "docs_url": "https://github.com/bytedance/deer-flow/tree/main/examples/deerflow-extension-jev-classify#readme"
+ },
+ "deerflow-extension-jev-context": {
+ "schema_version": 1,
+ "id": "community.jev-context",
+ "locales": {
+ "zh-CN": {
+ "title": "上下文裁剪",
+ "description": "按需缩短旧的只读工具结果,保留近期消息与原生摘要。",
+ "usage": [
+ {
+ "title": "正常进行对话",
+ "description": "启用后自动检查长对话中的旧工具输出,不需要打开独立页面或输入额外命令。"
+ },
+ {
+ "title": "达到阈值时自动处理",
+ "description": "满足配置的上下文阈值和调用间隔后,裁剪已过时的只读工具结果,保留近期消息和受保护内容。"
+ }
+ ],
+ "requirements": [
+ "管理员已配置 TypeSafe Jev 服务凭据及触发阈值;处理时会向该服务发送限定范围的对话内容。"
+ ]
+ },
+ "en-US": {
+ "title": "Context pruning",
+ "description": "Shorten older read-only tool results while preserving recent messages and native summaries.",
+ "usage": [
+ {
+ "title": "Continue your conversation",
+ "description": "Once enabled, it automatically checks old tool output in long conversations. No separate page or command is needed."
+ },
+ {
+ "title": "Prune when the threshold is reached",
+ "description": "After the configured token threshold and call interval, obsolete read-only results are shortened while recent and protected content is retained."
+ }
+ ],
+ "requirements": [
+ "An administrator must configure TypeSafe Jev credentials and thresholds. Processing sends bounded conversation content to that service."
+ ]
+ }
+ },
+ "docs_url": "https://github.com/bytedance/deer-flow/tree/main/examples/deerflow-extension-jev-context#readme"
+ },
+ "deerflow-extension-jev-screening": {
+ "schema_version": 1,
+ "id": "catalog/screening",
+ "locales": {
+ "zh-CN": {
+ "title": "内容风险提示",
+ "description": "检查获取的外部内容,为可疑工具结果添加提示,不阻断工具执行。",
+ "usage": [
+ {
+ "title": "按平常方式搜索与读取",
+ "description": "启用后,在 Agent 获取网页、搜索或 MCP 工具结果时自动检查部分内容。"
+ },
+ {
+ "title": "结合提示判断内容",
+ "description": "检测到可疑指令时,会在工具结果中增加风险提示。它提供提醒,不阻断工具,也不保证识别所有风险。"
+ }
+ ],
+ "requirements": [
+ "管理员已配置 TypeSafe Jev 服务;待检查的内容片段会发送给该服务。"
+ ]
+ },
+ "en-US": {
+ "title": "Content risk warnings",
+ "description": "Screen fetched content and add advisory warnings to suspicious tool results without blocking tools.",
+ "usage": [
+ {
+ "title": "Search and read as usual",
+ "description": "Once enabled, the middleware checks bounded excerpts of fetched pages, search results and MCP tool output."
+ },
+ {
+ "title": "Review advisory warnings",
+ "description": "Suspicious instructions add a warning to the tool result. Screening is advisory; it does not block tools or guarantee detection."
+ }
+ ],
+ "requirements": [
+ "An administrator must configure TypeSafe Jev. Content excerpts are sent to that service for screening."
+ ]
+ }
+ },
+ "docs_url": "https://github.com/bytedance/deer-flow/tree/main/examples/deerflow-extension-jev-screening#readme"
+ },
+ "deerflow-extension-knowledge": {
+ "schema_version": 1,
+ "id": "catalog/knowledge",
+ "locales": {
+ "zh-CN": {
+ "title": "知识范围选择示例",
+ "description": "用示例文档演示知识库和文件范围选择、检索及来源引用。",
+ "usage": [
+ {
+ "title": "选择示例知识范围",
+ "description": "在对话的知识范围选择器中选择 Synthetic guides,可进一步选择 Synthetic knowledge note。"
+ },
+ {
+ "title": "提问并查看引用",
+ "description": "让 Agent 搜索 Cedar,检查回答中的来源引用。此插件使用公开的虚构演示文档。"
+ }
+ ],
+ "requirements": [
+ "管理员已启用知识检索工具和范围选择,并将工具连接到此示例 Provider。"
+ ]
+ },
+ "en-US": {
+ "title": "Knowledge scope example",
+ "description": "Search sample documents within selected knowledge bases or files and inspect source citations.",
+ "usage": [
+ {
+ "title": "Select the example scope",
+ "description": "Choose Synthetic guides in the chat scope selector, optionally narrowing it to Synthetic knowledge note."
+ },
+ {
+ "title": "Ask and inspect citations",
+ "description": "Ask the Agent to search for Cedar and inspect its source citations. This example uses a public synthetic document."
+ }
+ ],
+ "requirements": [
+ "The administrator must enable knowledge retrieval and scope selection and connect the tool to this example provider."
+ ]
+ }
+ },
+ "docs_url": "https://github.com/bytedance/deer-flow/tree/main/examples/deerflow-extension-knowledge#readme"
+ },
+ "deerflow-extension-knowledge-center": {
+ "schema_version": 1,
+ "id": "knowledge-center",
+ "locales": {
+ "zh-CN": {
+ "title": "知识中心",
+ "description": "在独立页面浏览知识空间、导入资料,并查看文档处理状态。",
+ "usage": [
+ {
+ "title": "选择空间与知识库",
+ "description": "打开知识中心,选择你有权限访问的空间及其中的知识库。"
+ },
+ {
+ "title": "导入资料",
+ "description": "按页面提供的操作导入文件、网址或文本。可用按钮取决于你的权限和服务支持。"
+ },
+ {
+ "title": "查看处理状态",
+ "description": "检查返回的文档记录或按文档标识查询状态,必要时手动刷新。"
+ }
+ ],
+ "requirements": [
+ "已安装并启用知识空间与连接、知识入库与处理扩展,并为你分配可访问的空间和资源。"
+ ]
+ },
+ "en-US": {
+ "title": "Knowledge center",
+ "description": "Browse knowledge spaces, import content and inspect document processing on a dedicated page.",
+ "usage": [
+ {
+ "title": "Choose a space and knowledge base",
+ "description": "Open Knowledge center and select an accessible space and mapped knowledge base."
+ },
+ {
+ "title": "Import content",
+ "description": "Use the available controls to import a file, URL or text. Controls depend on your permissions and provider support."
+ },
+ {
+ "title": "Inspect processing",
+ "description": "Inspect the returned document record or look up a document by ID, refreshing its status when needed."
+ }
+ ],
+ "requirements": [
+ "Enable the knowledge platform and management extensions and grant you access to spaces and resources."
+ ]
+ }
+ },
+ "docs_url": "https://github.com/bytedance/deer-flow/tree/main/examples/deerflow-extension-knowledge-center#readme"
+ },
+ "deerflow-extension-knowledge-management": {
+ "schema_version": 1,
+ "id": "knowledge-management",
+ "locales": {
+ "zh-CN": {
+ "title": "知识入库与处理",
+ "description": "导入文件、网址和文本,并查询、管理文档处理任务。",
+ "usage": [
+ {
+ "title": "连接已有管理入口",
+ "description": "由管理员通过知识中心或管理 API,选择已授权的知识库与操作。此扩展本身提供后台接口。"
+ },
+ {
+ "title": "导入并查询处理结果",
+ "description": "提交文件、网址或文本,保留返回的文档标识,再查询处理状态;可用操作由上游能力和权限共同决定。"
+ }
+ ],
+ "requirements": [
+ "管理员已配置 WeKnora 管理连接及明确的用户、知识库和操作授权。"
+ ]
+ },
+ "en-US": {
+ "title": "Knowledge ingestion and processing",
+ "description": "Import files, URLs and text, then inspect and manage document processing.",
+ "usage": [
+ {
+ "title": "Use an existing management entry",
+ "description": "Use Knowledge center or the management API to select an authorized knowledge base and operation. This extension provides backend endpoints."
+ },
+ {
+ "title": "Ingest and inspect processing",
+ "description": "Submit a file, URL or text, retain the returned document ID and query its status. Available operations depend on upstream capabilities and permissions."
+ }
+ ],
+ "requirements": [
+ "The administrator must configure a WeKnora management connection and explicit user, knowledge-base and operation grants."
+ ]
+ }
+ },
+ "docs_url": "https://github.com/bytedance/deer-flow/tree/main/examples/deerflow-extension-knowledge-management#readme"
+ },
+ "deerflow-extension-knowledge-platform": {
+ "schema_version": 1,
+ "id": "knowledge-platform",
+ "locales": {
+ "zh-CN": {
+ "title": "知识空间与连接",
+ "description": "组织知识空间、连接与成员权限,让团队按授权范围使用资料。",
+ "usage": [
+ {
+ "title": "由管理员配置空间与资源",
+ "description": "通过平台管理接口配置连接、空间成员和可访问资源,明确操作权限。"
+ },
+ {
+ "title": "在授权范围内使用",
+ "description": "通过知识中心或平台接口选择可访问的空间和知识库,再进行导入与状态查询。"
+ }
+ ],
+ "requirements": [
+ "已安装知识入库与处理扩展,并配置连接、持久化存储和空间策略。"
+ ]
+ },
+ "en-US": {
+ "title": "Knowledge spaces and connections",
+ "description": "Organize knowledge spaces, connections and membership for authorized access.",
+ "usage": [
+ {
+ "title": "Configure spaces and resources",
+ "description": "An administrator configures connections, space membership, accessible resources and permitted operations through the platform API."
+ },
+ {
+ "title": "Work within an authorized space",
+ "description": "Use Knowledge center or platform APIs to select an accessible space and knowledge base, then ingest content or inspect processing."
+ }
+ ],
+ "requirements": [
+ "Install the knowledge management extension and configure connections, persistent storage and space policies."
+ ]
+ }
+ },
+ "docs_url": "https://github.com/bytedance/deer-flow/tree/main/examples/deerflow-extension-knowledge-platform#readme"
+ },
+ "deerflow-extension-lightrag": {
+ "schema_version": 1,
+ "id": "catalog/lightrag",
+ "locales": {
+ "zh-CN": {
+ "title": "LightRAG",
+ "description": "从 LightRAG 检索文档与实体关系,并使用统一的来源引用。",
+ "usage": [
+ {
+ "title": "在对话中提出检索问题",
+ "description": "让 Agent 查询已连接知识库中的文档、实体或实体间关系。"
+ },
+ {
+ "title": "查看来源依据",
+ "description": "检查结果的文档引用;实体关系只展示服务返回的边,有文档片段支持时才提供对应引用。"
+ }
+ ],
+ "requirements": [
+ "管理员已配置 LightRAG 连接、凭据、用户绑定和相关检索工具。"
+ ]
+ },
+ "en-US": {
+ "title": "LightRAG",
+ "description": "Retrieve documents and entity relations from LightRAG with shared source citations.",
+ "usage": [
+ {
+ "title": "Ask a retrieval question",
+ "description": "Ask the Agent to search the connected knowledge base for documents, entities or relationships."
+ },
+ {
+ "title": "Inspect supporting sources",
+ "description": "Check document citations. Relations reflect returned edges and receive document citations only when supporting excerpts are available."
+ }
+ ],
+ "requirements": [
+ "The administrator must configure the LightRAG connection, credentials, user bindings and retrieval tools."
+ ]
+ }
+ },
+ "docs_url": "https://github.com/bytedance/deer-flow/tree/main/examples/deerflow-extension-lightrag#readme"
+ },
+ "deerflow-extension-report-sections": {
+ "schema_version": 1,
+ "id": "community.report-sections",
+ "locales": {
+ "zh-CN": {
+ "title": "分段长文写作",
+ "description": "逐段撰写长报告,并将各章节汇总为可下载的 Markdown 文档。",
+ "usage": [
+ {
+ "title": "在对话里提出写作任务",
+ "description": "告诉 Agent 报告主题、目标读者和章节要求,并要求使用分段长文写作工具。"
+ },
+ {
+ "title": "先定大纲,再逐章完成",
+ "description": "让 Agent 先保存大纲,再逐章撰写和保存。需要修改时,在同一对话中指出要修改的章节。"
+ },
+ {
+ "title": "汇总并下载",
+ "description": "全部章节完成后,让 Agent 合并报告;在对话的文件成果中下载 Markdown 文档。"
+ }
+ ]
+ },
+ "en-US": {
+ "title": "Sectioned long-form writing",
+ "description": "Write long reports section by section and assemble them into a downloadable Markdown document.",
+ "usage": [
+ {
+ "title": "Describe the report in chat",
+ "description": "Tell the Agent the topic, audience and chapter requirements, and ask it to use the sectioned writing tool."
+ },
+ {
+ "title": "Outline, then write each chapter",
+ "description": "Ask the Agent to save an outline, then write and save chapters one at a time. Request chapter revisions in the same conversation."
+ },
+ {
+ "title": "Assemble and download",
+ "description": "Ask the Agent to assemble the finished chapters, then download the Markdown document from the conversation artifacts."
+ }
+ ]
+ }
+ },
+ "docs_url": "https://github.com/bytedance/deer-flow/tree/main/examples/deerflow-extension-report-sections#readme"
+ },
+ "deerflow-extension-retrieval": {
+ "schema_version": 1,
+ "id": "example.retrieval",
+ "locales": {
+ "zh-CN": {
+ "title": "本地检索示例",
+ "description": "使用固定示例文档演示只读检索与结构化工具结果,不连接外部知识库。",
+ "usage": [
+ {
+ "title": "在对话里试一次检索",
+ "description": "让 Agent 使用本地检索示例搜索 release,查看匹配到的固定演示文档。"
+ },
+ {
+ "title": "检查返回结果",
+ "description": "观察文字摘要与结构化工具成果;示例数据是虚构的,不代表真实知识库内容。"
+ }
+ ]
+ },
+ "en-US": {
+ "title": "Local retrieval example",
+ "description": "Demonstrate read-only retrieval and structured tool results using fixed sample documents, without an external knowledge service.",
+ "usage": [
+ {
+ "title": "Try a search in chat",
+ "description": "Ask the Agent to use the local retrieval example to search for release and inspect the matching demonstration document."
+ },
+ {
+ "title": "Inspect the result",
+ "description": "Review its text summary and structured tool artifact. The fixed corpus is synthetic, not a real knowledge base."
+ }
+ ]
+ }
+ },
+ "docs_url": "https://github.com/bytedance/deer-flow/tree/main/examples/deerflow-extension-retrieval#readme"
+ },
+ "deerflow-extension-weknora": {
+ "schema_version": 1,
+ "id": "catalog/weknora",
+ "locales": {
+ "zh-CN": {
+ "title": "WeKnora 知识检索",
+ "description": "检索 WeKnora 知识库和所选文档,并展示来源引用。",
+ "usage": [
+ {
+ "title": "选择检索范围",
+ "description": "在对话中选择已授权的 WeKnora 知识库,按需缩小到具体文档。"
+ },
+ {
+ "title": "提问并核对来源",
+ "description": "提出与已入库资料相关的问题,查看返回片段和来源引用。"
+ }
+ ],
+ "requirements": [
+ "已有完成索引的 WeKnora 知识库;管理员已配置连接、凭据、用户授权和检索工具。"
+ ]
+ },
+ "en-US": {
+ "title": "WeKnora knowledge retrieval",
+ "description": "Search WeKnora knowledge bases and selected documents with source citations.",
+ "usage": [
+ {
+ "title": "Select a search scope",
+ "description": "Choose an authorized WeKnora knowledge base in chat and narrow it to specific documents when needed."
+ },
+ {
+ "title": "Ask and verify sources",
+ "description": "Ask a question about indexed content and inspect the retrieved excerpts and source citations."
+ }
+ ],
+ "requirements": [
+ "An indexed WeKnora knowledge base is required, with an administrator-configured connection, credentials, user grants and retrieval tool."
+ ]
+ }
+ },
+ "docs_url": "https://github.com/bytedance/deer-flow/tree/main/examples/deerflow-extension-weknora#readme"
+ },
+ "deerflow-extension-workflows": {
+ "schema_version": 1,
+ "id": "community.workflows",
+ "locales": {
+ "zh-CN": {
+ "title": "脚本工作流",
+ "description": "运行可检查的 Agent 脚本工作流,跟踪步骤进度,并按保留版本确认成果。",
+ "usage": [
+ {
+ "title": "打开脚本工作流",
+ "description": "进入工作流页面,准备一个受支持的 JavaScript 工作流及其输入参数。"
+ },
+ {
+ "title": "提交并查看执行过程",
+ "description": "启动后查看步骤进度、Agent 子会话和执行结果;需要人工确认的步骤会等待你的处理。"
+ },
+ {
+ "title": "按保留版本确认成果",
+ "description": "检查实际保留的输出版本,再确认或继续工作流。重启恢复取决于工作流和部署是否支持。"
+ }
+ ],
+ "requirements": [
+ "管理员已配置可调用的 Agent 和持久化存储;Gateway 运行在 Linux 或 macOS。"
+ ]
+ },
+ "en-US": {
+ "title": "Script workflows",
+ "description": "Run inspectable Agent workflows, track steps and review retained output versions.",
+ "usage": [
+ {
+ "title": "Open Script workflows",
+ "description": "Open the workflow page and prepare a supported JavaScript workflow and its input arguments."
+ },
+ {
+ "title": "Run and inspect",
+ "description": "Start the workflow and inspect steps, Agent conversations and results. Human checkpoints wait for your response."
+ },
+ {
+ "title": "Review retained output",
+ "description": "Inspect the retained output version before approving or continuing. Restart recovery depends on workflow and deployment compatibility."
+ }
+ ],
+ "requirements": [
+ "The administrator must configure allowed Agents and persistent storage on a Linux or macOS Gateway."
+ ]
+ }
+ },
+ "docs_url": "https://github.com/bytedance/deer-flow/tree/main/examples/deerflow-extension-workflows#readme"
+ }
+}
diff --git a/frontend/src/core/extensions/catalog.ts b/frontend/src/core/extensions/catalog.ts
index 025d85c139..2524ed182f 100644
--- a/frontend/src/core/extensions/catalog.ts
+++ b/frontend/src/core/extensions/catalog.ts
@@ -1,11 +1,13 @@
import type { Translations } from "@/core/i18n/locales/types";
+import { localizeDocumentation } from "./documentation";
import type { LoadedContribution } from "./registry";
// Discovery metadata only: catalog entries never enter the runtime module loader.
export const extensionCatalog = [
{
key: "agentTeams",
+ icon: "users",
package: "deerflow-extension-agent-teams",
namespace: "community.agent-teams",
},
@@ -78,11 +80,14 @@ export interface ExtensionCatalogEntry {
guide?: string;
icon?: string;
loaded?: LoadedContribution;
+ gettingStarted?: { title: string; description: string }[];
+ requirements?: string[];
}
export function extensionDirectory(
loaded: LoadedContribution[],
copy: Translations["extensions"]["catalog"],
+ locale = "en-US",
): ExtensionCatalogEntry[] {
const entries: ExtensionCatalogEntry[] = extensionCatalog.map((item) => ({
id: item.namespace ?? `catalog/${item.key}`,
@@ -96,11 +101,26 @@ export function extensionDirectory(
extensionCatalog.map((item) => item.namespace),
);
for (const entry of loaded) {
+ const page =
+ entry.documentation?.id === entry.namespace
+ ? entry.documentation
+ : undefined;
+ const existing = entries.find((item) => item.id === entry.namespace);
+ if (existing) {
+ existing.loaded = entry;
+ if (page)
+ Object.assign(existing, localizeDocumentation(page, locale), {
+ guide: page.docs_url ?? existing.guide,
+ });
+ continue;
+ }
if (known.has(entry.namespace)) continue;
entries.push({
id: entry.namespace,
title: entry.title,
description: entry.description,
+ ...(page ? localizeDocumentation(page, locale) : {}),
+ guide: page?.docs_url,
loaded: entry,
});
}
diff --git a/frontend/src/core/extensions/contracts.ts b/frontend/src/core/extensions/contracts.ts
index 2059b40afc..f9f82b15cf 100644
--- a/frontend/src/core/extensions/contracts.ts
+++ b/frontend/src/core/extensions/contracts.ts
@@ -3,6 +3,8 @@ import type { Message } from "@langchain/langgraph-sdk";
import type { Translations } from "@/core/i18n";
import type { AgentThread } from "@/core/threads/types";
+import type { ExtensionDocumentation } from "./documentation";
+
export type ExtensionSettings = Readonly<
Record
>;
@@ -49,6 +51,9 @@ export type FrontendContribution = {
description: string;
settings: ExtensionSettings;
backend_actions?: string[];
+ model_tools?: string[];
+ documentation?: ExtensionDocumentation;
+ runtime_only?: boolean;
};
export type ConversationActionContext = {
thread: AgentThread;
diff --git a/frontend/src/core/extensions/documentation.ts b/frontend/src/core/extensions/documentation.ts
new file mode 100644
index 0000000000..717fe9b053
--- /dev/null
+++ b/frontend/src/core/extensions/documentation.ts
@@ -0,0 +1,56 @@
+import { z } from "zod";
+
+import bundled from "./bundled-documentation.json" with { type: "json" };
+
+const text = z.string().trim().min(1).max(4000);
+const localized = z.object({
+ title: text,
+ description: text,
+ usage: z
+ .array(z.object({ title: text, description: text }))
+ .min(1)
+ .max(12),
+ requirements: z.array(text).max(12).optional(),
+});
+export const documentationSchema = z.object({
+ schema_version: z.literal(1),
+ id: z.string(),
+ locales: z
+ .record(z.string(), localized)
+ .refine((value) => Object.keys(value).length > 0),
+ docs_url: z
+ .url()
+ .refine((value) => {
+ try {
+ const url = new URL(value);
+ return url.protocol === "https:" && !url.username && !url.password;
+ } catch {
+ return false;
+ }
+ })
+ .optional(),
+});
+export type ExtensionDocumentation = z.infer;
+
+export function localizeDocumentation(
+ doc: Pick,
+ locale: string,
+) {
+ const entries = Object.entries(doc.locales);
+ const target = locale.toLowerCase();
+ const match = (tag: string) =>
+ entries.find(([key]) => key.toLowerCase() === tag)?.[1] ??
+ entries.find(
+ ([key]) => key.toLowerCase().split("-")[0] === tag.split("-")[0],
+ )?.[1];
+ const copy = match(target) ?? match("en-us") ?? entries[0]![1];
+ return {
+ ...copy,
+ gettingStarted: copy.usage,
+ requirements: copy.requirements,
+ };
+}
+
+export function bundledCopy(name: keyof typeof bundled, locale: string) {
+ return localizeDocumentation(bundled[name], locale);
+}
diff --git a/frontend/src/core/extensions/registry.ts b/frontend/src/core/extensions/registry.ts
index b9310a1382..192f469e1f 100644
--- a/frontend/src/core/extensions/registry.ts
+++ b/frontend/src/core/extensions/registry.ts
@@ -5,6 +5,7 @@ import {
FileJsonIcon,
FileTextIcon,
PuzzleIcon,
+ UsersIcon,
type LucideIcon,
} from "lucide-react";
@@ -17,6 +18,7 @@ import type { FrontendContribution, FrontendExtension } from "./contracts";
export function extensionIcon(name?: string): LucideIcon {
const icons: Record = {
bell: BellIcon,
+ users: UsersIcon,
bookmark: BookmarkIcon,
download: DownloadIcon,
"file-json": FileJsonIcon,
diff --git a/frontend/src/core/i18n/locales/en-US.ts b/frontend/src/core/i18n/locales/en-US.ts
index 390c8df289..79aa37fb9d 100644
--- a/frontend/src/core/i18n/locales/en-US.ts
+++ b/frontend/src/core/i18n/locales/en-US.ts
@@ -9,6 +9,8 @@ import {
VideoIcon,
} from "lucide-react";
+import { bundledCopy } from "@/core/extensions/documentation";
+
import type { Translations } from "./types";
export const enUS: Translations = {
@@ -93,82 +95,61 @@ export const enUS: Translations = {
catalogHint:
"This entry comes from the repository extension catalog. Ask your deployment administrator to confirm its installation and activation status.",
installationGuide: "Installation guide",
+ detail: {
+ guide: "Read the guide",
+ gettingStarted: "Get started",
+ requirements: "Before you start",
+ loaded: "Loaded",
+ loadedHint:
+ "This extension is loaded. Follow the usage instructions to get started.",
+ tools: "Registered tools",
+ pages: "Workspace pages",
+ management: "Usage & management",
+ readyHint:
+ "Your administrator has enabled this extension. It is ready to use.",
+ disabledHint:
+ "This extension is disabled. Ask your administrator to enable it.",
+ failedHint:
+ "The extension page could not load. Refresh extensions to try again.",
+ noPageHint:
+ "This extension has no standalone page. Check the guide for how to use it in conversations or tasks.",
+ guideHint:
+ "Read the guide to explore what this extension adds to DeerFlow and how to use it.",
+ technicalDetails: "Technical details",
+ package: "Package",
+ namespace: "Extension ID",
+ refresh: "Refresh extensions",
+ refreshHint:
+ "Refresh the page after your administrator updates an extension.",
+ },
catalog: {
- workflows: {
- title: "Script workflows",
- description:
- "Save versioned Agent workflows, resume within the same Gateway process and review exact retained output versions. No restart replay or hard monetary budget.",
- },
- reportSections: {
- title: "Sectioned long-form writing",
- description:
- "Write long reports section by section and assemble them into a downloadable Markdown document.",
- },
- agentTeams: {
- title: "Agent teams",
- description:
- "Let full Custom Agents collaborate through @mentions, shared records and task handoffs.",
- },
- bookmarks: {
- title: "Bookmarks",
- description:
- "Save useful answers and find and organize your own bookmarks on a dedicated page.",
- },
- context: {
- title: "Context pruning",
- description:
- "Shorten older read-only tool results while preserving recent messages and native summaries.",
- },
- classify: {
- title: "Text classification",
- description:
- "Label a list of texts with supplied categories using the deployment-configured classification service.",
- },
- screening: {
- title: "Content risk warnings",
- description:
- "Screen fetched content and add advisory warnings to suspicious tool results without blocking tools.",
- },
- example: {
- title: "Extension development example",
- description:
- "Explore middleware, task lifecycle, model observers, services and HTTP route contributions.",
- },
- retrieval: {
- title: "Local retrieval example",
- description:
- "Demonstrate read-only retrieval and structured tool results using fixed sample documents, without an external knowledge service.",
- },
- lightrag: {
- title: "LightRAG",
- description:
- "Retrieve documents and entity relations from LightRAG with shared source citations.",
- },
- knowledgePlatform: {
- title: "Knowledge spaces and connections",
- description:
- "Manage hierarchical spaces, resource sharing and inherited policies with explicit administrator grants. Requires installation and configuration; no management page or global retrieval ACL is included.",
- },
- knowledgeCenter: {
- title: "Knowledge center",
- description:
- "Browse authorized spaces, import files, URLs and text, and inspect document processing. Requires the separately configured knowledge platform and management extensions.",
- },
- knowledgeManagement: {
- title: "Knowledge ingestion and processing",
- description:
- "Ingest files, URLs and text through WeKnora and manage processing with explicit administrator grants. Requires installation and configuration; no management page is included.",
- },
- weknora: {
- title: "WeKnora knowledge retrieval",
- description:
- "Search WeKnora knowledge bases and selected documents with source citations. Requires administrator installation and access configuration.",
- },
- knowledge: {
- title: "Knowledge scope example",
- description:
- "Search sample documents within selected knowledge bases or files and inspect source citations.",
- },
+ agentTeams: bundledCopy("deerflow-extension-agent-teams", "en-US"),
+ bookmarks: bundledCopy("deerflow-extension-bookmarks", "en-US"),
+ context: bundledCopy("deerflow-extension-jev-context", "en-US"),
+ classify: bundledCopy("deerflow-extension-jev-classify", "en-US"),
+ screening: bundledCopy("deerflow-extension-jev-screening", "en-US"),
+ workflows: bundledCopy("deerflow-extension-workflows", "en-US"),
+ reportSections: bundledCopy(
+ "deerflow-extension-report-sections",
+ "en-US",
+ ),
+ retrieval: bundledCopy("deerflow-extension-retrieval", "en-US"),
+ knowledge: bundledCopy("deerflow-extension-knowledge", "en-US"),
+ knowledgeManagement: bundledCopy(
+ "deerflow-extension-knowledge-management",
+ "en-US",
+ ),
+ knowledgePlatform: bundledCopy(
+ "deerflow-extension-knowledge-platform",
+ "en-US",
+ ),
+ knowledgeCenter: bundledCopy(
+ "deerflow-extension-knowledge-center",
+ "en-US",
+ ),
+ lightrag: bundledCopy("deerflow-extension-lightrag", "en-US"),
+ weknora: bundledCopy("deerflow-extension-weknora", "en-US"),
+ example: bundledCopy("deerflow-extension-example", "en-US"),
},
pageUnavailable: "Extension page unavailable",
pageUnavailableHint:
diff --git a/frontend/src/core/i18n/locales/types.ts b/frontend/src/core/i18n/locales/types.ts
index cd8d3188af..eaf8770f05 100644
--- a/frontend/src/core/i18n/locales/types.ts
+++ b/frontend/src/core/i18n/locales/types.ts
@@ -77,6 +77,26 @@ export interface Translations {
catalogEntry: string;
catalogHint: string;
installationGuide: string;
+ detail: {
+ guide: string;
+ gettingStarted: string;
+ requirements: string;
+ loaded: string;
+ loadedHint: string;
+ tools: string;
+ pages: string;
+ management: string;
+ readyHint: string;
+ disabledHint: string;
+ failedHint: string;
+ noPageHint: string;
+ guideHint: string;
+ technicalDetails: string;
+ package: string;
+ namespace: string;
+ refresh: string;
+ refreshHint: string;
+ };
catalog: Record<
| "workflows"
| "reportSections"
@@ -93,7 +113,12 @@ export interface Translations {
| "weknora"
| "lightrag"
| "example",
- { title: string; description: string }
+ {
+ title: string;
+ description: string;
+ gettingStarted?: { title: string; description: string }[];
+ requirements?: string[];
+ }
>;
pageUnavailable: string;
pageUnavailableHint: string;
diff --git a/frontend/src/core/i18n/locales/zh-CN.ts b/frontend/src/core/i18n/locales/zh-CN.ts
index 8076bfd498..20530c862d 100644
--- a/frontend/src/core/i18n/locales/zh-CN.ts
+++ b/frontend/src/core/i18n/locales/zh-CN.ts
@@ -9,6 +9,8 @@ import {
VideoIcon,
} from "lucide-react";
+import { bundledCopy } from "@/core/extensions/documentation";
+
import type { Translations } from "./types";
export const zhCN: Translations = {
@@ -92,75 +94,55 @@ export const zhCN: Translations = {
catalogHint:
"此条目来自仓库扩展目录;实际安装和启用状态请由部署管理员确认。",
installationGuide: "查看安装说明",
+ detail: {
+ guide: "阅读使用说明",
+ gettingStarted: "开始使用",
+ requirements: "使用前准备",
+ loaded: "已加载",
+ loadedHint: "此扩展已加载,使用方式见本页说明。",
+ tools: "已注册工具",
+ pages: "工作区入口",
+ management: "使用与管理",
+ readyHint: "此扩展已由管理员启用,可以直接使用。",
+ disabledHint: "此扩展已停用。请联系管理员启用后再使用。",
+ failedHint: "扩展页面未能加载,请刷新扩展后重试。",
+ noPageHint:
+ "此扩展没有独立工作页面。请查看使用说明,了解它在对话或任务中的使用方式。",
+ guideHint: "通过使用说明,了解此扩展提供的功能和使用方式。",
+ technicalDetails: "技术信息",
+ package: "安装包",
+ namespace: "扩展标识",
+ refresh: "刷新扩展",
+ refreshHint: "管理员更新扩展后,刷新页面以获取最新状态。",
+ },
catalog: {
- workflows: {
- title: "脚本工作流",
- description:
- "保存版本化 Agent 工作流,在同一 Gateway 进程内恢复,并确认确切的保留输出版本。不支持重启回放或硬金额预算。",
- },
- reportSections: {
- title: "分段长文写作",
- description: "逐段撰写长报告,并将各章节汇总为可下载的 Markdown 文档。",
- },
- agentTeams: {
- title: "Agent 团队",
- description: "让完整 Custom Agent 通过 @成员、共享记录与任务交接协作。",
- },
- bookmarks: {
- title: "会话书签",
- description: "收藏有用的回答,在独立页面查找与整理自己的书签。",
- },
- context: {
- title: "上下文裁剪",
- description: "按需缩短旧的只读工具结果,保留近期消息与原生摘要。",
- },
- classify: {
- title: "文本分类",
- description: "按给定类别为文本列表打标签,使用部署方配置的分类服务。",
- },
- screening: {
- title: "内容风险提示",
- description:
- "检查获取的外部内容,为可疑工具结果添加提示,不阻断工具执行。",
- },
- example: {
- title: "扩展开发示例",
- description:
- "演示中间件、任务生命周期、模型观察器、服务和 HTTP 路由的扩展方式。",
- },
- retrieval: {
- title: "本地检索示例",
- description:
- "使用固定示例文档演示只读检索与结构化工具结果,不连接外部知识库。",
- },
- lightrag: {
- title: "LightRAG",
- description: "从 LightRAG 检索文档与实体关系,并使用统一的来源引用。",
- },
- knowledgePlatform: {
- title: "知识空间与连接",
- description:
- "在管理员明确授权后管理多层级空间、资源共享与继承策略。需要安装和配置,不包含管理页面,也不改变全局检索权限。",
- },
- knowledgeCenter: {
- title: "知识中心",
- description:
- "浏览授权空间,导入文件、网址和文本,并查询文档处理状态。需要单独配置知识平台与知识管理扩展。",
- },
- knowledgeManagement: {
- title: "知识入库与处理",
- description:
- "通过 WeKnora 导入文件、网址和文本,并在管理员明确授权后管理处理任务。需要安装和配置,不包含管理页面。",
- },
- weknora: {
- title: "WeKnora 知识检索",
- description:
- "检索 WeKnora 知识库和所选文档,并展示来源引用。需要管理员安装并配置访问权限。",
- },
- knowledge: {
- title: "知识范围选择示例",
- description: "用示例文档演示知识库和文件范围选择、检索及来源引用。",
- },
+ agentTeams: bundledCopy("deerflow-extension-agent-teams", "zh-CN"),
+ bookmarks: bundledCopy("deerflow-extension-bookmarks", "zh-CN"),
+ context: bundledCopy("deerflow-extension-jev-context", "zh-CN"),
+ classify: bundledCopy("deerflow-extension-jev-classify", "zh-CN"),
+ screening: bundledCopy("deerflow-extension-jev-screening", "zh-CN"),
+ workflows: bundledCopy("deerflow-extension-workflows", "zh-CN"),
+ reportSections: bundledCopy(
+ "deerflow-extension-report-sections",
+ "zh-CN",
+ ),
+ retrieval: bundledCopy("deerflow-extension-retrieval", "zh-CN"),
+ knowledge: bundledCopy("deerflow-extension-knowledge", "zh-CN"),
+ knowledgeManagement: bundledCopy(
+ "deerflow-extension-knowledge-management",
+ "zh-CN",
+ ),
+ knowledgePlatform: bundledCopy(
+ "deerflow-extension-knowledge-platform",
+ "zh-CN",
+ ),
+ knowledgeCenter: bundledCopy(
+ "deerflow-extension-knowledge-center",
+ "zh-CN",
+ ),
+ lightrag: bundledCopy("deerflow-extension-lightrag", "zh-CN"),
+ weknora: bundledCopy("deerflow-extension-weknora", "zh-CN"),
+ example: bundledCopy("deerflow-extension-example", "zh-CN"),
},
pageUnavailable: "扩展页面不可用",
pageUnavailableHint: "此页面未注册,或插件已停用、未能加载。",
diff --git a/frontend/tests/e2e/bookmark-plugin.spec.ts b/frontend/tests/e2e/bookmark-plugin.spec.ts
index 7148630c4d..75b8bbcef8 100644
--- a/frontend/tests/e2e/bookmark-plugin.spec.ts
+++ b/frontend/tests/e2e/bookmark-plugin.spec.ts
@@ -230,10 +230,14 @@ for (const source of ["default", "custom-toolbar", "custom-sidebar"]) {
await expect(
page.getByRole("searchbox", { name: "Search bookmarks" }),
).toHaveCount(0);
+ await expect(page.getByText("Enabled", { exact: true })).toBeVisible();
await expect(
- page.getByText("Enabled · Managed by your administrator", {
- exact: true,
- }),
+ page.getByText(
+ "Your administrator has enabled this extension. It is ready to use.",
+ {
+ exact: true,
+ },
+ ),
).toBeVisible();
await libraryLink.click();
await expect(page).toHaveURL(new RegExp(`${libraryURL}$`));
@@ -475,8 +479,8 @@ for (const locale of ["en-US", "zh-CN"]) {
await expect(management).toHaveCount(1);
await expect(management).toContainText(
zh
- ? "需要安装和配置,不包含管理页面。"
- : "Requires installation and configuration; no management page is included.",
+ ? "导入文件、网址和文本,并查询、管理文档处理任务。"
+ : "Import files, URLs and text, then inspect and manage document processing.",
);
const lightrag = page.getByRole("article").filter({ hasText: "LightRAG" });
await expect(lightrag).toHaveCount(1);
@@ -492,8 +496,8 @@ for (const locale of ["en-US", "zh-CN"]) {
await expect(weknora).toHaveCount(1);
await expect(weknora).toContainText(
zh
- ? "需要管理员安装并配置访问权限。"
- : "Requires administrator installation and access configuration.",
+ ? "检索 WeKnora 知识库和所选文档,并展示来源引用。"
+ : "Search WeKnora knowledge bases and selected documents with source citations.",
);
await weknora
.getByRole("button", {
@@ -503,9 +507,17 @@ for (const locale of ["en-US", "zh-CN"]) {
.click();
await expect(
page.getByRole("link", {
- name: zh ? "查看安装说明" : "Installation guide",
+ name: zh ? "阅读使用说明" : "Read the guide",
}),
).toHaveAttribute("href", /deerflow-extension-weknora#readme$/);
+ await expect(
+ page.getByRole("heading", {
+ name: zh ? "使用前准备" : "Before you start",
+ }),
+ ).toBeVisible();
+ await expect(page.getByRole("article")).toContainText(
+ zh ? "管理员已配置连接" : "administrator-configured connection",
+ );
await page
.getByRole("button", {
name: zh ? "全部扩展" : "All extensions",
@@ -533,7 +545,7 @@ for (const locale of ["en-US", "zh-CN"]) {
).toBeVisible();
await expect(
page.getByRole("link", {
- name: zh ? "查看安装说明" : "Installation guide",
+ name: zh ? "阅读使用说明" : "Read the guide",
}),
).toHaveAttribute("href", /deerflow-extension-knowledge#readme$/);
await page
@@ -547,9 +559,7 @@ for (const locale of ["en-US", "zh-CN"]) {
});
await expect(workflow).toHaveCount(1);
await expect(workflow).toContainText(
- zh
- ? "不支持重启回放或硬金额预算"
- : "No restart replay or hard monetary budget",
+ zh ? "按保留版本确认成果" : "review retained output versions",
);
await workflow
.getByRole("button", {
@@ -559,7 +569,7 @@ for (const locale of ["en-US", "zh-CN"]) {
.click();
await expect(
page.getByRole("link", {
- name: zh ? "查看安装说明" : "Installation guide",
+ name: zh ? "阅读使用说明" : "Read the guide",
}),
).toHaveAttribute("href", /deerflow-extension-workflows#readme$/);
await page
@@ -594,6 +604,12 @@ for (const locale of ["en-US", "zh-CN"]) {
{ exact: true },
),
).toBeVisible();
+ await expect(
+ page.getByText("deerflow-extension-agent-teams", { exact: true }),
+ ).not.toBeVisible();
+ await page
+ .getByText(zh ? "技术信息" : "Technical details", { exact: true })
+ .click();
await expect(
page.getByText("deerflow-extension-agent-teams", { exact: true }),
).toBeVisible();
@@ -605,7 +621,7 @@ for (const locale of ["en-US", "zh-CN"]) {
).toHaveCount(0);
await expect(
page.getByRole("link", {
- name: zh ? "查看安装说明" : "Installation guide",
+ name: zh ? "阅读使用说明" : "Read the guide",
}),
).toHaveAttribute("href", /deerflow-extension-agent-teams#readme$/);
await page
@@ -634,7 +650,7 @@ for (const locale of ["en-US", "zh-CN"]) {
).toBeVisible();
await expect(
page.getByRole("link", {
- name: zh ? "查看安装说明" : "Installation guide",
+ name: zh ? "阅读使用说明" : "Read the guide",
}),
).toHaveAttribute("href", /deerflow-extension-retrieval#readme$/);
await page
@@ -657,7 +673,7 @@ for (const locale of ["en-US", "zh-CN"]) {
.click();
await expect(
page.getByRole("link", {
- name: zh ? "查看安装说明" : "Installation guide",
+ name: zh ? "阅读使用说明" : "Read the guide",
}),
).toHaveAttribute("href", /deerflow-extension-jev-screening#readme$/);
await page
@@ -702,17 +718,17 @@ test("extension detail fits a narrow viewport with its tab in view", async ({
await expect(
page.getByRole("tab", { name: "Extensions", exact: true }),
).toBeInViewport({ ratio: 1 });
+ await page.getByText("Technical details", { exact: true }).click();
await expect(
- page.getByRole("button", {
- name: "Reload extensions (refresh page)",
- exact: true,
- }),
+ page.getByRole("button", { name: "Refresh extensions", exact: true }),
).toBeVisible();
const scroller = await capabilityCenterScroller(page);
expect(
await scroller.evaluate((node) => node.scrollWidth - node.clientWidth),
).toBe(0);
- const note = page.getByRole("note");
+ const note = page
+ .getByRole("complementary")
+ .filter({ hasText: "Usage & management" });
const noteBox = await note.boundingBox();
const textBox = await note
.getByText(
@@ -833,6 +849,6 @@ test("catalog remains discoverable when runtime discovery fails", async ({
page.getByRole("heading", { name: "Knowledge scope example", exact: true }),
).toBeVisible();
await expect(
- page.getByRole("link", { name: "Installation guide" }),
+ page.getByRole("link", { name: "Read the guide", exact: true }),
).toHaveAttribute("href", /deerflow-extension-knowledge#readme$/);
});
diff --git a/frontend/tests/e2e/extension-detail.spec.ts b/frontend/tests/e2e/extension-detail.spec.ts
new file mode 100644
index 0000000000..66ec7927fd
--- /dev/null
+++ b/frontend/tests/e2e/extension-detail.spec.ts
@@ -0,0 +1,189 @@
+import { mkdir } from "node:fs/promises";
+import path from "node:path";
+
+import { expect, test } from "@playwright/test";
+
+import { mockLangGraphAPI } from "./utils/mock-api";
+
+test("backend-only writing instructions and optional prerequisites are visible", async ({
+ page,
+ baseURL,
+}) => {
+ await page
+ .context()
+ .addCookies([{ name: "locale", value: "zh-CN", url: baseURL! }]);
+ mockLangGraphAPI(page, { threads: [] });
+ await page.route("**/api/plugins", (route) => route.fulfill({ json: [] }));
+ await page.setViewportSize({ width: 1512, height: 1000 });
+ await page.goto(
+ "/workspace/capabilities?tab=extensions&extension=community.report-sections",
+ );
+ await expect(page.getByRole("heading", { name: "汇总并下载" })).toBeVisible();
+ await expect(page.getByRole("heading", { name: "使用前准备" })).toHaveCount(
+ 0,
+ );
+ await expect(page.getByRole("heading", { name: "工作区入口" })).toHaveCount(
+ 0,
+ );
+ const directory = process.env.CAPABILITY_SCREENSHOT_DIR;
+ if (directory) {
+ await mkdir(directory, { recursive: true });
+ await page.screenshot({
+ path: path.join(directory, "report-sections-desktop.png"),
+ fullPage: true,
+ });
+ }
+ await page.setViewportSize({ width: 390, height: 844 });
+ expect(
+ await page.evaluate(
+ () => document.documentElement.scrollWidth - innerWidth,
+ ),
+ ).toBe(0);
+ if (directory)
+ await page.screenshot({
+ path: path.join(directory, "report-sections-mobile.png"),
+ fullPage: true,
+ });
+ await page.goto(
+ "/workspace/capabilities?tab=extensions&extension=community.jev-context",
+ );
+ await expect(page.getByRole("heading", { name: "使用前准备" })).toBeVisible();
+ await expect(
+ page.getByRole("heading", { name: "达到阈值时自动处理" }),
+ ).toBeVisible();
+ await page.setViewportSize({ width: 1512, height: 1000 });
+ if (directory)
+ await page.screenshot({
+ path: path.join(directory, "context-pruning-desktop.png"),
+ fullPage: true,
+ });
+});
+
+const modulePath = `/api/plugins/modules/teams.v1/${"a".repeat(64)}.mjs`;
+const moduleSource = `export default {
+ apiVersion: 1, module: "teams.v1",
+ surfaces: [{ id: "teams", slot: "page", title: "Agent teams",
+ navigation: {label: "Agent teams", labelZh: "Agent 团队"},
+ mount(root) {
+ const heading = document.createElement("h2");
+ heading.textContent = "Team workspace loaded";
+ root.append(heading);
+ return {dispose() { heading.remove(); }};
+ }
+ }]
+};`;
+
+for (const locale of ["zh-CN", "en-US"]) {
+ test(`extension detail opens its installed page and fits desktop/mobile (${locale})`, async ({
+ page,
+ baseURL,
+ }) => {
+ test.setTimeout(120_000);
+ const errors: string[] = [];
+ page.on("pageerror", (error) => errors.push(error.message));
+ await page
+ .context()
+ .addCookies([{ name: "locale", value: locale, url: baseURL! }]);
+ mockLangGraphAPI(page, { threads: [] });
+ await page.route("**/api/plugins", (route) =>
+ route.fulfill({
+ json: [
+ {
+ namespace: "community.agent-teams",
+ module: "teams.v1",
+ entry: modulePath,
+ title: "Agent teams",
+ description: "Team work",
+ settings: { enabled: true },
+ },
+ ],
+ }),
+ );
+ await page.route(`**${modulePath}`, (route) =>
+ route.fulfill({ contentType: "text/javascript", body: moduleSource }),
+ );
+ await page.setViewportSize({ width: 1512, height: 1050 });
+ await page.goto(
+ "/workspace/capabilities?tab=extensions&extension=community.agent-teams",
+ );
+ const zh = locale === "zh-CN";
+ const open = page.getByRole("link", {
+ name: zh ? "打开 Agent 团队" : "Open Agent teams",
+ exact: true,
+ });
+ await expect(open).toHaveAttribute(
+ "href",
+ "/workspace/extensions/community.agent-teams/teams",
+ );
+ await expect(
+ page.getByRole("heading", {
+ name: zh ? "组建你的团队" : "Build your team",
+ exact: true,
+ }),
+ ).toBeVisible();
+ await expect(
+ page.getByText("deerflow-extension-agent-teams", { exact: true }),
+ ).not.toBeVisible();
+ const directory = process.env.CAPABILITY_SCREENSHOT_DIR;
+ if (directory) {
+ await mkdir(directory, { recursive: true });
+ await page.screenshot({
+ path: path.join(directory, `extension-detail-${locale}-desktop.png`),
+ fullPage: true,
+ });
+ }
+ await open.click();
+ await expect(page).toHaveURL(
+ /\/workspace\/extensions\/community.agent-teams\/teams$/,
+ { timeout: 30_000 },
+ );
+ await expect(
+ page.getByRole("heading", { name: "Team workspace loaded" }),
+ ).toBeVisible();
+ await page.goto(
+ "/workspace/capabilities?tab=extensions&extension=community.agent-teams",
+ );
+ await page.setViewportSize({ width: 390, height: 844 });
+ await expect(open).toBeInViewport({ ratio: 1 });
+ await expect
+ .poll(() =>
+ page.evaluate(
+ () => document.documentElement.scrollWidth - window.innerWidth,
+ ),
+ )
+ .toBe(0);
+ const article = page.getByRole("article");
+ expect(
+ await article.evaluate((node) => node.scrollWidth - node.clientWidth),
+ ).toBe(0);
+ if (directory)
+ await page.screenshot({
+ path: path.join(directory, `extension-detail-${locale}-mobile.png`),
+ fullPage: true,
+ });
+ await page
+ .getByText(zh ? "技术信息" : "Technical details", { exact: true })
+ .click();
+ await expect(
+ page.getByText("deerflow-extension-agent-teams", { exact: true }),
+ ).toBeVisible();
+ await expect(
+ page.getByRole("button", {
+ name: zh ? "刷新扩展" : "Refresh extensions",
+ exact: true,
+ }),
+ ).toBeVisible();
+ await page
+ .getByRole("button", {
+ name: zh ? "全部扩展" : "All extensions",
+ exact: true,
+ })
+ .click();
+ await expect(
+ page.getByPlaceholder(
+ zh ? "按名称或用途搜索扩展" : "Search extensions by name or purpose",
+ ),
+ ).toBeVisible();
+ expect(errors).toEqual([]);
+ });
+}
diff --git a/frontend/tests/unit/components/workspace/capabilities/extension-gallery.dom.test.tsx b/frontend/tests/unit/components/workspace/capabilities/extension-gallery.dom.test.tsx
index ea1f67937f..ca9084b56f 100644
--- a/frontend/tests/unit/components/workspace/capabilities/extension-gallery.dom.test.tsx
+++ b/frontend/tests/unit/components/workspace/capabilities/extension-gallery.dom.test.tsx
@@ -1,10 +1,18 @@
import { afterEach, beforeEach, describe, expect, it, rs } from "@rstest/core";
-import { cleanup, render, screen, within } from "@testing-library/react";
+import {
+ cleanup,
+ fireEvent,
+ render,
+ screen,
+ within,
+} from "@testing-library/react";
const galleryMock = rs.hoisted(() => ({
search: "",
loaded: [] as unknown[],
replace: rs.fn(),
+ pending: false,
+ error: false,
}));
rs.mock("next/navigation", () => ({
@@ -15,8 +23,8 @@ rs.mock("next/navigation", () => ({
rs.mock("@/core/extensions/hooks", () => ({
useFrontendExtensions: () => ({
data: galleryMock.loaded,
- isPending: false,
- isError: false,
+ isPending: galleryMock.pending,
+ isError: galleryMock.error,
refetch: rs.fn(),
}),
}));
@@ -43,6 +51,8 @@ function contribution(overrides: Record) {
}
beforeEach(() => {
+ galleryMock.pending = false;
+ galleryMock.error = false;
galleryMock.search = "";
galleryMock.loaded = [];
galleryMock.replace.mockClear();
@@ -50,6 +60,126 @@ beforeEach(() => {
afterEach(cleanup);
describe("extension detail", () => {
+ it("shows package-owned usage and omits unspecified requirements", () => {
+ galleryMock.search = "tab=extensions&extension=community.report-sections";
+ render();
+ expect(
+ screen.getByRole("heading", { name: "Assemble and download" }),
+ ).toBeTruthy();
+ expect(
+ screen.queryByRole("heading", { name: "Before you start" }),
+ ).toBeNull();
+ expect(
+ screen.queryByRole("heading", { name: "Workspace pages" }),
+ ).toBeNull();
+ });
+
+ it("renders a backend extension's own documentation without a frontend module", () => {
+ galleryMock.search = "tab=extensions&extension=company.helper";
+ galleryMock.loaded = [
+ contribution({
+ namespace: "company.helper",
+ runtime_only: true,
+ documentation: {
+ schema_version: 1,
+ id: "company.helper",
+ locales: {
+ "en-US": {
+ title: "Background helper",
+ description: "Keeps statistics.",
+ usage: [
+ {
+ title: "Just run a task",
+ description: "Works automatically.",
+ },
+ ],
+ requirements: ["Enable statistics in your deployment."],
+ },
+ },
+ },
+ }),
+ ];
+ render();
+ expect(
+ screen.getByRole("heading", { name: "Background helper" }),
+ ).toBeTruthy();
+ expect(screen.getByText("Works automatically.")).toBeTruthy();
+ expect(
+ screen.getByRole("heading", { name: "Before you start" }),
+ ).toBeTruthy();
+ expect(screen.getByText("Loaded")).toBeTruthy();
+ expect(screen.queryByRole("link", { name: /^Open / })).toBeNull();
+ });
+ it("opens the installed page declared by the extension, not a guessed catalog URL", () => {
+ galleryMock.search = "tab=extensions&extension=community.agent-teams";
+ galleryMock.loaded = [
+ contribution({
+ namespace: "community.agent-teams",
+ extension: {
+ apiVersion: 1,
+ module: "teams.v1",
+ surfaces: [
+ {
+ id: "custom-team-page",
+ slot: "page",
+ title: "Teams",
+ mount: () => ({ dispose: () => undefined }),
+ },
+ ],
+ },
+ }),
+ ];
+ render();
+ expect(
+ screen
+ .getByRole("link", { name: "Open Agent teams" })
+ .getAttribute("href"),
+ ).toBe("/workspace/extensions/community.agent-teams/custom-team-page");
+ expect(
+ within(screen.getAllByRole("list")[0]!).getAllByRole("listitem"),
+ ).toHaveLength(3);
+ });
+
+ it.each([
+ "catalog",
+ "disabled",
+ "failed",
+ "pending",
+ "discovery-error",
+ "no-page",
+ ])("never offers a launch link for %s", (state) => {
+ galleryMock.search = "tab=extensions&extension=community.agent-teams";
+ galleryMock.pending = state === "pending";
+ galleryMock.error = state === "discovery-error";
+ galleryMock.loaded =
+ state === "catalog"
+ ? []
+ : [
+ contribution({
+ namespace: "community.agent-teams",
+ settings: { enabled: state !== "disabled" },
+ error: state === "failed" ? "failed" : undefined,
+ extension: {
+ apiVersion: 1,
+ module: "teams.v1",
+ surfaces:
+ state === "no-page"
+ ? []
+ : [
+ {
+ id: "teams",
+ slot: "page",
+ title: "Teams",
+ mount: () => ({ dispose: () => undefined }),
+ },
+ ],
+ },
+ }),
+ ];
+ render();
+ expect(screen.queryByRole("link", { name: "Open Agent teams" })).toBeNull();
+ });
+
it("shows identity, catalog status and the guide for a catalog entry", () => {
galleryMock.search = "tab=extensions&extension=community.agent-teams";
render();
@@ -60,26 +190,31 @@ describe("extension detail", () => {
expect(screen.getByText(copy.catalog.agentTeams.description)).toBeTruthy();
expect(screen.getByText("deerflow-extension-agent-teams")).toBeTruthy();
expect(screen.getByRole("button", { name: copy.all })).toBeTruthy();
- expect(screen.getByRole("button", { name: copy.reloadAll })).toBeTruthy();
+ fireEvent.click(screen.getByText(copy.detail.technicalDetails));
+ expect(
+ screen.getByRole("button", { name: copy.detail.refresh }),
+ ).toBeTruthy();
- const notice = screen.getByRole("note");
+ const notice = screen.getByRole("complementary");
expect(within(notice).getByText(copy.catalogHint)).toBeTruthy();
expect(
within(notice)
- .getByRole("link", { name: copy.installationGuide })
+ .getByRole("link", { name: copy.detail.guide })
.getAttribute("href"),
).toMatch(/deerflow-extension-agent-teams#readme$/);
});
it.each([
- ["enabled", { settings: { enabled: true } }, copy.enabledManaged],
- ["disabled", { settings: { enabled: false } }, copy.disabledManaged],
- ])("reports a loaded %s extension as a note", (_name, overrides, text) => {
+ ["enabled", { settings: { enabled: true } }, copy.detail.readyHint],
+ ["disabled", { settings: { enabled: false } }, copy.detail.disabledHint],
+ ])("explains the loaded %s state", (_name, overrides, text) => {
galleryMock.search = "tab=extensions&extension=community.bookmarks";
galleryMock.loaded = [contribution(overrides)];
render();
- expect(within(screen.getByRole("note")).getByText(text)).toBeTruthy();
+ expect(
+ within(screen.getByRole("complementary")).getByText(text),
+ ).toBeTruthy();
expect(screen.queryByRole("alert")).toBeNull();
});
@@ -89,7 +224,7 @@ describe("extension detail", () => {
render();
expect(
- within(screen.getByRole("alert")).getByText(copy.moduleUnavailable),
+ within(screen.getByRole("alert")).getByText(copy.detail.failedHint),
).toBeTruthy();
expect(screen.queryByRole("note")).toBeNull();
});
diff --git a/frontend/tests/unit/core/extensions/api.test.ts b/frontend/tests/unit/core/extensions/api.test.ts
index b3a4248b7a..178a2730e9 100644
--- a/frontend/tests/unit/core/extensions/api.test.ts
+++ b/frontend/tests/unit/core/extensions/api.test.ts
@@ -15,10 +15,66 @@ const entry = {
description: "",
};
+const documentation = {
+ schema_version: 1,
+ id: "company.helper",
+ locales: {
+ "en-US": {
+ title: "Helper",
+ description: "Package instructions",
+ usage: [{ title: "Start", description: "Open the helper" }],
+ },
+ },
+};
+
afterEach(() => {
rs.restoreAllMocks();
});
+for (const docs_url of [
+ "https://example.org:bad/docs",
+ "https://example.org:65536/docs",
+ "https://[",
+ "http://example.org/docs",
+ "https://user:password@example.org/docs",
+]) {
+ test(`invalid optional documentation URL is isolated: ${docs_url}`, async () => {
+ const healthy = { ...entry, settings: { enabled: true } };
+ const helper = {
+ ...healthy,
+ namespace: documentation.id,
+ documentation: { ...documentation, docs_url },
+ };
+ rs.spyOn(globalThis, "fetch").mockResolvedValue(
+ new Response(JSON.stringify([healthy, helper])),
+ );
+
+ const discovered = await fetchFrontendExtensions();
+ expect(discovered).toHaveLength(2);
+ expect(discovered[0]).toEqual(healthy);
+ expect(discovered[1]?.namespace).toBe(documentation.id);
+ expect(discovered[1]?.documentation).toBeUndefined();
+ });
+}
+
+test("valid HTTPS documentation with an explicit port is retained", async () => {
+ const helper = {
+ ...entry,
+ settings: {},
+ namespace: documentation.id,
+ documentation: {
+ ...documentation,
+ docs_url: "https://example.org:8443/docs",
+ },
+ };
+ rs.spyOn(globalThis, "fetch").mockResolvedValue(
+ new Response(JSON.stringify([helper])),
+ );
+ expect((await fetchFrontendExtensions())[0]?.documentation).toEqual(
+ helper.documentation,
+ );
+});
+
for (const [label, settings] of [
["empty", {}],
[
diff --git a/frontend/tests/unit/core/extensions/catalog.test.ts b/frontend/tests/unit/core/extensions/catalog.test.ts
index 80e132f6cb..8632a61c78 100644
--- a/frontend/tests/unit/core/extensions/catalog.test.ts
+++ b/frontend/tests/unit/core/extensions/catalog.test.ts
@@ -111,7 +111,7 @@ test("preserves custom deployment extensions and module failures", () => {
);
});
-test("workflow metadata is localized, explains Phase 2 limits and merges its registration", () => {
+test("workflow metadata is localized and merges its registration", () => {
const workflow: LoadedContribution = {
...contribution,
namespace: "community.workflows",
@@ -122,9 +122,9 @@ test("workflow metadata is localized, explains Phase 2 limits and merges its reg
[
enUS.extensions.catalog,
"Script workflows",
- "No restart replay or hard monetary budget",
+ "review retained output versions",
],
- [zhCN.extensions.catalog, "脚本工作流", "不支持重启回放或硬金额预算"],
+ [zhCN.extensions.catalog, "脚本工作流", "按保留版本确认成果"],
] as const) {
const catalogOnly = extensionDirectory([], copy).find(
(entry) => entry.package === "deerflow-extension-workflows",
@@ -213,3 +213,68 @@ test("knowledge platform discovery is localized and does not imply a management
expect(entry?.loaded).toBeUndefined();
}
});
+
+test("installed documentation clears bundled requirements while retaining the README fallback", () => {
+ for (const requirements of [undefined, []]) {
+ const loaded = {
+ ...contribution,
+ documentation: {
+ schema_version: 1 as const,
+ id: contribution.namespace,
+ locales: {
+ en: {
+ title: "Installed",
+ description: "Current package",
+ usage: [{ title: "Start", description: "Use the package" }],
+ ...(requirements ? { requirements } : {}),
+ },
+ },
+ },
+ };
+ const entry = extensionDirectory([loaded], enUS.extensions.catalog).find(
+ (item) => item.id === loaded.namespace,
+ )!;
+ expect(entry.title).toBe("Installed");
+ expect(entry.requirements ?? []).toEqual([]);
+ expect(entry.guide).toContain("deerflow-extension-bookmarks#readme");
+ }
+});
+
+test("documentation matches language tags by case-insensitive exact match then language", () => {
+ const page = {
+ ...contribution,
+ documentation: {
+ schema_version: 1 as const,
+ id: contribution.namespace,
+ locales: {
+ "en-us": {
+ title: "English",
+ description: "English",
+ usage: [{ title: "Start", description: "Start" }],
+ },
+ zh: {
+ title: "中文",
+ description: "中文",
+ usage: [{ title: "开始", description: "开始" }],
+ },
+ "zh-cn": {
+ title: "简体",
+ description: "简体",
+ usage: [{ title: "开始", description: "开始" }],
+ },
+ },
+ },
+ };
+ for (const [locale, title] of [
+ ["en-US", "English"],
+ ["zh-CN", "简体"],
+ ["zh-TW", "中文"],
+ ["fr-FR", "English"],
+ ]) {
+ expect(
+ extensionDirectory([page], enUS.extensions.catalog, locale).find(
+ (item) => item.id === page.namespace,
+ )?.title,
+ ).toBe(title);
+ }
+});
diff --git a/scripts/AGENTS.md b/scripts/AGENTS.md
index 604ee99da5..4616ff4f6c 100644
--- a/scripts/AGENTS.md
+++ b/scripts/AGENTS.md
@@ -25,7 +25,7 @@ path variables and compare storage defaults with the launcher's actual shell blo
## Dependency and Label Checks
-`check_extension.py`: Gateway wheel asset validator; example CI:
+`check_extension.py`: Wheel asset/documentation validator; example CI:
`extension-package-check.yml`. Tests: `backend/tests/test_extension_check_script.py`.
`check.py`: UTF-8 replacement decoding; pnpm uses
diff --git a/scripts/check_extension.py b/scripts/check_extension.py
index 2dfb46f7d7..6065d507c3 100644
--- a/scripts/check_extension.py
+++ b/scripts/check_extension.py
@@ -9,6 +9,7 @@ from __future__ import annotations
import argparse
import ast
import configparser
+import json
import subprocess
import sys
import tempfile
@@ -23,6 +24,11 @@ from packaging.utils import canonicalize_name
from deerflow.extensions.browser_assets import load_browser_assets
from deerflow.extensions.manager import _validate_entry_point
+from deerflow.extensions.documentation import (
+ FILENAME,
+ MAX_BYTES,
+ validate_documentation,
+)
def check_wheel(source: Path, wheel: Path, *, manifests: Sequence[str] = ()) -> None:
@@ -90,6 +96,22 @@ def check_wheel(source: Path, wheel: Path, *, manifests: Sequence[str] = ()) ->
if name.endswith(".py"):
ast.parse(archive.read(name), filename=name)
+ module_file = (
+ f"{module_path}/__init__.py"
+ if f"{module_path}/__init__.py" in names
+ else f"{module_path}.py"
+ )
+ documentation_path = str(PurePosixPath(module_file).parent / FILENAME)
+ for import_root in (source, source / "src"):
+ if (
+ import_root / documentation_path
+ ).is_file() and documentation_path not in names:
+ raise ValueError("Extension documentation is not packaged")
+ if documentation_path in names:
+ if archive.getinfo(documentation_path).file_size > MAX_BYTES:
+ raise ValueError("Extension documentation exceeds 64 KiB")
+ validate_documentation(json.loads(archive.read(documentation_path)))
+
# Existing examples use flat or src-layout packages and ui_manifest.json.
# Custom manifest names are explicit rather than guessing from plugin code.
manifest_paths = set(manifests)
diff --git a/scripts/sync_extension_documentation.py b/scripts/sync_extension_documentation.py
new file mode 100644
index 0000000000..5a1a1ee4aa
--- /dev/null
+++ b/scripts/sync_extension_documentation.py
@@ -0,0 +1,47 @@
+"""Validate package documentation and regenerate the frontend's discovery copy."""
+
+import argparse
+import json
+import runpy
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+CONTRACT = runpy.run_path(
+ str(ROOT / "backend/packages/harness/deerflow/extensions/documentation.py")
+)
+VALIDATE = CONTRACT["validate_documentation"]
+MAX_BYTES = CONTRACT["MAX_BYTES"]
+DESTINATION = ROOT / "frontend/src/core/extensions/bundled-documentation.json"
+
+
+def render():
+ pages = {}
+ for path in sorted(
+ (ROOT / "examples").glob("deerflow-extension-*/*/deerflow-extension.json")
+ ):
+ raw = path.read_bytes()
+ if len(raw) > MAX_BYTES:
+ raise ValueError(f"Documentation is too large: {path}")
+ page = VALIDATE(json.loads(raw))
+ languages = {tag.lower().split("-")[0] for tag in page["locales"]}
+ if not {"en", "zh"}.issubset(languages):
+ raise ValueError(f"Bundled documentation requires English and Chinese: {path}")
+ if any(existing["id"] == page["id"] for existing in pages.values()):
+ raise ValueError(f"Duplicate extension documentation id: {page['id']}")
+ pages[path.parent.parent.name] = page
+ return json.dumps(pages, ensure_ascii=False, indent=2) + "\n"
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--check", action="store_true")
+ args = parser.parse_args()
+ content = render()
+ if args.check:
+ if DESTINATION.read_text(encoding="utf-8") != content:
+ parser.exit(
+ 1,
+ "Extension copy is stale; run python scripts/sync_extension_documentation.py\n",
+ )
+ else:
+ DESTINATION.write_text(content, encoding="utf-8")