Match the _BOCHA_SEARCH_API_URL / _VOLCENGINE_SEARCH_API_URL convention
instead of hardcoding the URL inline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Keenable's public endpoint serves the free tier (1000 req/hour) without
auth. Route to /v1/search/public with the X-Keenable-Title header when no
key is configured, instead of falling back to DuckDuckGo; keep the
authenticated /v1/search path when an apiKey or KEENABLE_API_KEY is set.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Remove overly broad 'not found' match that could trigger on transient
errors like 'chat not found' or 'message to reply not found'
- Keep only 'method not found' and 'unknown method' which indicate
the server genuinely doesn't support sendRichMessage
- Demote timeout log from warning to debug since it falls back to
legacy path successfully
- Try sendRichMessage for all non-blockquote messages (tables, task lists,
math, collapsible, etc.) with graceful fallback to legacy HTML
- Fix payload format to use top-level 'markdown' field per Bot API 10.1 spec
- Add rich path in send_delta() stream end (delete preview + send rich)
- Latch off permanently if server returns capability error
- Removes _needs_rich_rendering() gate and 4 regex patterns
WhatsApp can deliver a sender LID instead of a phone number. The channel
already learns the LID->phone mapping at runtime, but only after a message
that carries both values, so the first message from a contact can't be
resolved to a phone number.
Seed the mapping on startup from two sources:
- reverse mapping files the bridge persists in the auth directory
(lid-mapping-<lid>_reverse.json), resolved via get_runtime_subdir so it
respects a custom runtime dir
- a new optional channels.whatsapp.lidMappings config dict for static
mappings (takes precedence over the on-disk files)
Malformed/empty mapping files are ignored rather than failing startup.
Adds tests for both sources, precedence, malformed files and the
no-auth-dir case, plus docs for the new config field.
timeout=None causes the httpx client to wait indefinitely when connecting
to MCP servers via streamableHttp transport, blocking the entire event loop.
This prevents Telegram polling and other async operations from running.
Set timeout=httpx.Timeout(30.0, connect=10.0) to match the pattern used
in the SSE transport's httpx_client_factory.
SessionManager._load() migrates sessions from the legacy directory
(~/.nanobot/sessions/) to the workspace path, but delete_session only
checked the workspace path. A user deleting a session could therefore
see its history come back the next time the session was loaded.
- delete_session now attempts to unlink both paths
- returns True if at least one file was removed
- added regression tests: legacy-only, both-paths, and no-revival
Maintainer edit: parse Feishu card table columns and rows so forwarded table cards do not fall back to [interactive], and add parser regression coverage.
Feishu cards arriving via WebSocket have a different structure than
expected: elements are nested lists with tag:text/text fields instead
of flat lists with tag:markdown/content. Also extract user_dsl for
richer card data and support body.elements (schema 2.0).
The _strip_image_content methods now use a fixed non-descriptive
placeholder instead of path-derived text. Update the 3 existing
test_provider_retry assertions to match the new placeholder format.
The image-strip fallback (triggered when a model errors on image input)
replaced image_url blocks with [image: <path>] or [image omitted]. Both
read like a live, available image to the LLM, causing it to:
1. hallucinate about image contents it never received
2. attempt read_file on the leaked server path
3. expose internal file paths to the model
Replace with an explicit '[Image not delivered to model — do not describe
or reference it]' placeholder that tells the LLM the image was stripped.
Fixes#4345
When the primary model (e.g. DeepSeek during peak hours) returns an empty
choices response with HTTP 200, the error carries no status code or
structured error metadata. The existing _FALLBACK_ERROR_TOKENS had no
matching token, so _should_fallback() returned False and fallback models
were never tried.
Changes:
- Add 'empty' token to _FALLBACK_ERROR_TOKENS so 'Error: API returned
empty choices.' text matches the fallback path
- Set error_kind='empty' in openai_compat_provider when returning
the empty-choices error, making the classification explicit
- Add test coverage for both text-only and error_kind matching paths
Fixes: glebov reported primary never falls back when DeepSeek returns
empty responses
maintainer edit: remove the Feishu-specific config-file guard from the shared CLI login command. Feishu now follows the Weixin pattern where channel.login owns its setup and persistence.
maintainer edit: fix the lint failure, report the active config path, fail fast when the registration response lacks a login URL, and cover the new Feishu login writeback path.
Manual testing against the live API showed the Keenable REST endpoint
(/v1/search) returns 401 without a key — the keyless "free tier" applies
only to the CLI, not the HTTP API. Treat Keenable like every other
key-based provider: fall back to DuckDuckGo when no key is configured,
and drop the now-inaccurate free-tier wording from the docs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the bespoke nanobot/<version> User-Agent in favor of self.user_agent
for consistency with every other search provider, and add an
X-Keenable-Title: nanobot header so Keenable can attribute traffic.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Keep the Keenable search test consistent with the surrounding mocks
(test_tavily/test_brave) rather than introducing a multi-line outlier.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bring the Keenable search provider in line with the established
multi-file provider pattern so it surfaces everywhere the others do:
- settings_api.py: register in the web-search provider options so it
appears in the WebUI settings dropdown (credential: api_key, optional).
- WebUI provider-brand: add keenable brand entry + brand test.
- docs/configuration.md: provider table row + config example.
- Harden _search_keenable: honor config.timeout and return explicit
messages on HTTP status errors (429 / other), matching peer providers.
- Add env-key and HTTP-error tests; add the websocket settings whitelist
assertion.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Keenable is a first-party API, so identify as nanobot/<version> instead
of the shared spoofed-browser User-Agent used by scraping providers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>