fix: secure quick start webui setup

Add Quick Start endpoint choices for subscription plan providers, require explicit WebSocket confirmation, and require a WebUI password when enabling the WebSocket channel. Update docs to route Quick Start users through the WebUI instead of agent -m.
This commit is contained in:
chengyongru 2026-06-22 01:09:58 +08:00 committed by Xubin Ren
parent e5294002ed
commit fca1f2ad02
9 changed files with 361 additions and 35 deletions

View File

@ -234,7 +234,7 @@ Windows PowerShell:
irm https://raw.githubusercontent.com/HKUDS/nanobot/main/scripts/install.ps1 | iex irm https://raw.githubusercontent.com/HKUDS/nanobot/main/scripts/install.ps1 | iex
``` ```
The default command installs or upgrades `nanobot-ai` from PyPI, then starts `nanobot onboard --wizard`. It avoids system-wide pip installs by using an active virtual environment, `uv`, `pipx`, or a managed venv under `~/.nanobot/venv`. If Quick Start finishes, skip the manual initialize/configure steps below and go straight to **Test one message**. The default command installs or upgrades `nanobot-ai` from PyPI, then starts `nanobot onboard --wizard`. It avoids system-wide pip installs by using an active virtual environment, `uv`, `pipx`, or a managed venv under `~/.nanobot/venv`. If Quick Start finishes and you enabled the WebSocket channel, skip the manual initialize/configure steps below and go straight to **Open the WebUI**.
To preview the plan without changing your environment, pass `--dry-run`; combine it with `--dev` when you want to preview the main-branch install. To preview the plan without changing your environment, pass `--dry-run`; combine it with `--dev` when you want to preview the main-branch install.
@ -353,7 +353,17 @@ For another provider, the same config shape still applies:
| Model ID | `modelPresets.primary.model` | | Model ID | `modelPresets.primary.model` |
| Endpoint URL, only when needed | `providers.<provider>.apiBase` | | Endpoint URL, only when needed | `providers.<provider>.apiBase` |
**3. Test one message** **3. Open the WebUI**
If Quick Start enabled the WebSocket channel, start the gateway:
```bash
nanobot gateway
```
Leave that terminal open, then open `http://127.0.0.1:8765` in your browser. Enter the WebUI password you set in the wizard, then send your first message there.
For manual or terminal-only setup, test one CLI message:
```bash ```bash
nanobot status nanobot status

View File

