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)}