Route WebUI quick start through channel config

This commit is contained in:
chengyongru 2026-06-18 17:39:01 +08:00 committed by Xubin Ren
parent 0097488eed
commit f9f5a19910
3 changed files with 40 additions and 11 deletions

View File

@ -190,14 +190,14 @@ For the first setup, choose `[Q] Quick Start (recommended)`. It asks for the mod
If you are following the OpenRouter example: If you are following the OpenRouter example:
1. Choose `[Q] Quick Start (recommended)`. 1. Choose `[Q] Quick Start (recommended)`.
2. Choose `No chat channel yet (recommended)` unless you already have a Telegram, Feishu/Lark, Slack, or Discord bot token ready. 2. Choose `No chat channel yet` unless you already want to configure WebUI, Telegram, Feishu/Lark, Slack, or Discord now.
3. Select OpenRouter. 3. Select OpenRouter.
4. Paste your OpenRouter API key. 4. Paste your OpenRouter API key.
5. Enter a model ID, for example `anthropic/claude-sonnet-4.5`. 5. Enter a model ID, for example `anthropic/claude-sonnet-4.5`.
6. Review the Quick Start summary. 6. Review the Quick Start summary.
7. Choose `[S] Save and Exit`. 7. Choose `[S] Save and Exit`.
Enable WebUI later from `[C] Chat Channel` -> `WebSocket` after the first local reply works. If you choose `WebUI / local browser`, the wizard will open the WebSocket settings before it enables WebUI.
If OpenRouter says your account cannot use that model, use another OpenRouter model ID that your account can access. If OpenRouter says your account cannot use that model, use another OpenRouter model ID that your account can access.

View File

@ -54,7 +54,8 @@ _BACK_PRESSED = object() # Sentinel value for back navigation
_MODEL_PRESET_CACHE: set[str] = set() _MODEL_PRESET_CACHE: set[str] = set()
_QUICK_START_TARGETS = { _QUICK_START_TARGETS = {
"No chat channel yet (recommended)": None, "WebUI / local browser (recommended)": "websocket",
"No chat channel yet": None,
"Telegram": "telegram", "Telegram": "telegram",
"Feishu / Lark": "feishu", "Feishu / Lark": "feishu",
"Slack": "slack", "Slack": "slack",
@ -414,7 +415,7 @@ def _show_main_menu_header() -> None:
body = Table.grid(expand=True) body = Table.grid(expand=True)
body.add_column(ratio=1) body.add_column(ratio=1)
body.add_row(f"{__logo__} [bold {_UI_TEXT}]nanobot[/] [{_UI_MUTED}]v{__version__}[/]") body.add_row(f"{__logo__} [bold {_UI_TEXT}]nanobot[/] [{_UI_MUTED}]v{__version__}[/]")
body.add_row(f"[{_UI_ACCENT}]Quick Start configures one AI model first.[/]") body.add_row(f"[{_UI_ACCENT}]Quick Start configures one AI model and one entry point.[/]")
body.add_row( body.add_row(
f"[{_UI_MUTED}]WebUI, chat channels, and advanced settings stay available when you need them.[/]" f"[{_UI_MUTED}]WebUI, chat channels, and advanced settings stay available when you need them.[/]"
) )
@ -1443,6 +1444,19 @@ def _configure_quick_start_channel(config: Config, channel_name: str | None) ->
current = getattr(config.channels, channel_name, None) or {} current = getattr(config.channels, channel_name, None) or {}
model = config_cls.model_validate(current) model = config_cls.model_validate(current)
if channel_name == "websocket":
enable = _input_bool("Enable WebSocket channel for local WebUI", True)
if not enable:
console.print("[yellow]! WebSocket channel was not enabled[/yellow]")
return False
if hasattr(model, "enabled"):
setattr(model, "enabled", True)
updated = _configure_pydantic_model(model, "WebSocket")
if updated is None:
return False
setattr(config.channels, channel_name, updated.model_dump(by_alias=True, exclude_none=True))
return True
required_fields = _QUICK_START_CHANNEL_FIELDS.get(channel_name, ()) required_fields = _QUICK_START_CHANNEL_FIELDS.get(channel_name, ())
for field_name, prompt in required_fields: for field_name, prompt in required_fields:
value = _input_with_existing(prompt, getattr(model, field_name, ""), "str") value = _input_with_existing(prompt, getattr(model, field_name, ""), "str")
@ -1472,9 +1486,15 @@ def _show_quick_start_summary(config: Config, channel_name: str | None) -> None:
("Provider", preset.provider if preset else "[not set]"), ("Provider", preset.provider if preset else "[not set]"),
("Model", preset.model if preset else "[not set]"), ("Model", preset.model if preset else "[not set]"),
("Entry point", "Not enabled yet" if channel_name is None else channel_name), ("Entry point", "Not enabled yet" if channel_name is None else channel_name),
("Next", "Save, then run `nanobot agent -m \"Hello!\"`"), (
("WebUI", "Use [C] Chat Channel -> WebSocket when you are ready"), "Next",
"Save, then run `nanobot gateway`"
if channel_name
else "Save, then run `nanobot agent -m \"Hello!\"`",
),
] ]
if channel_name == "websocket":
rows.append(("WebUI", "Open http://127.0.0.1:8765 after the gateway starts"))
_print_summary_panel(rows, "Quick Start") _print_summary_panel(rows, "Quick Start")
@ -1489,7 +1509,7 @@ def _configure_quick_start(config: Config) -> None:
answer = _select_with_back( answer = _select_with_back(
"How do you want to use nanobot first?", "How do you want to use nanobot first?",
list(_QUICK_START_TARGETS) + ["<- Back"], list(_QUICK_START_TARGETS) + ["<- Back"],
default="No chat channel yet (recommended)", default="WebUI / local browser (recommended)",
) )
if answer is _BACK_PRESSED or answer is None or answer == "<- Back": if answer is _BACK_PRESSED or answer is None or answer == "<- Back":
return return

