* refactor(dashboard): complete the plugin pages to views rename
Finish the remaining Pages -> Views rename on the dashboard:
- Fix the plugin card "Open View" button crash by pointing
openPluginWebui at the renamed PluginView route
- Rename PluginPagePage.vue to PluginViewPage.vue and update its
internals (loadPluginView, pluginViewTitle, .plugin-view-* classes)
- Rename openPluginWebui -> openPluginView and the open-webui emit to
open-view; pluginPageTitle/Description -> pluginViewTitle/Description;
isPluginPageRoute -> isPluginViewRoute
- Rename i18n keys (pluginWebui -> pluginView, openWebui -> openView,
pluginPageType -> pluginViewType, messages) across en/zh/ja/ru locales
The postMessage bridge channel name (astrbot-plugin-page) is kept
unchanged because it is part of the wire protocol shared with
plugin_page_bridge.js.
* feat(dashboard): authenticate plugin view assets with scoped path tokens
Serve plugin view assets from
/api/v1/plugins/{id}/views/{page}/_t/{token}/... so the scoped token
travels in the URL path. Relative URLs inside the view then inherit the
token through normal path resolution, which removes the need for
server-side HTML/CSS/JS URL rewriting on this route.
- Add the _t routes with a require_plugin_view_token dependency
(v1 paths bypass the dashboard auth middleware)
- Extend the plugin page asset token to a session-length TTL (7 days,
aligned with the dashboard session) with a page_session purpose claim,
still scoped to a single plugin and page
- Serialize the view entry content_path as the _t URL, keeping the
asset_token query for plugins that parse location.search
- Keep the legacy /api/plugin/page/content/...?asset_token= form working
during the transition
- Rename the OpenAPI tag Plugin Pages -> Plugin Views, document the new
route, regenerate the frontend API client, and update the zh/en
plugin-pages docs
* refactor(dashboard): remove the legacy plugin page content route and URL rewriting
The path-token view route makes the compatibility surface unnecessary:
- Drop /api/plugin/page/content/... (query asset_token form)
- Drop the redundant authenticated asset routes /plugins/page/assets,
/plugins/view/assets, and /plugins/{id}/(pages|views)/{page}/assets/...
which had no callers outside the generated client
- Remove the HTML/CSS/JS URL rewriting helpers; view assets are always
served raw and only HTML gets bridge SDK injection plus theme handling
- Slim PluginPageAuth down to the bridge SDK endpoint, the only
non-v1 path still authenticating scoped query tokens
- serialize_plugin_page no longer falls back to the legacy content path
- Update the plugin-pages docs (zh/en) and the OpenAPI spec, and
regenerate the frontend API client
* refactor(dashboard): drop legacy plugin page API aliases
Remove the page-named plugin view API aliases now that the views rename
is complete and legacy compatibility is intentionally dropped:
- DELETE /api/v1/plugins/page(s) and /plugins/{plugin_id}/pages{,/{page_name}}
aliases; /plugins/view(s) and /plugins/{plugin_id}/views{,/{page_name}}
are the only entry config routes
- Remove the unused /api/v1/plugins/page-bridge-sdk.js route; the live
bridge SDK stays at /api/plugin/page/bridge-sdk.js
- Dashboard calls the view-named endpoints; regenerate the API client
and the OpenAPI scopes docs
* refactor(dashboard): keep plugin page APIs as deprecated view aliases
Restore the page-named plugin view routes removed in the previous commit
and keep them for a transition period, with view as the canonical API:
- /plugins/page(s) and /plugins/{plugin_id}/pages{,/{page_name}} serve as
aliases of the corresponding /views routes (same handlers)
- The OpenAPI spec marks the page operations deprecated and points new
clients at the view routes; the v1 page-bridge-sdk.js route is restored
- The dashboard keeps calling the view endpoints by default
* refactor(dashboard): rename the plugin view path parameter to view_name
The view entry and path-token asset routes now use {view_name} instead of
{page_name} in the OpenAPI-visible path, and the service layer renames its
internal page_name variables to match. Wire-compatible surfaces stay
unchanged: the page_name JWT claim, the pageName JSON field, query
parameters, and the deprecated /pages aliases. Error messages and the
plugin view docs (zh/en) complete the Page to View rename.
* feat(dashboard): regroup the sidebar into labeled System and Extensions sections
- Add static group headers (System / Extensions) to the sidebar, in the
style of AI Studio's labeled sections
- System: welcome, platforms, providers, extensions, config profiles,
data & logs, custom rules
- Extensions: knowledge base, cron, subagent orchestration, personas;
plugin pages are flattened into this section and the collapsible
plugin-pages group is removed
- Dissolve the collapsible More Features group; headers hide in rail mode
- Skip the legacy sidebar customization when building the menu (the
feature is slated for removal)
- Update zh/en docs for the new locations
* feat(dashboard): place personas before knowledge base in the extensions group
* feat(dashboard): make the extensions sidebar group collapsible
Add a chevron toggle to the extensions group header; collapsing hides
the group's items and the state persists in localStorage. Rail mode is
unaffected and always shows every icon.
* feat(dashboard): pin extension sidebar items to the top of their group
Extension group items show a pin button on hover; pinned items move to
the top of the extensions group (hidden together with the group when
collapsed) and the pinned set is persisted in localStorage.
* feat(dashboard): keep the unpin icon always visible on pinned items
Pinned items now show a PinOff icon in the primary color without
requiring hover; unpinned items still reveal the Pin icon on hover.
* feat(dashboard): show a hover card with plugin details on plugin page items
Hovering a plugin-provided page in the extensions group reveals a card
with the page name, plugin ID, and author. The data comes from the
existing plugin list API (no backend change); static items and rail
mode are unaffected.
* fix(dashboard): add author and version to the PluginEntry type
* feat(dashboard): refine the plugin hover card
- Show the card immediately on hover (no open delay) and drop the
expand animation
- Plugin row shows an icon plus 'display_name (id)' with the id muted,
or just the id when the plugin has no display name
- Author row gets its own icon
* feat(dashboard): add a type row to the plugin hover card
Defaults to '插件可视化视图' (localized in all four locales).
* feat: rename plugin Pages to Views and group plugin views by plugin
Rename (Views preferred, Pages kept as a compatible alias everywhere):
- StarMetadata.pages -> views; metadata.yaml accepts views: (pages: alias)
- Plugin view directory views/ preferred, pages/ still accepted
- v1 API gains /plugins/view(s) alias routes alongside /plugins/page(s)
- Bridge global window.AstrBotPluginView with AstrBotPluginPage as alias
- Plugin i18n keys views.<name>.* preferred, pages.<name>.* alias
- Frontend route /plugin-view/... with /plugin-page/... redirecting
- OpenAPI spec + regenerated client; docs use Views (zh: 插件可视化视图)
Sidebar (extensions group):
- Default mode shows the view name for plugin views
- New group-by-plugin toggle on the extensions header (grid icon, off by
default, persisted in localStorage): plugin views are grouped per plugin
under the extensions group, each group listing all of the plugin's views
- Hover card on plugin view items shows page name, plugin display name
(with id), author, and type; white card style reused for the toggle
tooltip
- Pinning works for nested views too, lifting them to the group top
* 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.
* 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>
* 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
* refactor: migrate to fastapi
* structure refactor
* fix: pyright fix
* refactor: improve error handling and public messages in plugin services
* feat(api): refactor API client integration and enhance request handling
- Updated API client configuration to use a dedicated HTTP client.
- Introduced utility functions for generating options, queries, and form data for API requests.
- Refactored multiple API methods to utilize the new utility functions for improved consistency and readability.
- Renamed types for clarity and updated import statements accordingly.
feat(docs): add script to update OpenAPI JSON from YAML spec
- Created a Python script to convert OpenAPI YAML specification to JSON format.
- The script supports customizable input and output paths.
- Ensured the script handles directory creation for output paths and validates the YAML structure.
* fix
* feat(auth): implement rate limiting for v1 login endpoint and enhance request handling
* Refactor dashboard API routers to use legacy_router for backward compatibility
- Changed all instances of dashboard_router to legacy_router across multiple API modules including platform, plugins, providers, sessions, skills, stats, subagents, t2i, tools, updates, and asgi_runtime.
- Updated route definitions to ensure existing endpoints remain functional under the new router structure.
- Introduced support for Quart request context in asgi_runtime to enhance compatibility with existing Quart-based plugins.
- Added a test case to validate the functionality of the new Quart request context handling in plugin extensions.
* chore: remove cli test
* fix: update dashboard tests for fastapi migration
* chore: satisfy ruff checks
* fix: update openapi api key scopes
* fix: sync config scope chip selection
* fix: restore quart dependency
* docs: clarify quart plugin api compatibility
* docs: update openapi scope documentation
* fix: use singular skill openapi scope
* fix: hide update service exception details
* fix: address fastapi review comments
* fix: address dashboard review findings
* docs: revert unrelated package deployment changes
* docs: update agent api generation guidance
* feat: add plugin page web api helpers
* docs: add plugin page bridge demo
* fix: type plugin upload files
* fix: stabilize plugin page uploads
* fix: type plugin web request proxy
* docs: remove plugin page docs example
* fix: authenticate plugin page SSE bridge