Files
beaver_project/app-instance/backend/nanobot/config/schema.py
2026-03-13 16:40:08 +08:00

539 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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 | Noneprovider 的 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="__")