* 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>
The local-agent runner's system prompt is a static string with no
template-variable support, so the model had no anchor for "today" and
resolved relative time references (e.g. "this quarter", "latest")
against whichever period was best represented in training data instead
of the real date, sometimes confidently answering with stale
information for time-sensitive questions.
PreProcessor now appends a short, deterministically-computed
"Current date: ..." note to the system prompt on every request for
local-agent pipelines, alongside guidance to verify time-sensitive
facts with a search tool rather than answering from memory. The
existing skill-awareness prompt injection is refactored to share the
same append-to-system-prompt helper.
* feat: report independent instance and workspace identities
* test: include workspace in OAuth callback fixture
* ci: pin production cloud adapter to Space release
* fix: preserve authenticated Workspace telemetry attribution
* ci: pin production cloud adapter to final Space release
---------
Co-authored-by: dadachann <185672915+dadachann@users.noreply.github.com>
* fix(cloud): show owner model balance and enforce single owner
* fix(migrations): create owner index idempotently
---------
Co-authored-by: dadachann <185672915+dadachann@users.noreply.github.com>
Telegram file.file_path is a full URL of the form
https://api.telegram.org/file/bot<TOKEN>/<path> that embeds the bot
token. Since #2362 this URL was copied into Image.url, so the token was
serialized into the message chain and thereby persisted to the
monitoring database, shown in the dashboard, and forwarded to every
installed plugin via event dispatch. Anyone with dashboard or plugin
access could recover the token and take full control of the bot.
Unlike the public CDN URLs used by the other adapters changed in #2362,
Telegram file URLs are only usable with the embedded token, so there is
no safe URL to expose. Store base64 only (as before #2362); the vision
path already relies solely on base64, so nothing downstream changes.
Add a regression test asserting the token never appears in the
converted Image or the serialized message chain.
Co-authored-by: Constantine1916 <Constantine1916@users.noreply.github.com>
Preserve the platform CDN URL in Image.url alongside base64 data,
enabling plugins to use ContentElement.from_image_url() for direct
vision API access without redundant local download.
- aiocqhttp: use msg_data["data"]["url"] and msg.data["url"]
- discord: use attachment.url
- telegram: use file.file_path
- slack: use pic_url
- wecom: use picurl
- qqofficial: use pic_url
Satori adapter already follows this pattern (satori.py:168).
The change is purely additive — base64 is preserved for backward
compatibility, and get_bytes() priority (url → base64 → path)
ensures plugins can choose the optimal path.
Closes#2355
Co-authored-by: douxt <8429023+douxt@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Previously, MCP tool calls via call_tool() had no timeout, so a hung MCP
server would block the entire session indefinitely (exacerbated by
concurrency.session=1). This wraps the call in asyncio.timeout(30) and
raises an Exception on expiry, letting the LLM recover gracefully.
Closes#2339