539 lines
20 KiB
Python
539 lines
20 KiB
Python
"""nanobot 配置 Schema(基于 Pydantic)。
|
||
|
||
这份文件是“配置系统的单一结构定义”:
|
||
1. 定义配置长什么样(字段、默认值、嵌套结构)
|
||
2. 负责配置的类型校验与兼容(camelCase / snake_case)
|
||
3. 提供若干读取辅助方法(如 provider 匹配、api_key/api_base 解析)
|
||
|
||
你可以把它理解为:
|
||
- `loader.py` 负责“读写配置文件”
|
||
- `schema.py` 负责“配置对象的结构和规则”
|
||
"""
|
||
|
||
from pathlib import Path
|
||
from typing import Literal
|
||
|
||
from pydantic import BaseModel, ConfigDict, Field
|
||
from pydantic.alias_generators import to_camel
|
||
from pydantic_settings import BaseSettings
|
||
|
||
|
||
class Base(BaseModel):
|
||
"""所有配置模型的基类。
|
||
|
||
关键点:
|
||
- `alias_generator=to_camel`:自动把 `api_key` 这种字段映射到 `apiKey`
|
||
- `populate_by_name=True`:读取时同时接受 snake_case 和 camelCase
|
||
|
||
结果:
|
||
- Python 代码内部统一使用 snake_case,便于可读性和一致性
|
||
- 配置文件对外保持 camelCase,贴近 README 和用户习惯
|
||
"""
|
||
|
||
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
||
|
||
|
||
class WhatsAppConfig(Base):
|
||
"""WhatsApp 渠道配置。
|
||
|
||
说明:
|
||
- nanobot 通过单独的 bridge 进程与 WhatsApp 交互
|
||
- 这里配置的是 bridge 的连接地址和访问控制
|
||
"""
|
||
|
||
enabled: bool = False
|
||
bridge_url: str = "ws://localhost:3001"
|
||
bridge_token: str = "" # Shared token for bridge auth (optional, recommended)
|
||
allow_from: list[str] = Field(default_factory=list) # Allowed phone numbers
|
||
|
||
|
||
class TelegramConfig(Base):
|
||
"""Telegram 渠道配置。
|
||
|
||
常用字段:
|
||
- token:机器人凭证(必须)
|
||
- allow_from:白名单(可选,空列表表示不限制)
|
||
- proxy:在网络受限场景下可配置代理
|
||
"""
|
||
|
||
enabled: bool = False
|
||
token: str = "" # Bot token from @BotFather
|
||
allow_from: list[str] = Field(default_factory=list) # Allowed user IDs or usernames
|
||
proxy: str | None = None # HTTP/SOCKS5 proxy URL, e.g. "http://127.0.0.1:7890" or "socks5://127.0.0.1:1080"
|
||
reply_to_message: bool = False # If true, bot replies quote the original message
|
||
|
||
|
||
class FeishuConfig(Base):
|
||
"""飞书/Lark 渠道配置(基于长连接模式)。"""
|
||
|
||
enabled: bool = False
|
||
app_id: str = "" # App ID from Feishu Open Platform
|
||
app_secret: str = "" # App Secret from Feishu Open Platform
|
||
encrypt_key: str = "" # Encrypt Key for event subscription (optional)
|
||
verification_token: str = "" # Verification Token for event subscription (optional)
|
||
allow_from: list[str] = Field(default_factory=list) # Allowed user open_ids
|
||
|
||
|
||
class DingTalkConfig(Base):
|
||
"""钉钉渠道配置(Stream 模式)。"""
|
||
|
||
enabled: bool = False
|
||
client_id: str = "" # AppKey
|
||
client_secret: str = "" # AppSecret
|
||
allow_from: list[str] = Field(default_factory=list) # Allowed staff_ids
|
||
|
||
|
||
class DiscordConfig(Base):
|
||
"""Discord 渠道配置。"""
|
||
|
||
enabled: bool = False
|
||
token: str = "" # Bot token from Discord Developer Portal
|
||
allow_from: list[str] = Field(default_factory=list) # Allowed user IDs
|
||
gateway_url: str = "wss://gateway.discord.gg/?v=10&encoding=json"
|
||
intents: int = 37377 # GUILDS + GUILD_MESSAGES + DIRECT_MESSAGES + MESSAGE_CONTENT
|
||
|
||
|
||
class MatrixConfig(Base):
|
||
"""Matrix (Element) 渠道配置。"""
|
||
|
||
enabled: bool = False
|
||
homeserver: str = "https://matrix.org"
|
||
access_token: str = ""
|
||
user_id: str = "" # @bot:matrix.org
|
||
device_id: str = ""
|
||
e2ee_enabled: bool = True # Enable Matrix E2EE support (encryption + encrypted room handling).
|
||
sync_stop_grace_seconds: int = (
|
||
2 # Max seconds to wait for sync_forever to stop gracefully before cancellation fallback.
|
||
)
|
||
max_media_bytes: int = (
|
||
20 * 1024 * 1024
|
||
) # Max attachment size accepted for Matrix media handling (inbound + outbound).
|
||
allow_from: list[str] = Field(default_factory=list)
|
||
group_policy: Literal["open", "mention", "allowlist"] = "open"
|
||
group_allow_from: list[str] = Field(default_factory=list)
|
||
allow_room_mentions: bool = False
|
||
|
||
|
||
class EmailConfig(Base):
|
||
"""Email 渠道配置(IMAP 收件 + SMTP 发件)。
|
||
|
||
设计思路:
|
||
- IMAP 负责拉取新邮件
|
||
- SMTP 负责自动回复
|
||
- 行为参数控制轮询频率、正文截断、标记已读等策略
|
||
"""
|
||
|
||
enabled: bool = False
|
||
consent_granted: bool = False # Explicit owner permission to access mailbox data
|
||
|
||
# IMAP (receive)
|
||
imap_host: str = ""
|
||
imap_port: int = 993
|
||
imap_username: str = ""
|
||
imap_password: str = ""
|
||
imap_mailbox: str = "INBOX"
|
||
imap_use_ssl: bool = True
|
||
|
||
# SMTP (send)
|
||
smtp_host: str = ""
|
||
smtp_port: int = 587
|
||
smtp_username: str = ""
|
||
smtp_password: str = ""
|
||
smtp_use_tls: bool = True
|
||
smtp_use_ssl: bool = False
|
||
from_address: str = ""
|
||
|
||
# Behavior
|
||
auto_reply_enabled: bool = True # If false, inbound email is read but no automatic reply is sent
|
||
poll_interval_seconds: int = 30
|
||
mark_seen: bool = True
|
||
max_body_chars: int = 12000
|
||
subject_prefix: str = "Re: "
|
||
allow_from: list[str] = Field(default_factory=list) # Allowed sender email addresses
|
||
|
||
|
||
class MochatMentionConfig(Base):
|
||
"""Mochat 提及(mention)规则。"""
|
||
|
||
require_in_groups: bool = False
|
||
|
||
|
||
class MochatGroupRule(Base):
|
||
"""Mochat 群组级别规则(可按群单独配置是否必须 @)。"""
|
||
|
||
require_mention: bool = False
|
||
|
||
|
||
class MochatConfig(Base):
|
||
"""Mochat 渠道配置。
|
||
|
||
包含三类参数:
|
||
- 连接参数:base_url / socket_url / socket_path
|
||
- 重连与轮询参数:各类 *_ms 与 retry 相关字段
|
||
- 权限与会话参数:allow_from / sessions / panels / mention / groups
|
||
"""
|
||
|
||
enabled: bool = False
|
||
base_url: str = "https://mochat.io"
|
||
socket_url: str = ""
|
||
socket_path: str = "/socket.io"
|
||
socket_disable_msgpack: bool = False
|
||
socket_reconnect_delay_ms: int = 1000
|
||
socket_max_reconnect_delay_ms: int = 10000
|
||
socket_connect_timeout_ms: int = 10000
|
||
refresh_interval_ms: int = 30000
|
||
watch_timeout_ms: int = 25000
|
||
watch_limit: int = 100
|
||
retry_delay_ms: int = 500
|
||
max_retry_attempts: int = 0 # 0 means unlimited retries
|
||
claw_token: str = ""
|
||
agent_user_id: str = ""
|
||
sessions: list[str] = Field(default_factory=list)
|
||
panels: list[str] = Field(default_factory=list)
|
||
allow_from: list[str] = Field(default_factory=list)
|
||
mention: MochatMentionConfig = Field(default_factory=MochatMentionConfig)
|
||
groups: dict[str, MochatGroupRule] = Field(default_factory=dict)
|
||
reply_delay_mode: str = "non-mention" # off | non-mention
|
||
reply_delay_ms: int = 120000
|
||
|
||
|
||
class SlackDMConfig(Base):
|
||
"""Slack 私聊(DM)策略配置。"""
|
||
|
||
enabled: bool = True
|
||
policy: str = "open" # "open" or "allowlist"
|
||
allow_from: list[str] = Field(default_factory=list) # Allowed Slack user IDs
|
||
|
||
|
||
class SlackConfig(Base):
|
||
"""Slack 渠道配置。"""
|
||
|
||
enabled: bool = False
|
||
mode: str = "socket" # "socket" supported
|
||
webhook_path: str = "/slack/events"
|
||
bot_token: str = "" # xoxb-...
|
||
app_token: str = "" # xapp-...
|
||
user_token_read_only: bool = True
|
||
reply_in_thread: bool = True
|
||
react_emoji: str = "eyes"
|
||
group_policy: str = "mention" # "mention", "open", "allowlist"
|
||
group_allow_from: list[str] = Field(default_factory=list) # Allowed channel IDs if allowlist
|
||
dm: SlackDMConfig = Field(default_factory=SlackDMConfig)
|
||
|
||
|
||
class QQConfig(Base):
|
||
"""QQ 渠道配置(botpy SDK)。"""
|
||
|
||
enabled: bool = False
|
||
app_id: str = "" # 机器人 ID (AppID) from q.qq.com
|
||
secret: str = "" # 机器人密钥 (AppSecret) from q.qq.com
|
||
allow_from: list[str] = Field(default_factory=list) # Allowed user openids (empty = public access)
|
||
|
||
|
||
class ChannelsConfig(Base):
|
||
"""所有聊天渠道的总配置。
|
||
|
||
除了具体渠道参数外,还有两个全局开关:
|
||
- send_progress:是否把“处理中进度”推送到渠道
|
||
- send_tool_hints:是否把“工具调用提示”推送到渠道
|
||
"""
|
||
|
||
send_progress: bool = True # stream agent's text progress to the channel
|
||
send_tool_hints: bool = False # stream tool-call hints (e.g. read_file("…"))
|
||
whatsapp: WhatsAppConfig = Field(default_factory=WhatsAppConfig)
|
||
telegram: TelegramConfig = Field(default_factory=TelegramConfig)
|
||
discord: DiscordConfig = Field(default_factory=DiscordConfig)
|
||
feishu: FeishuConfig = Field(default_factory=FeishuConfig)
|
||
mochat: MochatConfig = Field(default_factory=MochatConfig)
|
||
dingtalk: DingTalkConfig = Field(default_factory=DingTalkConfig)
|
||
email: EmailConfig = Field(default_factory=EmailConfig)
|
||
slack: SlackConfig = Field(default_factory=SlackConfig)
|
||
qq: QQConfig = Field(default_factory=QQConfig)
|
||
matrix: MatrixConfig = Field(default_factory=MatrixConfig)
|
||
|
||
|
||
class AgentDefaults(Base):
|
||
"""Agent 默认行为配置。
|
||
|
||
关键参数建议理解:
|
||
- model:主模型标识
|
||
- max_tokens:单次回复上限
|
||
- max_tool_iterations:一次请求里最多工具循环次数
|
||
- memory_window:每次送给模型的历史窗口大小
|
||
"""
|
||
|
||
workspace: str = "~/.nanobot/workspace"
|
||
model: str = "anthropic/claude-opus-4-5"
|
||
max_tokens: int = 8192
|
||
temperature: float = 0.1
|
||
max_tool_iterations: int = 40
|
||
memory_window: int = 100
|
||
|
||
|
||
class AgentsConfig(Base):
|
||
"""Agent 顶层配置(当前主要是 defaults)。"""
|
||
|
||
defaults: AgentDefaults = Field(default_factory=AgentDefaults)
|
||
|
||
|
||
class ProviderConfig(Base):
|
||
"""单个 LLM Provider 的通用配置结构。
|
||
|
||
字段说明:
|
||
- api_key:访问凭证
|
||
- api_base:可选自定义网关/代理地址
|
||
- extra_headers:额外 HTTP 头(某些网关会要求)
|
||
"""
|
||
|
||
api_key: str = ""
|
||
api_base: str | None = None
|
||
extra_headers: dict[str, str] | None = None # Custom headers (e.g. APP-Code for AiHubMix)
|
||
|
||
|
||
class ProvidersConfig(Base):
|
||
"""所有 Provider 的配置集合。
|
||
|
||
这里的字段名必须和 `providers/registry.py` 里的 ProviderSpec.name 对齐。
|
||
这样 `_match_provider()` 才能通过 `getattr(self.providers, spec.name)` 正确取值。
|
||
"""
|
||
|
||
custom: ProviderConfig = Field(default_factory=ProviderConfig) # Any OpenAI-compatible endpoint
|
||
anthropic: ProviderConfig = Field(default_factory=ProviderConfig)
|
||
openai: ProviderConfig = Field(default_factory=ProviderConfig)
|
||
openrouter: ProviderConfig = Field(default_factory=ProviderConfig)
|
||
deepseek: ProviderConfig = Field(default_factory=ProviderConfig)
|
||
groq: ProviderConfig = Field(default_factory=ProviderConfig)
|
||
zhipu: ProviderConfig = Field(default_factory=ProviderConfig)
|
||
dashscope: ProviderConfig = Field(default_factory=ProviderConfig) # 阿里云通义千问
|
||
vllm: ProviderConfig = Field(default_factory=ProviderConfig)
|
||
gemini: ProviderConfig = Field(default_factory=ProviderConfig)
|
||
moonshot: ProviderConfig = Field(default_factory=ProviderConfig)
|
||
minimax: ProviderConfig = Field(default_factory=ProviderConfig)
|
||
aihubmix: ProviderConfig = Field(default_factory=ProviderConfig) # AiHubMix API gateway
|
||
siliconflow: ProviderConfig = Field(default_factory=ProviderConfig) # SiliconFlow (硅基流动) API gateway
|
||
volcengine: ProviderConfig = Field(default_factory=ProviderConfig) # VolcEngine (火山引擎) API gateway
|
||
openai_codex: ProviderConfig = Field(default_factory=ProviderConfig) # OpenAI Codex (OAuth)
|
||
github_copilot: ProviderConfig = Field(default_factory=ProviderConfig) # Github Copilot (OAuth)
|
||
|
||
|
||
class GatewayConfig(Base):
|
||
"""Gateway 服务监听配置。"""
|
||
|
||
host: str = "0.0.0.0"
|
||
port: int = 18790
|
||
|
||
|
||
class WebSearchConfig(Base):
|
||
"""Web 搜索工具配置(当前主要是 Brave Search)。"""
|
||
|
||
api_key: str = "" # Brave Search API key
|
||
max_results: int = 5
|
||
|
||
|
||
class WebToolsConfig(Base):
|
||
"""Web 工具总配置。"""
|
||
|
||
search: WebSearchConfig = Field(default_factory=WebSearchConfig)
|
||
|
||
|
||
class ExecToolConfig(Base):
|
||
"""Shell 执行工具配置。"""
|
||
|
||
timeout: int = 60
|
||
|
||
|
||
class MCPServerConfig(Base):
|
||
"""单个 MCP 服务器配置(支持 stdio 与 HTTP 两种连接方式)。
|
||
|
||
使用方式:
|
||
- stdio:配置 `command + args + env`
|
||
- HTTP:配置 `url + headers`
|
||
"""
|
||
|
||
command: str = "" # Stdio: command to run (e.g. "npx")
|
||
args: list[str] = Field(default_factory=list) # Stdio: command arguments
|
||
env: dict[str, str] = Field(default_factory=dict) # Stdio: extra env vars
|
||
url: str = "" # HTTP: streamable HTTP endpoint URL
|
||
headers: dict[str, str] = Field(default_factory=dict) # HTTP: Custom HTTP Headers
|
||
auth_mode: str = "none" # none | oauth_backend_token
|
||
auth_audience: str = ""
|
||
auth_scopes: list[str] = Field(default_factory=list)
|
||
tool_timeout: int = 30 # Seconds before a tool call is cancelled
|
||
sensitive: bool = False # Redact secrets/args from Web views and process events
|
||
|
||
|
||
class A2AConfig(Base):
|
||
"""A2A agent 委派配置。"""
|
||
|
||
# 总开关,预留给未来需要完全禁用远程委派的场景。
|
||
enabled: bool = True
|
||
# 单次远程任务的最长等待时间(秒)。
|
||
timeout_seconds: int = 30
|
||
# 非流式任务轮询间隔(秒)。
|
||
poll_interval_seconds: int = 2
|
||
# agent card 本地缓存 TTL,避免每次委派都重新拉远端元数据。
|
||
card_cache_ttl_seconds: int = 300
|
||
# group delegation 并发上限,防止一次性打爆本地或远端资源。
|
||
max_parallel_agents: int = 4
|
||
# 是否允许从 skill 元数据里暴露 agent cards。
|
||
allow_skill_cards: bool = True
|
||
# 是否允许读取 workspace/agents/registry.json 中的手工登记 agent。
|
||
allow_workspace_agents: bool = True
|
||
# 允许访问的远端 host 白名单;为空表示不限制。
|
||
allowed_hosts: list[str] = Field(default_factory=list)
|
||
|
||
|
||
class ToolsConfig(Base):
|
||
"""工具层总配置。
|
||
|
||
关键安全字段:
|
||
- restrict_to_workspace:开启后,工具访问将被限制在 workspace 内
|
||
"""
|
||
|
||
web: WebToolsConfig = Field(default_factory=WebToolsConfig)
|
||
exec: ExecToolConfig = Field(default_factory=ExecToolConfig)
|
||
restrict_to_workspace: bool = False # If true, restrict all tool access to workspace directory
|
||
mcp_servers: dict[str, MCPServerConfig] = Field(default_factory=dict)
|
||
a2a: A2AConfig = Field(default_factory=A2AConfig)
|
||
|
||
|
||
class AuthzConfig(Base):
|
||
"""外部 AuthZ/OAuth 服务配置。"""
|
||
|
||
enabled: bool = False
|
||
base_url: str = "http://127.0.0.1:19090"
|
||
request_timeout_seconds: int = 10
|
||
outlook_mcp_url: str = ""
|
||
|
||
|
||
class BackendIdentityConfig(Base):
|
||
"""当前 backend 在 AuthZ 服务里的身份配置。"""
|
||
|
||
backend_id: str = ""
|
||
client_id: str = ""
|
||
client_secret: str = ""
|
||
name: str = "Local Backend"
|
||
public_base_url: str = ""
|
||
|
||
|
||
class Config(BaseSettings):
|
||
"""nanobot 根配置对象。
|
||
|
||
这是业务代码中最常使用的配置入口:
|
||
- `config.agents.defaults.model`
|
||
- `config.channels.telegram.token`
|
||
- `config.tools.restrict_to_workspace`
|
||
等都会从这里往下访问。
|
||
"""
|
||
|
||
agents: AgentsConfig = Field(default_factory=AgentsConfig)
|
||
channels: ChannelsConfig = Field(default_factory=ChannelsConfig)
|
||
providers: ProvidersConfig = Field(default_factory=ProvidersConfig)
|
||
gateway: GatewayConfig = Field(default_factory=GatewayConfig)
|
||
tools: ToolsConfig = Field(default_factory=ToolsConfig)
|
||
authz: AuthzConfig = Field(default_factory=AuthzConfig)
|
||
backend_identity: BackendIdentityConfig = Field(default_factory=BackendIdentityConfig)
|
||
|
||
@property
|
||
def workspace_path(self) -> Path:
|
||
"""返回展开后的 workspace 绝对路径对象。
|
||
|
||
`~` 会被替换成用户 home 目录,避免下游代码重复处理路径展开。
|
||
"""
|
||
return Path(self.agents.defaults.workspace).expanduser()
|
||
|
||
def _match_provider(self, model: str | None = None) -> tuple["ProviderConfig | None", str | None]:
|
||
"""根据模型名与当前配置,匹配最合适的 provider。
|
||
|
||
返回值:
|
||
- ProviderConfig | None:匹配到的配置项(含 api_key/api_base)
|
||
- str | None:provider 的 registry 名称(例如 openrouter/deepseek)
|
||
|
||
匹配优先级(非常重要):
|
||
1. 显式前缀匹配:`github-copilot/...` 这种明确前缀优先
|
||
2. 关键字匹配:按 PROVIDERS 顺序匹配关键词
|
||
3. 兜底匹配:选第一个“已配置 api_key 的非 OAuth provider”
|
||
"""
|
||
from nanobot.providers.registry import PROVIDERS
|
||
|
||
# 统一做小写与连字符归一化,减少字符串匹配分歧。
|
||
model_lower = (model or self.agents.defaults.model).lower()
|
||
model_normalized = model_lower.replace("-", "_")
|
||
model_prefix = model_lower.split("/", 1)[0] if "/" in model_lower else ""
|
||
normalized_prefix = model_prefix.replace("-", "_")
|
||
|
||
# 关键字匹配函数:同时兼容 dash/underscore 两种写法。
|
||
def _kw_matches(kw: str) -> bool:
|
||
kw = kw.lower()
|
||
return kw in model_lower or kw.replace("-", "_") in model_normalized
|
||
|
||
# 第 1 轮:显式前缀优先
|
||
# 例如 `github-copilot/gpt-5.3-codex`,必须匹配 github_copilot,
|
||
# 不能被 `codex` 关键字误匹配成 openai_codex。
|
||
for spec in PROVIDERS:
|
||
p = getattr(self.providers, spec.name, None)
|
||
if p and model_prefix and normalized_prefix == spec.name:
|
||
if spec.is_oauth or p.api_key:
|
||
return p, spec.name
|
||
|
||
# 第 2 轮:按关键字匹配(顺序由 PROVIDERS 决定)
|
||
# 顺序很关键:registry 里前面的 provider 具有更高优先级。
|
||
for spec in PROVIDERS:
|
||
p = getattr(self.providers, spec.name, None)
|
||
if p and any(_kw_matches(kw) for kw in spec.keywords):
|
||
if spec.is_oauth or p.api_key:
|
||
return p, spec.name
|
||
|
||
# 第 3 轮:兜底匹配
|
||
# 规则:仅考虑“非 OAuth + 有 api_key”的 provider。
|
||
# 原因:OAuth provider 需要显式模型选择,不能静默兜底。
|
||
for spec in PROVIDERS:
|
||
if spec.is_oauth:
|
||
continue
|
||
p = getattr(self.providers, spec.name, None)
|
||
if p and p.api_key:
|
||
return p, spec.name
|
||
return None, None
|
||
|
||
def get_provider(self, model: str | None = None) -> ProviderConfig | None:
|
||
"""获取匹配到的 ProviderConfig(含 api_key/api_base/extra_headers)。"""
|
||
p, _ = self._match_provider(model)
|
||
return p
|
||
|
||
def get_provider_name(self, model: str | None = None) -> str | None:
|
||
"""获取匹配到的 provider 名称(例如 deepseek/openrouter)。"""
|
||
_, name = self._match_provider(model)
|
||
return name
|
||
|
||
def get_api_key(self, model: str | None = None) -> str | None:
|
||
"""获取当前模型对应的 API key(无则返回 None)。"""
|
||
p = self.get_provider(model)
|
||
return p.api_key if p else None
|
||
|
||
def get_api_base(self, model: str | None = None) -> str | None:
|
||
"""获取当前模型的 api_base。
|
||
|
||
规则:
|
||
1. 若用户显式配置了 api_base,优先返回用户值
|
||
2. 否则若匹配到的是 gateway provider,则可回退到 registry 默认 base
|
||
3. 标准 provider(非 gateway)默认不在这里强制写 api_base
|
||
"""
|
||
from nanobot.providers.registry import find_by_name
|
||
|
||
p, name = self._match_provider(model)
|
||
if p and p.api_base:
|
||
return p.api_base
|
||
# 仅 gateway 在此处应用默认 api_base。
|
||
# 标准 provider(如 moonshot)通常在 provider 初始化时通过环境变量处理,
|
||
# 避免污染全局 litellm.api_base。
|
||
if name:
|
||
spec = find_by_name(name)
|
||
if spec and spec.is_gateway and spec.default_api_base:
|
||
return spec.default_api_base
|
||
return None
|
||
|
||
# BaseSettings 相关:
|
||
# - env_prefix="NANOBOT_":环境变量前缀,例如 NANOBOT_AGENTS__DEFAULTS__MODEL
|
||
# - env_nested_delimiter="__":双下划线用于拆分嵌套层级
|
||
model_config = ConfigDict(env_prefix="NANOBOT_", env_nested_delimiter="__")
|