mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
* 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
594 lines
19 KiB
Python
594 lines
19 KiB
Python
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, Depends, Query, Request
|
|
|
|
from astrbot.dashboard.responses import error, ok
|
|
from astrbot.dashboard.schemas import (
|
|
EnabledPatch,
|
|
ProviderConfigRequest,
|
|
ProviderSourceRequest,
|
|
)
|
|
from astrbot.dashboard.services.config_service import ProviderConfigService
|
|
|
|
from .auth import AuthContext, require_scope
|
|
|
|
router = APIRouter(tags=["Providers"])
|
|
legacy_router = APIRouter(
|
|
prefix="/api/config",
|
|
tags=["Dashboard Providers"],
|
|
include_in_schema=False,
|
|
)
|
|
|
|
|
|
async def require_provider_scope(request: Request) -> AuthContext:
|
|
return await require_scope(request, "provider")
|
|
|
|
|
|
def get_service(request: Request) -> ProviderConfigService:
|
|
return request.app.state.services.providers
|
|
|
|
|
|
async def _json_or_empty(request: Request) -> dict:
|
|
try:
|
|
data = await request.json()
|
|
except Exception:
|
|
return {}
|
|
return data if isinstance(data, dict) else {}
|
|
|
|
|
|
def _required_text(value: object, name: str) -> str:
|
|
text = str(value or "").strip()
|
|
if not text:
|
|
raise ValueError(f"Missing key: {name}")
|
|
return text
|
|
|
|
|
|
def _model_dict(payload) -> dict:
|
|
if payload is None:
|
|
return {}
|
|
if hasattr(payload, "model_dump"):
|
|
return payload.model_dump(exclude_none=True)
|
|
return payload if isinstance(payload, dict) else {}
|
|
|
|
|
|
def _config_from_body(body: dict) -> dict:
|
|
config = body.get("config")
|
|
if isinstance(config, dict):
|
|
return config
|
|
return {
|
|
key: value
|
|
for key, value in body.items()
|
|
if key
|
|
not in {
|
|
"provider_id",
|
|
"source_id",
|
|
"config",
|
|
"enabled",
|
|
"provider_config",
|
|
}
|
|
}
|
|
|
|
|
|
def _provider_config_for_dimension(
|
|
service: ProviderConfigService,
|
|
provider_id: str,
|
|
body: dict,
|
|
) -> dict:
|
|
provider = service.get_provider(provider_id, merged=True)
|
|
base_config = provider.get("provider") if isinstance(provider, dict) else {}
|
|
if not isinstance(base_config, dict):
|
|
base_config = {}
|
|
provider_config = body.get("provider_config")
|
|
if isinstance(provider_config, dict):
|
|
return {**base_config, **provider_config}
|
|
return base_config
|
|
|
|
|
|
def _alias_error(message: str):
|
|
return error(message)
|
|
|
|
|
|
@router.get("/providers/schema")
|
|
async def get_provider_schema(
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
return ok(service.get_provider_schema())
|
|
|
|
|
|
@router.get("/provider-sources")
|
|
async def list_provider_sources(
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
return ok(service.list_provider_sources())
|
|
|
|
|
|
@router.post("/provider-sources")
|
|
async def create_provider_source(
|
|
payload: ProviderSourceRequest,
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
config = payload.to_dashboard_config()
|
|
source_id = config.get("id")
|
|
if not source_id:
|
|
raise ValueError("Provider source config must have an 'id' field")
|
|
await service.upsert_provider_source(source_id, config)
|
|
return ok(message="更新 provider source 成功")
|
|
|
|
|
|
@router.get("/provider-sources/by-id")
|
|
async def get_provider_source_by_id(
|
|
source_id: str = Query(...),
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
return ok(service.get_provider_source(source_id))
|
|
|
|
|
|
@router.put("/provider-sources/by-id")
|
|
async def upsert_provider_source_by_id(
|
|
payload: ProviderSourceRequest,
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
source_id = _required_text(payload.source_id, "source_id")
|
|
await service.upsert_provider_source(
|
|
source_id,
|
|
payload.to_dashboard_config(fallback_id=source_id),
|
|
)
|
|
return ok(message="更新 provider source 成功")
|
|
|
|
|
|
@router.delete("/provider-sources/by-id")
|
|
async def delete_provider_source_by_id(
|
|
source_id: str = Query(...),
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
await service.delete_provider_source(source_id)
|
|
return ok(message="删除 provider source 成功")
|
|
|
|
|
|
@router.get("/provider-sources/models")
|
|
async def list_provider_source_models_by_id(
|
|
source_id: str = Query(...),
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
return ok(await service.list_provider_source_models(source_id))
|
|
|
|
|
|
@router.get("/provider-sources/providers")
|
|
async def list_providers_by_source_id(
|
|
source_id: str = Query(...),
|
|
capability: str | None = Query(default=None),
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
return ok(service.list_providers(capability=capability, source_id=source_id))
|
|
|
|
|
|
@router.post("/provider-sources/providers")
|
|
async def create_provider_in_source_by_id(
|
|
payload: ProviderConfigRequest,
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
source_id = _required_text(payload.source_id, "source_id")
|
|
await service.create_provider(
|
|
payload.to_dashboard_config(source_id=source_id),
|
|
source_id,
|
|
)
|
|
return ok(message="新增服务提供商配置成功")
|
|
|
|
|
|
@router.get("/provider-sources/{source_id:path}/models")
|
|
async def list_provider_source_models(
|
|
source_id: str,
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
return ok(await service.list_provider_source_models(source_id))
|
|
|
|
|
|
@router.get("/provider-sources/{source_id:path}/providers")
|
|
async def list_providers_by_source(
|
|
source_id: str,
|
|
capability: str | None = Query(default=None),
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
return ok(service.list_providers(capability=capability, source_id=source_id))
|
|
|
|
|
|
@router.post("/provider-sources/{source_id:path}/providers")
|
|
async def create_provider_in_source(
|
|
source_id: str,
|
|
payload: ProviderConfigRequest,
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
await service.create_provider(
|
|
payload.to_dashboard_config(source_id=source_id), source_id
|
|
)
|
|
return ok(message="新增服务提供商配置成功")
|
|
|
|
|
|
@router.get("/provider-sources/{source_id:path}")
|
|
async def get_provider_source(
|
|
source_id: str,
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
return ok(service.get_provider_source(source_id))
|
|
|
|
|
|
@router.put("/provider-sources/{source_id:path}")
|
|
async def upsert_provider_source(
|
|
source_id: str,
|
|
payload: ProviderSourceRequest,
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
await service.upsert_provider_source(
|
|
source_id,
|
|
payload.to_dashboard_config(),
|
|
)
|
|
return ok(message="更新 provider source 成功")
|
|
|
|
|
|
@router.delete("/provider-sources/{source_id:path}")
|
|
async def delete_provider_source(
|
|
source_id: str,
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
await service.delete_provider_source(source_id)
|
|
return ok(message="删除 provider source 成功")
|
|
|
|
|
|
@router.get("/providers")
|
|
async def list_providers(
|
|
capability: str | None = Query(default=None),
|
|
source_id: str | None = Query(default=None),
|
|
enabled: bool | None = Query(default=None),
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
return ok(
|
|
service.list_providers(
|
|
capability=capability,
|
|
source_id=source_id,
|
|
enabled=enabled,
|
|
)
|
|
)
|
|
|
|
|
|
@router.post("/providers")
|
|
async def create_provider(
|
|
payload: ProviderConfigRequest,
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
await service.create_provider(payload.to_dashboard_config())
|
|
return ok(message="新增服务提供商配置成功")
|
|
|
|
|
|
@router.get("/providers/by-id")
|
|
async def get_provider_by_id(
|
|
provider_id: str = Query(...),
|
|
merged: bool = Query(default=False),
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
return ok(service.get_provider(provider_id, merged=merged))
|
|
|
|
|
|
@router.put("/providers/by-id")
|
|
async def update_provider_by_id(
|
|
payload: ProviderConfigRequest,
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
provider_id = _required_text(payload.provider_id, "provider_id")
|
|
await service.update_provider(
|
|
provider_id,
|
|
payload.to_dashboard_config(fallback_id=provider_id),
|
|
)
|
|
return ok(message="更新成功,已经实时生效~")
|
|
|
|
|
|
@router.delete("/providers/by-id")
|
|
async def delete_provider_by_id(
|
|
provider_id: str = Query(...),
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
await service.delete_provider(provider_id)
|
|
return ok(message="删除成功,已经实时生效。")
|
|
|
|
|
|
@router.patch("/providers/enabled")
|
|
async def set_provider_enabled_by_id(
|
|
payload: ProviderConfigRequest,
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
provider_id = _required_text(payload.provider_id, "provider_id")
|
|
await service.set_provider_enabled(provider_id, bool(payload.enabled))
|
|
return ok(message="更新成功,已经实时生效~")
|
|
|
|
|
|
@router.post("/providers/test")
|
|
async def test_provider_by_id(
|
|
payload: ProviderConfigRequest,
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
provider_id = _required_text(payload.provider_id, "provider_id")
|
|
return ok(await service.test_provider(provider_id))
|
|
|
|
|
|
@router.post("/providers/embedding-dimension")
|
|
async def get_embedding_dimension_by_id(
|
|
payload: ProviderConfigRequest,
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
body = _model_dict(payload)
|
|
provider_id = _required_text(payload.provider_id, "provider_id")
|
|
return ok(
|
|
await service.get_embedding_dimension(
|
|
_provider_config_for_dimension(service, provider_id, body)
|
|
)
|
|
)
|
|
|
|
|
|
@router.patch("/providers/{provider_id:path}/enabled")
|
|
async def set_provider_enabled(
|
|
provider_id: str,
|
|
payload: EnabledPatch,
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
await service.set_provider_enabled(provider_id, payload.enabled)
|
|
return ok(message="更新成功,已经实时生效~")
|
|
|
|
|
|
@router.post("/providers/{provider_id:path}/test")
|
|
async def test_provider(
|
|
provider_id: str,
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
return ok(await service.test_provider(provider_id))
|
|
|
|
|
|
@router.post("/providers/{provider_id:path}/embedding-dimension")
|
|
async def get_embedding_dimension(
|
|
provider_id: str,
|
|
payload: ProviderConfigRequest | None = None,
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
body = _model_dict(payload)
|
|
return ok(
|
|
await service.get_embedding_dimension(
|
|
_provider_config_for_dimension(service, provider_id, body)
|
|
)
|
|
)
|
|
|
|
|
|
@router.get("/providers/{provider_id:path}")
|
|
async def get_provider(
|
|
provider_id: str,
|
|
merged: bool = Query(default=False),
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
return ok(service.get_provider(provider_id, merged=merged))
|
|
|
|
|
|
@router.put("/providers/{provider_id:path}")
|
|
async def update_provider(
|
|
provider_id: str,
|
|
payload: ProviderConfigRequest,
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
await service.update_provider(
|
|
provider_id,
|
|
payload.to_dashboard_config(fallback_id=provider_id),
|
|
)
|
|
return ok(message="更新成功,已经实时生效~")
|
|
|
|
|
|
@router.delete("/providers/{provider_id:path}")
|
|
async def delete_provider(
|
|
provider_id: str,
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
await service.delete_provider(provider_id)
|
|
return ok(message="删除成功,已经实时生效。")
|
|
|
|
|
|
@legacy_router.get("/provider/template")
|
|
async def get_dashboard_alias_provider_template(
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
return ok(service.get_provider_schema())
|
|
|
|
|
|
@legacy_router.get("/provider/list")
|
|
async def list_dashboard_alias_providers(
|
|
provider_type: str | None = Query(default=None),
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
if not provider_type:
|
|
return _alias_error("缺少参数 provider_type")
|
|
providers = []
|
|
seen_ids = set()
|
|
for item in provider_type.split(","):
|
|
for provider in service.list_providers(capability=item)["providers"]:
|
|
provider_id = provider.get("id")
|
|
if provider_id in seen_ids:
|
|
continue
|
|
seen_ids.add(provider_id)
|
|
providers.append(provider)
|
|
return ok(providers)
|
|
|
|
|
|
@legacy_router.post("/provider/new")
|
|
async def create_dashboard_alias_provider(
|
|
payload: ProviderConfigRequest,
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
try:
|
|
await service.create_provider(payload.to_dashboard_config())
|
|
return ok(message="新增服务提供商配置成功")
|
|
except ValueError as exc:
|
|
return _alias_error(str(exc))
|
|
|
|
|
|
@legacy_router.post("/provider/update")
|
|
async def update_dashboard_alias_provider(
|
|
request: Request,
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
body = await _json_or_empty(request)
|
|
provider_id = body.get("id")
|
|
config = body.get("config")
|
|
if not provider_id or not isinstance(config, dict):
|
|
return _alias_error("参数错误")
|
|
try:
|
|
await service.update_provider(
|
|
str(provider_id),
|
|
ProviderConfigRequest(config=config).to_dashboard_config(
|
|
fallback_id=str(provider_id),
|
|
),
|
|
)
|
|
return ok(message="更新成功,已经实时生效~")
|
|
except ValueError as exc:
|
|
return _alias_error(str(exc))
|
|
|
|
|
|
@legacy_router.post("/provider/delete")
|
|
async def delete_dashboard_alias_provider(
|
|
request: Request,
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
body = await _json_or_empty(request)
|
|
provider_id = body.get("id")
|
|
if not provider_id:
|
|
return _alias_error("缺少参数 id")
|
|
try:
|
|
await service.delete_provider(str(provider_id))
|
|
return ok(message="删除成功,已经实时生效。")
|
|
except ValueError as exc:
|
|
return _alias_error(str(exc))
|
|
|
|
|
|
@legacy_router.get("/provider/check_one")
|
|
async def check_dashboard_alias_provider(
|
|
id: str | None = Query(default=None),
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
if not id:
|
|
return _alias_error("Missing provider_id parameter")
|
|
try:
|
|
return ok(await service.test_provider(id))
|
|
except ValueError as exc:
|
|
return _alias_error(str(exc))
|
|
|
|
|
|
@legacy_router.get("/provider/model_list")
|
|
async def list_dashboard_alias_provider_models(
|
|
provider_id: str | None = Query(default=None),
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
try:
|
|
return ok(await service.list_provider_models_for_dashboard(provider_id))
|
|
except ValueError as exc:
|
|
return _alias_error(str(exc))
|
|
|
|
|
|
@legacy_router.post("/provider/get_embedding_dim")
|
|
async def get_dashboard_alias_provider_embedding_dimension(
|
|
request: Request,
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
body = await _json_or_empty(request)
|
|
try:
|
|
return ok(await service.get_embedding_dimension_from_dashboard_payload(body))
|
|
except ValueError as exc:
|
|
return _alias_error(str(exc))
|
|
|
|
|
|
@legacy_router.get("/provider_sources/models")
|
|
async def list_dashboard_alias_provider_source_models(
|
|
source_id: str | None = Query(default=None),
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
if not source_id:
|
|
return _alias_error("缺少参数 source_id")
|
|
try:
|
|
data = await service.list_provider_source_models(source_id)
|
|
data.pop("provider_source_id", None)
|
|
return ok(data)
|
|
except ValueError as exc:
|
|
return _alias_error(str(exc))
|
|
|
|
|
|
@legacy_router.post("/provider_sources/update")
|
|
async def update_dashboard_alias_provider_source(
|
|
request: Request,
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
body = await _json_or_empty(request)
|
|
source_id = body.get("original_id")
|
|
config = body.get("config") or body
|
|
if not source_id:
|
|
return _alias_error("缺少 original_id")
|
|
if not isinstance(config, dict):
|
|
return _alias_error("缺少或错误的配置数据")
|
|
try:
|
|
await service.upsert_provider_source(
|
|
str(source_id),
|
|
ProviderSourceRequest(config=config).to_dashboard_config(
|
|
fallback_id=str(source_id),
|
|
),
|
|
)
|
|
return ok(message="更新 provider source 成功")
|
|
except ValueError as exc:
|
|
return _alias_error(str(exc))
|
|
|
|
|
|
@legacy_router.post("/provider_sources/delete")
|
|
async def delete_dashboard_alias_provider_source(
|
|
request: Request,
|
|
_auth: AuthContext = Depends(require_provider_scope),
|
|
service: ProviderConfigService = Depends(get_service),
|
|
):
|
|
body = await _json_or_empty(request)
|
|
source_id = body.get("id")
|
|
if not source_id:
|
|
return _alias_error("缺少 provider_source_id")
|
|
try:
|
|
await service.delete_provider_source(str(source_id))
|
|
return ok(message="删除 provider source 成功")
|
|
except ValueError as exc:
|
|
return _alias_error(str(exc))
|