ForcePilot/backend/package/yuxi/channels/adapters/nextcloudtalk/setup_wizard.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

147 lines
5.2 KiB
Python

from __future__ import annotations
import logging
from typing import Any
logger = logging.getLogger(__name__)
WIZARD_STEPS = [
{
"step": 0,
"title": "Bot Installation Guide",
"description": (
"Before configuring the adapter, install a Talk bot on your Nextcloud server.\n\n"
"Run this command as an administrator on your Nextcloud server:\n\n"
"```bash\n"
"occ talk:bot:install <bot_name> <bot_secret> <bot_url>\n"
"```\n\n"
"Example:\n```bash\n"
'occ talk:bot:install "ForcePilot Bot" my_secret_key https://your-server.example.com\n'
"```\n\n"
"After installation, use the bot username and the secret as app_password in Step 2."
),
"field": None,
"required": False,
},
{
"step": 1,
"title": "Nextcloud Instance URL",
"description": "Enter the base URL of your Nextcloud server (e.g., https://cloud.example.com).",
"field": "server_url",
"required": True,
},
{
"step": 2,
"title": "Bot Credentials",
"description": (
"Enter the bot username and application password. "
"Create an app password in Nextcloud at Settings > Security.\n\n"
"Environment variables (alternative to config):\n"
"- `NEXTCLOUD_TALK_BOT_USER` for bot_user\n"
"- `NEXTCLOUD_TALK_APP_PASSWORD` or `NEXTCLOUD_TALK_API_PASSWORD` for app_password"
),
"fields": ["bot_user", "app_password"],
"required": True,
"preferredEnvVar": {
"app_password": "NEXTCLOUD_TALK_API_PASSWORD",
},
},
{
"step": 3,
"title": "API Credentials (Optional)",
"description": "Optionally provide separate API credentials for room/peer queries. Leave blank to reuse bot credentials.",
"fields": ["api_user", "api_password"],
"required": False,
},
{
"step": 4,
"title": "DM Policy",
"description": "Choose how the bot handles direct messages: open (anyone), pairing (require admin approval), allowlist (specified users only), or disabled.",
"field": "dm_policy",
"required": True,
"options": [
{"value": "open", "label": "Open - anyone can message the bot"},
{"value": "pairing", "label": "Pairing - users must be approved by admin"},
{"value": "allowlist", "label": "Allowlist - only specified users"},
{"value": "disabled", "label": "Disabled - no DMs allowed"},
],
},
{
"step": 5,
"title": "Allow From",
"description": "Configure allowed user IDs and room tokens. Format: nc:{user_id} for users.",
"field": "allow_from",
"required": False,
},
]
def generate_wizard_steps() -> list[dict[str, Any]]:
return WIZARD_STEPS
def validate_step(step_num: int, data: dict[str, Any]) -> dict[str, Any]:
step = next((s for s in WIZARD_STEPS if s["step"] == step_num), None)
if not step:
return {"valid": False, "error": f"Unknown step: {step_num}"}
errors: list[str] = []
if step_num == 1:
server_url = data.get("server_url", "").strip()
if step["required"] and not server_url:
errors.append("Server URL is required")
elif server_url and not (server_url.startswith("https://") or server_url.startswith("http://")):
errors.append("Server URL must start with https:// or http://")
elif step_num == 2:
bot_user = data.get("bot_user", "").strip()
app_password = data.get("app_password", "").strip()
if not bot_user:
errors.append("Bot username is required")
if not app_password:
errors.append("App password is required")
elif step_num == 3:
pass
elif step_num == 4:
dm_policy = data.get("dm_policy", "").strip()
valid_policies = {"open", "pairing", "allowlist", "disabled"}
if dm_policy and dm_policy not in valid_policies:
errors.append(f"DM policy must be one of: {', '.join(sorted(valid_policies))}")
elif step_num == 5:
allow_from = data.get("allow_from", [])
if isinstance(allow_from, list):
for entry in allow_from:
if not isinstance(entry, str) or not entry.startswith("nc:"):
errors.append(f"Each allow_from entry must start with 'nc:', got: {entry}")
if errors:
return {"valid": False, "errors": errors}
return {"valid": True}
def build_config_from_wizard(wizard_data: dict[str, Any]) -> dict[str, Any]:
config: dict[str, Any] = {
"server_url": wizard_data.get("server_url", "").rstrip("/"),
"bot_user": wizard_data.get("bot_user", ""),
"app_password": wizard_data.get("app_password", ""),
"dm_policy": wizard_data.get("dm_policy", "pairing"),
"try_websocket": True,
}
api_user = wizard_data.get("api_user", "")
if api_user:
config["api_user"] = api_user
api_password = wizard_data.get("api_password", "")
if api_password:
config["api_password"] = api_password
allow_from = wizard_data.get("allow_from", [])
if allow_from:
config["allow_from"] = allow_from
return config