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
+31 -1
View File
@@ -132,6 +132,36 @@ def bus() -> MagicMock:
return b
@pytest.mark.asyncio
async def test_start_extends_http_open_timeout_for_slow_settings_routes(
bus,
monkeypatch,
) -> None:
import nanobot.channels.websocket as websocket_module
channel = _ch(bus, port=0)
seen: dict[str, Any] = {}
class Server:
def close(self) -> None:
pass
async def wait_closed(self) -> None:
pass
async def fake_serve(*args: Any, **kwargs: Any) -> Server:
seen.update(kwargs)
assert channel._stop_event is not None
channel._stop_event.set()
return Server()
monkeypatch.setattr(websocket_module, "serve", fake_serve)
await channel.start()
assert seen["open_timeout"] >= 300
@pytest.fixture(autouse=True)
def isolate_webui_workspace_state(tmp_path, monkeypatch) -> None:
monkeypatch.setattr("nanobot.config.paths.get_data_dir", lambda: tmp_path)
@@ -285,7 +315,7 @@ def test_ssl_context_requires_both_cert_and_key_files() -> None:
def test_default_config_includes_safe_bind_and_streaming() -> None:
defaults = WebSocketChannel.default_config()
assert defaults["enabled"] is False
assert defaults["enabled"] is True
assert defaults["host"] == "127.0.0.1"
assert defaults["streaming"] is True
assert defaults["allowFrom"] == ["*"]