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

187 lines
6.5 KiB
Python

from __future__ import annotations
from collections.abc import Callable, Awaitable
from dataclasses import dataclass
from enum import StrEnum
from typing import Any
from slack_sdk.errors import SlackApiError
from slack_sdk.web.async_client import AsyncWebClient
from yuxi.utils.logging_config import logger
class CommandTier(StrEnum):
PLATFORM = "platform"
PLUGIN = "plugin"
SKILL = "skill"
CUSTOM = "custom"
@dataclass
class SlashCommand:
command: str
description: str
usage_hint: str = ""
tier: CommandTier = CommandTier.PLATFORM
handler: Callable[..., Awaitable[dict[str, Any]]] | None = None
def to_slack_definition(self) -> dict[str, Any]:
return {
"command": self.command,
"description": self.description,
"usage_hint": self.usage_hint,
}
DEFAULT_SLACK_COMMANDS: list[SlashCommand] = [
SlashCommand(
command="/fp-agentstatus",
description="查询 Bot 运行状态",
usage_hint="",
tier=CommandTier.PLATFORM,
),
SlashCommand(
command="/fp-status",
description="查询 Bot 运行状态 (alias: /fp-agentstatus)",
usage_hint="",
tier=CommandTier.PLATFORM,
),
SlashCommand(
command="/fp-help",
description="显示帮助信息",
usage_hint="[command]",
tier=CommandTier.PLATFORM,
),
SlashCommand(
command="/fp-dm-policy",
description="查看或修改 DM 安全策略",
usage_hint="[open|allowlist|pairing|disabled]",
tier=CommandTier.PLATFORM,
),
]
class SlackCommandRegistry:
def __init__(self):
self._commands: dict[str, SlashCommand] = {}
self._registered: set[str] = set()
self._tiers: dict[CommandTier, list[SlashCommand]] = {t: [] for t in CommandTier}
def register(self, command: SlashCommand) -> None:
self._commands[command.command] = command
self._tiers[command.tier].append(command)
def register_defaults(self) -> None:
for cmd in DEFAULT_SLACK_COMMANDS:
self.register(cmd)
def get_command(self, command_name: str) -> SlashCommand | None:
return self._commands.get(command_name)
def get_tier_commands(self, tier: CommandTier) -> list[SlashCommand]:
return list(self._tiers.get(tier, []))
@property
def commands(self) -> dict[str, SlashCommand]:
return dict(self._commands)
@property
def all_commands(self) -> list[SlashCommand]:
return list(self._commands.values())
@property
def platform_commands(self) -> list[SlashCommand]:
return self.get_tier_commands(CommandTier.PLATFORM)
@property
def plugin_commands(self) -> list[SlashCommand]:
return self.get_tier_commands(CommandTier.PLUGIN)
@property
def skill_commands(self) -> list[SlashCommand]:
return self.get_tier_commands(CommandTier.SKILL)
@property
def custom_commands(self) -> list[SlashCommand]:
return self.get_tier_commands(CommandTier.CUSTOM)
async def sync_to_slack(self, client: AsyncWebClient) -> dict[str, bool]:
commands_manifest = [cmd.to_slack_definition() for cmd in self._commands.values()]
results: dict[str, bool] = {}
for cmd_manifest in commands_manifest:
cmd_name = cmd_manifest["command"]
try:
await client.api_call(
api_method="apps.commands.create",
http_verb="POST",
params={
"command": cmd_name,
"description": cmd_manifest["description"],
"usage_hint": cmd_manifest.get("usage_hint", ""),
},
)
results[cmd_name] = True
self._registered.add(cmd_name)
logger.info(f"Registered Slack command: {cmd_name}")
except SlackApiError as e:
err = e.response.get("error", str(e))
if err in ("name_taken", "already_exists"):
self._registered.add(cmd_name)
results[cmd_name] = True
logger.debug(f"Slack command already registered: {cmd_name}")
else:
results[cmd_name] = False
logger.error(f"Failed to register Slack command {cmd_name}: {err}")
except Exception as e:
results[cmd_name] = False
logger.error(f"Failed to register Slack command {cmd_name}: {e}")
return results
async def handle_command(
self,
command_name: str,
text: str,
user_id: str,
channel_id: str,
) -> dict[str, Any]:
cmd = self._commands.get(command_name)
if not cmd:
return {"response_type": "ephemeral", "text": f"未知命令: {command_name}"}
if cmd.handler:
try:
return await cmd.handler(text=text, user_id=user_id, channel_id=channel_id)
except Exception as e:
logger.error(f"Slash command handler error for {command_name}: {e}")
return {"response_type": "ephemeral", "text": f"命令执行失败: {e}"}
if command_name in ("/fp-agentstatus", "/fp-status"):
return {"response_type": "ephemeral", "text": "Bot 运行中 ✅"}
if command_name == "/fp-help":
return self._build_help_response()
if command_name == "/fp-dm-policy":
return {"response_type": "ephemeral", "text": f"当前 DM 策略命令已接收: {text or '(未指定)'}"}
return {"response_type": "ephemeral", "text": f"命令 {command_name} 已收到"}
def _build_help_response(self) -> dict[str, Any]:
lines = ["*ForcePilot Slack 命令帮助*", ""]
for tier in (CommandTier.PLATFORM, CommandTier.PLUGIN, CommandTier.SKILL, CommandTier.CUSTOM):
tier_cmds = self.get_tier_commands(tier)
if not tier_cmds:
continue
tier_labels = {
CommandTier.PLATFORM: "平台命令",
CommandTier.PLUGIN: "插件命令",
CommandTier.SKILL: "技能命令",
CommandTier.CUSTOM: "自定义命令",
}
lines.append(f"*{tier_labels.get(tier, tier.value)}*")
for cmd in tier_cmds:
hint = f" {cmd.usage_hint}" if cmd.usage_hint else ""
lines.append(f"• `{cmd.command}{hint}` - {cmd.description}")
lines.append("")
return {"response_type": "ephemeral", "text": "\n".join(lines)}