* fix(cloud): launch new accounts through Space
* style: format Cloud entry URL
* fix(cloud): wait for launch workspace projection
---------
Co-authored-by: dadachann <185672915+dadachann@users.noreply.github.com>
The Ollama requester declared litellm_provider: ollama, which routes
every request through litellm's legacy /api/generate-based
OllamaConfig. That config's get_supported_openai_params() does not
include "tools"/"tool_choice" at all, so an Ollama-hosted model in a
local-agent pipeline could never receive a structured tool definition
or return a structured tool_calls response - it could only try to
express a tool call as free text (typically inside its own <think>
reasoning), which LangBot then has no way to execute.
litellm's "ollama_chat" provider targets Ollama's modern /api/chat
endpoint instead, which correctly forwards tools/tool_choice and
correctly surfaces the model's native message.tool_calls field.
Verified against a real local Ollama 0.33.2 instance with the exact
system prompt, RAG-augmented user message, and tool set a live
pipeline sends.
Two follow-on fixes needed because the Ollama requester definition is
shared by LLM and text-embedding models:
- get_reasoning_capabilities: match family in ('ollama', 'ollama_chat')
so the reasoning-level UI still works for this provider.
- scan_models: retry {base_url}/v1/models on a 404 from {base_url}/models,
since Ollama's base_url is a bare host (must not include /v1 - that
would break OllamaChatConfig.get_complete_url, which appends /api/chat
to it directly), unlike most other OpenAI-compatible providers whose
base_url already ends in /v1.
- invoke_embedding: litellm's embedding routing has no "ollama_chat"
case, only "ollama". Build the embedding model name with an explicit
custom_llm_provider="ollama" override when the requester is configured
for ollama_chat, so embedding models (e.g. bge-m3) keep working.
Co-authored-by: zx90316 <zx90316@users.noreply.github.com>
* fix(monitoring): restore SQLite token statistics
Allow the monitored SQLite strftime bucket expression through the tenant SQL guard and preserve structured backend errors in the token dashboard. Add persistence and frontend regressions.\n\nVerified-by: independent-review
* test(runtime): accept reconcile timeout in capacity stub
Keep the PostgreSQL capacity probe aligned with the runtime handler contract and assert the bounded reconcile timeout.
---------
Co-authored-by: dadachann <185672915+dadachann@users.noreply.github.com>
The LINE adapter passed text through as a single Plain component,
ignoring the mention payload (mentions[].index/length/isSelf) that the
Line Messaging API includes in the webhook. As a result:
- At(target=bot_account_id) never appeared in the message chain, so the
'at-bot' group respond rule silently dropped every @bot mention.
- The bot only replied when the message happened to match the prefix
rule (e.g. starting with 'ai').
Now LINEMessageConverter reads message.message.mention and builds the
chain per mention position:
- Bot mention (isSelf) -> At(target=bot_account_id) so AtBotRule matches
the same way as other adapters (dingtalk/lark etc.).
- Other mentions -> At(target=<line user id>, display=<mention text>).
At.__str__ already prepends '@', so the display text carries no
double '@' and the rendered text (prefix/regexp rules, quotes,
session context) is byte-identical to before.
- Missing/out-of-bounds mentions are skipped defensively.
target2yiri becomes an instance method (like wechatpad/aiocqhttp) so
the converters can hold bot_account_id; LINEAdapter passes it in from
its own config.
execute_func_call returns list[ContentElement] for MCP tools, but the
runner assigned that list directly to the tool-message content. The
OpenAI chat-completions spec requires tool-message content to be a
string, so OpenAI-compatible endpoints return HTTP 500 when the raw
list is sent.
Serialize the list to a string before building the tool message, using
ContentElement.__str__ which returns the text payload for text elements
and a human-readable placeholder for images and files. Fixes#2457.
SessionManager clears image_base64 on past turns to save memory, and
exclude_none serialization drops the hollowed field entirely, so a
replayed history part can arrive as {'type': 'image_base64'} with no
payload. The converter accessed the missing key unconditionally and
raised KeyError on every turn after an image was sent.
Prefer the base64 payload when present, fall back to an image_url that
survived on the same element, and drop hollow parts otherwise (same
strategy as the existing file-part handling). Fixes#2469.
* fix(cntfilter): allow legacy sensitive-word lists over 64 patterns
Legacy sensitive-words.json files shipped ~70 rules. After v4.10.7,
BanWordFilter treated the 64-pattern safe_regex cap as a hard failure
and blocked every message. Raise the cap only on the sensitive-word
path, keep the 50ms CPU budget, and truncate oversized lists with a
one-time warning.
Fixes#2443
* fix(cntfilter): reject oversized sensitive-word lists
---------
Co-authored-by: dadachann <185672915+dadachann@users.noreply.github.com>
LINEEventConverter.target2yiri() built Friend.id/Group.id from
event.message.id, which is unique per message. Every incoming message
therefore mapped to a new session key, so LINE users and groups lost
conversation context on every turn.
Use event.source.user_id/group_id/room_id instead, matching the stable
identifiers other adapters (e.g. Telegram) use for session identity.
Falls back to the group/room id when user_id is absent, per LINE's
documented behavior for some group/room members.
* fix(wecombot): align media upload protocol
* fix(wecombot): deliver outbox media in reply and fix tool call recording
- Integrate _send_media into reply_message and reply_message_chunk so
sandbox outbox images/voices/files are uploaded and sent instead of
being silently dropped.
- Add missing import base64 that caused _send_media to fail with a
NameError swallowed by its except clause.
- Change yiri2target to return component dicts (text/image/voice/file)
so callers can distinguish text from media.
- Fix _get_message_for_tool_context using result.first()/row[0] which
returned a raw string instead of the ORM object, causing
"'str' object has no attribute 'pipeline_id'" in tool call recording.
Use result.scalars().first() per SQLAlchemy 2.0 convention.
* fix(pipeline): collect outbox attachments on final chunk with empty content
When the last streaming chunk has is_final=True but empty content
(e.g. the LLM sends all text in earlier chunks), the 'if result.content'
branch is skipped entirely, so _append_outbound_attachments never runs
and sandbox outbox images are silently dropped.
Add an elif branch for _is_final_assistant_message that creates an
empty MessageChain and still collects outbox attachments, so images
are delivered even when the final chunk carries no text.
* fix(box): bypass stdout truncation when reading outbox via exec
_read_outbox_via_exec used execute_tool which returns _serialize_result
where stdout is truncated to output_limit_chars (4000). A 7KB JPEG
encodes to ~9400 base64 chars, so the JSON payload was truncated and
json.loads failed silently, returning an empty list.
Call client.execute directly to get the raw BoxExecutionResult with
untruncated stdout, so base64 file data is preserved.
* fix(tests): adapt box and wrapper tests for client.execute and strict is_final check
- wrapper.py: restrict outbox collection on empty-content chunks to
actual MessageChunk instances with is_final=True, not generic Mock
objects that happen to have role='assistant'
- test_box_service.py: update _read_outbox_via_exec tests to mock
client.execute (returning BoxExecutionResult) instead of
execute_tool, matching the implementation change
* chore(wecombot): remove temporary upload log
* test(box): preserve direct outbox read and cleanup coverage
---------
Co-authored-by: fdc310 <2213070223@qq.com>
Co-authored-by: dadachann <185672915+dadachann@users.noreply.github.com>