* feat: gzip dashboard static js, css, and html
Compress JS, CSS, and HTML from the dashboard static routes when the client sends Accept-Encoding: gzip. Images, fonts, event streams, and range requests stay unchanged. Cache-Control is not modified.
Refs #10186
* fix: gzip dashboard static files off the event loop
Read and compress in a worker thread so a large bundle does not stall other dashboard requests.
---------
Co-authored-by: RuochenPan <badbatch0x01@gmail.com>
* fix: separate reset and new conversation semantics
* Revert "fix: separate reset and new conversation semantics"
This reverts commit e24f5ffb04.
* Reapply "fix: separate reset and new conversation semantics"
This reverts commit 3fa0477afb.
The fullscreen view of ConsoleDisplayer kept Vuetify's default
`--v-theme-on-surface` and `--v-theme-surface-variant`, which on the
forced `#1e1e1e` background rendered body text nearly invisible and
left `.v-switch__track` blending into the chrome.
- Force `--v-theme-on-surface` to white and set `color: #fff` for
`#console-wrapper:fullscreen` so log output and controls stay
legible.
- Override `--v-theme-surface-variant` on the fullscreen switch track
so the toggle knob reads against the dark wrapper.
`astrbot_file_edit_tool` ran `old` and `new` through
`_decode_escaped_text` unconditionally, so a backslash-n in the arguments
always became a real line break.
The read, write and grep tools pass their strings through unchanged, so a
file written by `astrbot_file_write_tool` can hold a literal "\n" that the
edit tool could then never match: any Python or JavaScript source with
"\n" in a string literal, a regex such as r"\number", or a Windows path
such as C:\new\temp. Inserting such text was corrupted the same way.
Try the arguments as they were given first and fall back to the decoded
form only when that finds nothing to replace, so a model that escapes its
arguments keeps working.
The Xinference rerank provider swallowed upstream failures and an
uninitialized model by returning an empty list, which the retrieval
manager read as a legitimate empty rerank and used to overwrite the
fused candidates with nothing. Other providers (e.g. TEI) raise, and
the manager only preserves the fused results on that path, so a
transient Xinference outage silently degraded every knowledge-base
query to zero hits. Raise instead, matching the provider contract and
letting the manager's degradation keep the unreranked results.
Fixes#10000
The streaming branch built the final LLMResponse from the tool-call chunk\nalone and returned immediately, so assistant text and reasoning that had\nalready been streamed to the user were dropped and never written to the\nconversation history. Merge the accumulated text/reasoning into that final\nresponse before returning.
Fixes#10105
The dashboard conversation pages hardcoded exclude_ids="astrbot", and
the backend expanded each entry into `user_id NOT LIKE '<id>%'`. Since
user_id is the UMO "<platform_id>:<message_type>:<session_id>", every
conversation of a platform whose adapter id is "astrbot" (or any id
starting with it) was filtered out and the list showed 0 items (#10126).
The parameter never matched the built-in WebChat sessions it presumably
targeted (their UMO starts with "webchat:"), so it has had no functional
effect since it was introduced; remove it instead of replacing it.
Tighten the remaining exclude_ids semantics for API callers: match only
the exact user_id or its platform segment ("id:...") and escape LIKE
wildcards, so "astrbot" no longer swallows "astrbotweb".
Fixes#10126
* refactor(dashboard): consolidate upload saves into streaming helper
Five copies of the same upload-save logic existed: three file wrappers
(UploadFileAdapter, RequestUploadFile, PluginUploadFile) plus one
_save_upload() in each of BackupService and SkillsService. The backup
variant fell back to a full-body read() for raw FastAPI UploadFile,
buffering entire uploads in memory.
Introduce save_upload_stream() as the single streaming implementation
(1 MiB blocks, optional max_bytes with in-loop abort and partial-file
cleanup). The wrappers keep their public signatures and delegate to it;
skills/chat/config save paths call save() directly with max_bytes,
replacing their post-save size checks.
* fix(dashboard): bound backup upload size and harden chunked flow
Backup routes passed raw FastAPI UploadFile into the service layer,
hitting the full-body read() fallback: a single authenticated request
with an oversized chunk or whole-file upload could OOM-kill the
process (verified with a 512 MiB memory-limited container, exit 137).
- Wrap uploads in UploadFileAdapter so all saves stream to disk
- Enforce CHUNK_SIZE (1 MiB) per chunk and 128 MiB for whole-file
uploads, aborting mid-stream with partial-file cleanup
- Cap chunked backup totals at 8 GiB and point larger backups at
manual restore via the data directory
- Verify the merged backup size against the declared total
* fix(dashboard): enforce MAX_CONTENT_LENGTH with per-route overrides
The 128 MiB MAX_CONTENT_LENGTH value was written into the compat-layer
config dict but nothing read it, so the FastAPI/hypercorn path had no
request body limit at all.
Add a middleware that rejects oversized bodies with 413 before auth or
body parsing, using MAX_CONTENT_LENGTH as the default with per-route
overrides for legitimate large uploads (attachments 512 MiB, plugin
config files 500 MiB, knowledge bases 512 MiB) and a tight 2 MiB cap
on the backup chunk endpoint. Requests without a Content-Length header
pass through and remain bounded by per-endpoint max_bytes at save time.
* refactor(dashboard): extract generic chunked upload session service
Pull the chunk session machinery out of BackupService into a reusable
ChunkedUploadService owning session lifecycle, chunk storage, streaming
assembly and expiry cleanup, in preparation for webchat attachment
chunked uploads.
Sessions are now bound to an owner (the authenticated username) and a
purpose; chunk/complete/abort calls from another owner fail with the
same "not found or expired" error as unknown sessions, so upload IDs
cannot be probed or consumed cross-user. BackupService keeps business
concerns (zip validation, size caps, manifest marking) and delegates
the rest; API shapes and frontend flows are unchanged.
* feat(dashboard): add resumable upload status endpoint
Expose per-session progress (received chunk indexes, totals, expiry)
so clients can resume an interrupted chunked upload by sending only
the missing chunks instead of starting over.
Abuse considerations baked in: the lookup inherits owner binding and
answers "not found or expired" for both unknown sessions and sessions
owned by someone else (no probing), the response carries no filename
(minimal disclosure), and the query is strictly read-only — it never
touches last_activity, so polling it cannot anchor a session and its
on-disk chunks alive past expiry.
* fix(dashboard): bound file signature memory with chunked hashing
getFileSignature() read the entire file into browser memory via
arrayBuffer() before computing SHA-256, spiking renderer memory by the
full attachment size (up to the 512 MiB upload cap).
The signature is only used for in-session staging dedup and never
leaves the browser, so it does not need to be the canonical whole-file
SHA-256. Hash 8 MiB blocks individually and digest the concatenated
block digests instead; the prefix changes to sha256m: to mark the
algorithm change (old signatures vanish with the page session, so no
compatibility concern).
* feat(dashboard): resumable chunked upload UI for backup import
Extract the chunk scheduling logic from BackupDialog into a reusable
useChunkedUpload composable (5-way concurrency, per-chunk silent
retries, progress accounting) and add resume support on top of the
new upload status endpoint: a failed upload now offers a "Resume
Upload" action that queries received chunk indexes and sends only the
missing ones. Expired sessions transparently re-initialize and start
over; a failed complete() drops the poisoned session by design.
Wires the new /backups/upload/status endpoint through the OpenAPI
spec and regenerated client, and adds the resumeUpload label to all
four locales.
* feat(dashboard): chunked upload endpoints for webchat attachments
Mirror the backup chunked-upload flow for chat attachments so large
files (>128 MiB request-body default, up to the 512 MiB attachment
cap) can be uploaded in slices: POST /api/v1/files/upload/{init,
chunk,complete,abort,status}, owner-bound sessions, and a per-route
body limit on the chunk endpoint.
ChatService reuses the generic ChunkedUploadService with a new meta
passthrough carrying the client content_type through the session;
completion merges outside the chunk dir and feeds the result through
save_uploaded_file() unchanged via a move-based LocalUploadFile
adapter, keeping MIME sniffing, type classification and attachment
insertion in exactly one place.
Also repairs two pre-existing test stubs that predated the
max_bytes-aware save() contract and were already failing on this
branch.
* feat(dashboard): chunked upload with resume for webchat attachments
Route staged files >= 32 MiB through the new chunked upload endpoints
via useChunkedUpload; smaller files keep the single POST path so paste
and quick picks stay low-latency. The content_type travels through the
init payload so server-side attachment classification is unchanged.
A failed chunked upload is kept as an error chip in the input area
with a retry action that resumes from the last received chunk (and
transparently restarts when the session expired) plus a discard action
that aborts the session. Failed entries hold their uploader in a
shallowRef so Vue's deep ref unwrapping cannot strip the composable's
internal refs, and their signatures stay reserved to block duplicate
staging while a failed entry exists.
* docs: regenerate openapi artifacts for file upload endpoints
* fix(dashboard): harden chunk save and request body limits
Address review findings on the upload-hardening branch:
- save_chunk() now writes to a uniquely named temp file, verifies the
save() byte count against the expected chunk length, and publishes
via atomic rename. A failed or concurrent same-index retry can no
longer truncate and delete an already-received chunk (previously
left received_chunks claiming data that assemble() then failed to
find), and short writes are rejected instead of published.
- Requests with multipart bodies but no Content-Length are rejected
with 411: form parsing spools large bodies to disk before any
per-file size check can run. This is a documented stopgap — other
lengthless bodies still pass and are bounded only at save time;
closing that fully requires counting bytes as they arrive.
- Whole-file upload overrides gain 1 MiB of multipart framing slack
so a file exactly at the size cap is no longer rejected with 413.
The body-limit decision is extracted into a pure function for direct
unit tests; the assemble-time size recheck is now covered by an
out-of-band corruption test since chunk-level verification rejects
short writes earlier.
* fix(dashboard): drain upload pool and pin session on resume
A failed chunk made runPool() reject immediately while the remaining
in-flight requests kept writing; a resume could then race them on the
same chunk, stale retries read the mutable uploadId (potentially
writing into a re-initialized session), and late completions kept
counting progress (150% after resume).
The pool now stops scheduling on first failure but drains issued
requests via allSettled before surfacing the error, each request pins
the uploadId captured at run start, and a run-generation counter
(bumped on start/resume/cancel) blocks stale completions from the
progress accounting.
* fix(dashboard): close 411 bypass and temp-file residue on publish
Address the second round of review findings:
- The 411 media-type check used a raw startswith(), but the form
parser strips surrounding whitespace, so a leading-space
Content-Type was let through without a Content-Length while
Starlette still parsed the body as multipart. Detection now uses
the same parse_options_header the parser uses.
- A failing os.replace() during chunk publish left the uniquely
named temp file behind; it is now covered by the same cleanup
discipline as the save path.
Backup direct-upload routes deliberately get no framing slack: they
are legacy paths with no remaining callers, and the pre-existing
exact-limit 413 edge is answered by chunked upload.
* fix(dashboard): abort sessions created after a cancel
Cancelling while initUpload was still in flight cleared local state
before an upload id existed; when the delayed init returned, the run
flipped from idle to error and the freshly created server session was
never aborted. handleFailure now lets cancel win over any late
resolution and aborts the session id captured at that point.
* fix(dashboard): check cancel flag after completeUpload resolves
Cancelling while completeUpload was in flight let the late success
response flip the uploader from idle to done and hand the result to
the caller, which proceeded to the precheck. completeSession() now
re-checks the cancel flag after the response arrives and routes the
late success through the cancelled path instead.
* fix(dashboard): gate the 411 rule to body-capable methods
The 411 stopgap rejected GET /api/v1/logs/live: the SSE log client
sends a bogus "Content-Type: multipart/form-data" header on a GET
(copy-paste relic), and a GET with no Content-Length matched the
multipart-without-length rule. Body-less methods never trigger form
parsing, so no disk-spool risk exists for them — the 411 now applies
only to POST/PUT/PATCH. The junk header on the SSE client is removed
as well; the other two SSE consumers never had it.
* feat(dashboard): show in-progress chips for chunked attachments
Chunked uploads gave no feedback while in flight: the staged chip
only appeared after completion, leaving multi-minute uploads silent.
Track in-flight uploads alongside the failed ones and render progress
chips (filename, percent, thin progress bar) with a cancel action in
the input area; on success the chip hands off to the staged
attachment, on failure to the resumable error chip.
* fix(dashboard): type active upload chips upfront and align progress bar
The in-progress chip used a generic cloud icon even though the file
type is known locally before the upload starts; it now derives
icon/color/label from the filename via attachmentPresentation, same
as the staged chip. The progress bar's left edge is aligned with the
filename text instead of the card padding.
* fix(dashboard): keep active-chip progress bar inside the card
v-progress-linear ships width:100%, which wins over the right offset
in absolute positioning; the bar ran 52px past the card's right edge
and was clipped by overflow:hidden. Give it width:auto so the left/
right offsets determine the width.
* fix(dashboard): rework active-chip progress track and cancel button
Positioning a Vuetify component root directly proved fragile — the
progress bar drifted to the card's top edge. Wrap it in a plain track
div that owns the absolute positioning and let the component fill it.
Replace the corner tonal badge cancel with an inline text-variant
icon button after the percent label, and drop the padding reserved
for the badge.
* fix(dashboard): enforce session expiry and atomic save semantics
Address the third review round (sourcery-ai findings):
- get_session() now actually enforces the idle expiry it advertises
in its "not found or expired" error; expired sessions stay
abortable for cleanup, and backup upload_init starts the janitor
just like chat's does, so chunk dirs cannot linger indefinitely.
- cleanup_session() only unregisters a session after rmtree()
succeeds, letting the janitor retry on transient filesystem errors
instead of permanently leaking the chunk directory.
- save_upload_stream() writes to a uniquely named temp sibling and
atomically renames on success, so a failed or cancelled overwrite
can no longer delete a pre-existing destination (e.g. re-uploading
over an existing attachment), and all callers inherit the behavior.
The write()-return-value finding is a non-issue: BufferedWriter
writes fully or raises, and the chunk path already verifies byte
counts.
* fix(dashboard): guard failed-upload retry against double clicks
retryFailedUpload() ran resume() without any in-flight marker, so a
second click started a concurrent resume on the same session; the
loser hit "not found or expired" and left a failed chip next to the
successfully staged attachment. The entry now moves to activeUploads
while resuming, which blocks the second click and shows live
progress, and returns to failedUploads only if the resume errors.
* 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>