* fix(provider): disable OpenAI SDK built-in retries under AstrBot retry layer (#9663)
AsyncOpenAI/AsyncAzureOpenAI inherit the SDK default max_retries=2, which
nests underneath AstrBot's tenacity-based retry_provider_request(). As a
result provider_settings.request_max_retries no longer reflects the true
attempt count (e.g. request_max_retries=1 still sends up to 3 HTTP
requests) and delays fallback-provider switchover. Pass max_retries=0 so
AstrBot's retry layer is the single source of truth for retries.
* test(provider): add retry-count regression test, parameterize client retry tests
Address review feedback on #9669: merge the near-identical official/Azure
client construction tests into a parametrized test, and add a regression
test asserting _query() performs exactly request_max_retries attempts
through retry_provider_request() when the call keeps failing, guarding
against multiplicative retries being reintroduced.
* fix(media): guard oversized image inputs and attachment uploads
为模型图片输入与聊天附件上传增加大小护栏 / Add size guards for model
image inputs and chat attachment uploads:
- _convert_image_bytes_sync 复用原始编码字节的分支增加 2MB 上限;更大的
输入重编码,输出始终受像素尺寸与质量约束
- prepare_model_image 在读入前检查文件大小,超过 32MB 跳过并记录日志
- 聊天附件上传上限 100MB:路由先检查 Content-Length,服务层落盘后复核
实际大小
- Reuse the original encoded bytes in _convert_image_bytes_sync only for
stills up to 2MB; larger inputs are re-encoded so the output stays
bounded by pixel size and quality.
- prepare_model_image checks the file size before reading and skips
inputs above 32MB with a warning log.
- Chat attachment uploads are capped at 100MB: the route checks
Content-Length first, and save_uploaded_file re-checks the on-disk
size after saving.
Tests: tests/test_media_utils.py, tests/test_chat_route.py
* fix(media): sniff image headers instead of decoding for MIME detection
MIME 探测改为读取文件头魔数,不再解码整个图片 / Detect image MIME
types by reading header magic bytes instead of decoding the whole file:
- detect_image_mime_type 仅读取前 32 字节识别 PNG/JPEG/GIF/WebP/BMP/
TIFF/AVIF,内存占用与文件大小无关
- 聊天附件上传上限从 100MB 放宽到 512MB
- detect_image_mime_type reads only the first 32 bytes and recognizes
PNG/JPEG/GIF/WebP/BMP/TIFF/AVIF, so memory usage no longer scales
with file size
- Chat attachment upload limit raised from 100MB to 512MB
Tests: tests/test_media_utils.py
* test: remove obsolete image assertion and fix upload fixtures
* test: mock plugin marketplace requests in dashboard tests
---------
Co-authored-by: RuochenPan <badbatch0x01@gmail.com>
* fix: tell the model when an animation becomes a frame montage
Animated inputs (GIF/APNG/animated WebP) are tiled into a 3x3 frame grid
before being sent to the model, but nothing described that transform, so a
contact sheet read as a single picture (reported as "nine identical
characters" for a three-frame GIF).
prepare_model_image now returns PreparedModelImage, which carries the tiled
frame count next to the working path, and the request preparation appends a
transient notice to the model content describing the grid, the reading
order, the number of tiled frames, and the blank cells. The notice is only
built for montages, marked as not persisted, and still images are untouched.
* fix: simplify animation montage notices
* fix: clarify single-image animation montage notice
* fix: shorten animation notice and hide frame layout details
* fix: describe converted GIF input directly
---------
Co-authored-by: Pleiades1726 <192008989+Pleiades1726@users.noreply.github.com>
Co-authored-by: Soulter <905617992@qq.com>
* fix(cron): say *who* owns a task instead of returning an empty list
future_task only ever reported tasks owned by the calling sender, so a job
that fired in a group chat was invisible to the agent woken by it: the woken
event's sender is the group id while the job records the member id, and both
edit/delete and list silently dropped it. Agents then concluded the task had
disappeared, told users it was stopped, and kept being woken by it.
This keeps the ownership rule (only the creator may edit/delete) but makes the
result self-describing:
* edit/delete of somebody else's task now says the task exists and belongs to
another member of this group chat (or to another user when the session is
not the same), and points the user at the creator;
* list keeps filtering, and appends a note with the number of same-session
tasks created by other members that were filtered out.
No behaviour change for owned tasks; jobs from other sessions are still
neither listed nor counted.
* fix(cron): only count real member-owned tasks as hidden
Follow-up to the review on the listing note. A same-session job was counted
as "created by another member" even when it had no recorded creator, which
is the same kind of wrong statement the previous commit set out to remove:
CronService.create_job writes origin="api" with a session but no sender_id,
and older rows may miss it too.
* count only same-session active-agent jobs that recorded a sender id and
are not owned by the caller, comparing ownership directly instead of
diffing job ids;
* edit/delete now answer "has no chat member as its creator (created
outside this chat, e.g. from the dashboard)" for such rows instead of
blaming another member;
* cover basic jobs, creator-less rows and the creator-less delete message.
* refactor(cron): inline the ownership replies per AGENTS.md
AGENTS.md's "No Unnecessary Helpers" section is inline-first: a helper is only
justified at 3+ identical call sites, or when inlining would blow up the
enclosing function. The two previous commits added four module-level helpers,
three of which had one or two call sites, so fold them back into the
edit/delete/list branches. The diff now only changes the answers, not the
structure of the calling code.
Behaviour is unchanged and the same 22 tests cover it.
* refactor(cron): drop the hidden-task count from the list note
The count was the only part of the note that could state something false: it
needed a predicate that excludes basic jobs and dashboard rows without a sender
id, which is what review flagged. A static hint that the list only contains
tasks created by the caller is true in every case, needs no counting, and no
longer depends on the chat type.
---------
Co-authored-by: apet <noreply@astrbot.com>
* feat: sandbox local member execution on Linux
* feat: add macOS Seatbelt local sandbox
* feat: Implement race-resistant file access for local tools
- Added `open_file_in_allowed_roots` function in `local_file_security.py` to securely open files within specified directories, preventing path traversal attacks.
- Updated file reading functions in `file_read_utils.py` to accept file descriptors, allowing for safer file operations.
- Enhanced `FileReadTool`, `FileWriteTool`, and `FileEditTool` to utilize the new secure file access method.
- Added tests to ensure restricted access works correctly, including scenarios with symlinked directories and ancestor swaps.
- Improved error handling for file operations to provide clearer feedback on permission issues.
* feat: add role-based Local computer permissions
* fix: clarify Windows Local permission behavior
* style: use neutral permission matrix border
* fix: clarify Local runtime permission labels
* fix: localize computer runtime options
* fix: tighten default Local permission policy
* fix: allow macOS sandboxed shell forks
* refactor: extract platform process sandboxes
* refactor: expose cross-platform sandbox limits
* refactor: unify sandbox process lifecycle
* refactor: seal local sandbox abstractions
* test: align local sandbox tests with Windows support
* fix: improve local permission settings layout
* fix: initialize workspace for restricted file writes
* feat: expose runtime and sandbox dependency information
* feat: add disabled local access and platform-aware permission controls
* feat: display runtime information in local permission controls
* fix: validate local permission support before saving config
* fix: use supported local permission defaults on Windows
* fix: restrict local file sending and align Windows permission tests
* chore: remove redundant local permission hint
* fix: restore local sandbox DNS and approved file access
* ci: enable bubblewrap user namespaces on Ubuntu runners
* fix: report local sandbox startup failures
* fix: use a portable sandbox startup probe
* fix: preserve read-only Python virtualenv access in local sandboxes
* fix: revoke local shell sessions when permissions change
* fix: block shared-path attacks on local runtime file exchange
Sandboxed processes can write to shared temp roots, so a named session
log there could be replaced with a symlink to a host file that host-side
reads and appends would follow. Keep managed shell output on an anonymous
temporary file handle instead of a named path.
Also stop _resolve_path_from_sandbox from consulting a booter under the
local runtime: workspace and local-file checks already enforce caller
permissions, and the fallback branch probed the host shell without them.
Reject such paths with FileNotFoundError instead.
* fix: revoke local shell sessions when admin access is removed
* fix: skip missing default roots in local grep
* test: check sandbox file readability across platforms
* test: wait for watchdog retry without fixed sleeps
* test: synchronize watchdog log rotation assertions
---------
Co-authored-by: RuochenPan <badbatch0x01@gmail.com>
Telegram long-polling floods the log in two ways:
1. python-telegram-bot performs getUpdates through httpx, which logs every request at INFO level. httpx/httpcore were missing from LogManager._NOISY_LOGGER_LEVELS, so those per-request lines (one every ~10s) leaked through.
2. _on_polling_error logged a full traceback for every polling error, including non-network errors such as Conflict (two bot instances polling the same token). Log a concise one-line message for non-network errors and keep the traceback only for NetworkError, which still needs it for recovery.
* feat: group model selection by provider source
* feat: optimize model selector with source filters and sticky groups
* fix: align model filter borders and hide single-source filter
* fix(qqofficial): always send the state=10 closing frame in C2C streaming
QQ's C2C streaming protocol ends a stream with a state=10 frame whose
content must end with \n. AstrBot only sent it while unsent text
remained in the buffer: once the throttled middle frames had flushed
the full reply and the generator ended with an empty tail, no closing
frame went out at all. QQ then timed the stream out and rolled the
whole message back to the first packet, which is why a full 850-char
reply ended up displaying only its first few characters (#10066), and
why some streams hang in "generating" forever.
Close every open segment: when the stream is open (a frame id exists)
but the tail buffer is empty, send a minimal "\n" closing frame; when
nothing was ever sent (no id), keep sending nothing. The tool_call
break path closes the same way through a shared helper so an empty
segment cannot orphan its stream either.
Regression tests pin both paths against a fake _post_send: empty tail
after a middle flush, and break arriving on an empty but open segment.
* fix(qqofficial): treat an empty-Plain tail as empty for the closing frame
Review on #10069: a buffer holding only Plain("") components has a
truthy chain, so the "\n" fallback was skipped, _post_send_one rejected
the empty parsed content, and no state=10 frame went out for an opened
stream. Check for sendable content (non-empty text or a non-Plain
component) instead of chain non-emptiness. Regression test pins the
empty-Plain tail after a middle flush.
* refactor: prepare model images in the local process stage
Prepare current input images before agent construction and after the request hook. Keep original attachments intact and preserve existing tool image behavior.
Reuse the compression toggle for PNG stills and animation montages, with event-owned working files and portable history serialization.
Validation: 2571 Linux tests, 495 Windows regression tests, dashboard build, and live text/JPEG/GIF calls with agnes-3.0-flash.
* test: read source files as UTF-8 in image boundary check
* fix: avoid blanket PNG conversion of model input images
* fix: support updating installed plugins from uploaded archives
* fix: update existing plugins through URL installation
* fix: skip temporary directories during plugin discovery
* fix: stage plugin installs and backups in system temp
* test: align dashboard plugin installer mock with staging API
* feat(dashboard): add chat history pagination and reasoning blocks (#9652)
* refactor(dashboard): remove reasoning char count and loading text
* feat(dashboard): load earlier messages on scroll up
* refactor(dashboard): remove reasoning retry label
* refactor(dashboard): remove thread panel pagination and lazy reasoning
Restore the thread panel to pre-#9652 behavior: full history without
pagination and inline reasoning content. Reverts the out-of-scope thread
additions while keeping main-session pagination and reasoning lazy-loading.
* chore: retrigger smoke test
* fix(dashboard): pause automatic history loading after failure
* feat(dashboard): show progress while loading earlier messages
* fix(dashboard): show reasoning loading errors and retry action
* style(dashboard): align chat loading errors with chat UI
* style(dashboard): make chat loading errors more compact
* refactor(chat): retain complete message content in history pages
* refactor(dashboard): expand reasoning directly from history pages
* refactor(api): remove lazy chat message retrieval endpoint
* fix(kb): space out concurrent waiters in the URL cleaning RateLimiter
URL content cleaning processes all chunks with asyncio.gather and a
shared RateLimiter. Every coroutine read the same last_call_time
before sleeping, so they all woke up at the same moment and fired
their requests together; the configured RPM did not bound that burst
and larger documents could trip the upstream limit.
Reserve the next release slot under a lock (sleeping outside it), so
waiters are released one interval apart, in order. Idle time is still
credited and max_rpm=0 still disables limiting.
Fixes#9995
* fix(kb): reserve RateLimiter slots under a lock so waiters do not burst
The first commit only carried the test; this adds the RateLimiter change
it exercises. Each waiter now reserves the next release slot under an
asyncio.Lock and sleeps until that slot, so concurrent callers are
released one interval apart instead of waking up together.
* fix(kb): preserve rate limit spacing after event loop stalls
---------
Co-authored-by: Soulter <905617992@qq.com>
* fix(lark): fall back to private chat ID when open ID send fails
Private file sends can fail with error 230101 when addressed by open ID,
even though the same file can be delivered to the private chat ID.
Persist private open ID to chat ID mappings from incoming messages,
scoped by platform and app ID, while preserving existing session IDs.
Keep open ID as the primary recipient and retry each rejected message
once with its known private chat ID, reusing the content and UUID.
Do not resend successful components or retry transport exceptions.
Propagate proactive send and attachment upload failures to callers so
failed deliveries are not reported as successful. Preserve the existing
boolean failure handling for replies.
Refs: #9989
* test(lark): cover private chat fallback and delivery failures
Reproduce rejected open ID file sends and verify that the same uploaded
file is retried once with the private chat ID and original UUID.
Cover successful primary sends, mixed message chains, failed deliveries,
upload failures, group routing, transport timeouts, and reply behavior.
Verify stored routes survive adapter recreation, remain scoped to the
bot, and cannot be overwritten by group messages.
Isolate sender-name tests from persistent route storage and supply the
platform and app identity required by route persistence.
Validation: 26 related tests passed; ruff format and ruff check passed.
Live Feishu API validation remains pending application credentials.
Refs: #9989
* feat(chatui): refine settings, composer and streaming experience
* style(chatui): compact composer and narrow message layouts
* style(chatui): align composer gutters and remove excess height
* style(chatui): give composer editing area more height
* fix(chatui): preserve reading width when side panels open
* fix: correct At/AtAll branch order in message outline
Original logic checked At first and then AtAll. However, because AtAll is a subclass of At, the AtAll branch would never be executed. This change moves AtAll before At to ensure the code behaves correctly.
* test: assert AtAll outline renders as [At:全体成员]
WebChat generates its title on the platform session, while the conversation history reads the conversation title column, so WebChat rows were listed as untitled. Fall back to the session display name when a conversation has no title, and let conversation search match session titles as well.
Entering the Data & Logs -> Logs page froze the WebUI for seconds because ConsoleDisplayer rendered each historical log line with a separate appendChild plus a forced synchronous reflow (scrollTop = scrollHeight) per line, and used innerText which triggers a style recalc on every assignment.
Batch the rendered lines into a DocumentFragment and append it once, scroll to the bottom a single time, and use textContent instead of innerText. This collapses O(n) reflows into one and eliminates the main-thread freeze.
_run_job marked a cron job "completed" unless an exception escaped, but
_woke_main_agent never checked the agent runner's terminal state: a run
that dies with a malformed function call at max steps ends in
AgentState.ERROR with no exception, so cron_jobs showed
status=completed, last_error=NULL, and the user only ever saw the
intermediate messages (#9980).
_woke_main_agent now raises when the runner ends in AgentState.ERROR
(carrying the final error text when there is one) and when the main
agent fails to build at all; _run_job's existing handler records both as
failed with last_error. The runner base class gains a read-only `state`
property so callers no longer reach into `_state`.
Tests: two regression cases on _run_job (ERROR terminal state, build
failure) asserting status=failed with the detail in last_error; both red
on current master and green here. Existing fake runners in the file got
the state attribute the runner contract now documents.
Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
* fix(qqofficial): render markdown for proactive send_by_session messages
* fix(qqofficial): preserve use_markdown_ when splitting media chains
* fix(qqofficial): fall back to content when markdown payload is rejected
* feat(qqofficial): add use_markdown config to gate default markdown sending
* feat(dashboard): add i18n entries for qqofficial use_markdown config
* fix(qqofficial): expose use_markdown on webhook template and clarify label
Add use_markdown to the QQ Official (Webhook) config template so new
webhook platforms expose and save the setting in the WebUI, matching the
WebSocket template. Rename the field label from the ambiguous '主动消息发送模式'
to the clearer '主动消息使用 Markdown' (en/ru translations updated).
Add a regression test asserting both QQ Official templates expose use_markdown.
---------
Co-authored-by: OMSociety <OMSociety@users.noreply.github.com>