ForcePilot/backend/package/yuxi/channels/adapters/synologychat/config_schema.py
Kris 1f78c44b03 refactor: 整理并清理项目中的冗余代码与格式问题
这是一个批量整理提交,包含以下主要改动:
1.  删除多处冗余的空行和未使用的导入
2.  修复文件末尾缺少换行符的问题
3.  调整部分模块的导入顺序与代码排版
4.  修复部分配置默认值与策略逻辑
5.  新增多个功能模块与辅助工具
6.  完善异常处理与日志记录
7.  修复速率限制、消息缓存、权限校验等逻辑bug
8.  废弃部分旧有API与配置项并添加警告提示
2026-05-12 14:51:53 +08:00

224 lines
8.9 KiB
Python

"""Configuration schema validation for Synology Chat channel.
Provides Pydantic-based configuration validation for the Synology Chat adapter,
ensuring required fields are present and types are correct before connection.
"""
from __future__ import annotations
from typing import Any
from pydantic import BaseModel, Field, model_validator
class SynologyChatSecurityConfig(BaseModel):
dm_policy: str = Field(default="open", pattern=r"^(open|allowlist|pairing|disabled)$")
group_policy: str = Field(default="allowlist", pattern=r"^(open|allowlist|disabled)$")
allow_from: list[str] = Field(default_factory=list)
group_allow_from: list[str] = Field(default_factory=list)
approvers: list[str] = Field(default_factory=list)
class SynologyChatRetryConfig(BaseModel):
attempts: int = Field(default=3, ge=1, le=10)
min_delay_ms: int = Field(default=1000, ge=100)
max_delay_ms: int = Field(default=30000, ge=1000)
jitter: float = Field(default=0.1, ge=0.0, le=1.0)
class SynologyChatRateLimitConfig(BaseModel):
max_per_minute: int = Field(default=30, ge=1, le=120)
window_seconds: int = Field(default=60, ge=10, le=300)
class SynologyChatConfig(BaseModel):
dsm_url: str = Field(
default="",
description="Synology NAS URL (e.g. https://192.168.1.100:5001)",
json_schema_extra={"label": "DSM URL", "category": "connection"},
)
username: str = Field(
default="",
description="DSM account username for Chat integration",
json_schema_extra={"label": "DSM Username", "category": "connection"},
)
password: str = Field(
default="",
description="DSM account password",
json_schema_extra={"label": "DSM Password", "category": "credentials", "secret": True},
)
password_file: str = Field(
default="",
description="Path to file containing DSM password (Docker secrets compatible)",
json_schema_extra={"label": "Password File Path", "category": "credentials"},
)
verify_ssl: bool = Field(
default=True,
description="Verify SSL certificates (disable for self-signed NAS certs)",
json_schema_extra={"label": "Verify SSL", "category": "security"},
)
session_name: str = Field(
default="Chat",
description="DSM session name for the Chat connection",
json_schema_extra={"label": "Session Name", "category": "connection"},
)
enable_syno_token: bool = Field(
default=False,
description="Enable SynoToken for CSRF protection (DSM 6+)",
json_schema_extra={"label": "Enable SynoToken", "category": "security"},
)
name: str = Field(
default="synologychat",
description="Channel instance name",
json_schema_extra={"label": "Channel Name", "category": "general"},
)
default_agent_id: str = Field(
default="default",
description="Default agent ID for routing incoming messages",
json_schema_extra={"label": "Default Agent ID", "category": "routing"},
)
trigger_word: str = Field(
default="",
description="Optional prefix that triggers bot responses (e.g. '/bot')",
json_schema_extra={"label": "Trigger Word", "category": "behavior"},
)
request_timeout_seconds: float = Field(
default=30.0,
description="HTTP request timeout in seconds",
json_schema_extra={"label": "Request Timeout (s)", "category": "advanced"},
)
polling_interval_seconds: int = Field(
default=3,
ge=1,
le=60,
description="Polling interval in seconds for message retrieval",
json_schema_extra={"label": "Polling Interval (s)", "category": "polling"},
)
polling_max_backoff_seconds: int = Field(
default=60,
ge=10,
le=300,
description="Maximum backoff seconds when polling encounters errors",
json_schema_extra={"label": "Max Polling Backoff (s)", "category": "polling"},
)
text_chunk_limit: int = Field(
default=4000,
ge=100,
le=4000,
description="Maximum characters per message (Synology Chat limit: 4000)",
json_schema_extra={"label": "Text Chunk Limit", "category": "messaging"},
)
min_send_interval_ms: int = Field(
default=500,
ge=100,
le=5000,
description="Minimum interval between consecutive sends (ms)",
json_schema_extra={"label": "Min Send Interval (ms)", "category": "messaging"},
)
reply_to_mode: str = Field(
default="first",
pattern=r"^(off|first)$",
description="Reply quoting mode: 'off' or 'first'",
json_schema_extra={"label": "Reply-to Mode", "category": "messaging"},
)
security: SynologyChatSecurityConfig = Field(
default_factory=SynologyChatSecurityConfig,
description="Access control and security policy configuration",
json_schema_extra={"label": "Security Policy", "category": "security"},
)
retry: SynologyChatRetryConfig = Field(
default_factory=SynologyChatRetryConfig,
description="Send retry configuration",
json_schema_extra={"label": "Retry Policy", "category": "advanced"},
)
rate_limit: SynologyChatRateLimitConfig = Field(
default_factory=SynologyChatRateLimitConfig,
description="Per-user rate limiting configuration",
json_schema_extra={"label": "Rate Limit", "category": "security"},
)
user_list_cache_ttl_seconds: int = Field(
default=300,
ge=30,
le=3600,
description="User list cache TTL in seconds",
json_schema_extra={"label": "User List Cache TTL (s)", "category": "performance"},
)
agent_timeout_seconds: int = Field(
default=120,
ge=30,
le=600,
description="Agent response timeout in seconds",
json_schema_extra={"label": "Agent Timeout (s)", "category": "behavior"},
)
enable_experimental_message_actions: bool = Field(
default=False,
description="Enable experimental edit/delete/reaction API calls (not officially supported)",
json_schema_extra={"label": "Experimental Actions", "category": "advanced", "experimental": True},
)
send_mode: str = Field(
default="dsm_api",
pattern=r"^(dsm_api|webhook)$",
description="Outbound send mode: 'dsm_api' (direct) or 'webhook' (via incoming webhook)",
json_schema_extra={"label": "Send Mode", "category": "connection"},
)
connect_mode: str = Field(
default="polling",
pattern=r"^(polling|webhook)$",
description="Connection mode: 'polling' (DSM API) or 'webhook' (HTTP endpoint)",
json_schema_extra={"label": "Connect Mode", "category": "connection"},
)
webhook_path: str = Field(
default="/webhook/synology",
description="HTTP path for incoming webhook events",
json_schema_extra={"label": "Webhook Path", "category": "connection"},
)
webhook_token: str = Field(
default="",
description="Token for validating incoming webhook requests",
json_schema_extra={"label": "Webhook Token", "category": "credentials", "secret": True},
)
incoming_webhook_url: str = Field(
default="",
description="Incoming webhook URL for sending messages via webhook mode",
json_schema_extra={"label": "Incoming Webhook URL", "category": "connection"},
)
dangerously_allow_name_matching: bool = Field(
default=False,
description="Allow matching users by username across dual ID spaces (webhook vs Chat API). "
"Enabling this bypasses strict ID-based security and may allow impersonation.",
json_schema_extra={"label": "Dangerous: Allow Name Matching", "category": "security", "dangerous": True},
)
dangerously_allow_inherited_webhook_path: bool = Field(
default=False,
description="Allow named accounts to inherit the default account's webhook_path. "
"Disable to require explicit webhook_path for each named account.",
json_schema_extra={
"label": "Dangerous: Allow Inherited Webhook Path",
"category": "security",
"dangerous": True,
},
)
@model_validator(mode="after")
def check_credentials(self) -> SynologyChatConfig:
if self.connect_mode == "webhook":
return self
if not self.password and not self.password_file:
raise ValueError("Either password or password_file must be configured for polling mode")
return self
@model_validator(mode="after")
def check_dsm_url(self) -> SynologyChatConfig:
if self.dsm_url and not (self.dsm_url.startswith("http://") or self.dsm_url.startswith("https://")):
raise ValueError("dsm_url must start with http:// or https://")
return self
def validate_config(config: dict[str, Any]) -> tuple[bool, list[str]]:
try:
SynologyChatConfig(**config)
return True, []
except Exception as e:
return False, [str(e)]