Files
ROMFASTSQL/proxmox/lxc171-claude-agent/discord-bridge/commands_slash.py
Claude Agent 5f34320e3e feat(discord-bridge): comenzi slash in loc de prefixul !
Comenzile devin application commands inregistrate pe guild (sync instantaneu,
spre deosebire de cel global care dureaza ~1h): /new [fork], /cd <cale>,
/model <sonnet|opus> cu Choice, /status, /stop, /cleanup [force], /help.

- allowlist-ul se aplica identic la interactiuni (check_ids comun, ca sa nu
  existe a doua implementare care diverge); refuz efemer, fara executie
- fiecare comanda face defer() inainte de lucru — altfel Discord marcheaza
  interactiunea esuata dupa 3s desi comanda a rulat
- sync tolerant: la esec (lipsa scope applications.commands) botul porneste
  normal si logheaza linkul de reinvitare necesar
- mesajele obisnuite raman neschimbate, inclusiv steering-ul mid-tur
- linkul de invitatie primeste scope=bot%20applications.commands; referintele
  la ! din ops/ si documentatie trecute pe /

Verificat in productie: 7 comenzi inregistrate pe guild, citite inapoi din API.
Suita: 296 passed cu discord.py, 293 passed + 3 skipped fara.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B29CApsP1JkSdjYaGaHpE7
2026-08-30 12:23:33 +00:00

117 lines
4.8 KiB
Python

"""Comenzile slash ale puntii: declararea si inregistrarea lor in Discord.
Tot ce depinde de `discord.app_commands` sta aici. Logica ramane in `bot.Bridge`
(`cmd_new`, `cmd_cd`, ...), deci comenzile slash si allowlist-ul nu au a doua
implementare care sa divergheze.
Modulul se importa si FARA discord.py instalat (ca `bot.py`): atunci `build_tree`
returneaza None si `sync_guilds` nu face nimic.
"""
from __future__ import annotations
import logging
log = logging.getLogger("discord-bridge.slash")
try: # pragma: no cover - depinde de mediu
import discord # type: ignore
from discord import app_commands # type: ignore
except ImportError: # pragma: no cover
discord = None # type: ignore
app_commands = None # type: ignore
# Permisiunile din README (309237763136) + scope-ul de comenzi. Fara
# `applications.commands` in invitatie, sync-ul da 403 Missing Access.
INVITE_TEMPLATE = (
"https://discord.com/oauth2/authorize?client_id={app_id}"
"&scope=bot%20applications.commands&permissions=309237763136"
)
def invite_url(app_id) -> str:
return INVITE_TEMPLATE.format(app_id=app_id or "APPLICATION_ID")
def build_tree(client, bridge):
"""Construieste arborele de comenzi. Fara discord.py: None."""
if app_commands is None: # pragma: no cover - mediu fara discord.py
log.warning("discord.py lipseste: nu declar comenzi slash")
return None
tree = app_commands.CommandTree(client)
@tree.command(name="new", description="Sesiune Claude noua in firul curent")
@app_commands.describe(fork="Porneste noua sesiune din contextul celei curente")
async def _new(interaction, fork: bool = False):
await bridge.handle_slash(interaction, "new", fork=fork)
@tree.command(name="cd", description="Schimba directorul de lucru al firului")
@app_commands.describe(cale="Cale absoluta, oriunde in /workspace")
async def _cd(interaction, cale: str):
await bridge.handle_slash(interaction, "cd", cale=cale)
@tree.command(name="model", description="Schimba modelul pentru firul curent")
@app_commands.describe(model="sonnet (implicit) sau opus")
@app_commands.choices(model=[
app_commands.Choice(name="sonnet", value="sonnet"),
app_commands.Choice(name="opus", value="opus"),
])
async def _model(interaction, model: app_commands.Choice[str]):
await bridge.handle_slash(interaction, "model", model=model.value)
@tree.command(name="status", description="Sesiune, director, model, cost, proces")
async def _status(interaction):
await bridge.handle_slash(interaction, "status")
@tree.command(name="stop", description="Opreste turul in desfasurare din firul curent")
async def _stop(interaction):
await bridge.handle_slash(interaction, "stop")
@tree.command(name="cleanup", description="Procese lasate in urma (implicit: rulare seaca)")
@app_commands.describe(force="Opreste efectiv procesele gasite (implicit: doar lista)")
async def _cleanup(interaction, force: bool = False):
await bridge.handle_slash(interaction, "cleanup", force=force)
@tree.command(name="help", description="Lista comenzilor puntii")
async def _help(interaction):
await bridge.handle_slash(interaction, "help")
return tree
async def sync_guilds(tree, guild_ids) -> dict[str, str]:
"""Inregistreaza comenzile pe fiecare guild din allowlist.
Sync-ul pe guild e instantaneu; cel global se propaga in ~1h, deci nu-l folosim.
NU arunca niciodata: daca invitatia botului nu are scope-ul `applications.commands`,
Discord raspunde 403 Missing Access — logam linkul de reinvitare si mergem mai
departe. Botul ramane functional pentru mesajele obisnuite.
"""
results: dict[str, str] = {}
if tree is None or discord is None:
log.warning("fara arbore de comenzi: sar peste inregistrarea comenzilor slash")
return results
gids = [str(g).strip() for g in (guild_ids or []) if str(g).strip()]
if not gids:
log.warning("DISCORD_GUILD_IDS gol: nu inregistrez comenzi slash nicaieri")
return results
app_id = getattr(getattr(tree, "client", None), "application_id", None)
for gid in gids:
try:
guild = discord.Object(id=int(gid))
tree.copy_global_to(guild=guild)
cmds = await tree.sync(guild=guild)
results[gid] = f"ok ({len(cmds)} comenzi)"
log.info("comenzi slash inregistrate pe guild %s: %d", gid, len(cmds))
except Exception as exc:
results[gid] = f"esuat: {exc}"
log.error("sync de comenzi slash esuat pe guild %s: %s", gid, exc)
log.error(
"daca e 403 Missing Access, botul a fost invitat fara scope-ul "
"`applications.commands`. Reinvita-l cu: %s",
invite_url(app_id),
)
return results