ForcePilot/backend/package/yuxi/channels/adapters/nextcloudtalk/config_schema.py
Kris b018ad25da feat(backend): add Nextcloud Talk channel adapter implementation
实现了完整的Nextcloud Talk聊天适配器,包含以下核心功能:
1. 基础客户端通信与认证
2. 会话路由与聊天类型解析
3. 消息分块与格式转换
4. 用户配对与权限校验
5. 轮询式消息监听
6. 配置验证与诊断工具
7. 安全策略与去重缓存
8. 指标统计与状态快照

新增配套的配对用户缓存文件与模块导出入口。
2026-05-12 00:47:06 +08:00

189 lines
8.1 KiB
Python

from __future__ import annotations
from typing import Any
from pydantic import BaseModel, Field, model_validator
class NextcloudTalkRoomConfig(BaseModel):
enabled: bool = Field(default=True, description="Enable bot responses in this room")
system_prompt: str | None = Field(
default=None, alias="systemPrompt", description="Override system prompt for this room"
)
require_mention: bool = Field(
default=True, alias="requireMention", description="Only respond when bot is @mentioned"
)
allow_from: list[str] = Field(
default_factory=list, alias="allowFrom", description="Allowlist of user IDs for this room"
)
skills: list[str] = Field(default_factory=list, description="Skills filter for this room")
history_limit: int = Field(
default=0, alias="historyLimit", description="Max history messages to load (0=unlimited)"
)
model_config = {"populate_by_name": True, "extra": "allow"}
class NextcloudTalkDMConfig(BaseModel):
enabled: bool = Field(default=True, description="Enable bot responses in this DM")
system_prompt: str | None = Field(
default=None, alias="systemPrompt", description="Override system prompt for this DM"
)
allow_from: list[str] = Field(
default_factory=list, alias="allowFrom", description="Allowlist of user IDs for this DM"
)
skills: list[str] = Field(default_factory=list, description="Skills filter for this DM")
history_limit: int = Field(
default=0, alias="historyLimit", description="Max history messages to load (0=unlimited)"
)
model_config = {"populate_by_name": True, "extra": "allow"}
class NextcloudTalkWebhookConfig(BaseModel):
enabled: bool = Field(default=False, description="Enable standalone webhook server")
port: int = Field(default=8788, description="Webhook server listen port")
host: str = Field(default="0.0.0.0", description="Webhook server bind address")
path: str = Field(default="/nextcloud-talk-webhook", description="Webhook endpoint path")
rate_limit_max_requests: int = Field(
default=10,
alias="rateLimitMaxRequests",
description="Max failed auth attempts in window",
)
rate_limit_window_s: int = Field(
default=60,
alias="rateLimitWindowS",
description="Rate limiting window in seconds",
)
rate_limit_lockout_s: int = Field(
default=300,
alias="rateLimitLockoutS",
description="Lockout duration after exceeding rate limit",
)
max_body_size: int = Field(
default=1 * 1024 * 1024,
alias="maxBodySize",
description="Maximum webhook request body size in bytes (default: 1MB)",
)
pre_auth_max_body_size: int = Field(
default=64 * 1024,
alias="preAuthMaxBodySize",
description="Maximum body size before HMAC auth verification (default: 64KB)",
)
model_config = {"populate_by_name": True, "extra": "allow"}
class NextcloudTalkAccountConfig(BaseModel):
server_url: str = Field(default="", alias="serverUrl", description="Nextcloud server base URL (https://...)")
bot_user: str = Field(default="", alias="botUser", description="Bot username for authentication")
app_password: str = Field(
default="",
alias="appPassword",
description="App password or device-specific password",
)
display_name: str = Field(
default="ForcePilot Bot",
alias="displayName",
description="Bot display name shown in chats",
)
dm_policy: str = Field(
default="pairing",
alias="dmPolicy",
description="DM policy: open|pairing|allowlist|disabled",
)
group_policy: str = Field(
default="allowlist",
alias="groupPolicy",
description="Group chat policy: open|allowlist|disabled",
)
try_websocket: bool = Field(default=True, alias="tryWebsocket", description="Attempt WebSocket monitor mode")
reaction_level: str = Field(
default="minimal",
alias="reactionLevel",
description="Reaction verbosity: minimal|normal|verbose",
)
poll_interval: float = Field(default=0.5, description="Polling interval in seconds")
poll_timeout: int = Field(default=30, description="Polling long-poll timeout in seconds")
allow_from: list[str] = Field(default_factory=list, alias="allowFrom", description="Global allowlist of user IDs")
group_allow_from: list[str] = Field(
default_factory=list, alias="groupAllowFrom", description="Global group allowlist"
)
rooms: dict[str, NextcloudTalkRoomConfig] = Field(
default_factory=dict,
description="Per-room configuration overrides, keyed by room token",
)
dms: dict[str, NextcloudTalkDMConfig] = Field(
default_factory=dict,
description="Per-user DM config overrides, keyed by user ID",
)
groups: dict[str, Any] = Field(default_factory=dict, description="Group policy details")
accounts: dict[str, Any] = Field(default_factory=dict, alias="accounts", description="Multi-account configurations")
retry: dict[str, int] = Field(
default_factory=lambda: {"attempts": 3, "min_delay_ms": 400, "max_delay_ms": 30000},
description="Send retry policy: attempts, min/max delay",
)
history_limit: int = Field(default=0, alias="historyLimit", description="Global history limit for group chats")
dm_history_limit: int = Field(default=0, alias="dmHistoryLimit", description="Global history limit for DMs")
markdown: dict[str, Any] = Field(default_factory=dict, description="Markdown rendering options")
block_streaming_coalesce: dict[str, Any] = Field(
default_factory=dict,
alias="blockStreamingCoalesce",
description="Block streaming coalesce config (min_chars, max_delay_ms)",
)
response_prefix: str = Field(
default="",
alias="responsePrefix",
description="Text prepended to every bot response",
)
silent: bool = Field(default=False, description="Send messages with silent flag")
chunk_mode: str = Field(
default="split",
alias="chunkMode",
description="Chunking strategy: split|stop|truncate",
)
media_max_mb: int = Field(default=20, alias="mediaMaxMb", description="Max media file size in MB")
webhook: NextcloudTalkWebhookConfig = Field(
default_factory=NextcloudTalkWebhookConfig,
description="Standalone webhook server configuration",
)
dangerously_allow_private_network: bool = Field(
default=False,
alias="dangerouslyAllowPrivateNetwork",
description="Allow connections to private/reserved IPs (SSRF risk)",
)
network: dict[str, Any] = Field(default_factory=dict, description="Network config: proxy, etc.")
model_config = {"populate_by_name": True, "extra": "allow"}
@model_validator(mode="after")
def validate_server_url(self) -> NextcloudTalkAccountConfig:
if self.server_url and not (self.server_url.startswith("https://") or self.server_url.startswith("http://")):
raise ValueError(f"server_url must start with https:// or http://, got: {self.server_url}")
return self
@model_validator(mode="after")
def validate_dm_policy(self) -> NextcloudTalkAccountConfig:
valid = {"open", "pairing", "allowlist", "disabled"}
if self.dm_policy not in valid:
raise ValueError(f"dm_policy must be one of {sorted(valid)}, got: {self.dm_policy}")
return self
@model_validator(mode="after")
def validate_group_policy(self) -> NextcloudTalkAccountConfig:
valid = {"open", "allowlist", "disabled"}
if self.group_policy not in valid:
raise ValueError(f"group_policy must be one of {sorted(valid)}, got: {self.group_policy}")
return self
@model_validator(mode="after")
def validate_chunk_mode(self) -> NextcloudTalkAccountConfig:
valid = {"split", "stop", "truncate"}
if self.chunk_mode not in valid:
raise ValueError(f"chunk_mode must be one of {sorted(valid)}, got: {self.chunk_mode}")
return self
def validate_config(config: dict[str, Any]) -> NextcloudTalkAccountConfig:
return NextcloudTalkAccountConfig.model_validate(config)