fix: 채널 중복 완전 근절 - reconnect 시 이름 dedup + 자동 삭제 + project name 최소 5자
This commit is contained in:
69
bot.py
69
bot.py
@@ -81,12 +81,14 @@ class ApprovalView(discord.ui.View):
|
||||
|
||||
def detect_project_name(conv_dir: Path) -> str:
|
||||
"""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]
|
||||
|
||||
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.strip(" —-–")
|
||||
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)
|
||||
if match:
|
||||
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
|
||||
except (OSError, UnicodeDecodeError):
|
||||
pass
|
||||
@@ -161,20 +164,40 @@ class GravityBot(commands.Bot):
|
||||
await self._reconnect_existing_channels()
|
||||
|
||||
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:
|
||||
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:
|
||||
if ch.topic and "Antigravity Session:" in ch.topic:
|
||||
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
|
||||
if ch.name in name_to_channel:
|
||||
# DUPLICATE — mark for cleanup
|
||||
duplicates.append(ch)
|
||||
else:
|
||||
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(
|
||||
self, channel: discord.TextChannel, conversation_id: str
|
||||
@@ -196,21 +219,25 @@ class GravityBot(commands.Bot):
|
||||
async def _ensure_channel(
|
||||
self, conversation_id: str, project_name: str
|
||||
) -> discord.TextChannel:
|
||||
"""Get or create a channel. Uses GLOBAL lock + OWN dict lookup (no cache issues)."""
|
||||
# Fast path: already mapped
|
||||
"""Get or create a channel. SINGLE channel per project name, guaranteed."""
|
||||
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:
|
||||
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:
|
||||
# Double-check after lock
|
||||
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}"
|
||||
target_name = channel_name.lower().replace(" ", "-")
|
||||
|
||||
# Check OWN dict for a channel with the same NAME
|
||||
# (different conv IDs can share one channel)
|
||||
# Check ALL mapped channels for same name
|
||||
for cid, ch in self.session_channels.items():
|
||||
if ch.name == target_name:
|
||||
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]}")
|
||||
return ch
|
||||
|
||||
# Create new channel (truly first time)
|
||||
# Create new channel (truly no match anywhere)
|
||||
try:
|
||||
channel = await self.guild.create_text_channel(
|
||||
name=channel_name,
|
||||
|
||||
Reference in New Issue
Block a user