From 1dbec3da50a5457bd4ea17bf595f56a7ad248374 Mon Sep 17 00:00:00 2001 From: chengyongru <2755839590@qq.com> Date: Fri, 19 Jun 2026 17:49:45 +0800 Subject: [PATCH] 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. --- docs/start-without-technical-background.md | 18 ++- nanobot/cli/onboard.py | 130 ++++++++++++++++++--- tests/agent/test_onboard_logic.py | 75 +++++++++++- 3 files changed, 194 insertions(+), 29 deletions(-) diff --git a/docs/start-without-technical-background.md b/docs/start-without-technical-background.md index 7a39f845..d5a383e4 100644 --- a/docs/start-without-technical-background.md +++ b/docs/start-without-technical-background.md @@ -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: diff --git a/nanobot/cli/onboard.py b/nanobot/cli/onboard.py index a481941f..7239725d 100644 --- a/nanobot/cli/onboard.py +++ b/nanobot/cli/onboard.py @@ -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"], diff --git a/tests/agent/test_onboard_logic.py b/tests/agent/test_onboard_logic.py index 5341d346..3fa919b0 100644 --- a/tests/agent/test_onboard_logic.py +++ b/tests/agent/test_onboard_logic.py @@ -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()