Commit Graph
5287 Commits
Author SHA1 Message Date
卷卷andRuochenPan 67c74b7ed8 feat: gzip dashboard static js, css, and html (#10187)
* 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>
2026-09-24 11:49:24 +08:00
Ruochen Pan 28c6aef1e8 fix: preserve datetime storage across SQLModel upgrades (#10211) 2026-09-24 11:28:59 +08:00
Wei Chengqian 95e98b8aed fix: read WebUI versions from the served asset directory (#10156) 2026-09-21 16:27:08 +08:00
w31rd 3ada8aa0ae fix: separate reset and new conversation semantics (#10118)
* 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.
2026-09-21 16:22:39 +08:00
Wei Chengqian 8b5e24ba2e fix: respect session plugin filters in cron agents (#10163)
* fix: respect session plugin filters in cron agents

* test: simplify cron session configuration regression coverage

* test: preserve existing cron tests and isolate regression coverage
2026-09-21 15:08:19 +08:00
卷卷andsourcery-ai[bot] 96d9504b6c docs: explain HTTP proxy address when AstrBot runs in Docker (#10162)
* docs: explain HTTP proxy address when AstrBot runs in Docker

* Apply suggestion from @sourcery-ai[bot]

Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
2026-09-21 12:40:26 +08:00
Soulter 6914bc3aa6 fix: move local network policy notice into system prompts (#10134)
* fix: move local network policy notice into system prompts

* fix: skip network policy notices for inactive tools
2026-09-20 11:46:12 +08:00
俞可 · YU KEandSoulter b06550567b fix: clean up invalid materialized images (#10090)
* fix: clean up invalid materialized images

* style: restore LF line endings in image cleanup fix

---------

Co-authored-by: Soulter <905617992@qq.com>
2026-09-19 13:19:55 +08:00
xiongyuyang 54c98e8ac4 fix: resolve each OneBot file segment by its own ID (#10098) 2026-09-19 13:10:49 +08:00
xizhuomengcontin 774cc9498f docs: name the dashboard host/port environment variables and their precedence (#10095) 2026-09-19 13:09:51 +08:00
一张白纸 b13338dbda fix(dashboard): restore readable text and switch contrast in fullscreen console (#10135)
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.
2026-09-19 13:07:51 +08:00
燃烧军团团支部书记andkonodiodaaaaa1 dae7c24e12 fix(provider): recognize thinking tags in compatible completions (#10116)
Co-authored-by: konodiodaaaaa1 <konodiodaaaaa1@users.noreply.github.com>
2026-09-19 13:07:17 +08:00
ionase 27824540c5 fix: use GitHub HEAD when repository branch is unspecified (#10080)
* fix: use GitHub HEAD when repository branch is unspecified

* fix: preserve download errors when archive cleanup fails
2026-09-19 13:05:51 +08:00
beeminesandbeemines 9c73295738 fix: preserve scheduled reminders when an edit is invalid (#10075)
Co-authored-by: beemines <182060364+beemines@users.noreply.github.com>
2026-09-19 12:54:46 +08:00
Lukas 1f940d1539 fix(tools): keep literal escape sequences when editing a file (#10112)
`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.
2026-09-19 12:52:13 +08:00
Yufeng He 081261664c fix: propagate Xinference rerank failures instead of returning empty results (#10051)
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
2026-09-19 12:40:42 +08:00
xiongyuyang 8492df45e0 fix: serialize Azure TTS backend client lifetimes (#10101) 2026-09-19 12:38:51 +08:00
Anfyya f09760001a fix(gemini): keep narration streamed before a tool call in history (#10132)
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
2026-09-19 12:32:49 +08:00
Soulter 3c149fed76 feat: add agent step budget reminders and migrate legacy limits (#10133)
* feat: add agent step budget reminders and migrate legacy limits

* docs: leave configuration reference unchanged
2026-09-19 12:13:17 +08:00
Soulter b62d9a9612 feat: extend shell session waits and raise default agent step limit (#10131) 2026-09-19 10:53:07 +08:00
Ruochen Pan bb79c083e3 fix(dashboard): drop stale exclude_ids hardcode hiding conversations (#10129)
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
2026-09-19 10:43:59 +08:00
Ruochen Pan f99c76ec10 fix(dashboard): bound upload memory and add resumable chunked uploads (#10123)
* 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.
2026-09-18 17:55:30 +08:00
Soulter 8b82d03d94 fix: bound model input images below 512 KiB (#10120)
* fix: bound model input images below 512 KiB

* fix: reuse compliant model image files without copying

* docs: simplify image size setting hint

* fix: centralize model image preparation and attachment notices

* fix: clarify input image maximum edge length label
2026-09-17 23:55:18 +08:00
这只猫猫加了糖andRuochenPan 6ddcb2fc08 fix(provider): Dsable OpenAI SDK built-in retries to prevent request multiplication from stacked retry layers (#9669)
* 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>
2026-09-17 21:15:01 +08:00
2704b4c0d3 fix: tell the model when an animation becomes a frame montage (#10119)
* 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>
2026-09-17 18:23:52 +08:00
Soulter e0aa8d3861 fix: clarify local file access menu descriptions 2026-09-16 15:42:45 +08:00
Soulter e00442647b feat: improve conversation data previews (#10108) 2026-09-16 15:40:49 +08:00
AstrBotandapet 763a5ee971 fix(cron): say who owns a task instead of returning an empty list (#10104)
* 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>
2026-09-16 01:08:53 +08:00
Soulter 553096b0c1 docs: sync guides with current WebUI navigation (#10093) 2026-09-15 22:10:21 +08:00
dependabot[bot] 5950d7ce3b chore(deps): bump the github-actions group with 2 updates (#10088)
Bumps the github-actions group with 2 updates: [pnpm/action-setup](https://github.com/pnpm/action-setup) and [github/codeql-action](https://github.com/github/codeql-action).


Updates `pnpm/action-setup` from 6.0.10 to 6.1.0
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/v6.0.10...v6.1.0)

Updates `github/codeql-action` from 4.37.9 to 4.38.0
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v4.37.9...v4.38.0)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: github/codeql-action
  dependency-version: 4.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-15 22:06:29 +08:00
Ruochen Pan fce1bbc97a Fix:local sandbox policy feedback (#10096)
* docs(webui): clarify local file access scopes

* fix: report network restrictions in local tool results

* test: simplify local network policy feedback coverage
2026-09-15 17:05:41 +08:00
SoulterandRuochenPan 06261c532a feat: add role-based Local computer permissions (#9472)
* 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>
2026-09-15 15:48:01 +08:00
Soulter ab42c0d9b7 chore: bump version to 4.28.1 (#10085) v4.28.1 2026-09-14 23:11:06 +08:00
氕氙 02fef48c26 fix: preserve input image geometry under CUA sandbox and warn on oversized images (#10071)
* fix: skip input image resize under CUA sandbox to keep pixel coordinates 1:1

* feat: warn when CUA passthrough images may exceed provider upload limits

* fix: keep configured montage cap for animated inputs under CUA sandbox
2026-09-14 23:27:50 +09:00
玲因 e79c0d7aa0 fix: reduce Telegram log spam from polling and conflict errors (#10027) (#10046)
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.
2026-09-14 22:27:33 +08:00
Wei Chengqian 7ee03f2293 fix: preserve OpenAI endpoint resolution for empty api_base (#10079) 2026-09-14 21:48:46 +08:00
Soulter 56cbc4a4a6 feat: optimize model selection with source filters and sticky groups (#10084)
* 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
2026-09-14 21:43:02 +08:00
Yufeng He 31d798944d fix(qqofficial): always send the state=10 closing frame in C2C streaming (#10069)
* 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.
2026-09-14 21:30:35 +08:00
Soulter d524b8708a fix: standardize provider user agents with the AstrBot version (#10082) 2026-09-14 21:24:00 +08:00
氕氙 bd046ed299 feat: adaptively prepare model input images in the local process stage (#9703)
* 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
2026-09-14 01:43:09 +08:00
Soulter 4007d645c9 fix: use selected model in webchat side threads (#10065) 2026-09-13 20:07:24 +08:00
Soulter 7ec39bdef0 fix: support plugin updates through URL and file installation (#10053)
* 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
2026-09-12 15:05:58 +08:00
w31rd c264e1d92a feat: unify /new and /reset behavior (#10004)
* feat: unify new and reset conversation behavior

* fix(i18n): align conversation isolation labels

* fix(i18n): clarify group conversation permission hints

* feat: centralize conversation command permissions

* fix(i18n): clarify administrator-only command permissions

* docs: correct conversation command permission instructions

* test: isolate conversation restart command registration

* feat: follow conversation isolation for restart permissions
2026-09-12 14:19:58 +08:00
Soulter 11cab79dc7 feat(dashboard): show desktop update download progress (#10050) 2026-09-12 13:57:12 +08:00
Wei Chengqian fae2c120ef feat(chatui): paginate chat history (#9667)
* 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
2026-09-12 13:56:22 +08:00
Soulter 72928aa7c9 fix: preserve image attachment paths after event cleanup (#10042)
* fix: preserve image attachment paths after event cleanup

* fix: retain local image references when normalization fails

* revert: undo local image reference normalization change
2026-09-12 12:31:10 +08:00
beeminesandbeemines f2ead2922e fix: restore session model fallback for proactive agents (#10028)
Co-authored-by: beemines <182060364+beemines@users.noreply.github.com>
2026-09-12 11:16:38 +08:00
Lukas BuckandSoulter 8b5f38885e fix(kb): space out concurrent waiters in the URL cleaning RateLimiter (#10015)
* 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>
2026-09-10 23:42:17 +08:00
若月千鸮 0f445f5eff fix(agent): deliver skills-like fallback replies when streaming (#10025)
* fix(agent): deliver skills-like fallback replies when streaming

* fix(agent): limit fallback fix to missing streaming events
2026-09-10 23:34:33 +08:00
PyuraMazo 0d40d34185 feat: enhance slider robustness (#10037)
* fix: validate numeric config bounds if slider exists

* feat: add min/max labels to slider
2026-09-10 23:33:04 +08:00