@ -201,7 +201,7 @@ Tracing covers the providers that go through nanobot's OpenAI-compatible client
> - **VolcEngine / BytePlus Coding Plan**: Subscription endpoints are configured through dedicated providers `volcengineCodingPlan` or `byteplusCodingPlan`, separate from the pay-per-use `volcengine` / `byteplus` providers. > - **VolcEngine / BytePlus Coding Plan**: Subscription endpoints are configured through dedicated providers `volcengineCodingPlan` or `byteplusCodingPlan`, separate from the pay-per-use `volcengine` / `byteplus` providers.
> - **Zhipu Coding Plan**: If you're on Zhipu's coding plan, set `"apiBase": "https://open.bigmodel.cn/api/coding/paas/v4"` in your zhipu provider config. > - **Zhipu Coding Plan**: If you're on Zhipu's coding plan, set `"apiBase": "https://open.bigmodel.cn/api/coding/paas/v4"` in your zhipu provider config.
> - **Alibaba Cloud BaiLian**: If you're using Alibaba Cloud BaiLian's OpenAI-compatible endpoint, set `"apiBase": "https://dashscope.aliyuncs.com/compatible-mode/v1"` in your dashscope provider config. > - **Alibaba Cloud BaiLian**: If you're using Alibaba Cloud BaiLian's OpenAI-compatible endpoint, set `"apiBase": "https://dashscope.aliyuncs.com/compatible-mode/v1"` in your dashscope provider config.
> - **StepFun Step Plan**: If you're on StepFun's Step Plan subscription, set `"apiBase": "https://api.stepfun.com/step_plan/v1"` in your stepfun provider config. Supported models include `step-3.5-flash`, `step-3.5-flash-2603`, and `step-router-v1`. > - **StepFun Step Plan**: If you're on StepFun's Step Plan subscription, set `"apiBase": "https://api.stepfun.ai/step_plan/v1"` in your stepfun provider config. Supported models include `step-3.5-flash`, `step-3.5-flash-2603`, and `step-router-v1`.
> - **Step Fun (Mainland China)**: If your API key is from Step Fun's mainland China platform (stepfun.com), set `"apiBase": "https://api.stepfun.com/v1"` in your stepfun provider config. > - **Step Fun (Mainland China)**: If your API key is from Step Fun's mainland China platform (stepfun.com), set `"apiBase": "https://api.stepfun.com/v1"` in your stepfun provider config.
> - **Xiaomi MiMo thinking mode**: MiMo models (e.g. `mimo-v2.5-pro`) default to enabled thinking. Use `agents.defaults.reasoningEffort: "none"` to disable it, or `"low"` / `"medium"` / `"high"` to keep it on. Omitting the field preserves the provider's per-model default. > - **Xiaomi MiMo thinking mode**: MiMo models (e.g. `mimo-v2.5-pro`) default to enabled thinking. Use `agents.defaults.reasoningEffort: "none"` to disable it, or `"low"` / `"medium"` / `"high"` to keep it on. Omitting the field preserves the provider's per-model default.
> - **Xiaomi MiMo Token Plan**: If you're on MiMo's token plan, set `"apiBase": "https://token-plan-sgp.xiaomimimo.com/v1"` in your xiaomi_mimo provider config. > - **Xiaomi MiMo Token Plan**: If you're on MiMo's token plan, set `"apiBase": "https://token-plan-sgp.xiaomimimo.com/v1"` in your xiaomi_mimo provider config.
@ -752,7 +752,7 @@ Step Plan is StepFun's subscription-based service for high-frequency AI develope
"providers": { "providers": {
"stepfun": { "stepfun": {
"apiKey": "${STEPFUN_API_KEY}", "apiKey": "${STEPFUN_API_KEY}",
"apiBase": "https://api.stepfun.com/step_plan/v1" "apiBase": "https://api.stepfun.ai/step_plan/v1"
} }
}, },
"modelPresets": { "modelPresets": {

View File

@ -272,7 +272,7 @@ StepPlan is StepFun's subscription tier and uses a different API base URL. The i
"providers": { "providers": {
"stepfun": { "stepfun": {
"apiKey": "${STEPFUN_API_KEY}", "apiKey": "${STEPFUN_API_KEY}",
"apiBase": "https://api.stepfun.com/step_plan/v1" "apiBase": "https://api.stepfun.ai/step_plan/v1"
} }
}, },
"tools": { "tools": {
@ -285,7 +285,7 @@ StepPlan is StepFun's subscription tier and uses a different API base URL. The i
} }
``` ```
`apiBase` takes precedence over the registry default, so with the StepPlan base URL configured, image requests are sent to `https://api.stepfun.com/step_plan/v1/images/generations` — the same path prefix used for LLM calls. The API key is shared with the standard StepFun provider. `apiBase` takes precedence over the registry default, so with the StepPlan base URL configured, image requests are sent to `https://api.stepfun.ai/step_plan/v1/images/generations` — the same path prefix used for LLM calls. The API key is shared with the standard StepFun provider.
### Zhipu ### Zhipu

View File

@ -32,7 +32,7 @@ On Windows PowerShell:
irm https://raw.githubusercontent.com/HKUDS/nanobot/main/scripts/install.ps1 | iex irm https://raw.githubusercontent.com/HKUDS/nanobot/main/scripts/install.ps1 | iex
``` ```
The default command installs or upgrades `nanobot-ai` from PyPI, then starts `nanobot onboard --wizard`. It avoids system-wide pip installs by using an active virtual environment, `uv`, `pipx`, or a managed venv under `~/.nanobot/venv`. If Quick Start finishes, skip the manual initialize/configure steps and go straight to [Check the Setup](#4-check-the-setup). The default command installs or upgrades `nanobot-ai` from PyPI, then starts `nanobot onboard --wizard`. It avoids system-wide pip installs by using an active virtual environment, `uv`, `pipx`, or a managed venv under `~/.nanobot/venv`. If Quick Start finishes and you enabled the WebSocket channel, go straight to [Open the WebUI](#5-open-the-webui).
To preview the plan without changing your environment, pass `--dry-run`; combine it with `--dev` when you want to preview the main-branch install. To preview the plan without changing your environment, pass `--dry-run`; combine it with `--dev` when you want to preview the main-branch install.
@ -233,7 +233,19 @@ Read it like this:
| `Model` | The model or preset you expect. | | `Model` | The model or preset you expect. |
| Provider list | Most providers can say `not set`; the provider used by the active preset should show a check mark, OAuth status, or local URL. | | Provider list | Most providers can say `not set`; the provider used by the active preset should show a check mark, OAuth status, or local URL. |
## 5. Test One Message ## 5. Open the WebUI
If Quick Start enabled the WebSocket channel, start the gateway:
```bash
nanobot gateway
```
Leave that terminal open, then open `http://127.0.0.1:8765` in your browser. Enter the WebUI password you set in the wizard, then send your first message there.
## 6. Test One CLI Message
Use this path if you skipped Quick Start, declined the WebSocket channel, or want a terminal-only check.
Run a one-shot CLI message: Run a one-shot CLI message:

View File

@ -167,6 +167,7 @@ Move through the wizard like this:
|---|---| |---|---|
| A menu | Use the arrow keys to highlight an option, then press `Enter`. | | A menu | Use the arrow keys to highlight an option, then press `Enter`. |
| The provider menu | Choose the company or service you want to use. | | The provider menu | Choose the company or service you want to use. |
| An endpoint menu | Choose the standard API or subscription plan endpoint that matches your key. |
| An API key field | Paste the key, then press `Enter`. | | An API key field | Paste the key, then press `Enter`. |
| A provider base URL field | Paste the provider base URL from its docs, then press `Enter`. | | A provider base URL field | Paste the provider base URL from its docs, then press `Enter`. |
| The Model ID field | Paste a model name from your provider, then press `Enter`. | | The Model ID field | Paste a model name from your provider, then press `Enter`. |
@ -176,12 +177,15 @@ For the first setup, choose `[Q] Quick Start`. It configures the recommended loc
1. Choose `[Q] Quick Start`. 1. Choose `[Q] Quick Start`.
2. Choose the provider you want to use. 2. Choose the provider you want to use.
3. Paste your API key if the wizard asks for one. 3. Choose the endpoint if the wizard asks, such as Standard API, Coding Plan, Token Plan, or Step Plan.
4. Paste the provider base URL if the wizard asks for one. 4. Paste your API key if the wizard asks for one.
5. Paste a model ID that provider can run. 5. Paste the provider base URL if the wizard asks for one.
6. Review the Quick Start summary. The wizard saves and exits when Quick Start finishes. 6. Paste a model ID that provider can run.
7. Confirm that Quick Start should enable the WebSocket channel for the local WebUI.
8. Set the WebUI password when prompted.
9. Review the Quick Start summary. The wizard saves and exits when Quick Start finishes.
The recommended path enables the local WebUI and default AI settings. You do not need to choose a chat channel for the first run. The recommended path enables `channels.websocket` for the local WebUI, requires a WebUI password, and writes default AI settings. You do not need to choose a separate chat app for the first run.
If you already know that you need custom headers, provider-specific request fields, a chat app, or tools, choose `Advanced Settings` instead. [`provider-cookbook.md`](./provider-cookbook.md) has copyable examples for several common provider setups. After you change advanced settings, a save option appears in the main menu. Choose `[S] Save and Exit`. If you already know that you need custom headers, provider-specific request fields, a chat app, or tools, choose `Advanced Settings` instead. [`provider-cookbook.md`](./provider-cookbook.md) has copyable examples for several common provider setups. After you change advanced settings, a save option appears in the main menu. Choose `[S] Save and Exit`.
@ -192,7 +196,7 @@ The wizard creates or updates:
| `~/.nanobot/config.json` | Settings file. | | `~/.nanobot/config.json` | Settings file. |
| `~/.nanobot/workspace/` | Working folder for memory, sessions, and generated files. | | `~/.nanobot/workspace/` | Working folder for memory, sessions, and generated files. |
If Quick Start finished successfully, skip to [Send the First Message](#7-send-the-first-message). The next two sections are only for manual setup. If Quick Start finished successfully, skip to [Open the WebUI](#7-open-the-webui). The next two sections are only for manual setup.
## Manual Setup: How to Merge JSON Snippets ## Manual Setup: How to Merge JSON Snippets
@ -294,7 +298,7 @@ For copyable provider-specific examples, use [`provider-cookbook.md`](./provider
Save the file. Save the file.
## 7. Send the First Message ## 7. Open the WebUI
First check that nanobot can read the saved setup: First check that nanobot can read the saved setup:
@ -312,7 +316,9 @@ Start the local browser UI:
nanobot gateway nanobot gateway
``` ```
Leave that terminal open, then open `http://127.0.0.1:8765` in your browser and send: Leave that terminal open, then open `http://127.0.0.1:8765` in your browser. If Quick Start enabled the WebSocket channel, enter the WebUI password you set in the wizard.
Send this first message in the browser:
```text ```text
Hello! Hello!

View File

@ -26,7 +26,8 @@ Add to `config.json` under `channels.websocket`:
"host": "127.0.0.1", "host": "127.0.0.1",
"port": 8765, "port": 8765,
"path": "/", "path": "/",
"websocketRequiresToken": false, "tokenIssueSecret": "your-webui-password",
"websocketRequiresToken": true,
"allowFrom": ["*"], "allowFrom": ["*"],
"streaming": true "streaming": true
} }

View File

@ -15,10 +15,19 @@ First confirm your provider and model can answer:
nanobot agent -m "Hello!" nanobot agent -m "Hello!"
``` ```
Then merge the WebSocket channel into your existing `~/.nanobot/config.json`: Then merge the WebSocket channel into your existing `~/.nanobot/config.json`.
Set `tokenIssueSecret` to the password you will enter in the WebUI login form:
```json ```json
{ "channels": { "websocket": { "enabled": true } } } {
"channels": {
"websocket": {
"enabled": true,
"tokenIssueSecret": "your-webui-password",
"websocketRequiresToken": true
}
}
}
``` ```
If you are new to JSON snippets, see If you are new to JSON snippets, see
@ -34,6 +43,7 @@ Leave the gateway running and open
[`http://127.0.0.1:8765`](http://127.0.0.1:8765). The WebUI is served by the [`http://127.0.0.1:8765`](http://127.0.0.1:8765). The WebUI is served by the
WebSocket channel on port `8765` by default. The gateway health endpoint, WebSocket channel on port `8765` by default. The gateway health endpoint,
`18790` by default, is not the browser UI. `18790` by default, is not the browser UI.
Enter `tokenIssueSecret` when the WebUI asks for a password.
## What It Is For ## What It Is For

View File

@ -46,6 +46,13 @@ class _QuickStartProviderInfo(NamedTuple):
is_direct: bool is_direct: bool
class _QuickStartEndpointChoice(NamedTuple):
"""Provider endpoint option used by Quick Start."""
label: str
api_base: str
# --- Field Hints for Select Fields --- # --- Field Hints for Select Fields ---
# Maps field names to (choices, hint_text) # Maps field names to (choices, hint_text)
# To add a new select field with hints, add an entry: # To add a new select field with hints, add an entry:
@ -69,7 +76,32 @@ _QUICK_START_CUSTOM_PROVIDER_CHOICE = "Other OpenAI-compatible"
_CLEAR_CHOICE = "Clear value" _CLEAR_CHOICE = "Clear value"
_QUICK_START_MENU_CHOICE = "[Q] Quick Start" _QUICK_START_MENU_CHOICE = "[Q] Quick Start"
_QUICK_START_STEPS = ("Provider + model", "WebUI", "Review") _QUICK_START_STEPS = ("Provider setup", "WebSocket channel", "Review")
_QUICK_START_ENDPOINT_CHOICES: dict[str, tuple[_QuickStartEndpointChoice, ...]] = {
"zhipu": (
_QuickStartEndpointChoice("Standard API", "https://open.bigmodel.cn/api/paas/v4"),
_QuickStartEndpointChoice("Coding Plan", "https://open.bigmodel.cn/api/coding/paas/v4"),
),
"minimax": (
_QuickStartEndpointChoice("Global API", "https://api.minimax.io/v1"),
_QuickStartEndpointChoice("Mainland China Token Plan", "https://api.minimaxi.com/v1"),
),
"minimax_anthropic": (
_QuickStartEndpointChoice("Global Anthropic API", "https://api.minimax.io/anthropic"),
_QuickStartEndpointChoice(
"Mainland China Anthropic Token Plan",
"https://api.minimaxi.com/anthropic",
),
),
"stepfun": (
_QuickStartEndpointChoice("Standard API", "https://api.stepfun.com/v1"),
_QuickStartEndpointChoice("Step Plan", "https://api.stepfun.ai/step_plan/v1"),
),
"xiaomi_mimo": (
_QuickStartEndpointChoice("Standard API", "https://api.xiaomimimo.com/v1"),
_QuickStartEndpointChoice("Token Plan", "https://token-plan-sgp.xiaomimimo.com/v1"),
),
}
# Low-contrast terminal palette inspired by JetBrains Darcula/Islands. # Low-contrast terminal palette inspired by JetBrains Darcula/Islands.
_UI_ACCENT = "#6B9BFA" _UI_ACCENT = "#6B9BFA"
@ -487,6 +519,17 @@ def _input_text(display_name: str, current: Any, field_type: str, field_info=Non
return value return value
def _input_secret(display_name: str) -> str | None:
"""Get a secret value without echoing it when questionary supports password input."""
prompt_factory = getattr(_get_questionary(), "password", None)
if prompt_factory is None:
prompt_factory = _get_questionary().text
value = prompt_factory(f"{display_name}:").ask()
if value is None:
return None
return str(value).strip()
def _input_with_existing( def _input_with_existing(
display_name: str, current: Any, field_type: str, field_info=None display_name: str, current: Any, field_type: str, field_info=None
) -> Any: ) -> Any:
@ -1467,6 +1510,8 @@ def _quick_start_requires_base_url(provider_name: str, info: _QuickStartProvider
"""Return whether Quick Start must ask for a provider base URL.""" """Return whether Quick Start must ask for a provider base URL."""
if provider_name == "custom": if provider_name == "custom":
return True return True
if provider_name in _QUICK_START_ENDPOINT_CHOICES:
return False
if info is None or info.default_api_base: if info is None or info.default_api_base:
return False return False
return info.backend == "azure_openai" or ( return info.backend == "azure_openai" or (
@ -1474,6 +1519,43 @@ def _quick_start_requires_base_url(provider_name: str, info: _QuickStartProvider
) )
def _select_quick_start_api_base(
provider_name: str,
provider_display: str,
info: _QuickStartProviderInfo | None,
) -> tuple[str, bool] | None:
"""Return the api_base and whether the user explicitly selected or entered it."""
endpoint_choices = _QUICK_START_ENDPOINT_CHOICES.get(provider_name)
if endpoint_choices:
choices = {choice.label: choice.api_base for choice in endpoint_choices}
answer = _select_with_back(
f"Which {provider_display} endpoint should Quick Start use?",
list(choices) + ["<- Back"],
default=endpoint_choices[0].label,
)
if answer is _BACK_PRESSED or answer is None or answer == "<- Back":
return None
assert isinstance(answer, str)
return choices[answer], True
api_base = info.default_api_base if info else ""
if not _quick_start_requires_base_url(provider_name, info):
return api_base, False
base_answer = _input_text(
"Provider base URL",
api_base,
"str",
)
if base_answer is None:
return None
api_base = base_answer.strip().rstrip("/")
if not api_base:
console.print("[yellow]! Provider base URL is required for this provider[/yellow]")
return None
return api_base, True
def _configure_quick_start_provider(config: Config) -> bool: def _configure_quick_start_provider(config: Config) -> bool:
"""Configure the beginner path from provider credentials and model.""" """Configure the beginner path from provider credentials and model."""
_show_quick_start_progress(1) _show_quick_start_progress(1)
@ -1489,6 +1571,14 @@ def _configure_quick_start_provider(config: Config) -> bool:
provider_name = provider_choices[answer] provider_name = provider_choices[answer]
provider_info = _get_quick_start_provider_info().get(provider_name) provider_info = _get_quick_start_provider_info().get(provider_name)
api_base = provider_info.default_api_base if provider_info else ""
base_was_prompted = False
if provider_name in _QUICK_START_ENDPOINT_CHOICES:
api_base_result = _select_quick_start_api_base(provider_name, answer, provider_info)
if api_base_result is None:
return False
api_base, base_was_prompted = api_base_result
api_key: str | None = None api_key: str | None = None
if _quick_start_requires_api_key(provider_name, provider_info): if _quick_start_requires_api_key(provider_name, provider_info):
api_key = _input_text(f"{answer} API key", "", "str") api_key = _input_text(f"{answer} API key", "", "str")
@ -1499,21 +1589,14 @@ def _configure_quick_start_provider(config: Config) -> bool:
console.print("[yellow]! API key is required for Quick Start[/yellow]") console.print("[yellow]! API key is required for Quick Start[/yellow]")
return False return False
api_base = provider_info.default_api_base if provider_info else "" if (
base_was_prompted = False provider_name not in _QUICK_START_ENDPOINT_CHOICES
if _quick_start_requires_base_url(provider_name, provider_info): and _quick_start_requires_base_url(provider_name, provider_info)
base_answer = _input_text( ):
"Provider base URL", api_base_result = _select_quick_start_api_base(provider_name, answer, provider_info)
api_base, if api_base_result is None:
"str",
)
if base_answer is None:
return False
base_was_prompted = True
api_base = base_answer.strip().rstrip("/")
if not api_base:
console.print("[yellow]! Provider base URL is required for this provider[/yellow]")
return False return False
api_base, base_was_prompted = api_base_result
provider_config = getattr(config.providers, provider_name, None) provider_config = getattr(config.providers, provider_name, None)
if provider_config is None: if provider_config is None:
@ -1545,6 +1628,25 @@ def _configure_quick_start_provider(config: Config) -> bool:
def _enable_quick_start_websocket_defaults(config: Config) -> bool: def _enable_quick_start_websocket_defaults(config: Config) -> bool:
"""Enable local WebUI with the default WebSocket settings.""" """Enable local WebUI with the default WebSocket settings."""
_show_quick_start_progress(2) _show_quick_start_progress(2)
console.print(
f"[{_UI_ACCENT}]Quick Start will enable the WebSocket channel for the local WebUI.[/]"
)
console.print(
f"[{_UI_MUTED}]This lets the browser UI at http://127.0.0.1:8765 connect to nanobot.[/]"
)
console.print()
answer = _get_questionary().confirm(
"Enable WebSocket channel now?",
default=True,
).ask()
if not answer:
console.print("[yellow]! Quick Start needs the WebSocket channel for the local WebUI[/yellow]")
return False
webui_secret = _input_secret("Set a WebUI password")
if not webui_secret:
console.print("[yellow]! WebUI password is required when enabling WebSocket[/yellow]")
return False
config_cls = _get_channel_config_class("websocket") config_cls = _get_channel_config_class("websocket")
if config_cls is None: if config_cls is None:
console.print("[red]No configuration class found for websocket[/red]") console.print("[red]No configuration class found for websocket[/red]")
@ -1554,6 +1656,10 @@ def _enable_quick_start_websocket_defaults(config: Config) -> bool:
model = config_cls.model_validate(current) model = config_cls.model_validate(current)
if hasattr(model, "enabled"): if hasattr(model, "enabled"):
setattr(model, "enabled", True) setattr(model, "enabled", True)
if hasattr(model, "token_issue_secret"):
setattr(model, "token_issue_secret", webui_secret)
if hasattr(model, "websocket_requires_token"):
setattr(model, "websocket_requires_token", True)
setattr(config.channels, "websocket", model.model_dump(by_alias=True, exclude_none=True)) setattr(config.channels, "websocket", model.model_dump(by_alias=True, exclude_none=True))
return True return True
@ -1581,6 +1687,7 @@ def _show_quick_start_summary(config: Config) -> None:
rows = [ rows = [
("Status", status), ("Status", status),
("Next", next_step), ("Next", next_step),
("WebSocket channel", "enabled"),
("Open", "http://127.0.0.1:8765"), ("Open", "http://127.0.0.1:8765"),
] ]
_print_summary_panel(rows, "Quick Start") _print_summary_panel(rows, "Quick Start")
@ -1591,7 +1698,7 @@ def _configure_quick_start(config: Config) -> bool:
console.clear() console.clear()
_show_section_header( _show_section_header(
"Quick Start", "Quick Start",
"Choose the API provider, paste the key, enter the model, then use the local WebUI.", "Choose provider endpoint, add credentials and model, then enable the local WebUI channel.",
) )
if not _configure_quick_start_provider(config): if not _configure_quick_start_provider(config):
_pause() _pause()

View File

@ -942,6 +942,13 @@ class TestMainMenuUpdate:
pause_messages: list[str] = [] pause_messages: list[str] = []
class FakePrompt:
def __init__(self, response):
self.response = response
def ask(self):
return self.response
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", lambda *a, **kw: "DeepSeek") monkeypatch.setattr(onboard_wizard, "_select_with_back", lambda *a, **kw: "DeepSeek")
@ -951,6 +958,14 @@ class TestMainMenuUpdate:
"_input_model_with_autocomplete", "_input_model_with_autocomplete",
lambda *a, **kw: "deepseek-v4-flash", lambda *a, **kw: "deepseek-v4-flash",
) )
monkeypatch.setattr(
onboard_wizard,
"questionary",
SimpleNamespace(
confirm=lambda *a, **kw: FakePrompt(True),
password=lambda *a, **kw: FakePrompt("webui-secret"),
),
)
monkeypatch.setattr(onboard_wizard, "_configure_pydantic_model", fail_websocket_config) 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, "_print_summary_panel", lambda *a, **kw: None)
monkeypatch.setattr(onboard_wizard, "_pause", lambda message="": pause_messages.append(message)) monkeypatch.setattr(onboard_wizard, "_pause", lambda message="": pause_messages.append(message))
@ -966,6 +981,7 @@ class TestMainMenuUpdate:
websocket = getattr(config.channels, "websocket") websocket = getattr(config.channels, "websocket")
assert websocket["enabled"] is True assert websocket["enabled"] is True
assert websocket["websocketRequiresToken"] is True assert websocket["websocketRequiresToken"] is True
assert websocket["tokenIssueSecret"] == "webui-secret"
def test_quick_start_provider_choice_asks_for_model_id(self, monkeypatch): def test_quick_start_provider_choice_asks_for_model_id(self, monkeypatch):
"""Known providers should ask users for the model instead of fetching one.""" """Known providers should ask users for the model instead of fetching one."""
@ -1034,6 +1050,90 @@ class TestMainMenuUpdate:
assert config.model_presets["primary"].provider == "openai" assert config.model_presets["primary"].provider == "openai"
assert config.model_presets["primary"].model == "gpt-4o-mini" assert config.model_presets["primary"].model == "gpt-4o-mini"
def test_quick_start_zhipu_coding_plan_uses_coding_base_url(self, monkeypatch):
"""Zhipu Coding Plan should not use the standard Zhipu base URL."""
config = Config()
choices = iter(["Zhipu AI", "Coding Plan"])
monkeypatch.setattr(onboard_wizard, "_show_quick_start_progress", lambda *_args: None)
monkeypatch.setattr(onboard_wizard, "_select_with_back", lambda *a, **kw: next(choices))
monkeypatch.setattr(onboard_wizard, "_input_text", lambda *a, **kw: "zhipu-key")
monkeypatch.setattr(
onboard_wizard,
"_input_model_with_autocomplete",
lambda *a, **kw: "glm-4.6",
)
assert onboard_wizard._configure_quick_start_provider(config) is True
assert config.providers.zhipu.api_key == "zhipu-key"
assert config.providers.zhipu.api_base == "https://open.bigmodel.cn/api/coding/paas/v4"
assert config.model_presets["primary"].provider == "zhipu"
assert config.model_presets["primary"].model == "glm-4.6"
def test_quick_start_minimax_mainland_token_plan_uses_mainland_base_url(self, monkeypatch):
"""MiniMax mainland token plan should not use the global MiniMax base URL."""
config = Config()
choices = iter(["MiniMax", "Mainland China Token Plan"])
monkeypatch.setattr(onboard_wizard, "_show_quick_start_progress", lambda *_args: None)
monkeypatch.setattr(onboard_wizard, "_select_with_back", lambda *a, **kw: next(choices))
monkeypatch.setattr(onboard_wizard, "_input_text", lambda *a, **kw: "minimax-key")
monkeypatch.setattr(
onboard_wizard,
"_input_model_with_autocomplete",
lambda *a, **kw: "MiniMax-M2",
)
assert onboard_wizard._configure_quick_start_provider(config) is True
assert config.providers.minimax.api_key == "minimax-key"
assert config.providers.minimax.api_base == "https://api.minimaxi.com/v1"
assert config.model_presets["primary"].provider == "minimax"
assert config.model_presets["primary"].model == "MiniMax-M2"
def test_quick_start_stepfun_step_plan_uses_plan_base_url(self, monkeypatch):
"""StepFun Step Plan should not use the standard StepFun base URL."""
config = Config()
choices = iter(["Step Fun", "Step Plan"])
monkeypatch.setattr(onboard_wizard, "_show_quick_start_progress", lambda *_args: None)
monkeypatch.setattr(onboard_wizard, "_select_with_back", lambda *a, **kw: next(choices))
monkeypatch.setattr(onboard_wizard, "_input_text", lambda *a, **kw: "stepfun-key")
monkeypatch.setattr(
onboard_wizard,
"_input_model_with_autocomplete",
lambda *a, **kw: "step-3.5-flash",
)
assert onboard_wizard._configure_quick_start_provider(config) is True
assert config.providers.stepfun.api_key == "stepfun-key"
assert config.providers.stepfun.api_base == "https://api.stepfun.ai/step_plan/v1"
assert config.model_presets["primary"].provider == "stepfun"
assert config.model_presets["primary"].model == "step-3.5-flash"
def test_quick_start_xiaomi_mimo_token_plan_uses_token_plan_base_url(self, monkeypatch):
"""Xiaomi MiMo Token Plan should not use the standard MiMo base URL."""
config = Config()
choices = iter(["Xiaomi MIMO", "Token Plan"])
monkeypatch.setattr(onboard_wizard, "_show_quick_start_progress", lambda *_args: None)
monkeypatch.setattr(onboard_wizard, "_select_with_back", lambda *a, **kw: next(choices))
monkeypatch.setattr(onboard_wizard, "_input_text", lambda *a, **kw: "mimo-key")
monkeypatch.setattr(
onboard_wizard,
"_input_model_with_autocomplete",
lambda *a, **kw: "mimo-v2.5-pro",
)
assert onboard_wizard._configure_quick_start_provider(config) is True
assert config.providers.xiaomi_mimo.api_key == "mimo-key"
assert config.providers.xiaomi_mimo.api_base == "https://token-plan-sgp.xiaomimimo.com/v1"
assert config.model_presets["primary"].provider == "xiaomi_mimo"
assert config.model_presets["primary"].model == "mimo-v2.5-pro"
def test_quick_start_custom_base_url_asks_for_model_id(self, monkeypatch): def test_quick_start_custom_base_url_asks_for_model_id(self, monkeypatch):
"""Custom providers should ask for base URL and model ID.""" """Custom providers should ask for base URL and model ID."""
config = Config() config = Config()
@ -1080,6 +1180,85 @@ class TestMainMenuUpdate:
assert config.model_presets["primary"].provider == "azure_openai" assert config.model_presets["primary"].provider == "azure_openai"
assert config.model_presets["primary"].model == "deployment-name" assert config.model_presets["primary"].model == "deployment-name"
def test_quick_start_websocket_step_explains_channel_enablement(self, monkeypatch):
"""Quick Start should confirm and protect WebSocket for WebUI."""
config = Config()
messages: list[str] = []
class FakePrompt:
def __init__(self, response):
self.response = response
def ask(self):
return self.response
monkeypatch.setattr(onboard_wizard, "_show_quick_start_progress", lambda *_args: None)
monkeypatch.setattr(onboard_wizard.console, "print", lambda message="", *a, **kw: messages.append(str(message)))
monkeypatch.setattr(
onboard_wizard,
"questionary",
SimpleNamespace(
confirm=lambda *a, **kw: FakePrompt(True),
password=lambda *a, **kw: FakePrompt("webui-secret"),
),
)
assert onboard_wizard._enable_quick_start_websocket_defaults(config) is True
assert any("WebSocket channel" in message for message in messages)
assert any("http://127.0.0.1:8765" in message for message in messages)
websocket = getattr(config.channels, "websocket")
assert websocket["enabled"] is True
assert websocket["websocketRequiresToken"] is True
assert websocket["tokenIssueSecret"] == "webui-secret"
def test_quick_start_websocket_step_can_be_declined(self, monkeypatch):
"""Declining WebSocket should stop Quick Start before changing channel config."""
config = Config()
class FakePrompt:
def __init__(self, response):
self.response = response
def ask(self):
return self.response
monkeypatch.setattr(onboard_wizard, "_show_quick_start_progress", lambda *_args: None)
monkeypatch.setattr(onboard_wizard.console, "print", lambda *a, **kw: None)
monkeypatch.setattr(
onboard_wizard,
"questionary",
SimpleNamespace(confirm=lambda *a, **kw: FakePrompt(False)),
)
assert onboard_wizard._enable_quick_start_websocket_defaults(config) is False
assert getattr(config.channels, "websocket", None) is None
def test_quick_start_websocket_requires_password(self, monkeypatch):
"""Accepting WebSocket with an empty password should not enable the channel."""
config = Config()
class FakePrompt:
def __init__(self, response):
self.response = response
def ask(self):
return self.response
monkeypatch.setattr(onboard_wizard, "_show_quick_start_progress", lambda *_args: None)
monkeypatch.setattr(onboard_wizard.console, "print", lambda *a, **kw: None)
monkeypatch.setattr(
onboard_wizard,
"questionary",
SimpleNamespace(
confirm=lambda *a, **kw: FakePrompt(True),
password=lambda *a, **kw: FakePrompt(""),
),
)
assert onboard_wizard._enable_quick_start_websocket_defaults(config) is False
assert getattr(config.channels, "websocket", None) is None
def test_quick_start_requires_api_key_before_setting_defaults(self, monkeypatch): def test_quick_start_requires_api_key_before_setting_defaults(self, monkeypatch):
"""Quick Start should not create a ready-looking config without an API key.""" """Quick Start should not create a ready-looking config without an API key."""
config = Config() config = Config()
@ -1135,6 +1314,7 @@ class TestMainMenuUpdate:
assert rows["Status"] == "DeepSeek API key missing" assert rows["Status"] == "DeepSeek API key missing"
assert "API key" in rows["Next"] assert "API key" in rows["Next"]
assert "nanobot gateway" in rows["Next"] assert "nanobot gateway" in rows["Next"]
assert "agent -m" not in rows["Next"]
assert labels.index("Next") < labels.index("Open") assert labels.index("Next") < labels.index("Open")
assert "Model" not in rows assert "Model" not in rows
assert "Entry point" not in rows assert "Entry point" not in rows