ForcePilot/backend/package/yuxi/channels/adapters/synologychat/config_schema.py
Kris 69f4319023 feat(synologychat): 完成Synology Chat适配器的多维度优化
此提交对Synology Chat适配器进行了全面改进:
1. 新增消息去重、分布式轮询租约、bot名称配置等功能
2. 优化URL提取逻辑,自动清理尾部标点符号
3. 重构发送逻辑,提取通用重试工具函数并优化SID缓存
4. 完善文档提示与配置项,新增轮询租约类型支持
5. 修复认证API路径硬编码问题,调整交互组件提示文案
6. 增加Webhook模式下的DSM客户端兜底初始化
7. 优化导入顺序与代码结构,清理冗余空行
2026-05-13 16:15:22 +08:00

235 lines
9.4 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"},
)
bot_name: str = Field(
default="",
description="Bot display name in Synology Chat (used for @mention detection)",
json_schema_extra={"label": "Bot 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"},
)
polling_lease_type: str = Field(
default="memory",
pattern=r"^(memory|redis)$",
description="Polling lease type for distributed coordination: 'memory' or 'redis'",
json_schema_extra={"label": "Polling Lease Type", "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)]