新增 Slack 渠道扩展,支持在 Yuxi 平台中集成 Slack 团队协作平台。 包含以下功能模块: - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - outbound: 外发消息管理 - streaming: 流式消息处理 - pairing: 用户配对与绑定 - security: 安全校验 - monitor: 渠道状态监控 - status: 会话状态管理 - actions: 交互动作处理 - interactive: 交互式消息 - commands: 斜杠指令 - threading: 线程管理 - mentions: @提及 - constants: 常量定义 - types: 类型定义
226 lines
8.0 KiB
Python
226 lines
8.0 KiB
Python
import time
|
|
import logging
|
|
|
|
from yuxi.channel.extensions.slack.types import SlackProbeResult
|
|
from yuxi.channel.extensions.slack.config import SlackConfigAdapter
|
|
from yuxi.channel.protocols import ChannelAccountSnapshot, ChannelStatusIssue
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class SlackStatus:
|
|
default_runtime: ChannelAccountSnapshot | None = None
|
|
|
|
async def probe(self, account: dict | None = None) -> bool:
|
|
if not account:
|
|
return False
|
|
bot_token = account.get("bot_token", "")
|
|
if not bot_token:
|
|
return False
|
|
probe_result = await self._probe_slack(bot_token)
|
|
return probe_result.ok
|
|
|
|
async def _probe_slack(self, token: str, timeout_ms: int = 5000) -> SlackProbeResult:
|
|
try:
|
|
from slack_sdk.web.async_client import AsyncWebClient
|
|
except ImportError:
|
|
return SlackProbeResult(ok=False, error="slack_sdk not installed")
|
|
|
|
client = AsyncWebClient(token=token)
|
|
start = time.monotonic()
|
|
|
|
try:
|
|
resp = await client.auth_test()
|
|
latency_ms = (time.monotonic() - start) * 1000
|
|
return SlackProbeResult(
|
|
ok=resp.get("ok", False),
|
|
user=resp.get("user", ""),
|
|
user_id=resp.get("user_id", ""),
|
|
bot_id=resp.get("bot_id", ""),
|
|
team=resp.get("team", ""),
|
|
team_id=resp.get("team_id", ""),
|
|
url=resp.get("url", ""),
|
|
api_app_id=resp.get("api_app_id", ""),
|
|
latency_ms=latency_ms,
|
|
)
|
|
except Exception as e:
|
|
latency_ms = (time.monotonic() - start) * 1000
|
|
return SlackProbeResult(ok=False, error=str(e), latency_ms=latency_ms)
|
|
|
|
def build_summary(self, snapshot: object) -> dict:
|
|
if not isinstance(snapshot, ChannelAccountSnapshot):
|
|
return {}
|
|
from yuxi.channel.protocols import build_standard_summary
|
|
|
|
base = build_standard_summary(snapshot, "slack")
|
|
base["status"] = getattr(snapshot, "status_state", "unknown")
|
|
return base
|
|
|
|
def build_channel_summary(
|
|
self,
|
|
account: dict,
|
|
config: dict,
|
|
default_account_id: str,
|
|
snapshot: ChannelAccountSnapshot,
|
|
) -> dict:
|
|
account_id = account.get("account_id", "")
|
|
return {
|
|
"channel": "slack",
|
|
"account_id": account_id,
|
|
"name": account.get("name", account_id),
|
|
"is_default": account_id == default_account_id,
|
|
"configured": snapshot.configured,
|
|
"enabled": snapshot.enabled,
|
|
"connected": snapshot.connected,
|
|
"running": snapshot.running,
|
|
"status": snapshot.status_state,
|
|
"health_state": snapshot.health_state,
|
|
"mode": snapshot.mode,
|
|
"dm_policy": snapshot.dm_policy,
|
|
"last_error": snapshot.last_error,
|
|
"last_connected_at": snapshot.last_connected_at,
|
|
"last_message_at": snapshot.last_message_at,
|
|
"last_event_at": snapshot.last_event_at,
|
|
"probe": snapshot.probe,
|
|
"audit": snapshot.audit,
|
|
}
|
|
|
|
def build_account_snapshot(
|
|
self,
|
|
account: dict,
|
|
config: dict,
|
|
runtime: ChannelAccountSnapshot | None = None,
|
|
probe: object | None = None,
|
|
audit: object | None = None,
|
|
) -> ChannelAccountSnapshot:
|
|
adapter = SlackConfigAdapter()
|
|
account_id = account.get("account_id", "")
|
|
configured = adapter.is_configured(account)
|
|
enabled = adapter.is_enabled(account, config)
|
|
status_state = self.resolve_account_state(account, config, configured, enabled)
|
|
|
|
return ChannelAccountSnapshot(
|
|
account_id=account_id,
|
|
name=account.get("name", account_id),
|
|
enabled=enabled,
|
|
configured=configured,
|
|
status_state=status_state,
|
|
mode=account.get("mode", "socket"),
|
|
dm_policy=account.get("dm_policy", "pairing"),
|
|
allow_from=account.get("allow_from", []),
|
|
probe=probe,
|
|
audit=audit,
|
|
)
|
|
|
|
def format_capabilities_probe(self, probe: object) -> list[dict]:
|
|
if probe is None:
|
|
return []
|
|
return [
|
|
{
|
|
"label": "Slack 连接",
|
|
"ok": getattr(probe, "ok", False),
|
|
"detail": f"{getattr(probe, 'team', '')} / {getattr(probe, 'user', '')}",
|
|
"latency_ms": getattr(probe, "latency_ms", None),
|
|
}
|
|
]
|
|
|
|
async def audit_account(
|
|
self,
|
|
account: dict,
|
|
timeout_ms: int,
|
|
config: dict,
|
|
probe: object | None = None,
|
|
) -> object:
|
|
bot_token = account.get("bot_token", "")
|
|
if not bot_token:
|
|
return {"ok": False, "error": "No bot token configured"}
|
|
|
|
probe_result = await self._probe_slack(bot_token, timeout_ms=timeout_ms)
|
|
return {
|
|
"ok": probe_result.ok,
|
|
"user": probe_result.user,
|
|
"user_id": probe_result.user_id,
|
|
"bot_id": probe_result.bot_id,
|
|
"team": probe_result.team,
|
|
"team_id": probe_result.team_id,
|
|
"url": probe_result.url,
|
|
"api_app_id": probe_result.api_app_id,
|
|
"latency_ms": probe_result.latency_ms,
|
|
"error": probe_result.error,
|
|
}
|
|
|
|
async def build_capabilities_diagnostics(
|
|
self,
|
|
account: dict,
|
|
timeout_ms: int,
|
|
config: dict,
|
|
probe: object | None = None,
|
|
audit: object | None = None,
|
|
target: str | None = None,
|
|
) -> dict | None:
|
|
return {
|
|
"channel": "slack",
|
|
"probe": self.format_capabilities_probe(probe),
|
|
"audit": audit,
|
|
}
|
|
|
|
def log_self_id(
|
|
self,
|
|
account: dict,
|
|
config: dict,
|
|
runtime: object,
|
|
include_channel_prefix: bool = False,
|
|
) -> None:
|
|
prefix = "Slack " if include_channel_prefix else ""
|
|
account_id = account.get("account_id", "default")
|
|
name = account.get("name", account_id)
|
|
logger.info(f"{prefix}Bot account loaded: {name} (id={account_id})")
|
|
|
|
def resolve_account_state(
|
|
self,
|
|
account: dict,
|
|
config: dict,
|
|
configured: bool,
|
|
enabled: bool,
|
|
) -> str:
|
|
if not configured:
|
|
return "unconfigured"
|
|
if not enabled:
|
|
return "disabled"
|
|
return "enabled"
|
|
|
|
def collect_status_issues(self, accounts: list[ChannelAccountSnapshot]) -> list[ChannelStatusIssue]:
|
|
issues: list[ChannelStatusIssue] = []
|
|
for snapshot in accounts:
|
|
if snapshot.connected is False:
|
|
issues.append(
|
|
ChannelStatusIssue(
|
|
channel="slack",
|
|
account_id=snapshot.account_id,
|
|
kind="disconnected",
|
|
message=f"Slack 账户 {snapshot.name or snapshot.account_id} 连接断开",
|
|
fix="检查 Bot Token 和网络连接",
|
|
)
|
|
)
|
|
if snapshot.last_error:
|
|
issues.append(
|
|
ChannelStatusIssue(
|
|
channel="slack",
|
|
account_id=snapshot.account_id,
|
|
kind="error",
|
|
message=f"Slack 账户 {snapshot.name or snapshot.account_id}: {snapshot.last_error}",
|
|
fix="查看日志排查错误原因",
|
|
)
|
|
)
|
|
if snapshot.status_state == "unconfigured":
|
|
issues.append(
|
|
ChannelStatusIssue(
|
|
channel="slack",
|
|
account_id=snapshot.account_id,
|
|
kind="unconfigured",
|
|
message=f"Slack 账户 {snapshot.name or snapshot.account_id} 未配置凭证",
|
|
fix="在管理面板中配置 Slack Bot Token",
|
|
)
|
|
)
|
|
return issues
|