新增Slack适配器全套核心模块,包括消息处理流水线、会话管理、配置适配、权限控制等完整功能: 1. 新增语音、视觉相关的TTS和图像分析导出接口 2. 实现消息预处理、路由、线程上下文处理的完整流水线 3. 新增账号管理、缓存机制、房间上下文提取功能 4. 支持Webhook和Socket Mode两种事件接收方式 5. 实现权限白名单、审批配对、自动状态管理功能 6. 新增配置迁移、作用域校验、重连策略等辅助模块
141 lines
4.1 KiB
Python
141 lines
4.1 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import time
|
|
from dataclasses import dataclass
|
|
|
|
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
|
|
|
|
DRAFT_STREAM_MAX_CHARS = 8000
|
|
DRAFT_STREAM_THROTTLE_MS = 1000
|
|
DRAFT_STREAM_MIN_THROTTLE_MS = 250
|
|
|
|
|
|
class DraftStream:
|
|
def __init__(
|
|
self,
|
|
client: AsyncWebClient,
|
|
*,
|
|
max_chars: int = DRAFT_STREAM_MAX_CHARS,
|
|
throttle_ms: int = DRAFT_STREAM_THROTTLE_MS,
|
|
):
|
|
self._client = client
|
|
self._max_chars = max_chars
|
|
self._throttle_ms = max(throttle_ms, DRAFT_STREAM_MIN_THROTTLE_MS)
|
|
self._active: dict[str, _DraftState] = {}
|
|
self._lock = asyncio.Lock()
|
|
|
|
async def start(
|
|
self,
|
|
channel: str,
|
|
text: str,
|
|
*,
|
|
thread_ts: str | None = None,
|
|
) -> DeliveryResult:
|
|
try:
|
|
params: dict = {
|
|
"channel": channel,
|
|
"text": text[: self._max_chars],
|
|
"mrkdwn": True,
|
|
}
|
|
if thread_ts:
|
|
params["thread_ts"] = thread_ts
|
|
|
|
result = await self._client.chat_postMessage(**params)
|
|
ts = result.get("ts", "")
|
|
if not ts:
|
|
return DeliveryResult(success=False, error="No message ts returned")
|
|
|
|
async with self._lock:
|
|
self._active[channel] = _DraftState(
|
|
channel=channel,
|
|
ts=ts,
|
|
text=text,
|
|
last_update=time.monotonic(),
|
|
)
|
|
|
|
return DeliveryResult(success=True, message_id=ts)
|
|
|
|
except SlackApiError as e:
|
|
err = e.response.get("error", str(e))
|
|
logger.error(f"Draft stream start failed: {err}")
|
|
return DeliveryResult(success=False, error=err)
|
|
|
|
async def append(
|
|
self,
|
|
channel: str,
|
|
text: str,
|
|
*,
|
|
ts: str = "",
|
|
) -> DeliveryResult:
|
|
async with self._lock:
|
|
state = self._active.get(channel)
|
|
if not state:
|
|
return DeliveryResult(success=False, error="No active draft stream for channel")
|
|
|
|
now = time.monotonic()
|
|
elapsed_ms = (now - state.last_update) * 1000
|
|
if elapsed_ms < self._throttle_ms:
|
|
await asyncio.sleep((self._throttle_ms - elapsed_ms) / 1000)
|
|
|
|
try:
|
|
result = await self._client.chat_update(
|
|
channel=channel,
|
|
ts=ts or state.ts,
|
|
text=text[: self._max_chars],
|
|
mrkdwn=True,
|
|
)
|
|
|
|
async with self._lock:
|
|
state.text = text
|
|
state.last_update = time.monotonic()
|
|
|
|
return DeliveryResult(
|
|
success=result.get("ok", False),
|
|
message_id=result.get("ts"),
|
|
error=result.get("error"),
|
|
)
|
|
|
|
except SlackApiError as e:
|
|
err = e.response.get("error", str(e))
|
|
if err == "message_not_found":
|
|
return await self.start(channel, text)
|
|
logger.error(f"Draft stream append failed: {err}")
|
|
return DeliveryResult(success=False, error=err)
|
|
|
|
async def seal(self, channel: str) -> DeliveryResult:
|
|
state = self._active.pop(channel, None)
|
|
if not state:
|
|
return DeliveryResult(success=False, error="No active draft stream")
|
|
return DeliveryResult(success=True, message_id=state.ts)
|
|
|
|
async def clear(self, channel: str) -> None:
|
|
self._active.pop(channel, None)
|
|
|
|
async def stop(self, channel: str) -> None:
|
|
self._active.pop(channel, None)
|
|
|
|
async def force_new_message(
|
|
self,
|
|
channel: str,
|
|
text: str,
|
|
) -> DeliveryResult:
|
|
self._active.pop(channel, None)
|
|
return await self.start(channel, text)
|
|
|
|
@property
|
|
def active_channels(self) -> set[str]:
|
|
return set(self._active.keys())
|
|
|
|
|
|
@dataclass
|
|
class _DraftState:
|
|
channel: str
|
|
ts: str
|
|
text: str
|
|
last_update: float
|