新增Slack适配器全套核心模块,包括消息处理流水线、会话管理、配置适配、权限控制等完整功能: 1. 新增语音、视觉相关的TTS和图像分析导出接口 2. 实现消息预处理、路由、线程上下文处理的完整流水线 3. 新增账号管理、缓存机制、房间上下文提取功能 4. 支持Webhook和Socket Mode两种事件接收方式 5. 实现权限白名单、审批配对、自动状态管理功能 6. 新增配置迁移、作用域校验、重连策略等辅助模块
201 lines
7.3 KiB
Python
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
|