from __future__ import annotations from dataclasses import dataclass, field from typing import Any @dataclass class SlashCommand: command: str description: str auto_complete: bool = True hint: str = "" auto_complete_desc: str = "" auto_complete_hint: str = "" DEFAULT_COMMANDS = [ SlashCommand( command="/forcepilot", description="与 ForcePilot AI 助手对话", hint="[消息内容]", auto_complete_desc="向 AI 发送消息", auto_complete_hint="输入你想问的问题", ), SlashCommand( command="/fp", description="ForcePilot 快捷命令", hint="[消息内容]", auto_complete_desc="快速对话", auto_complete_hint="输入消息", ), SlashCommand( command="/model", description="切换 AI 模型", auto_complete_desc="选择 AI 模型", auto_complete_hint="选择要切换到的模型", ), SlashCommand(command="/clear", description="清除对话上下文", auto_complete_desc="清除对话历史"), SlashCommand(command="/help", description="显示帮助信息", auto_complete_desc="查看命令帮助"), ] @dataclass class SlashCommandConfig: commands: list[SlashCommand] = field(default_factory=lambda: list(DEFAULT_COMMANDS)) auto_register: bool = False callback_url: str = "" @classmethod def from_config(cls, config: dict) -> SlashCommandConfig: raw_commands = config.get("commands", []) if not raw_commands: return cls(commands=list(DEFAULT_COMMANDS)) commands = [] for raw in raw_commands: if isinstance(raw, str): commands.append(SlashCommand(command=raw, description=f"执行 {raw} 命令")) elif isinstance(raw, dict): commands.append( SlashCommand( command=raw.get("command", ""), description=raw.get("description", ""), auto_complete=raw.get("auto_complete", True), ) ) return cls( commands=commands if commands else list(DEFAULT_COMMANDS), auto_register=bool(config.get("auto_register", False)), callback_url=config.get("callback_url", ""), ) def build_command_payload(cmd: SlashCommand, team_id: str, callback_url: str) -> dict[str, Any]: """构建 Mattermost Slash Command 注册请求体。""" payload: dict[str, Any] = { "team_id": team_id, "method": "P", "trigger": cmd.command.lstrip("/"), "url": callback_url or "", "display_name": cmd.command, "description": cmd.description, "auto_complete": cmd.auto_complete, } if cmd.hint: payload["hint"] = cmd.hint if cmd.auto_complete_desc: payload["auto_complete_desc"] = cmd.auto_complete_desc if cmd.auto_complete_hint: payload["auto_complete_hint"] = cmd.auto_complete_hint return payload def get_supported_commands(config: SlashCommandConfig | None = None) -> list[SlashCommand]: if config: return config.commands return list(DEFAULT_COMMANDS) def build_skill_commands(skills: list[str]) -> list[SlashCommand]: return [ SlashCommand( command=f"/oc_{skill}", description=f"执行技能: {skill}", auto_complete_desc=f"运行 {skill} 技能", auto_complete_hint=f"输入 {skill} 的命令参数", ) for skill in skills ] @dataclass class ManagedSlashCommand: trigger: str description: str url: str = "" method: str = "POST" auto_complete: bool = False managed: bool = True async def build_slash_command_payload( trigger: str, description: str, url: str = "", method: str = "POST", ) -> dict[str, Any]: return { "trigger": trigger, "url": url, "method": method, "description": description, } async def list_slash_commands(client: Any) -> list[dict[str, Any]]: if client and hasattr(client, "commands") and hasattr(client.commands, "get_commands"): try: result = await client.commands.get_commands() if isinstance(result, list): return result except Exception: pass return [] async def find_existing_command( client: Any, trigger: str, ) -> dict[str, Any] | None: existing = await list_slash_commands(client) for cmd in existing: if cmd.get("trigger") == trigger or cmd.get("id") == trigger: return cmd return None async def register_managed_commands( client: Any, managed_commands: list[ManagedSlashCommand], base_url: str, ) -> dict[str, bool]: results: dict[str, bool] = {} existing = await list_slash_commands(client) existing_triggers: dict[str, dict[str, Any]] = {} for cmd in existing: t = cmd.get("trigger", "") if t: existing_triggers[t] = cmd for mc in managed_commands: try: command_url = mc.url or f"{base_url.rstrip('/')}/api/mattermost/slash/{mc.trigger}" payload = await build_slash_command_payload(mc.trigger, mc.description, command_url, mc.method) if mc.trigger in existing_triggers: existing_cmd = existing_triggers[mc.trigger] if client and hasattr(client, "commands"): cmd_id = existing_cmd.get("id", mc.trigger) await client.commands.update_command(cmd_id, payload) results[mc.trigger] = True else: if client and hasattr(client, "commands"): await client.commands.create_command(payload) results[mc.trigger] = True except Exception: results[mc.trigger] = False return results async def unregister_managed_commands( client: Any, managed_command_triggers: set[str], ) -> dict[str, bool]: results: dict[str, bool] = {} existing = await list_slash_commands(client) for cmd in existing: trigger = cmd.get("trigger", "") if trigger in managed_command_triggers: try: cmd_id = cmd.get("id", trigger) if client and hasattr(client, "commands"): await client.commands.delete_command(cmd_id) results[trigger] = True except Exception: results[trigger] = False return results