mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-20 02:55:08 +08:00
feat: reorganize settings system configuration (#8777)
* feat: reorganize settings system configuration * fix: float system config restart notice * fix: blur system config restart notice * fix: tint restart notice blur background * fix: allow totp setup without body * fix: filter public openapi docs * fix: handle settings autosave cleanup * chore: ui
This commit is contained in:
@@ -48,7 +48,7 @@ If the API Key does not include the required scope for the target endpoint, the
|
||||
|
||||
`config` is a broad management scope. When an API key is created with `config`, AstrBot grants the key `config`, `bot`, and `provider` access together. The WebUI mirrors this dependency: selecting `config` selects `bot` and `provider`; deselecting `bot` or `provider` removes `config`.
|
||||
|
||||
Developer API keys currently support only the 9 scopes listed above. `file`, `tool`, `skills`, `kb`, `data`, and `system` are not valid developer API key scopes. Use the singular `skill` scope for `/api/v1/skills/*` endpoints. Related endpoints may still appear in the `/api/v1` reference, but they are not available to developer API keys unless their scope is one of the supported scopes above.
|
||||
Developer API keys currently support only the 9 scopes listed above. `file`, `tool`, `skills`, `kb`, `data`, and `system` are not valid developer API key scopes. Use the singular `skill` scope for `/api/v1/skills/*` endpoints. The public OpenAPI reference only includes endpoints covered by supported developer API key scopes.
|
||||
|
||||
## Common Endpoints
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,29 @@ import yaml
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
DEFAULT_SPEC = REPO_ROOT / "openspec" / "openapi-v1.yaml"
|
||||
DEFAULT_OUTPUT = REPO_ROOT / "docs" / "public" / "openapi.json"
|
||||
PUBLIC_OPEN_API_TAGS = {
|
||||
"System Config",
|
||||
"Config Profiles",
|
||||
"Bot Config Routes",
|
||||
"Bots",
|
||||
"Provider Sources",
|
||||
"Providers",
|
||||
"Chat",
|
||||
"IM",
|
||||
"Plugins",
|
||||
"Plugin Sources",
|
||||
"Plugin Pages",
|
||||
"MCP",
|
||||
"Skills",
|
||||
"Personas",
|
||||
"T2I",
|
||||
"Subagents",
|
||||
}
|
||||
PUBLIC_OPEN_API_EXCLUDED_PATHS = {
|
||||
"/api/v1/live-chat/ws",
|
||||
"/api/v1/unified-chat/ws",
|
||||
}
|
||||
COMPONENT_REF_PREFIX = "#/components/"
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
@@ -39,12 +62,111 @@ def load_yaml(path: Path) -> dict[str, Any]:
|
||||
return data
|
||||
|
||||
|
||||
def iter_refs(value: Any):
|
||||
"""Yield local component refs from an OpenAPI value.
|
||||
|
||||
Args:
|
||||
value: Arbitrary OpenAPI object value.
|
||||
|
||||
Yields:
|
||||
Local component reference strings.
|
||||
"""
|
||||
if isinstance(value, dict):
|
||||
ref = value.get("$ref")
|
||||
if isinstance(ref, str) and ref.startswith(COMPONENT_REF_PREFIX):
|
||||
yield ref
|
||||
for child in value.values():
|
||||
yield from iter_refs(child)
|
||||
elif isinstance(value, list):
|
||||
for child in value:
|
||||
yield from iter_refs(child)
|
||||
|
||||
|
||||
def parse_component_ref(ref: str) -> tuple[str, str] | None:
|
||||
"""Parse a local component ref into its section and name.
|
||||
|
||||
Args:
|
||||
ref: OpenAPI local component reference.
|
||||
|
||||
Returns:
|
||||
The component section and name, or None if the ref is not a component ref.
|
||||
"""
|
||||
if not ref.startswith(COMPONENT_REF_PREFIX):
|
||||
return None
|
||||
rest = ref.removeprefix(COMPONENT_REF_PREFIX)
|
||||
if "/" not in rest:
|
||||
return None
|
||||
section, name = rest.split("/", 1)
|
||||
return section, name
|
||||
|
||||
|
||||
def filter_public_openapi(spec: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Filter the full v1 spec down to developer API key endpoints.
|
||||
|
||||
Args:
|
||||
spec: Full OpenAPI spec loaded from the YAML source.
|
||||
|
||||
Returns:
|
||||
A filtered OpenAPI spec for the public docs site.
|
||||
"""
|
||||
output = dict(spec)
|
||||
output["tags"] = [
|
||||
tag
|
||||
for tag in spec.get("tags", [])
|
||||
if isinstance(tag, dict) and tag.get("name") in PUBLIC_OPEN_API_TAGS
|
||||
]
|
||||
|
||||
paths = {}
|
||||
for path, methods in spec.get("paths", {}).items():
|
||||
if path in PUBLIC_OPEN_API_EXCLUDED_PATHS:
|
||||
continue
|
||||
kept_methods = {
|
||||
method: operation
|
||||
for method, operation in methods.items()
|
||||
if any(tag in PUBLIC_OPEN_API_TAGS for tag in operation.get("tags", []))
|
||||
}
|
||||
if kept_methods:
|
||||
paths[path] = kept_methods
|
||||
output["paths"] = paths
|
||||
|
||||
used_refs: dict[str, set[str]] = {}
|
||||
pending = list(iter_refs(paths))
|
||||
components = output.get("components", {})
|
||||
while pending:
|
||||
parsed = parse_component_ref(pending.pop())
|
||||
if parsed is None:
|
||||
continue
|
||||
section, name = parsed
|
||||
used_names = used_refs.setdefault(section, set())
|
||||
if name in used_names:
|
||||
continue
|
||||
used_names.add(name)
|
||||
component = components.get(section, {}).get(name)
|
||||
pending.extend(iter_refs(component))
|
||||
|
||||
pruned_components = {}
|
||||
for section, values in components.items():
|
||||
if section == "securitySchemes":
|
||||
pruned_components[section] = values
|
||||
continue
|
||||
if not isinstance(values, dict):
|
||||
pruned_components[section] = values
|
||||
continue
|
||||
names = used_refs.get(section, set())
|
||||
kept_values = {name: values[name] for name in values if name in names}
|
||||
if kept_values:
|
||||
pruned_components[section] = kept_values
|
||||
output["components"] = pruned_components
|
||||
return output
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
spec_path = args.spec.resolve()
|
||||
output_path = args.output.resolve()
|
||||
|
||||
spec = load_yaml(spec_path)
|
||||
spec = filter_public_openapi(spec)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_path.write_text(
|
||||
json.dumps(spec, ensure_ascii=False, indent=2) + "\n",
|
||||
|
||||
@@ -48,7 +48,7 @@ X-API-Key: abk_xxx
|
||||
|
||||
`config` 是较大的管理 scope。创建 API Key 时如果包含 `config`,AstrBot 会同时授予该 Key `config`、`bot` 和 `provider` 访问权限。WebUI 的勾选逻辑也会体现这个依赖关系:选中 `config` 会同时选中 `bot` 和 `provider`;取消选中 `bot` 或 `provider` 时,会同步取消 `config`。
|
||||
|
||||
当前开发者 API Key 仅开放以上 9 个 scope。`file`、`tool`、`skills`、`kb`、`data`、`system` 暂不支持作为开发者 API Key scope。`/api/v1/skills/*` 接口使用单数 `skill` scope,不使用复数 `skills`。相关接口可能仍出现在 `/api/v1` 文档中,但只有 scope 属于上表支持范围时才对开发者 API Key 开放。
|
||||
当前开发者 API Key 仅开放以上 9 个 scope。`file`、`tool`、`skills`、`kb`、`data`、`system` 暂不支持作为开发者 API Key scope。`/api/v1/skills/*` 接口使用单数 `skill` scope,不使用复数 `skills`。公开 OpenAPI 文档只包含这些开发者 API Key scope 覆盖的接口。
|
||||
|
||||
## 常用接口
|
||||
|
||||
|
||||
Reference in New Issue
Block a user