ForcePilot/backend/package/yuxi/channels/adapters/slack/targets.py
Kris a2aa782b86 feat(slack adapter): 实现完整的Slack频道适配器基础功能
新增Slack适配器全套核心模块,包括消息处理流水线、会话管理、配置适配、权限控制等完整功能:
1. 新增语音、视觉相关的TTS和图像分析导出接口
2. 实现消息预处理、路由、线程上下文处理的完整流水线
3. 新增账号管理、缓存机制、房间上下文提取功能
4. 支持Webhook和Socket Mode两种事件接收方式
5. 实现权限白名单、审批配对、自动状态管理功能
6. 新增配置迁移、作用域校验、重连策略等辅助模块
2026-05-12 00:48:57 +08:00

201 lines
7.3 KiB
Python

from __future__ import annotations
import re
from dataclasses import dataclass
from typing import Any
_SLACK_ID_RE = re.compile(r"^[UGCD][A-Z0-9]{8,}$")
@dataclass
class SlackTarget:
raw: str
target_type: str = ""
target_id: str = ""
@property
def is_user(self) -> bool:
return self.target_type == "user"
@property
def is_channel(self) -> bool:
return self.target_type == "channel"
@property
def is_valid(self) -> bool:
return bool(self.target_type and self.target_id)
def parse_slack_target(raw: str) -> SlackTarget:
raw = raw.strip()
if not raw:
return SlackTarget(raw=raw)
mention_match = re.match(r"^<@([A-Z0-9]+)>$", raw)
if mention_match:
return SlackTarget(raw=raw, target_type="user", target_id=mention_match.group(1))
channel_match = re.match(r"^<#([A-Z0-9]+)\|?[^>]*>$", raw)
if channel_match:
return SlackTarget(raw=raw, target_type="channel", target_id=channel_match.group(1))
if _SLACK_ID_RE.match(raw):
if raw.startswith("U"):
return SlackTarget(raw=raw, target_type="user", target_id=raw)
if raw.startswith("C") or raw.startswith("G"):
return SlackTarget(raw=raw, target_type="channel", target_id=raw)
if raw.startswith("D"):
return SlackTarget(raw=raw, target_type="channel", target_id=raw)
if "@" in raw:
return SlackTarget(raw=raw, target_type="email", target_id=raw)
if raw.startswith("#"):
return SlackTarget(raw=raw, target_type="channel_name", target_id=raw[1:])
return SlackTarget(raw=raw, target_type="user_name", target_id=raw)
def parse_slack_targets(raw: str) -> list[SlackTarget]:
parts = re.split(r"[,\s]+", raw)
return [parse_slack_target(p) for p in parts if p.strip()]
@dataclass
class BatchResolveResult:
targets: list[SlackTarget]
resolved: list[dict[str, Any]] = None
unresolved: list[str] = None
errors: list[str] = None
def __post_init__(self):
self.resolved = self.resolved or []
self.unresolved = self.unresolved or []
self.errors = self.errors or []
@property
def count(self) -> int:
return len(self.targets)
@property
def resolved_count(self) -> int:
return len(self.resolved)
@property
def unresolved_count(self) -> int:
return len(self.unresolved)
async def resolve_targets(
client,
targets: list[SlackTarget],
) -> BatchResolveResult:
result = BatchResolveResult(targets=targets)
for target in targets:
if not target.is_valid:
result.unresolved.append(target.raw)
continue
try:
if target.target_type == "user":
resp = await client.users_info(user=target.target_id)
if resp.get("ok"):
user = resp.get("user", {})
result.resolved.append(
{
"raw": target.raw,
"type": "user",
"id": user.get("id", target.target_id),
"name": user.get("name", ""),
"display_name": user.get("profile", {}).get("display_name", ""),
}
)
else:
result.unresolved.append(target.raw)
elif target.target_type == "channel":
resp = await client.conversations_info(channel=target.target_id)
if resp.get("ok"):
ch = resp.get("channel", {})
result.resolved.append(
{
"raw": target.raw,
"type": "channel",
"id": ch.get("id", target.target_id),
"name": ch.get("name", ""),
}
)
else:
result.unresolved.append(target.raw)
elif target.target_type == "email":
resp = await client.users_lookupByEmail(email=target.target_id)
if resp.get("ok"):
user = resp.get("user", {})
result.resolved.append(
{
"raw": target.raw,
"type": "user",
"id": user.get("id", ""),
"name": user.get("name", ""),
"display_name": user.get("profile", {}).get("display_name", ""),
}
)
else:
result.unresolved.append(target.raw)
elif target.target_type == "channel_name":
found = False
cursor = ""
while True:
resp = await client.conversations_list(
types="public_channel,private_channel",
cursor=cursor or None,
limit=200,
)
if resp.get("ok"):
for ch in resp.get("channels", []):
if ch.get("name") == target.target_id:
result.resolved.append(
{
"raw": target.raw,
"type": "channel",
"id": ch.get("id", ""),
"name": ch.get("name", ""),
}
)
found = True
break
cursor = resp.get("response_metadata", {}).get("next_cursor", "")
if not cursor or found:
break
if not found:
result.unresolved.append(target.raw)
elif target.target_type == "user_name":
found = False
cursor = ""
while True:
resp = await client.users_list(cursor=cursor or None, limit=200)
if resp.get("ok"):
for user in resp.get("members", []):
if user.get("name") == target.target_id or user.get("real_name") == target.target_id:
result.resolved.append(
{
"raw": target.raw,
"type": "user",
"id": user.get("id", ""),
"name": user.get("name", ""),
"display_name": user.get("profile", {}).get("display_name", ""),
}
)
found = True
break
cursor = resp.get("response_metadata", {}).get("next_cursor", "")
if not cursor or found:
break
if not found:
result.unresolved.append(target.raw)
except Exception as e:
result.errors.append(f"{target.raw}: {e}")
result.unresolved.append(target.raw)
return result