Add optional Nanobot plugin controls (#4396)

* feat: add optional nanobot features

* test: update azure install hint expectation

* fix: validate optional feature extras

maintainer edit: verify requested dependency extras before treating optional features as installed, propagate restart state from feature enablement, and align docs with the new plugins enable command.

* fix: bound optional feature installs

maintainer edit: make optional feature installs time out as a normal install failure instead of leaving the WebUI or CLI action waiting indefinitely.

* feat: slim optional channel dependencies

* fix: log optional install commands

* fix(webui): gate remote feature installs

* docs: clarify webhook plugin example

* fix(webui): harden optional feature installs

* fix: install optional deps without package fallback

* fix(cli): refine plugin feature controls

* fix(webui): count enabled nanobot features

* fix(webui): allow slow feature install routes

* fix(webui): allow disabling websocket channel

* fix(plugins): simplify optional feature controls

* fix(webui): polish apps catalog states

* fix(webui): confirm nanobot support installs

* fix(webui): polish nanobot install dialog

* fix(webui): suppress empty websocket handshakes

* fix(webui): clarify apps plugin summary

* fix(webui): localize workspace access copy

* fix(plugins): polish optional feature controls (#4691)

---------

Co-authored-by: Xubin Ren <52506698+Re-bin@users.noreply.github.com>
This commit is contained in:
chengyongru
2026-07-03 18:17:52 +08:00
committed by GitHub
co-authored by Xubin Ren
parent 00cc0da530
commit 5283ceae85
61 changed files with 3061 additions and 258 deletions
+8 -2
View File
@@ -11,6 +11,7 @@ if TYPE_CHECKING:
from nanobot.channels.base import BaseChannel
_INTERNAL = frozenset({"base", "manager", "registry"})
DEFAULT_ENABLED_CHANNELS = frozenset({"websocket"})
def discover_channel_names() -> list[str]:
@@ -57,6 +58,7 @@ def discover_enabled(
*,
_names: list[str] | None = None,
_include_all_external: bool = False,
warn_import_errors: bool = False,
) -> dict[str, type[BaseChannel]]:
"""Return channels whose module names are in *enabled_names*.
@@ -72,10 +74,14 @@ def discover_enabled(
try:
result[modname] = load_channel_class(modname)
except ImportError as e:
logger.debug("Skipping built-in channel '{}': {}", modname, e)
message = "Enabled built-in channel '{}' is not available: {}"
if warn_import_errors:
logger.warning(message, modname, e)
else:
logger.debug(message, modname, e)
external = discover_plugins(None if _include_all_external else enabled_names)
shadowed = set(external) & set(result)
shadowed = set(external) & set(names)
if shadowed:
logger.warning("Plugin(s) shadowed by built-in channels (ignored): {}", shadowed)
if _include_all_external: