fix: 채널 중복 완전 근절 - reconnect 시 이름 dedup + 자동 삭제 + project name 최소 5자

This commit is contained in:
2026-03-07 12:55:41 +09:00
parent aa9be854b4
commit 8b3d723650

69
bot.py
View File

@@ -81,12 +81,14 @@ class ApprovalView(discord.ui.View):
def detect_project_name(conv_dir: Path) -> str: def detect_project_name(conv_dir: Path) -> str:
"""Extract project name from conversation artifacts. """Extract project name from conversation artifacts.
Output: lowercase_with_underscores (e.g. 'gravity_control') Returns: lowercase_with_underscores (e.g. 'gravity_control')
Uses FIRST successful extraction and caches it.
""" """
short_id = conv_dir.name[:8] short_id = conv_dir.name[:8]
def _sanitize(raw: str) -> str: def _sanitize(raw: str) -> str:
for suffix in ["Task Tracker", "— Task Tracker", "태스크", "구현 계획"]: for suffix in ["Task Tracker", "— Task Tracker", "태스크", "구현 계획",
"Implementation Plan", "Walkthrough"]:
raw = raw.replace(suffix, "") raw = raw.replace(suffix, "")
raw = raw.strip(" —-") raw = raw.strip(" —-")
raw = re.sub(r'[^a-zA-Z0-9가-힣\s\-]', '', raw) raw = re.sub(r'[^a-zA-Z0-9가-힣\s\-]', '', raw)
@@ -102,7 +104,8 @@ def detect_project_name(conv_dir: Path) -> str:
match = re.match(r'^#\s+(.+)', line) match = re.match(r'^#\s+(.+)', line)
if match: if match:
name = _sanitize(match.group(1)) name = _sanitize(match.group(1))
if name and name != "task": # Require at least 5 chars to avoid short generic names
if name and name != "task" and len(name) >= 5:
return name return name
except (OSError, UnicodeDecodeError): except (OSError, UnicodeDecodeError):
pass pass
@@ -161,20 +164,40 @@ class GravityBot(commands.Bot):
await self._reconnect_existing_channels() await self._reconnect_existing_channels()
async def _reconnect_existing_channels(self): async def _reconnect_existing_channels(self):
"""Scan existing Discord channels and map them to conversation IDs.""" """Scan existing Discord channels and map them — MERGE same-name channels."""
if not self.session_category: if not self.session_category:
return return
count = 0 # Group channels by normalized name
name_to_channel: dict[str, discord.TextChannel] = {}
duplicates: list[discord.TextChannel] = []
for ch in self.session_category.text_channels: for ch in self.session_category.text_channels:
if ch.topic and "Antigravity Session:" in ch.topic: if ch.topic and "Antigravity Session:" in ch.topic:
conv_id = ch.topic.replace("Antigravity Session:", "").strip() if ch.name in name_to_channel:
if conv_id: # DUPLICATE — mark for cleanup
self.session_channels[conv_id] = ch duplicates.append(ch)
await self._recover_task_message(ch, conv_id) else:
count += 1 name_to_channel[ch.name] = ch
logger.info(f"Reconnected to {count} existing channels") # Map the primary channel for each name
count = 0
for ch in name_to_channel.values():
conv_id = ch.topic.replace("Antigravity Session:", "").strip()
if conv_id:
self.session_channels[conv_id] = ch
await self._recover_task_message(ch, conv_id)
count += 1
# Delete duplicate channels
for ch in duplicates:
try:
await ch.delete(reason="Duplicate channel cleanup")
logger.info(f"Deleted duplicate channel: #{ch.name}")
except (discord.Forbidden, discord.HTTPException) as e:
logger.warning(f"Failed to delete duplicate #{ch.name}: {e}")
logger.info(f"Reconnected to {count} channels, cleaned {len(duplicates)} duplicates")
async def _recover_task_message( async def _recover_task_message(
self, channel: discord.TextChannel, conversation_id: str self, channel: discord.TextChannel, conversation_id: str
@@ -196,21 +219,25 @@ class GravityBot(commands.Bot):
async def _ensure_channel( async def _ensure_channel(
self, conversation_id: str, project_name: str self, conversation_id: str, project_name: str
) -> discord.TextChannel: ) -> discord.TextChannel:
"""Get or create a channel. Uses GLOBAL lock + OWN dict lookup (no cache issues).""" """Get or create a channel. SINGLE channel per project name, guaranteed."""
# Fast path: already mapped channel_name = f"{Config.CHANNEL_PREFIX}-{project_name}"
target_name = channel_name.lower().replace(" ", "-")
# Fast path: this conv_id already mapped
if conversation_id in self.session_channels: if conversation_id in self.session_channels:
return self.session_channels[conversation_id] ch = self.session_channels[conversation_id]
# Verify the channel name matches (project name might have changed)
if ch.name == target_name:
return ch
async with self._channel_create_lock: async with self._channel_create_lock:
# Double-check after lock # Double-check after lock
if conversation_id in self.session_channels: if conversation_id in self.session_channels:
return self.session_channels[conversation_id] ch = self.session_channels[conversation_id]
if ch.name == target_name:
return ch
channel_name = f"{Config.CHANNEL_PREFIX}-{project_name}" # Check ALL mapped channels for same name
target_name = channel_name.lower().replace(" ", "-")
# Check OWN dict for a channel with the same NAME
# (different conv IDs can share one channel)
for cid, ch in self.session_channels.items(): for cid, ch in self.session_channels.items():
if ch.name == target_name: if ch.name == target_name:
self.session_channels[conversation_id] = ch self.session_channels[conversation_id] = ch
@@ -218,7 +245,7 @@ class GravityBot(commands.Bot):
logger.info(f"Reusing channel #{ch.name} for {conversation_id[:8]}") logger.info(f"Reusing channel #{ch.name} for {conversation_id[:8]}")
return ch return ch
# Create new channel (truly first time) # Create new channel (truly no match anywhere)
try: try:
channel = await self.guild.create_text_channel( channel = await self.guild.create_text_channel(
name=channel_name, name=channel_name,