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:
@@ -234,7 +234,7 @@ Windows PowerShell:
|
||||
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.
|
||||
|
||||
@@ -353,7 +353,17 @@ For another provider, the same config shape still applies:
|
||||
| Model ID | `modelPresets.primary.model` |
|
||||
| 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
|
||||
nanobot status
|
||||
|
||||
@@ -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.
|
||||
> - **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.
|
||||
> - **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.
|
||||
> - **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.
|
||||
@@ -752,7 +752,7 @@ Step Plan is StepFun's subscription-based service for high-frequency AI develope
|
||||
"providers": {
|
||||
"stepfun": {
|
||||
"apiKey": "${STEPFUN_API_KEY}",
|
||||
"apiBase": "https://api.stepfun.com/step_plan/v1"
|
||||
"apiBase": "https://api.stepfun.ai/step_plan/v1"
|
||||
}
|
||||
},
|
||||
"modelPresets": {
|
||||
|
||||
@@ -272,7 +272,7 @@ StepPlan is StepFun's subscription tier and uses a different API base URL. The i
|
||||
"providers": {
|
||||
"stepfun": {
|
||||
"apiKey": "${STEPFUN_API_KEY}",
|
||||
"apiBase": "https://api.stepfun.com/step_plan/v1"
|
||||
"apiBase": "https://api.stepfun.ai/step_plan/v1"
|
||||
}
|
||||
},
|
||||
"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
|
||||
|
||||
|
||||
+14
-2
@@ -32,7 +32,7 @@ On Windows PowerShell:
|
||||
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.
|
||||
|
||||
@@ -233,7 +233,19 @@ Read it like this:
|
||||
| `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. |
|
||||
|
||||
## 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:
|
||||
|
||||
|
||||
@@ -167,6 +167,7 @@ Move through the wizard like this:
|
||||
|---|---|
|
||||
| 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. |
|
||||
| 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`. |
|
||||
| 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`. |
|
||||
@@ -176,12 +177,15 @@ For the first setup, choose `[Q] Quick Start`. It configures the recommended loc
|
||||
|
||||
1. Choose `[Q] Quick Start`.
|
||||
2. Choose the provider you want to use.
|
||||
3. Paste your API key if the wizard asks for one.
|
||||
4. Paste the provider base URL if the wizard asks for one.
|
||||
5. Paste a model ID that provider can run.
|
||||
6. Review the Quick Start summary. The wizard saves and exits when Quick Start finishes.
|
||||
3. Choose the endpoint if the wizard asks, such as Standard API, Coding Plan, Token Plan, or Step Plan.
|
||||
4. Paste your API key if the wizard asks for one.
|
||||
5. Paste the provider base URL if the wizard asks for one.
|
||||
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`.
|
||||
|
||||
@@ -192,7 +196,7 @@ The wizard creates or updates:
|
||||
| `~/.nanobot/config.json` | Settings file. |
|
||||
| `~/.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
|
||||
|
||||
@@ -294,7 +298,7 @@ For copyable provider-specific examples, use [`provider-cookbook.md`](./provider
|
||||
|
||||
Save the file.
|
||||
|
||||
## 7. Send the First Message
|
||||
## 7. Open the WebUI
|
||||
|
||||
First check that nanobot can read the saved setup:
|
||||
|
||||
@@ -312,7 +316,9 @@ Start the local browser UI:
|
||||
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
|
||||
Hello!
|
||||
|
||||
+2
-1
@@ -26,7 +26,8 @@ Add to `config.json` under `channels.websocket`:
|
||||
"host": "127.0.0.1",
|
||||
"port": 8765,
|
||||
"path": "/",
|
||||
"websocketRequiresToken": false,
|
||||
"tokenIssueSecret": "your-webui-password",
|
||||
"websocketRequiresToken": true,
|
||||
"allowFrom": ["*"],
|
||||
"streaming": true
|
||||
}
|
||||
|
||||
+12
-2
@@ -15,10 +15,19 @@ First confirm your provider and model can answer:
|
||||
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
|
||||
{ "channels": { "websocket": { "enabled": true } } }
|
||||
{
|
||||
"channels": {
|
||||
"websocket": {
|
||||
"enabled": true,
|
||||
"tokenIssueSecret": "your-webui-password",
|
||||
"websocketRequiresToken": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
WebSocket channel on port `8765` by default. The gateway health endpoint,
|
||||
`18790` by default, is not the browser UI.
|
||||
Enter `tokenIssueSecret` when the WebUI asks for a password.
|
||||
|
||||
## What It Is For
|
||||
|
||||
|
||||
+123
-16
@@ -46,6 +46,13 @@ class _QuickStartProviderInfo(NamedTuple):
|
||||
is_direct: bool
|
||||
|
||||
|
||||
class _QuickStartEndpointChoice(NamedTuple):
|
||||
"""Provider endpoint option used by Quick Start."""
|
||||
|
||||
label: str
|
||||
api_base: str
|
||||
|
||||
|
||||
# --- Field Hints for Select Fields ---
|
||||
# Maps field names to (choices, hint_text)
|
||||
# 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"
|
||||
_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.
|
||||
_UI_ACCENT = "#6B9BFA"
|
||||
@@ -487,6 +519,17 @@ def _input_text(display_name: str, current: Any, field_type: str, field_info=Non
|
||||
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(
|
||||
display_name: str, current: Any, field_type: str, field_info=None
|
||||
) -> 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."""
|
||||
if provider_name == "custom":
|
||||
return True
|
||||
if provider_name in _QUICK_START_ENDPOINT_CHOICES:
|
||||
return False
|
||||
if info is None or info.default_api_base:
|
||||
return False
|
||||
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:
|
||||
"""Configure the beginner path from provider credentials and model."""
|
||||
_show_quick_start_progress(1)
|
||||
@@ -1489,6 +1571,14 @@ def _configure_quick_start_provider(config: Config) -> bool:
|
||||
provider_name = provider_choices[answer]
|
||||
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
|
||||
if _quick_start_requires_api_key(provider_name, provider_info):
|
||||
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]")
|
||||
return False
|
||||
|
||||
api_base = provider_info.default_api_base if provider_info else ""
|
||||
base_was_prompted = False
|
||||
if _quick_start_requires_base_url(provider_name, provider_info):
|
||||
base_answer = _input_text(
|
||||
"Provider base URL",
|
||||
api_base,
|
||||
"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]")
|
||||
if (
|
||||
provider_name not in _QUICK_START_ENDPOINT_CHOICES
|
||||
and _quick_start_requires_base_url(provider_name, provider_info)
|
||||
):
|
||||
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
|
||||
|
||||
provider_config = getattr(config.providers, provider_name, 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:
|
||||
"""Enable local WebUI with the default WebSocket settings."""
|
||||
_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")
|
||||
if config_cls is None:
|
||||
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)
|
||||
if hasattr(model, "enabled"):
|
||||
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))
|
||||
return True
|
||||
|
||||
@@ -1581,6 +1687,7 @@ def _show_quick_start_summary(config: Config) -> None:
|
||||
rows = [
|
||||
("Status", status),
|
||||
("Next", next_step),
|
||||
("WebSocket channel", "enabled"),
|
||||
("Open", "http://127.0.0.1:8765"),
|
||||
]
|
||||
_print_summary_panel(rows, "Quick Start")
|
||||
@@ -1591,7 +1698,7 @@ def _configure_quick_start(config: Config) -> bool:
|
||||
console.clear()
|
||||
_show_section_header(
|
||||
"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):
|
||||
_pause()
|
||||
|
||||
@@ -942,6 +942,13 @@ class TestMainMenuUpdate:
|
||||
|
||||
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, "_show_section_header", lambda *a, **kw: None)
|
||||
monkeypatch.setattr(onboard_wizard, "_select_with_back", lambda *a, **kw: "DeepSeek")
|
||||
@@ -951,6 +958,14 @@ class TestMainMenuUpdate:
|
||||
"_input_model_with_autocomplete",
|
||||
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, "_print_summary_panel", lambda *a, **kw: None)
|
||||
monkeypatch.setattr(onboard_wizard, "_pause", lambda message="": pause_messages.append(message))
|
||||
@@ -966,6 +981,7 @@ class TestMainMenuUpdate:
|
||||
websocket = getattr(config.channels, "websocket")
|
||||
assert websocket["enabled"] is True
|
||||
assert websocket["websocketRequiresToken"] is True
|
||||
assert websocket["tokenIssueSecret"] == "webui-secret"
|
||||
|
||||
def test_quick_start_provider_choice_asks_for_model_id(self, monkeypatch):
|
||||
"""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"].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):
|
||||
"""Custom providers should ask for base URL and model ID."""
|
||||
config = Config()
|
||||
@@ -1080,6 +1180,85 @@ class TestMainMenuUpdate:
|
||||
assert config.model_presets["primary"].provider == "azure_openai"
|
||||
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):
|
||||
"""Quick Start should not create a ready-looking config without an API key."""
|
||||
config = Config()
|
||||
@@ -1135,6 +1314,7 @@ class TestMainMenuUpdate:
|
||||
assert rows["Status"] == "DeepSeek API key missing"
|
||||
assert "API key" in rows["Next"]
|
||||
assert "nanobot gateway" in rows["Next"]
|
||||
assert "agent -m" not in rows["Next"]
|
||||
assert labels.index("Next") < labels.index("Open")
|
||||
assert "Model" not in rows
|
||||
assert "Entry point" not in rows
|
||||
|
||||
Reference in New Issue
Block a user