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
@@ -21,7 +21,11 @@ from unittest.mock import AsyncMock
import pytest
from nanobot.bus.events import OutboundMessage
from nanobot.bus.outbound_events import ProgressEvent, outbound_message_for_event
from nanobot.bus.outbound_events import (
ProgressEvent,
outbound_event_from_message,
outbound_message_for_event,
)
from nanobot.bus.queue import MessageBus
from nanobot.channels.base import BaseChannel
from nanobot.channels.manager import ChannelManager
@@ -60,7 +64,8 @@ class _MockChannel(BaseChannel):
@pytest.fixture
def manager() -> ChannelManager:
mgr = ChannelManager(Config(), MessageBus())
config = Config.model_validate({"channels": {"websocket": {"enabled": False}}})
mgr = ChannelManager(config, MessageBus())
mgr.channels["mock"] = _MockChannel({}, mgr.bus)
return mgr
@@ -291,14 +296,22 @@ async def test_reasoning_routing_does_not_consult_send_progress(manager):
async def _pump_one(manager: ChannelManager) -> None:
"""Drive the dispatcher until the outbound queue drains, then cancel."""
task = asyncio.create_task(manager._dispatch_outbound())
for _ in range(50):
await asyncio.sleep(0.01)
if manager.bus.outbound.qsize() == 0:
"""Process currently queued messages through the reasoning dispatch branch."""
async def dispatch_one(msg: OutboundMessage) -> None:
event = outbound_event_from_message(msg)
if isinstance(event, ProgressEvent) and (
event.reasoning_delta
or event.reasoning_end
or event.reasoning
):
channel = manager.channels.get(msg.channel)
if channel is not None and channel.show_reasoning:
await manager._send_with_retry(channel, msg)
await dispatch_one(await asyncio.wait_for(manager.bus.consume_outbound(), timeout=1.0))
while True:
try:
await dispatch_one(manager.bus.outbound.get_nowait())
except asyncio.QueueEmpty:
break
task.cancel()
try:
await task
except asyncio.CancelledError:
pass