fix: simplify beginner quick start

Maintainer edit: reduce the default onboarding path to a recommended local WebUI setup that only asks for an OpenRouter key, while keeping the detailed provider/channel flow available for advanced setup.
This commit is contained in:
chengyongru
2026-06-22 13:04:05 +08:00
committed by Xubin Ren
parent 84143d31b2
commit 1dbec3da50
3 changed files with 194 additions and 29 deletions
+8 -10
View File
@@ -62,7 +62,7 @@ If `python3` works but `python` does not, replace `python` with `python3` in the
## 3. Get a Provider API Key
nanobot does not create AI accounts or API keys for you. Use an AI provider account, company endpoint, subscription endpoint, or local model server that you already control. The steps below use OpenRouter only as a concrete example so the commands and wizard choices have real names; it is not a ranking, default choice, or endorsement.
nanobot does not create AI accounts or API keys for you. Use an AI provider account, company endpoint, subscription endpoint, or local model server that you already control. The steps below use OpenRouter because it is the recommended beginner path in the wizard; it is not a ranking or endorsement.
If you use another provider, keep the same shape but replace the provider name, API key, and model ID with values from that provider. [`provider-cookbook.md`](./provider-cookbook.md) has copyable snippets for several common patterns.
@@ -185,21 +185,19 @@ Move through the wizard like this:
| A field you do not need | Keep the shown default or leave it blank, then press `Enter`. |
| A back option | Choose it to return to the previous menu. |
For the first setup, choose `[Q] Quick Start (recommended)`. It asks for the model provider, API key, model ID, and optionally one chat channel. The other menu items are advanced settings.
For the first setup, choose `[Q] Quick Start (recommended)`. It has a recommended path for a local browser UI and an advanced path where you can choose every detail yourself. The other main menu items are advanced settings.
If you are following the OpenRouter example:
1. Choose `[Q] Quick Start (recommended)`.
2. Choose `No chat channel yet` unless you already want to configure WebUI, Telegram, WeChat, WhatsApp, Feishu/Lark, Slack, or Discord now.
3. Select OpenRouter.
4. Paste your OpenRouter API key.
5. Enter a model ID, for example `anthropic/claude-sonnet-4.5`.
6. Review the Quick Start summary.
7. Choose `[S] Save and Exit`.
2. Choose `Recommended: local WebUI`.
3. Paste your OpenRouter API key, or press `Enter` and add it to the config file later.
4. Review the Quick Start summary.
5. Choose `[S] Save and Exit`.
If you choose `WebUI / local browser`, the wizard will open the WebSocket settings before it enables WebUI.
The recommended path enables the local WebUI with default WebSocket settings and uses a built-in model choice. You do not need to choose a model ID for the first run.
If OpenRouter says your account cannot use that model, use another OpenRouter model ID that your account can access.
If OpenRouter later says your account cannot use the built-in model choice, return to the wizard, choose `[Q] Quick Start (recommended)`, then choose `Choose provider and entry point` and enter another OpenRouter model ID that your account can access.
If you are using another provider, use the same wizard choices but substitute that provider's values:
+116 -14
View File
@@ -64,6 +64,10 @@ _QUICK_START_TARGETS = {
"Slack": "slack",
"Discord": "discord",
}
_QUICK_START_RECOMMENDED_CHOICE = "Recommended: local WebUI"
_QUICK_START_CUSTOM_CHOICE = "Choose provider and entry point"
_QUICK_START_RECOMMENDED_PROVIDER = "openrouter"
_QUICK_START_RECOMMENDED_MODEL = "anthropic/claude-sonnet-4.5"
_QUICK_START_CHANNEL_FIELDS = {
"telegram": (("token", "Telegram bot token from BotFather"),),
@@ -78,7 +82,7 @@ _QUICK_START_CHANNEL_FIELDS = {
"discord": (("token", "Discord bot token"),),
}
_QUICK_START_STEPS = ("Entry point", "AI provider", "Channel", "Review")
_QUICK_START_STEPS = ("Setup", "AI provider", "Entry point", "Review")
# Low-contrast terminal palette inspired by JetBrains Darcula/Islands.
_UI_ACCENT = "#6B9BFA"
@@ -1425,6 +1429,17 @@ def _quick_start_model_default(config: Config, provider_name: str) -> str:
return ""
def _set_primary_quick_start_preset(config: Config, provider_name: str, model: str) -> None:
"""Store the primary preset used by Quick Start."""
config.model_presets["primary"] = ModelPresetConfig(
label="Primary",
model=model,
provider=provider_name,
)
config.agents.defaults.model_preset = "primary"
_sync_preset_cache(config)
def _show_quick_start_progress(active_step: int) -> None:
"""Render a compact step tracker for Quick Start."""
parts = []
@@ -1494,13 +1509,54 @@ def _configure_quick_start_provider(config: Config) -> bool:
console.print("[yellow]! Model ID is required for Quick Start[/yellow]")
return False
config.model_presets["primary"] = ModelPresetConfig(
label="Primary",
model=model,
provider=provider_name,
_set_primary_quick_start_preset(config, provider_name, model)
return True
def _configure_recommended_provider(config: Config) -> bool:
"""Configure the beginner path provider with one API-key prompt."""
_show_quick_start_progress(2)
provider_name = _QUICK_START_RECOMMENDED_PROVIDER
provider_config = getattr(config.providers, provider_name, None)
if provider_config is None:
console.print(f"[red]Unknown provider: {provider_name}[/red]")
return False
_display, _is_gateway, _is_local, default_api_base = _get_provider_info().get(
provider_name, (provider_name, False, False, "")
)
config.agents.defaults.model_preset = "primary"
_sync_preset_cache(config)
if default_api_base and not provider_config.api_base:
provider_config.api_base = default_api_base
api_key = _input_with_existing(
"OpenRouter API key (get one at https://openrouter.ai/keys; Enter to add later)",
provider_config.api_key,
"str",
)
if api_key is not None:
provider_config.api_key = api_key or None
_set_primary_quick_start_preset(
config,
provider_name,
_QUICK_START_RECOMMENDED_MODEL,
)
return True
def _enable_quick_start_websocket_defaults(config: Config) -> bool:
"""Enable local WebUI with the default WebSocket settings."""
_show_quick_start_progress(3)
config_cls = _get_channel_config_class("websocket")
if config_cls is None:
console.print("[red]No configuration class found for websocket[/red]")
return False
current = getattr(config.channels, "websocket", None) or {}
model = config_cls.model_validate(current)
if hasattr(model, "enabled"):
setattr(model, "enabled", True)
setattr(config.channels, "websocket", model.model_dump(by_alias=True, exclude_none=True))
return True
@@ -1560,17 +1616,39 @@ def _show_quick_start_summary(config: Config, channel_name: str | None) -> None:
"""Show the small summary users need before returning to the menu."""
_show_quick_start_progress(4)
preset = config.model_presets.get("primary")
api_key_status = None
if preset:
provider_config = getattr(config.providers, preset.provider, None)
_display, _is_gateway, is_local, _api_base = _get_provider_info().get(
preset.provider, (preset.provider, False, False, "")
)
if not is_local:
api_key_status = (
"configured"
if provider_config and provider_config.api_key
else "add later"
)
next_step = (
"Save, then run `nanobot gateway`"
if channel_name
else "Save, then run `nanobot agent -m \"Hello!\"`"
)
if api_key_status == "add later":
next_step = (
"Save, add your API key to config, then run `nanobot gateway`"
if channel_name
else "Save, add your API key to config, then run `nanobot agent -m \"Hello!\"`"
)
rows = [
("Provider", preset.provider 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),
(
"Next",
"Save, then run `nanobot gateway`"
if channel_name
else "Save, then run `nanobot agent -m \"Hello!\"`",
),
("Next", next_step),
]
if api_key_status:
rows.insert(2, ("API key", api_key_status))
if channel_name == "websocket":
rows.append(("WebUI", "Open http://127.0.0.1:8765 after the gateway starts"))
_print_summary_panel(rows, "Quick Start")
@@ -1581,9 +1659,33 @@ def _configure_quick_start(config: Config) -> None:
console.clear()
_show_section_header(
"Quick Start",
"Set up one AI model and optionally one chat channel. Advanced settings stay unchanged.",
"Start with the local browser UI, or choose each detail yourself.",
)
_show_quick_start_progress(1)
answer = _select_with_back(
"Choose setup:",
[_QUICK_START_RECOMMENDED_CHOICE, _QUICK_START_CUSTOM_CHOICE, "<- Back"],
default=_QUICK_START_RECOMMENDED_CHOICE,
)
if answer is _BACK_PRESSED or answer is None or answer == "<- Back":
return
assert isinstance(answer, str)
if answer == _QUICK_START_RECOMMENDED_CHOICE:
if not _configure_recommended_provider(config):
_pause()
return
if not _enable_quick_start_websocket_defaults(config):
_pause()
return
_show_quick_start_summary(config, "websocket")
_pause()
return
if answer != _QUICK_START_CUSTOM_CHOICE:
return
answer = _select_with_back(
"How do you want to use nanobot first?",
list(_QUICK_START_TARGETS) + ["<- Back"],
+70 -5
View File
@@ -22,7 +22,7 @@ from nanobot.cli.onboard import (
_input_text,
run_onboard,
)
from nanobot.config.schema import Config
from nanobot.config.schema import Config, ModelPresetConfig
from nanobot.utils.helpers import sync_workspace_templates
@@ -903,7 +903,11 @@ class TestMainMenuUpdate:
def test_quick_start_configures_primary_preset_and_telegram(self, monkeypatch):
"""Quick Start should set only the minimum provider, model and channel fields."""
config = Config()
selections = iter(["Telegram", "OpenRouter"])
selections = iter([
onboard_wizard._QUICK_START_CUSTOM_CHOICE,
"Telegram",
"OpenRouter",
])
def fake_select_with_back(*_args, **_kwargs):
return next(selections)
@@ -938,10 +942,14 @@ class TestMainMenuUpdate:
assert telegram["enabled"] is True
assert telegram["token"] == "123:abc"
def test_quick_start_webui_opens_websocket_config(self, monkeypatch):
"""The recommended WebUI path should explicitly route through WebSocket config."""
def test_quick_start_custom_webui_opens_websocket_config(self, monkeypatch):
"""The custom WebUI path should still expose WebSocket settings."""
config = Config()
selections = iter(["WebUI / local browser (recommended)", "OpenRouter"])
selections = iter([
onboard_wizard._QUICK_START_CUSTOM_CHOICE,
"WebUI / local browser (recommended)",
"OpenRouter",
])
def fake_select_with_back(*_args, **_kwargs):
return next(selections)
@@ -972,6 +980,63 @@ class TestMainMenuUpdate:
assert websocket["websocketRequiresToken"] is True
assert config.agents.defaults.model_preset == "primary"
def test_quick_start_recommended_webui_skips_advanced_prompts(self, monkeypatch):
"""The beginner path should only ask for the API key and use safe defaults."""
config = Config()
def fail_model_input(*_args, **_kwargs):
raise AssertionError("recommended Quick Start should not ask for a model ID")
def fail_websocket_config(*_args, **_kwargs):
raise AssertionError("recommended Quick Start should not open WebSocket settings")
monkeypatch.setattr(onboard_wizard.console, "clear", lambda: None)
monkeypatch.setattr(onboard_wizard, "_show_section_header", lambda *a, **kw: None)
monkeypatch.setattr(
onboard_wizard,
"_select_with_back",
lambda *_args, **_kwargs: onboard_wizard._QUICK_START_RECOMMENDED_CHOICE,
)
monkeypatch.setattr(onboard_wizard, "_input_with_existing", lambda *a, **kw: "sk-or-test")
monkeypatch.setattr(onboard_wizard, "_input_model_with_autocomplete", fail_model_input)
monkeypatch.setattr(onboard_wizard, "_configure_pydantic_model", fail_websocket_config)
monkeypatch.setattr(onboard_wizard, "_print_summary_panel", lambda *a, **kw: None)
monkeypatch.setattr(onboard_wizard, "_pause", lambda: None)
onboard_wizard._configure_quick_start(config)
assert config.providers.openrouter.api_key == "sk-or-test"
assert config.providers.openrouter.api_base == "https://openrouter.ai/api/v1"
assert config.agents.defaults.model_preset == "primary"
assert config.model_presets["primary"].provider == "openrouter"
assert config.model_presets["primary"].model == onboard_wizard._QUICK_START_RECOMMENDED_MODEL
websocket = getattr(config.channels, "websocket")
assert websocket["enabled"] is True
assert websocket["websocketRequiresToken"] is True
def test_quick_start_summary_calls_out_missing_api_key(self, monkeypatch):
"""Quick Start summary should not tell users to run gateway before adding a key."""
config = Config()
config.model_presets["primary"] = ModelPresetConfig(
model=onboard_wizard._QUICK_START_RECOMMENDED_MODEL,
provider="openrouter",
)
captured: dict[str, list[tuple[str, str]]] = {}
monkeypatch.setattr(onboard_wizard, "_show_quick_start_progress", lambda *_args: None)
monkeypatch.setattr(
onboard_wizard,
"_print_summary_panel",
lambda rows, _title: captured.setdefault("rows", rows),
)
onboard_wizard._show_quick_start_summary(config, "websocket")
rows = dict(captured["rows"])
assert rows["API key"] == "add later"
assert "add your API key" in rows["Next"]
def test_quick_start_channel_requires_token_before_enable(self, monkeypatch):
"""Quick Start should not enable token-based channels with blank credentials."""
config = Config()