View File

@ -926,18 +926,25 @@ class TestMainMenuUpdate:
assert telegram["enabled"] is True assert telegram["enabled"] is True
assert telegram["token"] == "123:abc" assert telegram["token"] == "123:abc"
def test_quick_start_recommended_path_does_not_enable_channel(self, monkeypatch): def test_quick_start_webui_opens_websocket_config(self, monkeypatch):
"""The recommended Quick Start target should leave chat surfaces as explicit opt-ins.""" """The recommended WebUI path should explicitly route through WebSocket config."""
config = Config() config = Config()
selections = iter(["No chat channel yet (recommended)", "OpenRouter"]) selections = iter(["WebUI / local browser (recommended)", "OpenRouter"])
def fake_select_with_back(*_args, **_kwargs): def fake_select_with_back(*_args, **_kwargs):
return next(selections) return next(selections)
def fake_configure(model, display_name):
assert display_name == "WebSocket"
assert model.enabled is True
return model
monkeypatch.setattr(onboard_wizard.console, "clear", lambda: None) monkeypatch.setattr(onboard_wizard.console, "clear", lambda: None)
monkeypatch.setattr(onboard_wizard, "_show_section_header", lambda *a, **kw: None) monkeypatch.setattr(onboard_wizard, "_show_section_header", lambda *a, **kw: None)
monkeypatch.setattr(onboard_wizard, "_select_with_back", fake_select_with_back) monkeypatch.setattr(onboard_wizard, "_select_with_back", fake_select_with_back)
monkeypatch.setattr(onboard_wizard, "_input_with_existing", lambda *a, **kw: "sk-or-test") monkeypatch.setattr(onboard_wizard, "_input_with_existing", lambda *a, **kw: "sk-or-test")
monkeypatch.setattr(onboard_wizard, "_input_bool", lambda *a, **kw: True)
monkeypatch.setattr(onboard_wizard, "_configure_pydantic_model", fake_configure)
monkeypatch.setattr( monkeypatch.setattr(
onboard_wizard, onboard_wizard,
"_input_model_with_autocomplete", "_input_model_with_autocomplete",
@ -948,7 +955,9 @@ class TestMainMenuUpdate:
onboard_wizard._configure_quick_start(config) onboard_wizard._configure_quick_start(config)
assert getattr(config.channels, "websocket", None) is None websocket = getattr(config.channels, "websocket")
assert websocket["enabled"] is True
assert websocket["websocketRequiresToken"] is True
assert config.agents.defaults.model_preset == "primary" assert config.agents.defaults.model_preset == "primary"
def test_quick_start_channel_requires_token_before_enable(self, monkeypatch): def test_quick_start_channel_requires_token_before_enable(self, monkeypatch):