feat(config): add docs & auto-generated document
This commit is contained in:
135
.github/scripts/generate_config_doc.py
vendored
Normal file
135
.github/scripts/generate_config_doc.py
vendored
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from enum import Enum
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
from types import NoneType, UnionType
|
||||||
|
from typing import Any, Union, get_origin
|
||||||
|
|
||||||
|
from pydantic import AliasChoices, BaseModel, HttpUrl
|
||||||
|
from pydantic_settings import BaseSettings
|
||||||
|
|
||||||
|
file_path = Path("./app/config.py").resolve()
|
||||||
|
|
||||||
|
spec = importlib.util.spec_from_file_location("config", str(file_path))
|
||||||
|
module = importlib.util.module_from_spec(spec) # pyright: ignore[reportArgumentType]
|
||||||
|
sys.modules["my_module"] = module
|
||||||
|
spec.loader.exec_module(module) # pyright: ignore[reportOptionalMemberAccess]
|
||||||
|
|
||||||
|
model: type[BaseSettings] = module.Settings
|
||||||
|
|
||||||
|
commit = sys.argv[1] if len(sys.argv) > 1 else "unknown"
|
||||||
|
|
||||||
|
doc = []
|
||||||
|
uncategorized = []
|
||||||
|
|
||||||
|
|
||||||
|
def new_paragraph(name: str, has_sub_paragraph: bool) -> None:
|
||||||
|
doc.append("")
|
||||||
|
doc.append(f"## {name}")
|
||||||
|
if desc := model.model_config["json_schema_extra"]["paragraphs_desc"].get(name): # type: ignore
|
||||||
|
doc.append(desc)
|
||||||
|
if not has_sub_paragraph:
|
||||||
|
doc.append("| 变量名 | 描述 | 类型 | 默认值 |")
|
||||||
|
doc.append("|------|------|--------|------|")
|
||||||
|
|
||||||
|
|
||||||
|
def new_sub_paragraph(name: str) -> None:
|
||||||
|
doc.append("")
|
||||||
|
doc.append(f"### {name}")
|
||||||
|
doc.append("| 变量名 | 描述 | 类型 | 默认值 |")
|
||||||
|
doc.append("|------|------|--------|------|")
|
||||||
|
|
||||||
|
|
||||||
|
def serialize_default(value: Any) -> str:
|
||||||
|
if isinstance(value, Enum):
|
||||||
|
return value.value
|
||||||
|
if isinstance(value, str):
|
||||||
|
return value or '""'
|
||||||
|
try:
|
||||||
|
if isinstance(value, BaseModel):
|
||||||
|
return value.model_dump_json()
|
||||||
|
return json.dumps(value, ensure_ascii=False)
|
||||||
|
except Exception:
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
|
BASE_TYPE_MAPPING = {
|
||||||
|
str: "string",
|
||||||
|
int: "integer",
|
||||||
|
float: "float",
|
||||||
|
bool: "boolean",
|
||||||
|
list: "array",
|
||||||
|
dict: "object",
|
||||||
|
NoneType: "null",
|
||||||
|
HttpUrl: "string (url)",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def mapping_type(typ: type) -> str:
|
||||||
|
base_type = BASE_TYPE_MAPPING.get(typ)
|
||||||
|
if base_type:
|
||||||
|
return base_type
|
||||||
|
if (origin := get_origin(typ)) is Union or origin is UnionType:
|
||||||
|
args = list(typ.__args__)
|
||||||
|
if len(args) == 1:
|
||||||
|
return mapping_type(args[0])
|
||||||
|
return " / ".join(mapping_type(a) for a in args)
|
||||||
|
elif get_origin(typ) is list:
|
||||||
|
args = typ.__args__
|
||||||
|
if len(args) == 1:
|
||||||
|
return f"array[{mapping_type(args[0])}]"
|
||||||
|
return "array"
|
||||||
|
if issubclass(typ, Enum):
|
||||||
|
return f"enum({', '.join([e.value for e in typ])})"
|
||||||
|
elif issubclass(typ, BaseSettings):
|
||||||
|
return typ.__name__
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
last_paragraph = ""
|
||||||
|
last_sub_paragraph = ""
|
||||||
|
for name, field in model.model_fields.items():
|
||||||
|
if len(field.metadata) == 0:
|
||||||
|
uncategorized.append((name, field))
|
||||||
|
continue
|
||||||
|
sub_paragraph = ""
|
||||||
|
paragraph = field.metadata[0]
|
||||||
|
if len(field.metadata) > 1 and isinstance(field.metadata[1], str):
|
||||||
|
sub_paragraph = field.metadata[1]
|
||||||
|
if paragraph != last_paragraph:
|
||||||
|
last_paragraph = paragraph
|
||||||
|
new_paragraph(paragraph, has_sub_paragraph=bool(sub_paragraph))
|
||||||
|
if sub_paragraph and sub_paragraph != last_sub_paragraph:
|
||||||
|
last_sub_paragraph = sub_paragraph
|
||||||
|
new_sub_paragraph(sub_paragraph)
|
||||||
|
|
||||||
|
alias = field.alias or name
|
||||||
|
aliases = []
|
||||||
|
other_aliases = field.validation_alias
|
||||||
|
if isinstance(other_aliases, str):
|
||||||
|
aliases.append(other_aliases)
|
||||||
|
elif isinstance(other_aliases, AliasChoices):
|
||||||
|
for a in other_aliases.convert_to_aliases():
|
||||||
|
aliases.extend(a)
|
||||||
|
|
||||||
|
ins_doc = f"({', '.join([a.upper() for a in aliases])}) " if aliases else ""
|
||||||
|
doc.append(
|
||||||
|
f"| {alias.upper()} {ins_doc}| {field.description or ''} "
|
||||||
|
f"| {mapping_type(field.annotation)} | `{serialize_default(field.default)}` |" # pyright: ignore[reportArgumentType]
|
||||||
|
)
|
||||||
|
|
||||||
|
doc.extend(
|
||||||
|
[
|
||||||
|
module.SPECTATOR_DOC,
|
||||||
|
"",
|
||||||
|
f"> 上次生成:{datetime.datetime.now(datetime.UTC).strftime('%Y-%m-%d %H:%M:%S %Z')}",
|
||||||
|
f"于提交 {f'[`{commit}`](https://github.com/GooGuTeam/g0v0-server/commit/{commit})' if commit != 'unknown' else 'unknown'}", # noqa: E501
|
||||||
|
"",
|
||||||
|
"> **注意: 在生产环境中,请务必更改默认的密钥和密码!**",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
print("\n".join(doc))
|
||||||
56
.github/workflows/generate-configuration-doc.yml
vendored
Normal file
56
.github/workflows/generate-configuration-doc.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
name: Generate configuration Docs to Wiki
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- "app/config.py"
|
||||||
|
- ".github/scripts/generate_config_doc.py"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
generate-wiki:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout main repository
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
path: project
|
||||||
|
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
repository: ${{ github.repository }}.wiki
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
path: wiki
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: 3.12
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install pydantic pydantic-settings
|
||||||
|
|
||||||
|
- name: Generate Markdown
|
||||||
|
run: |
|
||||||
|
cd project
|
||||||
|
python ./.github/scripts/generate_config_doc.py ${{ github.sha }} > ../wiki/Configuration.md
|
||||||
|
|
||||||
|
- name: Commit and push to Wiki
|
||||||
|
run: |
|
||||||
|
cd wiki
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git add .
|
||||||
|
git commit -m "Update configuration docs from Actions [skip ci]" || echo "No changes"
|
||||||
|
git push origin main
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
579
app/config.py
579
app/config.py
@@ -1,11 +1,9 @@
|
|||||||
from __future__ import annotations
|
# ruff: noqa: I002
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Annotated, Any
|
from typing import Annotated, Any
|
||||||
|
|
||||||
from pydantic import (
|
from pydantic import (
|
||||||
AliasChoices,
|
AliasChoices,
|
||||||
BeforeValidator,
|
|
||||||
Field,
|
Field,
|
||||||
HttpUrl,
|
HttpUrl,
|
||||||
ValidationInfo,
|
ValidationInfo,
|
||||||
@@ -14,23 +12,6 @@ from pydantic import (
|
|||||||
from pydantic_settings import BaseSettings, NoDecode, SettingsConfigDict
|
from pydantic_settings import BaseSettings, NoDecode, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
def _parse_list(v):
|
|
||||||
if v is None or v == "" or str(v).strip() in ("[]", "{}"):
|
|
||||||
return []
|
|
||||||
if isinstance(v, list):
|
|
||||||
return v
|
|
||||||
s = str(v).strip()
|
|
||||||
try:
|
|
||||||
import json
|
|
||||||
|
|
||||||
parsed = json.loads(s)
|
|
||||||
if isinstance(parsed, list):
|
|
||||||
return parsed
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return [x.strip() for x in s.split(",") if x.strip()]
|
|
||||||
|
|
||||||
|
|
||||||
class AWSS3StorageSettings(BaseSettings):
|
class AWSS3StorageSettings(BaseSettings):
|
||||||
s3_access_key_id: str
|
s3_access_key_id: str
|
||||||
s3_secret_access_key: str
|
s3_secret_access_key: str
|
||||||
@@ -57,43 +38,214 @@ class StorageServiceType(str, Enum):
|
|||||||
AWS_S3 = "s3"
|
AWS_S3 = "s3"
|
||||||
|
|
||||||
|
|
||||||
|
SPECTATOR_DOC = """
|
||||||
|
## 旁观服务器设置
|
||||||
|
| 变量名 | 描述 | 类型 | 默认值 |
|
||||||
|
|--------|------|--------|--------|
|
||||||
|
| `SAVE_REPLAYS` | 是否保存回放,设置为 `1` 为启用 | boolean | `0` |
|
||||||
|
| `REDIS_HOST` | Redis 服务器地址 | string | `localhost` |
|
||||||
|
| `SHARED_INTEROP_DOMAIN` | API 服务器(即本服务)地址 | string (url) | `http://localhost:8000` |
|
||||||
|
| `SERVER_PORT` | 旁观服务器端口 | integer | `8006` |
|
||||||
|
| `SP_SENTRY_DSN` | 旁观服务器的 Sentry DSN | string | `null` |
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="allow")
|
model_config = SettingsConfigDict(
|
||||||
|
env_file=".env",
|
||||||
|
env_file_encoding="utf-8",
|
||||||
|
extra="allow",
|
||||||
|
json_schema_extra={
|
||||||
|
"paragraphs_desc": {
|
||||||
|
"Fetcher 设置": "Fetcher 用于从 osu! 官方 API 获取数据,使用 osu! 官方 API 的 OAuth 2.0 认证",
|
||||||
|
"监控设置": (
|
||||||
|
"配置应用的监控选项,如 Sentry 和 New Relic。\n\n"
|
||||||
|
"将 newrelic.ini 配置文件放入项目根目录即可自动启用 New Relic 监控。"
|
||||||
|
"如果配置文件不存在或 newrelic 包未安装,将跳过 New Relic 初始化。"
|
||||||
|
),
|
||||||
|
"存储服务设置": """用于存储回放文件、头像等静态资源。
|
||||||
|
|
||||||
|
### 本地存储 (推荐用于开发环境)
|
||||||
|
|
||||||
|
本地存储将文件保存在服务器的本地文件系统中,适合开发和小规模部署。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
STORAGE_SERVICE="local"
|
||||||
|
STORAGE_SETTINGS='{"local_storage_path": "./storage"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cloudflare R2 存储 (推荐用于生产环境)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
STORAGE_SERVICE="r2"
|
||||||
|
STORAGE_SETTINGS='{
|
||||||
|
"r2_account_id": "your_cloudflare_account_id",
|
||||||
|
"r2_access_key_id": "your_r2_access_key_id",
|
||||||
|
"r2_secret_access_key": "your_r2_secret_access_key",
|
||||||
|
"r2_bucket_name": "your_bucket_name",
|
||||||
|
"r2_public_url_base": "https://your-custom-domain.com"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### AWS S3 存储
|
||||||
|
|
||||||
|
```bash
|
||||||
|
STORAGE_SERVICE="s3"
|
||||||
|
STORAGE_SETTINGS='{
|
||||||
|
"s3_access_key_id": "your_aws_access_key_id",
|
||||||
|
"s3_secret_access_key": "your_aws_secret_access_key",
|
||||||
|
"s3_bucket_name": "your_s3_bucket_name",
|
||||||
|
"s3_region_name": "us-east-1",
|
||||||
|
"s3_public_url_base": "https://your-custom-domain.com"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
""",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
# 数据库设置
|
# 数据库设置
|
||||||
mysql_host: str = "localhost"
|
mysql_host: Annotated[
|
||||||
mysql_port: int = 3306
|
str,
|
||||||
mysql_database: str = "osu_api"
|
Field(default="localhost", description="MySQL 服务器地址"),
|
||||||
mysql_user: str = "osu_api"
|
"数据库设置",
|
||||||
mysql_password: str = "password"
|
]
|
||||||
mysql_root_password: str = "password"
|
mysql_port: Annotated[
|
||||||
redis_url: str = "redis://127.0.0.1:6379/0"
|
int,
|
||||||
|
Field(default=3306, description="MySQL 服务器端口"),
|
||||||
|
"数据库设置",
|
||||||
|
]
|
||||||
|
mysql_database: Annotated[
|
||||||
|
str,
|
||||||
|
Field(default="osu_api", description="MySQL 数据库名称"),
|
||||||
|
"数据库设置",
|
||||||
|
]
|
||||||
|
mysql_user: Annotated[
|
||||||
|
str,
|
||||||
|
Field(default="osu_api", description="MySQL 用户名"),
|
||||||
|
"数据库设置",
|
||||||
|
]
|
||||||
|
mysql_password: Annotated[
|
||||||
|
str,
|
||||||
|
Field(default="password", description="MySQL 密码"),
|
||||||
|
"数据库设置",
|
||||||
|
]
|
||||||
|
mysql_root_password: Annotated[
|
||||||
|
str,
|
||||||
|
Field(default="password", description="MySQL root 密码"),
|
||||||
|
"数据库设置",
|
||||||
|
]
|
||||||
|
redis_url: Annotated[
|
||||||
|
str,
|
||||||
|
Field(default="redis://127.0.0.1:6379/0", description="Redis 连接 URL"),
|
||||||
|
"数据库设置",
|
||||||
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def database_url(self) -> str:
|
def database_url(self) -> str:
|
||||||
return f"mysql+aiomysql://{self.mysql_user}:{self.mysql_password}@{self.mysql_host}:{self.mysql_port}/{self.mysql_database}"
|
return f"mysql+aiomysql://{self.mysql_user}:{self.mysql_password}@{self.mysql_host}:{self.mysql_port}/{self.mysql_database}"
|
||||||
|
|
||||||
# JWT 设置
|
# JWT 设置
|
||||||
secret_key: str = Field(default="your_jwt_secret_here", alias="jwt_secret_key")
|
secret_key: Annotated[
|
||||||
algorithm: str = "HS256"
|
str,
|
||||||
access_token_expire_minutes: int = 1440
|
Field(
|
||||||
jwt_audience: str = "5"
|
default="your_jwt_secret_here",
|
||||||
jwt_issuer: str | None = None
|
alias="jwt_secret_key",
|
||||||
|
description="JWT 签名密钥",
|
||||||
|
),
|
||||||
|
"JWT 设置",
|
||||||
|
]
|
||||||
|
algorithm: Annotated[
|
||||||
|
str,
|
||||||
|
Field(default="HS256", alias="jwt_algorithm", description="JWT 算法"),
|
||||||
|
"JWT 设置",
|
||||||
|
]
|
||||||
|
access_token_expire_minutes: Annotated[
|
||||||
|
int,
|
||||||
|
Field(default=1440, description="访问令牌过期时间(分钟)"),
|
||||||
|
"JWT 设置",
|
||||||
|
]
|
||||||
|
jwt_audience: Annotated[
|
||||||
|
str,
|
||||||
|
Field(default="5", description="JWT 受众"),
|
||||||
|
"JWT 设置",
|
||||||
|
]
|
||||||
|
jwt_issuer: Annotated[
|
||||||
|
str | None,
|
||||||
|
Field(default=None, description="JWT 签发者"),
|
||||||
|
"JWT 设置",
|
||||||
|
]
|
||||||
|
|
||||||
# OAuth 设置
|
# OAuth 设置
|
||||||
osu_client_id: int = 5
|
osu_client_id: Annotated[
|
||||||
osu_client_secret: str = "FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk"
|
int,
|
||||||
osu_web_client_id: int = 6
|
Field(default=5, description="OAuth 客户端 ID"),
|
||||||
osu_web_client_secret: str = "your_osu_web_client_secret_here"
|
"OAuth 设置",
|
||||||
|
]
|
||||||
|
osu_client_secret: Annotated[
|
||||||
|
str,
|
||||||
|
Field(
|
||||||
|
default="FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk",
|
||||||
|
description="OAuth 客户端密钥",
|
||||||
|
),
|
||||||
|
"OAuth 设置",
|
||||||
|
]
|
||||||
|
osu_web_client_id: Annotated[
|
||||||
|
int,
|
||||||
|
Field(default=6, description="Web OAuth 客户端 ID"),
|
||||||
|
"OAuth 设置",
|
||||||
|
]
|
||||||
|
osu_web_client_secret: Annotated[
|
||||||
|
str,
|
||||||
|
Field(
|
||||||
|
default="your_osu_web_client_secret_here",
|
||||||
|
description="Web OAuth 客户端密钥",
|
||||||
|
),
|
||||||
|
"OAuth 设置",
|
||||||
|
]
|
||||||
|
|
||||||
# 服务器设置
|
# 服务器设置
|
||||||
host: str = "0.0.0.0"
|
host: Annotated[
|
||||||
port: int = 8000
|
str,
|
||||||
debug: bool = False
|
Field(default="0.0.0.0", description="服务器监听地址"),
|
||||||
cors_urls: list[HttpUrl] = []
|
"服务器设置",
|
||||||
server_url: HttpUrl = HttpUrl("http://localhost:8000")
|
]
|
||||||
frontend_url: HttpUrl | None = None
|
port: Annotated[
|
||||||
enable_rate_limit: bool = True
|
int,
|
||||||
|
Field(default=8000, description="服务器监听端口"),
|
||||||
|
"服务器设置",
|
||||||
|
]
|
||||||
|
debug: Annotated[
|
||||||
|
bool,
|
||||||
|
Field(default=False, description="是否启用调试模式"),
|
||||||
|
"服务器设置",
|
||||||
|
]
|
||||||
|
cors_urls: Annotated[
|
||||||
|
list[HttpUrl],
|
||||||
|
Field(default=[], description="额外的 CORS 允许的域名列表 (JSON 格式)"),
|
||||||
|
"服务器设置",
|
||||||
|
]
|
||||||
|
server_url: Annotated[
|
||||||
|
HttpUrl,
|
||||||
|
Field(
|
||||||
|
default=HttpUrl("http://localhost:8000"),
|
||||||
|
description="服务器 URL",
|
||||||
|
),
|
||||||
|
"服务器设置",
|
||||||
|
]
|
||||||
|
frontend_url: Annotated[
|
||||||
|
HttpUrl | None,
|
||||||
|
Field(
|
||||||
|
default=None,
|
||||||
|
description="前端 URL,当访问从游戏打开的 URL 时会重定向到这个 URL,为空表示不重定向",
|
||||||
|
),
|
||||||
|
"服务器设置",
|
||||||
|
]
|
||||||
|
enable_rate_limit: Annotated[
|
||||||
|
bool,
|
||||||
|
Field(default=True, description="是否启用速率限制"),
|
||||||
|
"服务器设置",
|
||||||
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def web_url(self):
|
def web_url(self):
|
||||||
@@ -105,93 +257,312 @@ class Settings(BaseSettings):
|
|||||||
return "/"
|
return "/"
|
||||||
|
|
||||||
# SignalR 设置
|
# SignalR 设置
|
||||||
signalr_negotiate_timeout: int = 30
|
signalr_negotiate_timeout: Annotated[
|
||||||
signalr_ping_interval: int = 15
|
int,
|
||||||
|
Field(default=30, description="SignalR 协商超时时间(秒)"),
|
||||||
|
"SignalR 服务器设置",
|
||||||
|
]
|
||||||
|
signalr_ping_interval: Annotated[
|
||||||
|
int,
|
||||||
|
Field(default=15, description="SignalR ping 间隔(秒)"),
|
||||||
|
"SignalR 服务器设置",
|
||||||
|
]
|
||||||
|
|
||||||
# Fetcher 设置
|
# Fetcher 设置
|
||||||
fetcher_client_id: str = ""
|
fetcher_client_id: Annotated[
|
||||||
fetcher_client_secret: str = ""
|
str,
|
||||||
fetcher_scopes: Annotated[list[str], NoDecode] = ["public"]
|
Field(default="", description="Fetcher 客户端 ID"),
|
||||||
|
"Fetcher 设置",
|
||||||
|
]
|
||||||
|
fetcher_client_secret: Annotated[
|
||||||
|
str,
|
||||||
|
Field(default="", description="Fetcher 客户端密钥"),
|
||||||
|
"Fetcher 设置",
|
||||||
|
]
|
||||||
|
fetcher_scopes: Annotated[
|
||||||
|
list[str],
|
||||||
|
Field(default=["public"], description="Fetcher 权限范围,以逗号分隔每个权限"),
|
||||||
|
"Fetcher 设置",
|
||||||
|
NoDecode,
|
||||||
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fetcher_callback_url(self) -> str:
|
def fetcher_callback_url(self) -> str:
|
||||||
return f"{self.server_url}fetcher/callback"
|
return f"{self.server_url}fetcher/callback"
|
||||||
|
|
||||||
# 日志设置
|
# 日志设置
|
||||||
log_level: str = "INFO"
|
log_level: Annotated[
|
||||||
|
str,
|
||||||
|
Field(default="INFO", description="日志级别"),
|
||||||
|
"日志设置",
|
||||||
|
]
|
||||||
|
|
||||||
# 邮件服务设置
|
# 邮件服务设置
|
||||||
enable_email_verification: bool = Field(default=False, description="是否启用邮件验证功能")
|
enable_email_verification: Annotated[
|
||||||
smtp_server: str = "localhost"
|
bool,
|
||||||
smtp_port: int = 587
|
Field(default=False, description="是否启用邮件验证功能"),
|
||||||
smtp_username: str = ""
|
"邮件服务设置",
|
||||||
smtp_password: str = ""
|
]
|
||||||
from_email: str = "noreply@example.com"
|
smtp_server: Annotated[
|
||||||
from_name: str = "osu! server"
|
str,
|
||||||
|
Field(default="localhost", description="SMTP 服务器地址"),
|
||||||
|
"邮件服务设置",
|
||||||
|
]
|
||||||
|
smtp_port: Annotated[
|
||||||
|
int,
|
||||||
|
Field(default=587, description="SMTP 服务器端口"),
|
||||||
|
"邮件服务设置",
|
||||||
|
]
|
||||||
|
smtp_username: Annotated[
|
||||||
|
str,
|
||||||
|
Field(default="", description="SMTP 用户名"),
|
||||||
|
"邮件服务设置",
|
||||||
|
]
|
||||||
|
smtp_password: Annotated[
|
||||||
|
str,
|
||||||
|
Field(default="", description="SMTP 密码"),
|
||||||
|
"邮件服务设置",
|
||||||
|
]
|
||||||
|
from_email: Annotated[
|
||||||
|
str,
|
||||||
|
Field(default="noreply@example.com", description="发件人邮箱"),
|
||||||
|
"邮件服务设置",
|
||||||
|
]
|
||||||
|
from_name: Annotated[
|
||||||
|
str,
|
||||||
|
Field(default="osu! server", description="发件人名称"),
|
||||||
|
"邮件服务设置",
|
||||||
|
]
|
||||||
|
|
||||||
# Sentry 配置
|
# 监控配置
|
||||||
sentry_dsn: HttpUrl | None = None
|
sentry_dsn: Annotated[
|
||||||
|
HttpUrl | None,
|
||||||
# New Relic 配置
|
Field(default=None, description="Sentry DSN,为空不启用 Sentry"),
|
||||||
new_relic_environment: None | str = None
|
"监控设置",
|
||||||
|
]
|
||||||
|
new_relic_environment: Annotated[
|
||||||
|
str | None,
|
||||||
|
Field(default=None, description='New Relic 环境标识,设置为 "production" 或 "development"'),
|
||||||
|
"监控设置",
|
||||||
|
]
|
||||||
|
|
||||||
# GeoIP 配置
|
# GeoIP 配置
|
||||||
maxmind_license_key: str = ""
|
maxmind_license_key: Annotated[
|
||||||
geoip_dest_dir: str = "./geoip"
|
str,
|
||||||
geoip_update_day: int = 1 # 每周更新的星期几(0=周一,6=周日)
|
Field(default="", description="MaxMind License Key(用于下载离线IP库)"),
|
||||||
geoip_update_hour: int = 2 # 每周更新的小时数(0-23)
|
"GeoIP 配置",
|
||||||
|
]
|
||||||
|
geoip_dest_dir: Annotated[
|
||||||
|
str,
|
||||||
|
Field(default="./geoip", description="GeoIP 数据库存储目录"),
|
||||||
|
"GeoIP 配置",
|
||||||
|
]
|
||||||
|
geoip_update_day: Annotated[
|
||||||
|
int,
|
||||||
|
Field(default=1, description="GeoIP 每周更新的星期几(0=周一,6=周日)"),
|
||||||
|
"GeoIP 配置",
|
||||||
|
]
|
||||||
|
geoip_update_hour: Annotated[
|
||||||
|
int,
|
||||||
|
Field(default=2, description="GeoIP 每周更新时间(小时,0-23)"),
|
||||||
|
"GeoIP 配置",
|
||||||
|
]
|
||||||
|
|
||||||
# 游戏设置
|
# 游戏设置
|
||||||
enable_rx: bool = Field(default=False, validation_alias=AliasChoices("enable_rx", "enable_osu_rx"))
|
enable_rx: Annotated[
|
||||||
enable_ap: bool = Field(default=False, validation_alias=AliasChoices("enable_ap", "enable_osu_ap"))
|
bool,
|
||||||
enable_all_mods_pp: bool = False
|
Field(
|
||||||
enable_supporter_for_all_users: bool = False
|
default=False,
|
||||||
enable_all_beatmap_leaderboard: bool = False
|
validation_alias=AliasChoices("enable_rx", "enable_osu_rx"),
|
||||||
enable_all_beatmap_pp: bool = False
|
description="启用 RX mod 统计数据",
|
||||||
seasonal_backgrounds: Annotated[list[str], BeforeValidator(_parse_list)] = []
|
),
|
||||||
beatmap_tag_top_count: int = 2 # this is 10 in osu-web
|
"游戏设置",
|
||||||
|
]
|
||||||
|
enable_ap: Annotated[
|
||||||
|
bool,
|
||||||
|
Field(
|
||||||
|
default=False,
|
||||||
|
validation_alias=AliasChoices("enable_ap", "enable_osu_ap"),
|
||||||
|
description="启用 AP mod 统计数据",
|
||||||
|
),
|
||||||
|
"游戏设置",
|
||||||
|
]
|
||||||
|
enable_all_mods_pp: Annotated[
|
||||||
|
bool,
|
||||||
|
Field(default=False, description="启用所有 Mod 的 PP 计算"),
|
||||||
|
"游戏设置",
|
||||||
|
]
|
||||||
|
enable_supporter_for_all_users: Annotated[
|
||||||
|
bool,
|
||||||
|
Field(default=False, description="启用所有新注册用户的支持者状态"),
|
||||||
|
"游戏设置",
|
||||||
|
]
|
||||||
|
enable_all_beatmap_leaderboard: Annotated[
|
||||||
|
bool,
|
||||||
|
Field(default=False, description="启用所有谱面的排行榜"),
|
||||||
|
"游戏设置",
|
||||||
|
]
|
||||||
|
enable_all_beatmap_pp: Annotated[
|
||||||
|
bool,
|
||||||
|
Field(default=False, description="允许任何谱面获得 PP"),
|
||||||
|
"游戏设置",
|
||||||
|
]
|
||||||
|
seasonal_backgrounds: Annotated[
|
||||||
|
list[str],
|
||||||
|
Field(default=[], description="季节背景图 URL 列表"),
|
||||||
|
"游戏设置",
|
||||||
|
]
|
||||||
|
beatmap_tag_top_count: Annotated[
|
||||||
|
int,
|
||||||
|
Field(default=2, description="显示在结算列表的标签所需的最低票数"),
|
||||||
|
"游戏设置",
|
||||||
|
]
|
||||||
|
|
||||||
# 谱面缓存设置
|
# 谱面缓存设置
|
||||||
enable_beatmap_preload: bool = True
|
enable_beatmap_preload: Annotated[
|
||||||
beatmap_cache_expire_hours: int = 24
|
bool,
|
||||||
|
Field(default=True, description="启用谱面缓存预加载"),
|
||||||
|
"缓存设置",
|
||||||
|
"谱面缓存",
|
||||||
|
]
|
||||||
|
beatmap_cache_expire_hours: Annotated[
|
||||||
|
int,
|
||||||
|
Field(default=24, description="谱面缓存过期时间(小时)"),
|
||||||
|
"缓存设置",
|
||||||
|
"谱面缓存",
|
||||||
|
]
|
||||||
|
|
||||||
# 排行榜缓存设置
|
# 排行榜缓存设置
|
||||||
enable_ranking_cache: bool = True
|
enable_ranking_cache: Annotated[
|
||||||
ranking_cache_expire_minutes: int = 10 # 排行榜缓存过期时间(分钟)
|
bool,
|
||||||
ranking_cache_refresh_interval_minutes: int = 10 # 排行榜缓存刷新间隔(分钟)
|
Field(default=True, description="启用排行榜缓存"),
|
||||||
ranking_cache_max_pages: int = 20 # 最多缓存的页数
|
"缓存设置",
|
||||||
ranking_cache_top_countries: int = 20 # 缓存前N个国家的排行榜
|
"排行榜缓存",
|
||||||
|
]
|
||||||
|
ranking_cache_expire_minutes: Annotated[
|
||||||
|
int,
|
||||||
|
Field(default=10, description="排行榜缓存过期时间(分钟)"),
|
||||||
|
"缓存设置",
|
||||||
|
"排行榜缓存",
|
||||||
|
]
|
||||||
|
ranking_cache_refresh_interval_minutes: Annotated[
|
||||||
|
int,
|
||||||
|
Field(default=10, description="排行榜缓存刷新间隔(分钟)"),
|
||||||
|
"缓存设置",
|
||||||
|
"排行榜缓存",
|
||||||
|
]
|
||||||
|
ranking_cache_max_pages: Annotated[
|
||||||
|
int,
|
||||||
|
Field(default=20, description="最多缓存的页数"),
|
||||||
|
"缓存设置",
|
||||||
|
"排行榜缓存",
|
||||||
|
]
|
||||||
|
ranking_cache_top_countries: Annotated[
|
||||||
|
int,
|
||||||
|
Field(default=20, description="缓存前N个国家的排行榜"),
|
||||||
|
"缓存设置",
|
||||||
|
"排行榜缓存",
|
||||||
|
]
|
||||||
|
|
||||||
# 用户缓存设置
|
# 用户缓存设置
|
||||||
enable_user_cache_preload: bool = True # 启用用户缓存预加载
|
enable_user_cache_preload: Annotated[
|
||||||
user_cache_expire_seconds: int = 300 # 用户信息缓存过期时间(秒)
|
bool,
|
||||||
user_scores_cache_expire_seconds: int = 60 # 用户成绩缓存过期时间(秒)
|
Field(default=True, description="启用用户缓存预加载"),
|
||||||
user_beatmapsets_cache_expire_seconds: int = 600 # 用户谱面集缓存过期时间(秒)
|
"缓存设置",
|
||||||
user_cache_max_preload_users: int = 200 # 最多预加载的用户数量
|
"用户缓存",
|
||||||
user_cache_concurrent_limit: int = 10 # 并发缓存用户的限制
|
]
|
||||||
|
user_cache_expire_seconds: Annotated[
|
||||||
|
int,
|
||||||
|
Field(default=300, description="用户信息缓存过期时间(秒)"),
|
||||||
|
"缓存设置",
|
||||||
|
"用户缓存",
|
||||||
|
]
|
||||||
|
user_scores_cache_expire_seconds: Annotated[
|
||||||
|
int,
|
||||||
|
Field(default=60, description="用户成绩缓存过期时间(秒)"),
|
||||||
|
"缓存设置",
|
||||||
|
"用户缓存",
|
||||||
|
]
|
||||||
|
user_beatmapsets_cache_expire_seconds: Annotated[
|
||||||
|
int,
|
||||||
|
Field(default=600, description="用户谱面集缓存过期时间(秒)"),
|
||||||
|
"缓存设置",
|
||||||
|
"用户缓存",
|
||||||
|
]
|
||||||
|
user_cache_max_preload_users: Annotated[
|
||||||
|
int,
|
||||||
|
Field(default=200, description="最多预加载的用户数量"),
|
||||||
|
"缓存设置",
|
||||||
|
"用户缓存",
|
||||||
|
]
|
||||||
|
user_cache_concurrent_limit: Annotated[
|
||||||
|
int,
|
||||||
|
Field(default=10, description="并发缓存用户的限制"),
|
||||||
|
"缓存设置",
|
||||||
|
"用户缓存",
|
||||||
|
]
|
||||||
|
|
||||||
# 资源代理设置
|
# 资源代理设置
|
||||||
enable_asset_proxy: bool = True # 启用资源代理功能
|
enable_asset_proxy: Annotated[
|
||||||
custom_asset_domain: str = "g0v0.top" # 自定义资源域名
|
bool,
|
||||||
asset_proxy_prefix: str = "assets-ppy" # assets.ppy.sh的自定义前缀
|
Field(default=False, description="启用资源代理"),
|
||||||
avatar_proxy_prefix: str = "a-ppy" # a.ppy.sh的自定义前缀
|
"资源代理设置",
|
||||||
beatmap_proxy_prefix: str = "b-ppy" # b.ppy.sh的自定义前缀
|
]
|
||||||
|
custom_asset_domain: Annotated[
|
||||||
|
str,
|
||||||
|
Field(default="g0v0.top", description="自定义资源域名"),
|
||||||
|
"资源代理设置",
|
||||||
|
]
|
||||||
|
asset_proxy_prefix: Annotated[
|
||||||
|
str,
|
||||||
|
Field(default="assets-ppy", description="assets.ppy.sh 的自定义前缀"),
|
||||||
|
"资源代理设置",
|
||||||
|
]
|
||||||
|
avatar_proxy_prefix: Annotated[
|
||||||
|
str,
|
||||||
|
Field(default="a-ppy", description="a.ppy.sh 的自定义前缀"),
|
||||||
|
"资源代理设置",
|
||||||
|
]
|
||||||
|
beatmap_proxy_prefix: Annotated[
|
||||||
|
str,
|
||||||
|
Field(default="b-ppy", description="b.ppy.sh 的自定义前缀"),
|
||||||
|
"资源代理设置",
|
||||||
|
]
|
||||||
|
|
||||||
# 反作弊设置
|
# 反作弊设置
|
||||||
suspicious_score_check: bool = True
|
suspicious_score_check: Annotated[
|
||||||
banned_name: list[str] = [
|
bool,
|
||||||
"mrekk",
|
Field(default=True, description="启用可疑分数检查(star>25&acc<80 或 pp>3000)"),
|
||||||
"vaxei",
|
"反作弊设置",
|
||||||
"btmc",
|
]
|
||||||
"cookiezi",
|
banned_name: Annotated[
|
||||||
"peppy",
|
list[str],
|
||||||
"saragi",
|
Field(
|
||||||
"chocomint",
|
default=[
|
||||||
|
"mrekk",
|
||||||
|
"vaxei",
|
||||||
|
"btmc",
|
||||||
|
"cookiezi",
|
||||||
|
"peppy",
|
||||||
|
"saragi",
|
||||||
|
"chocomint",
|
||||||
|
],
|
||||||
|
description="禁止使用的用户名列表",
|
||||||
|
),
|
||||||
|
"反作弊设置",
|
||||||
]
|
]
|
||||||
|
|
||||||
# 存储设置
|
# 存储设置
|
||||||
storage_service: StorageServiceType = StorageServiceType.LOCAL
|
storage_service: Annotated[
|
||||||
storage_settings: LocalStorageSettings | CloudflareR2Settings | AWSS3StorageSettings = LocalStorageSettings()
|
StorageServiceType,
|
||||||
|
Field(default=StorageServiceType.LOCAL, description="存储服务类型:local、r2、s3"),
|
||||||
|
"存储服务设置",
|
||||||
|
]
|
||||||
|
storage_settings: Annotated[
|
||||||
|
LocalStorageSettings | CloudflareR2Settings | AWSS3StorageSettings,
|
||||||
|
Field(default=LocalStorageSettings(), description="存储服务配置 (JSON 格式)"),
|
||||||
|
"存储服务设置",
|
||||||
|
]
|
||||||
|
|
||||||
@field_validator("fetcher_scopes", mode="before")
|
@field_validator("fetcher_scopes", mode="before")
|
||||||
def validate_fetcher_scopes(cls, v: Any) -> list[str]:
|
def validate_fetcher_scopes(cls, v: Any) -> list[str]:
|
||||||
@@ -217,4 +588,4 @@ class Settings(BaseSettings):
|
|||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
settings = Settings()
|
settings = Settings() # pyright: ignore[reportCallIssue]
|
||||||
|
|||||||
Reference in New Issue
Block a user