ForcePilot/backend/package/yuxi/channels/adapters/irc/config_schema.py
Kris 2dd1a075f2 feat(irc): 实现完整的IRC适配器基础功能模块
新增IRC协议相关的全套工具模块,包括:
- 核心协议解析与CTCP处理
- 消息发送缓存与文本 sanitize
- 账号配置管理与运行时状态
- 命令处理与权限控制
- 服务发现与诊断工具
- 多账号网关与配置加载
2026-05-12 00:45:10 +08:00

334 lines
9.8 KiB
Python

from __future__ import annotations
from typing import Any
_CONFIG_SCHEMA: dict[str, dict[str, Any]] = {
"server": {
"type": "str",
"required": True,
"default": "irc.libera.chat",
"description": "IRC server hostname",
"env": "IRC_HOST",
},
"port": {
"type": "int",
"required": False,
"default": 6697,
"description": "IRC server port",
"env": "IRC_PORT",
"min": 1,
"max": 65535,
},
"use_tls": {
"type": "bool",
"required": False,
"default": True,
"description": "Enable TLS encryption",
"env": "IRC_TLS",
},
"nick": {
"type": "str",
"required": True,
"default": "ForcePilotBot",
"description": "IRC nickname",
"env": "IRC_NICK",
},
"username": {
"type": "str",
"required": False,
"description": "IRC username (defaults to nick)",
"env": "IRC_USERNAME",
},
"realname": {
"type": "str",
"required": False,
"default": "ForcePilot IRC Bot",
"description": "IRC real name / GECOS",
"env": "IRC_REALNAME",
},
"password": {
"type": "str",
"required": False,
"default": "",
"description": "IRC server connection password",
"env": "IRC_PASSWORD",
"sensitive": True,
},
"sasl_username": {
"type": "str",
"required": False,
"default": "",
"description": "SASL authentication username",
"env": "IRC_SASL_USERNAME",
},
"sasl_password": {
"type": "str",
"required": False,
"default": "",
"description": "SASL authentication password",
"env": "IRC_SASL_PASSWORD",
"sensitive": True,
},
"use_sasl": {
"type": "bool",
"required": False,
"default": True,
"description": "Enable SASL authentication when credentials available",
},
"sasl_mechanism": {
"type": "str",
"required": False,
"default": "PLAIN",
"description": "SASL mechanism (PLAIN, EXTERNAL, SCRAM-SHA-256)",
"choices": ["PLAIN", "EXTERNAL", "SCRAM-SHA-256"],
},
"nickserv_password": {
"type": "str",
"required": False,
"default": "",
"description": "NickServ password",
"env": "IRC_NICKSERV_PASSWORD",
"sensitive": True,
},
"nickserv_password_file": {
"type": "str",
"required": False,
"default": "",
"description": "Path to NickServ password file",
"env": "IRC_NICKSERV_PASSWORD_FILE",
},
"nickserv_register": {
"type": "bool",
"required": False,
"default": False,
"description": "Attempt NickServ REGISTER on connect",
},
"nickserv_register_email": {
"type": "str",
"required": False,
"default": "",
"description": "Email for NickServ REGISTER",
"env": "IRC_NICKSERV_REGISTER_EMAIL",
},
"nickserv_service": {
"type": "str",
"required": False,
"default": "NickServ",
"description": "NickServ service name",
"env": "IRC_NICKSERV_SERVICE",
},
"auto_join_channels": {
"type": "list[str]",
"required": False,
"default": [],
"description": "Channels to auto-join on connect",
"env": "IRC_CHANNELS",
},
"nick_recovery": {
"type": "bool",
"required": False,
"default": True,
"description": "Attempt ghost recovery on SASL login",
},
"dm_policy": {
"type": "str",
"required": False,
"default": "pairing",
"description": "DM policy: open, pairing, or disabled",
"choices": ["open", "pairing", "disabled"],
"env": "IRC_DM_POLICY",
},
"group_policy": {
"type": "str",
"required": False,
"default": "allowlist",
"description": "Group policy: open, allowlist, or disabled",
"choices": ["open", "allowlist", "disabled"],
"env": "IRC_GROUP_POLICY",
},
"allowFrom": {
"type": "list[str]",
"required": False,
"default": [],
"description": "DM allowlist entries (nick!user@host format)",
},
"groupAllowFrom": {
"type": "list[str]",
"required": False,
"default": [],
"description": "Group allowlist entries (nick!user@host format)",
},
"connect_timeout": {
"type": "float",
"required": False,
"default": 30.0,
"description": "Connection timeout in seconds",
},
"probe_timeout": {
"type": "float",
"required": False,
"default": 8.0,
"description": "Health probe timeout in seconds",
},
"chunker_mode": {
"type": "str",
"required": False,
"default": "length",
"description": "Message splitting strategy: length or markdown",
"choices": ["length", "markdown"],
},
"text_chunk_limit": {
"type": "int",
"required": False,
"default": 512,
"description": "Maximum bytes per IRC message chunk",
},
"reply_enabled": {
"type": "bool",
"required": False,
"default": False,
"description": "Enable reply-to prefix [reply:id]",
},
"blockStreaming": {
"type": "bool",
"required": False,
"default": True,
"description": "Enable block streaming mode",
},
"blockStreamingCoalesce": {
"type": "dict",
"required": False,
"default": {},
"description": "Block streaming coalesce config (delay_ms)",
},
"markdown.tableMode": {
"type": "str",
"required": False,
"default": "off",
"description": "Markdown table conversion mode: off, plain, list",
"choices": ["off", "plain", "list"],
},
"dangerouslyAllowNameMatching": {
"type": "bool",
"required": False,
"default": False,
"description": "Allow name-only matching in allowlists (less secure)",
},
"groupAllowFromFallbackToAllowFrom": {
"type": "bool",
"required": False,
"default": False,
"description": "Fall back to allowFrom when groupAllowFrom is empty",
},
"knownServer": {
"type": "str",
"required": False,
"description": "Use a known server from the built-in server list",
},
"dnsSrv": {
"type": "bool",
"required": False,
"default": False,
"description": "Use DNS SRV records for server discovery",
},
"mentionPatterns": {
"type": "list[str]",
"required": False,
"default": [],
"description": "Custom mention detection patterns",
},
"commands.enabled": {
"type": "bool",
"required": False,
"default": False,
"description": "Enable built-in text commands (!help, !ping, etc.)",
},
"server_password": {
"type": "str",
"required": False,
"default": "",
"description": "IRC server password",
"env": "IRC_SERVER_PASSWORD",
"sensitive": True,
},
"server_password_file": {
"type": "str",
"required": False,
"default": "",
"description": "Path to IRC server password file",
"env": "IRC_SERVER_PASSWORD_FILE",
},
"accounts": {
"type": "dict",
"required": False,
"default": {},
"description": "Multi-account configuration",
},
"dedup.max_entries": {
"type": "int",
"required": False,
"default": 1000,
"description": "Maximum deduplication entries",
},
}
def get_config_schema() -> dict[str, dict[str, Any]]:
return dict(_CONFIG_SCHEMA)
def get_schema_keys() -> list[str]:
return list(_CONFIG_SCHEMA.keys())
def validate_config(config: dict[str, Any]) -> list[str]:
errors: list[str] = []
schema = _CONFIG_SCHEMA
for key, spec in schema.items():
if "." in key:
top_key, sub_key = key.split(".", 1)
top_val = config.get(top_key, {})
if isinstance(top_val, dict):
value = top_val.get(sub_key)
else:
value = None
else:
value = config.get(key)
if spec.get("required") and (value is None or value == ""):
errors.append(f"{key}: {spec.get('description', 'required field')} is required")
if value is not None and value != "" and "choices" in spec:
if value not in spec["choices"]:
errors.append(f"{key}: must be one of {spec['choices']}, got '{value}'")
if value is not None and spec.get("type") == "int":
try:
int_val = int(value)
if "min" in spec and int_val < spec["min"]:
errors.append(f"{key}: must be >= {spec['min']}, got {int_val}")
if "max" in spec and int_val > spec["max"]:
errors.append(f"{key}: must be <= {spec['max']}, got {int_val}")
except (ValueError, TypeError):
errors.append(f"{key}: expected int, got {type(value).__name__}")
_add_cross_field_errors(config, errors)
return errors
def _add_cross_field_errors(config: dict[str, Any], errors: list[str]) -> None:
nickserv_register = config.get("nickserv_register", False)
if nickserv_register:
email = config.get("nickserv_register_email", "")
if not email:
errors.append("nickserv_register: NickServ REGISTER requires nickserv_register_email")
dm_policy = config.get("dm_policy", "pairing")
if dm_policy == "open":
allow_from = config.get("allowFrom", [])
if isinstance(allow_from, list) and "*" not in allow_from:
errors.append("dm_policy: dm_policy=open requires allowFrom to include '*'")