ForcePilot/backend/package/yuxi/channels/adapters/synologychat/setup_wizard.py
Kris 3ad218e537 feat(synologychat): 新增群晖Chat适配器完整实现
新增群晖Chat渠道适配器的全套实现,包括:
1. 基础适配器与导出接口定义
2. DSM API认证、探测与会话管理
3. 轮询与Webhook两种消息接收方式
4. 消息去重、格式化与规范化处理
5. 多账号支持与权限安全策略
6. 目录用户/群组发现功能
7. 审批配对与流量控制机制
8. 安全审计与配置检查功能
2026-05-12 00:49:16 +08:00

212 lines
8.3 KiB
Python

"""Interactive setup wizard for Synology Chat channel configuration.
Guides users through DSM URL, username, password, webhook path, and
security policy configuration via CLI prompts.
Supports dual-path configuration: default account uses top-level config keys,
named accounts use accounts.<account_id>.* nested keys.
For named accounts:
- Environment variable credential injection is NOT supported (only explicit config)
- Adding users to allow_from automatically switches dm_policy to 'allowlist'
"""
from __future__ import annotations
from typing import Any
DEFAULT_ACCOUNT_ID = "default"
def patch_config(
updates: dict[str, Any],
existing_config: dict[str, Any] | None = None,
account_id: str = DEFAULT_ACCOUNT_ID,
) -> dict[str, Any]:
"""Apply user-provided updates to the channel configuration.
When an allowFrom value is provided, automatically switches dm_policy
to 'allowlist' to ensure the allowlist is actually enforced.
"""
config = dict(existing_config or {})
if account_id != DEFAULT_ACCOUNT_ID:
target = config.setdefault("accounts", {}).setdefault(account_id, {})
path_prefix = f"accounts.{account_id}."
else:
target = config
path_prefix = ""
for key, value in updates.items():
if key.startswith(path_prefix):
clean_key = key[len(path_prefix) :]
else:
clean_key = key
if "." in clean_key:
parts = clean_key.split(".")
nested = target
for part in parts[:-1]:
nested = nested.setdefault(part, {})
nested[parts[-1]] = value
else:
target[clean_key] = value
allow_from_key = f"{path_prefix}security.allow_from"
allow_from_val = updates.get(allow_from_key, "")
if allow_from_val:
allow_from_list = (
[v.strip() for v in allow_from_val.split(",") if v.strip()]
if isinstance(allow_from_val, str)
else allow_from_val
)
if allow_from_list:
sec = target.setdefault("security", {})
sec["allow_from"] = allow_from_list
if sec.get("dm_policy", "open") == "open":
sec["dm_policy"] = "allowlist"
return config
def build_setup_prompt(
existing_config: dict[str, Any] | None = None,
account_id: str = DEFAULT_ACCOUNT_ID,
) -> dict[str, Any]:
"""Return the setup wizard configuration including field descriptions,
defaults from existing config, and validation rules.
Args:
existing_config: Current channel configuration.
account_id: Account ID for multi-account setups. 'default' uses
top-level config keys; named accounts use nested paths.
"""
config = existing_config or {}
if account_id != DEFAULT_ACCOUNT_ID:
account_cfg = config.get("accounts", {}).get(account_id, {}) if isinstance(config.get("accounts"), dict) else {}
path_prefix = f"accounts.{account_id}."
default_allow_env = False
else:
account_cfg = config
path_prefix = ""
default_allow_env = True
return {
"channel": "synologychat",
"label": "Synology Chat",
"description": (
"Connect to Synology NAS DSM Chat via polling-based API integration."
if account_id == DEFAULT_ACCOUNT_ID
else f"Configure named account '{account_id}' for Synology Chat."
),
"account_id": account_id,
"path_prefix": path_prefix,
"is_named_account": account_id != DEFAULT_ACCOUNT_ID,
"steps": [
{
"key": f"{path_prefix}dsm_url",
"label": "DSM URL",
"description": "Synology NAS URL (e.g. https://192.168.1.100:5001)",
"type": "string",
"required": True,
"default": account_cfg.get("dsm_url", ""),
"env": "DSM_URL" if default_allow_env else None,
"validate": {"pattern": r"^https?://", "message": "Must start with http:// or https://"},
},
{
"key": f"{path_prefix}username",
"label": "DSM Username",
"description": "DSM account username for Chat integration",
"type": "string",
"required": True,
"default": account_cfg.get("username", ""),
"env": "DSM_USERNAME" if default_allow_env else None,
},
{
"key": f"{path_prefix}password",
"label": "DSM Password",
"description": (
"DSM account password" + (" (use password_file for Docker secrets)" if default_allow_env else "")
),
"type": "password",
"required": False,
"default": "",
"env": "DSM_PASSWORD" if default_allow_env else None,
},
{
"key": f"{path_prefix}password_file",
"label": "Password File Path",
"description": "Path to file containing DSM password (Docker secrets compatible)",
"type": "string",
"required": False,
"default": account_cfg.get("password_file", ""),
"env": "DSM_PASSWORD_FILE" if default_allow_env else None,
},
{
"key": f"{path_prefix}verify_ssl",
"label": "Verify SSL",
"description": "Verify SSL certificates (disable for self-signed NAS certs)",
"type": "boolean",
"required": False,
"default": account_cfg.get("verify_ssl", True),
},
{
"key": f"{path_prefix}trigger_word",
"label": "Trigger Word",
"description": "Optional prefix that triggers bot responses (e.g. '/bot')",
"type": "string",
"required": False,
"default": account_cfg.get("trigger_word", ""),
},
],
"security_steps": [
{
"key": f"{path_prefix}security.dm_policy",
"label": "DM Policy",
"description": "Direct message access policy",
"type": "select",
"options": ["open", "allowlist", "pairing", "disabled"],
"default": account_cfg.get("security", {}).get("dm_policy", "open"),
},
{
"key": f"{path_prefix}security.group_policy",
"label": "Group Policy",
"description": "Group message access policy",
"type": "select",
"options": ["open", "allowlist", "disabled"],
"default": account_cfg.get("security", {}).get("group_policy", "allowlist"),
},
{
"key": f"{path_prefix}security.allow_from",
"label": "Allow From (User IDs)",
"description": "Comma-separated user IDs or '*' for all users. "
"Adding users will automatically switch DM policy to 'allowlist'.",
"type": "string",
"required": False,
"default": ",".join(account_cfg.get("security", {}).get("allow_from", [])),
"env": "DSM_ALLOW_FROM" if default_allow_env else None,
"force_dm_allowlist": True,
},
],
}
def get_setup_instructions(account_id: str = DEFAULT_ACCOUNT_ID) -> str:
prefix = f"[Named Account: {account_id}] " if account_id != DEFAULT_ACCOUNT_ID else ""
return (
f"{prefix}Synology Chat Setup Guide:\n"
"1. Install 'Synology Chat' package on your DSM via Package Center\n"
"2. Go to Chat > Integration > Bots and create a new Bot\n"
"3. Copy the Bot's credentials (DSM URL, username, password)\n"
"4. Configure the adapter with DSM URL, username, and password\n"
"5. Optionally set dm_policy to 'allowlist' or 'pairing' for access control\n"
"Note: This adapter uses DSM API Polling mode, no incoming/outgoing webhook URLs needed."
+ (
"\nEnvironment variables (DSM_URL, DSM_USERNAME, etc.) are only available "
"for the default account. Named accounts must use explicit configuration."
if account_id != DEFAULT_ACCOUNT_ID
else ""
)
)