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