Files
AstrBot/astrbot/dashboard/api/config_profiles.py
Weilong Liao 0d8e8682db refactor(core): migrate backend backbone from Quart to FastAPI and introduce more OpenAPI (#8688)
* 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
2026-06-14 15:03:26 +08:00

469 lines
14 KiB
Python

from __future__ import annotations
from typing import Any
from fastapi import APIRouter, Depends, Query, Request
from astrbot.dashboard.responses import error, ok
from astrbot.dashboard.schemas import (
ConfigContentRequest,
ConfigProfileCreateRequest,
ConfigRoutesReplaceRequest,
ConfigRouteUpsertRequest,
RenameRequest,
)
from astrbot.dashboard.services.config_service import (
ConfigDisplayService,
ConfigFileService,
ConfigProfileService,
ConfigRoutingService,
)
from .auth import AuthContext, require_scope
from .multipart import multipart_parts
router = APIRouter(tags=["Config Profiles"])
legacy_router = APIRouter(
prefix="/api/config",
tags=["Dashboard Config"],
include_in_schema=False,
)
async def require_config_scope(request: Request) -> AuthContext:
return await require_scope(request, "config")
def get_service(request: Request) -> ConfigProfileService:
return request.app.state.services.config_profiles
def get_routing_service(request: Request) -> ConfigRoutingService:
return request.app.state.services.config_routes
def get_display_service(request: Request) -> ConfigDisplayService:
return request.app.state.services.config_display
def get_file_service(request: Request) -> ConfigFileService:
return request.app.state.services.config_files
async def _json_or_empty(request: Request) -> dict:
try:
data = await request.json()
except Exception:
return {}
return data if isinstance(data, dict) else {}
def _alias_error(message: str):
return error(message)
def _model_dict(payload) -> dict[str, Any]:
return payload.model_dump(exclude_none=True)
@router.get("/config-profiles/schema")
async def get_config_profile_schema(
_auth: AuthContext = Depends(require_config_scope),
service: ConfigProfileService = Depends(get_service),
):
return ok(service.get_profile_schema())
@router.get("/config-profiles")
async def list_config_profiles(
_auth: AuthContext = Depends(require_config_scope),
service: ConfigProfileService = Depends(get_service),
):
return ok(service.list_profiles())
@router.post("/config-profiles")
async def create_config_profile(
payload: ConfigProfileCreateRequest,
_auth: AuthContext = Depends(require_config_scope),
service: ConfigProfileService = Depends(get_service),
):
return ok(await service.create_profile(payload.name, payload.config), "创建成功")
@router.get("/config-profiles/{config_id}")
async def get_config_profile(
config_id: str,
_auth: AuthContext = Depends(require_config_scope),
service: ConfigProfileService = Depends(get_service),
):
return ok(service.get_profile(config_id))
@router.put("/config-profiles/{config_id}")
async def update_config_profile(
config_id: str,
payload: ConfigContentRequest,
request: Request,
_auth: AuthContext = Depends(require_config_scope),
service: ConfigProfileService = Depends(get_service),
):
message = await service.update_profile(
config_id,
_model_dict(payload),
two_factor_code=request.headers.get("X-2FA-Code"),
)
return ok(message=message or "保存成功")
@router.patch("/config-profiles/{config_id}")
async def rename_config_profile(
config_id: str,
payload: RenameRequest,
_auth: AuthContext = Depends(require_config_scope),
service: ConfigProfileService = Depends(get_service),
):
service.rename_profile(config_id, payload.name)
return ok(message="更新成功")
@router.delete("/config-profiles/{config_id}")
async def delete_config_profile(
config_id: str,
_auth: AuthContext = Depends(require_config_scope),
service: ConfigProfileService = Depends(get_service),
):
service.delete_profile(config_id)
return ok(message="删除成功")
@router.get("/system-config/schema")
async def get_system_config_schema(
_auth: AuthContext = Depends(require_config_scope),
service: ConfigProfileService = Depends(get_service),
):
return ok(service.get_system_schema())
@router.get("/system-config")
async def get_system_config(
_auth: AuthContext = Depends(require_config_scope),
service: ConfigProfileService = Depends(get_service),
):
return ok(service.get_system_config())
@router.get("/system-config/runtime")
async def get_system_config_runtime(
_auth: AuthContext = Depends(require_config_scope),
service: ConfigDisplayService = Depends(get_display_service),
):
return ok(await service.get_configs())
@router.put("/system-config")
async def update_system_config(
payload: ConfigContentRequest,
request: Request,
_auth: AuthContext = Depends(require_config_scope),
service: ConfigProfileService = Depends(get_service),
):
message = await service.update_profile(
"default",
_model_dict(payload),
two_factor_code=request.headers.get("X-2FA-Code"),
)
return ok(message=message or "保存成功")
@router.get("/config-routes")
async def list_config_routes(
_auth: AuthContext = Depends(require_config_scope),
service: ConfigRoutingService = Depends(get_routing_service),
):
return ok(service.list_routes())
@router.put("/config-routes")
async def replace_config_routes(
payload: ConfigRoutesReplaceRequest,
_auth: AuthContext = Depends(require_config_scope),
service: ConfigRoutingService = Depends(get_routing_service),
):
await service.replace_route_mapping(payload.routing)
return ok(message="更新成功")
@router.put("/config-routes/{umo}")
async def upsert_config_route(
umo: str,
payload: ConfigRouteUpsertRequest,
_auth: AuthContext = Depends(require_config_scope),
service: ConfigRoutingService = Depends(get_routing_service),
):
await service.set_route(umo, payload.config_id)
return ok(message="更新成功")
@router.delete("/config-routes/{umo}")
async def delete_config_route(
umo: str,
_auth: AuthContext = Depends(require_config_scope),
service: ConfigRoutingService = Depends(get_routing_service),
):
await service.delete_route_by_umo(umo)
return ok(message="删除成功")
@legacy_router.get("/default")
async def get_dashboard_alias_default_config(
_auth: AuthContext = Depends(require_config_scope),
service: ConfigProfileService = Depends(get_service),
):
return ok(service.get_profile_schema())
@legacy_router.get("/abconfs")
async def list_dashboard_alias_config_profiles(
_auth: AuthContext = Depends(require_config_scope),
service: ConfigProfileService = Depends(get_service),
):
return ok(service.list_profiles())
@legacy_router.post("/abconf/new")
async def create_dashboard_alias_config_profile(
request: Request,
_auth: AuthContext = Depends(require_config_scope),
service: ConfigProfileService = Depends(get_service),
):
body = await _json_or_empty(request)
try:
return ok(
await service.create_profile(
body.get("name"),
body.get("config"),
),
"创建成功",
)
except ValueError as exc:
return _alias_error(str(exc))
@legacy_router.get("/abconf")
async def get_dashboard_alias_config_profile(
id: str | None = Query(default=None),
system_config: str = Query(default="0"),
_auth: AuthContext = Depends(require_config_scope),
service: ConfigProfileService = Depends(get_service),
):
if system_config.lower() == "1":
return ok(service.get_system_schema())
if not id:
return _alias_error("缺少配置文件 ID")
try:
return ok(service.get_profile(id))
except ValueError as exc:
return _alias_error(str(exc))
@legacy_router.post("/abconf/delete")
async def delete_dashboard_alias_config_profile(
request: Request,
_auth: AuthContext = Depends(require_config_scope),
service: ConfigProfileService = Depends(get_service),
):
body = await _json_or_empty(request)
config_id = body.get("id")
if not config_id:
return _alias_error("缺少配置文件 ID")
try:
service.delete_profile(str(config_id))
return ok(message="删除成功")
except ValueError as exc:
return _alias_error(str(exc))
@legacy_router.post("/abconf/update")
async def rename_dashboard_alias_config_profile(
request: Request,
_auth: AuthContext = Depends(require_config_scope),
service: ConfigProfileService = Depends(get_service),
):
body = await _json_or_empty(request)
config_id = body.get("id")
if not config_id:
return _alias_error("缺少配置文件 ID")
try:
service.rename_profile(str(config_id), body.get("name"))
return ok(message="更新成功")
except ValueError as exc:
return _alias_error(str(exc))
@legacy_router.post("/astrbot/update")
async def update_dashboard_alias_astrbot_config(
request: Request,
_auth: AuthContext = Depends(require_config_scope),
service: ConfigProfileService = Depends(get_service),
):
body = await _json_or_empty(request)
config = body.get("config")
config_id = body.get("conf_id")
if not isinstance(config, dict):
return _alias_error("Invalid config payload")
if not config_id:
return _alias_error("Config file None does not exist")
try:
message = await service.update_profile(
str(config_id),
config,
two_factor_code=request.headers.get("X-2FA-Code"),
)
return ok(message=message or "保存成功~")
except ValueError as exc:
return _alias_error(str(exc))
@legacy_router.get("/get")
async def get_dashboard_alias_configs(
request: Request,
_auth: AuthContext = Depends(require_config_scope),
service: ConfigDisplayService = Depends(get_display_service),
):
try:
return ok(await service.get_configs_from_dashboard_args(request.query_params))
except ValueError as exc:
return _alias_error(str(exc))
@legacy_router.post("/plugin/update")
async def update_dashboard_alias_plugin_configs(
request: Request,
plugin_name: str = Query(default="unknown"),
_auth: AuthContext = Depends(require_config_scope),
service: ConfigFileService = Depends(get_file_service),
):
body = await _json_or_empty(request)
try:
message = await service.save_plugin_configs_from_dashboard_payload(
body,
plugin_name=plugin_name,
)
return ok(message=message)
except ValueError as exc:
return _alias_error(str(exc))
@legacy_router.post("/file/upload")
async def upload_dashboard_alias_config_file(
request: Request,
scope: str | None = Query(default=None),
name: str | None = Query(default=None),
key: str | None = Query(default=None),
_auth: AuthContext = Depends(require_config_scope),
service: ConfigFileService = Depends(get_file_service),
):
_, files = await multipart_parts(request)
try:
return ok(
await service.upload_config_file(
scope=scope,
name=name,
key_path=key,
files=files,
)
)
except ValueError as exc:
return _alias_error(str(exc))
@legacy_router.post("/file/delete")
async def delete_dashboard_alias_config_file(
request: Request,
scope: str | None = Query(default=None),
name: str | None = Query(default=None),
_auth: AuthContext = Depends(require_config_scope),
service: ConfigFileService = Depends(get_file_service),
):
body = await _json_or_empty(request)
try:
message = service.delete_config_file_from_dashboard_payload(
scope=scope or "plugin",
name=name,
payload=body,
)
return ok(message=message)
except ValueError as exc:
return _alias_error(str(exc))
@legacy_router.get("/file/get")
async def list_dashboard_alias_config_files(
scope: str | None = Query(default=None),
name: str | None = Query(default=None),
key: str | None = Query(default=None),
_auth: AuthContext = Depends(require_config_scope),
service: ConfigFileService = Depends(get_file_service),
):
try:
return ok(
service.list_config_files(
scope=scope,
name=name,
key_path=key,
)
)
except ValueError as exc:
return _alias_error(str(exc))
@legacy_router.get("/umo_abconf_routes")
async def get_dashboard_alias_config_routes(
_auth: AuthContext = Depends(require_config_scope),
service: ConfigRoutingService = Depends(get_routing_service),
):
return ok(service.list_routes())
@legacy_router.post("/umo_abconf_route/update_all")
async def update_dashboard_alias_config_routes(
request: Request,
_auth: AuthContext = Depends(require_config_scope),
service: ConfigRoutingService = Depends(get_routing_service),
):
body = await _json_or_empty(request)
try:
await service.replace_routes(body)
except ValueError:
return _alias_error("缺少或错误的路由表数据")
return ok(message="更新成功")
@legacy_router.post("/umo_abconf_route/update")
async def upsert_dashboard_alias_config_route(
request: Request,
_auth: AuthContext = Depends(require_config_scope),
service: ConfigRoutingService = Depends(get_routing_service),
):
body = await _json_or_empty(request)
try:
await service.upsert_route(body)
except ValueError:
return _alias_error("缺少 UMO 或配置文件 ID")
return ok(message="更新成功")
@legacy_router.post("/umo_abconf_route/delete")
async def delete_dashboard_alias_config_route(
request: Request,
_auth: AuthContext = Depends(require_config_scope),
service: ConfigRoutingService = Depends(get_routing_service),
):
body = await _json_or_empty(request)
try:
await service.delete_route(body)
except ValueError:
return _alias_error("缺少 UMO")
return ok(message="删除成功")