ForcePilot/backend/package/yuxi/channels/adapters/slack/poll_manager.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

115 lines
3.3 KiB
Python

from __future__ import annotations
import uuid
from dataclasses import dataclass, field
from datetime import datetime, UTC, timedelta
from typing import Any
@dataclass
class PollOption:
index: int
text: str
votes: set[str] = field(default_factory=set)
@dataclass
class Poll:
poll_id: str
question: str
options: list[PollOption]
chat_id: str
message_ts: str
created_by: str = ""
created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
closed_at: datetime | None = None
@property
def is_closed(self) -> bool:
return self.closed_at is not None
@property
def total_votes(self) -> int:
return sum(len(opt.votes) for opt in self.options)
def vote(self, option_index: int, user_id: str) -> bool:
for opt in self.options:
opt.votes.discard(user_id)
if 0 <= option_index < len(self.options):
self.options[option_index].votes.add(user_id)
return True
return False
def results(self) -> dict[str, Any]:
option_results = [
{
"index": opt.index,
"text": opt.text,
"count": len(opt.votes),
}
for opt in self.options
]
return {
"poll_id": self.poll_id,
"question": self.question,
"options": option_results,
"total_votes": self.total_votes,
"is_closed": self.is_closed,
"chat_id": self.chat_id,
}
class PollManager:
def __init__(self, ttl_seconds: float = 86400.0):
self._polls: dict[str, Poll] = {}
self._ttl_seconds = ttl_seconds
def create_poll(
self, question: str, options: list[str], chat_id: str, message_ts: str, created_by: str = ""
) -> Poll:
poll_id = uuid.uuid4().hex[:12]
poll_options = [PollOption(index=i, text=opt) for i, opt in enumerate(options)]
poll = Poll(
poll_id=poll_id,
question=question,
options=poll_options,
chat_id=chat_id,
message_ts=message_ts,
created_by=created_by,
)
self._polls[poll_id] = poll
self._cleanup_expired()
return poll
def get_poll(self, poll_id: str) -> Poll | None:
return self._polls.get(poll_id)
def vote_poll(self, poll_id: str, option_index: int, user_id: str) -> bool:
poll = self._polls.get(poll_id)
if not poll or poll.is_closed:
return False
return poll.vote(option_index, user_id)
def close_poll(self, poll_id: str) -> bool:
poll = self._polls.get(poll_id)
if not poll or poll.is_closed:
return False
poll.closed_at = datetime.now(UTC)
return True
def list_polls(self, chat_id: str | None = None) -> list[dict[str, Any]]:
polls = self._polls.values()
if chat_id:
polls = [p for p in polls if p.chat_id == chat_id]
return [p.results() for p in polls if not p.is_closed]
def _cleanup_expired(self) -> None:
now = datetime.now(UTC)
expired = [
pid
for pid, poll in self._polls.items()
if poll.is_closed and now - poll.closed_at > timedelta(seconds=self._ttl_seconds)
]
for pid in expired:
self._polls.pop(pid, None)