新增Slack适配器全套核心模块,包括消息处理流水线、会话管理、配置适配、权限控制等完整功能: 1. 新增语音、视觉相关的TTS和图像分析导出接口 2. 实现消息预处理、路由、线程上下文处理的完整流水线 3. 新增账号管理、缓存机制、房间上下文提取功能 4. 支持Webhook和Socket Mode两种事件接收方式 5. 实现权限白名单、审批配对、自动状态管理功能 6. 新增配置迁移、作用域校验、重连策略等辅助模块
191 lines
5.8 KiB
Python
191 lines
5.8 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from enum import StrEnum
|
|
|
|
from slack_sdk.errors import SlackApiError
|
|
from slack_sdk.web.async_client import AsyncWebClient
|
|
|
|
from yuxi.channels.models import DeliveryResult
|
|
from yuxi.utils.logging_config import logger
|
|
|
|
|
|
class StreamState(StrEnum):
|
|
INIT = "init"
|
|
STREAMING = "streaming"
|
|
STOPPED = "stopped"
|
|
ERROR = "error"
|
|
|
|
|
|
_BENIGN_FINALIZE_ERRORS = frozenset(
|
|
{
|
|
"user_not_found",
|
|
"team_not_found",
|
|
"missing_recipient_user_id",
|
|
"not_in_channel",
|
|
"channel_not_found",
|
|
}
|
|
)
|
|
|
|
|
|
class SlackStreamNotDeliveredError(Exception):
|
|
pass
|
|
|
|
|
|
@dataclass
|
|
class NativeStreamSession:
|
|
channel: str
|
|
team_id: str = ""
|
|
user_id: str = ""
|
|
message_ts: str = ""
|
|
accumulated_text: str = ""
|
|
state: StreamState = StreamState.INIT
|
|
error: str = ""
|
|
|
|
|
|
_MISSING_OVERRIDE = object()
|
|
|
|
|
|
class NativeStreamManager:
|
|
def __init__(self, client: AsyncWebClient):
|
|
self._client = client
|
|
self._sessions: dict[str, NativeStreamSession] = {}
|
|
|
|
async def start_stream(
|
|
self,
|
|
channel: str,
|
|
*,
|
|
team_id: str = "",
|
|
user_id: str = "",
|
|
text: str = "",
|
|
) -> DeliveryResult:
|
|
session_key = f"{channel}:{team_id}:{user_id}"
|
|
existing = self._sessions.get(session_key)
|
|
if existing and existing.state == StreamState.STREAMING:
|
|
return await self.append_stream(channel, text, team_id=team_id, user_id=user_id)
|
|
|
|
try:
|
|
params: dict = {
|
|
"channel": channel,
|
|
"text": text or "...",
|
|
}
|
|
if user_id:
|
|
params["user_id"] = user_id
|
|
if team_id:
|
|
params["team_id"] = team_id
|
|
|
|
result = await self._client.api_call(
|
|
api_method="chat.startStream",
|
|
http_verb="POST",
|
|
params=params,
|
|
)
|
|
ok = result.get("ok", False)
|
|
ts = result.get("ts", "")
|
|
|
|
session = NativeStreamSession(
|
|
channel=channel,
|
|
team_id=team_id,
|
|
user_id=user_id,
|
|
message_ts=ts,
|
|
accumulated_text=text,
|
|
state=StreamState.STREAMING if ok else StreamState.ERROR,
|
|
error=result.get("error", ""),
|
|
)
|
|
self._sessions[session_key] = session
|
|
|
|
if ok:
|
|
logger.debug(f"Native stream started: channel={channel}, ts={ts}")
|
|
return DeliveryResult(success=True, message_id=ts)
|
|
|
|
logger.warning(f"Native stream start failed: {result.get('error')}")
|
|
return DeliveryResult(success=False, error=result.get("error", "Unknown error"))
|
|
|
|
except SlackApiError as e:
|
|
err = e.response.get("error", str(e))
|
|
logger.error(f"Native stream start error: {err}")
|
|
return DeliveryResult(success=False, error=err)
|
|
|
|
async def append_stream(
|
|
self,
|
|
channel: str,
|
|
text: str,
|
|
*,
|
|
team_id: str = "",
|
|
user_id: str = "",
|
|
ts: str = "",
|
|
) -> DeliveryResult:
|
|
session_key = f"{channel}:{team_id}:{user_id}"
|
|
session = self._sessions.get(session_key)
|
|
if not session or session.state != StreamState.STREAMING:
|
|
return await self.start_stream(channel, team_id=team_id, user_id=user_id, text=text)
|
|
|
|
try:
|
|
result = await self._client.api_call(
|
|
api_method="chat.appendStream",
|
|
http_verb="POST",
|
|
params={
|
|
"channel": channel,
|
|
"ts": ts or session.message_ts,
|
|
"text": text,
|
|
},
|
|
)
|
|
ok = result.get("ok", False)
|
|
if ok:
|
|
session.accumulated_text = text
|
|
return DeliveryResult(
|
|
success=ok,
|
|
error=result.get("error") if not ok else None,
|
|
)
|
|
|
|
except SlackApiError as e:
|
|
err = e.response.get("error", str(e))
|
|
logger.error(f"Native stream append error: {err}")
|
|
return DeliveryResult(success=False, error=err)
|
|
|
|
async def stop_stream(
|
|
self,
|
|
channel: str,
|
|
*,
|
|
team_id: str = "",
|
|
user_id: str = "",
|
|
ts: str = "",
|
|
final_text: str = _MISSING_OVERRIDE,
|
|
) -> DeliveryResult:
|
|
session_key = f"{channel}:{team_id}:{user_id}"
|
|
session = self._sessions.pop(session_key, None)
|
|
|
|
try:
|
|
result = await self._client.api_call(
|
|
api_method="chat.stopStream",
|
|
http_verb="POST",
|
|
params={
|
|
"channel": channel,
|
|
"ts": ts or (session.message_ts if session else ""),
|
|
"text": final_text
|
|
if final_text is not _MISSING_OVERRIDE
|
|
else (session.accumulated_text if session else ""),
|
|
},
|
|
)
|
|
ok = result.get("ok", False)
|
|
err = result.get("error", "")
|
|
|
|
if not ok and not _is_benign_stop_error(err):
|
|
logger.error(f"Native stream stop error: {err}")
|
|
raise SlackStreamNotDeliveredError(f"Stream not delivered: {err}")
|
|
|
|
return DeliveryResult(
|
|
success=True,
|
|
error=err if err and err not in _BENIGN_FINALIZE_ERRORS else None,
|
|
)
|
|
|
|
except SlackApiError as e:
|
|
err = e.response.get("error", str(e))
|
|
if _is_benign_stop_error(err):
|
|
return DeliveryResult(success=True)
|
|
logger.error(f"Native stream stop API error: {err}")
|
|
raise SlackStreamNotDeliveredError(f"Stream not delivered: {err}") from e
|
|
|
|
|
|
def _is_benign_stop_error(error: str) -> bool:
|
|
return error in _BENIGN_FINALIZE_ERRORS
|