mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-05-21 23:24:44 +02:00
Remove dead channel/mention code — DM-only bot cleanup
MeshAI is now DM-only. Removed all unreachable channel response paths, @mention detection, ChannelsConfig, and channel TUI menu. Fixed restart mechanism with integrated watcher and SIGKILL fallback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
32147ccaec
commit
1e033316fb
7 changed files with 81 additions and 199 deletions
|
|
@ -6,9 +6,8 @@
|
|||
|
||||
# === BOT IDENTITY ===
|
||||
bot:
|
||||
name: ai # Bot's trigger name (users say "@ai help")
|
||||
name: ai # Bot's display name
|
||||
owner: "" # Owner's callsign (optional)
|
||||
respond_to_mentions: true # Respond when name is mentioned
|
||||
respond_to_dms: true # Respond to direct messages
|
||||
filter_bbs_protocols: true # Ignore advBBS sync/notification messages
|
||||
|
||||
|
|
@ -19,12 +18,6 @@ connection:
|
|||
tcp_host: localhost # For TCP connection (meshtasticd)
|
||||
tcp_port: 4403
|
||||
|
||||
# === CHANNEL FILTERING ===
|
||||
channels:
|
||||
mode: all # all | whitelist
|
||||
whitelist: # Only respond on these channels (if mode=whitelist)
|
||||
- 0
|
||||
|
||||
# === RESPONSE BEHAVIOR ===
|
||||
response:
|
||||
delay_min: 2.2 # Min delay before responding (seconds)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ if [ ! -f "$MESHAI_CONFIG" ]; then
|
|||
bot:
|
||||
name: ai
|
||||
owner: ""
|
||||
respond_to_mentions: true
|
||||
respond_to_dms: true
|
||||
filter_bbs_protocols: true
|
||||
|
||||
|
|
@ -25,11 +24,6 @@ connection:
|
|||
tcp_host: localhost
|
||||
tcp_port: 4403
|
||||
|
||||
channels:
|
||||
mode: all
|
||||
whitelist:
|
||||
- 0
|
||||
|
||||
response:
|
||||
delay_min: 2.2
|
||||
delay_max: 3.0
|
||||
|
|
@ -74,38 +68,50 @@ ttyd -W -p 7682 \
|
|||
/bin/bash -c 'while true; do python3 -m meshai --config-file "$MESHAI_CONFIG" --config; sleep 1; done' &
|
||||
|
||||
# Keep ttyd running even if bot fails
|
||||
trap "kill %1 2>/dev/null; kill %2 2>/dev/null" EXIT
|
||||
trap "kill %1 2>/dev/null" EXIT
|
||||
|
||||
# Restart watcher - monitors for restart signal from config tool
|
||||
BOT_PID_FILE="/tmp/meshai_bot.pid"
|
||||
(
|
||||
while true; do
|
||||
if [ -f /tmp/meshai_restart ]; then
|
||||
rm -f /tmp/meshai_restart
|
||||
echo "Restart signal received, restarting bot..."
|
||||
# Kill bot using PID file
|
||||
if [ -f "$BOT_PID_FILE" ]; then
|
||||
BOT_PID=$(cat "$BOT_PID_FILE")
|
||||
if kill -0 "$BOT_PID" 2>/dev/null; then
|
||||
kill "$BOT_PID" 2>/dev/null || true
|
||||
echo "Sent TERM to bot (PID $BOT_PID)"
|
||||
fi
|
||||
fi
|
||||
# Debounce - wait before checking for more signals
|
||||
sleep 3
|
||||
fi
|
||||
sleep 2
|
||||
# Kill bot gracefully with SIGKILL fallback
|
||||
kill_bot() {
|
||||
local pid=$1
|
||||
if ! kill -0 "$pid" 2>/dev/null; then
|
||||
return
|
||||
fi
|
||||
kill "$pid" 2>/dev/null || true
|
||||
echo "Sent SIGTERM to bot (PID $pid)"
|
||||
# Wait up to 5 seconds for graceful shutdown
|
||||
for i in 1 2 3 4 5; do
|
||||
kill -0 "$pid" 2>/dev/null || return
|
||||
sleep 1
|
||||
done
|
||||
) &
|
||||
# Force kill if still alive
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
kill -9 "$pid" 2>/dev/null || true
|
||||
echo "Sent SIGKILL to bot (PID $pid)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Start the bot in a loop - retry on failure
|
||||
# Start the bot in a loop with integrated restart watcher
|
||||
echo "Starting MeshAI..."
|
||||
rm -f /tmp/meshai_restart
|
||||
while true; do
|
||||
python -m meshai --config-file "$MESHAI_CONFIG" &
|
||||
BOT_PID=$!
|
||||
echo "$BOT_PID" > "$BOT_PID_FILE"
|
||||
wait $BOT_PID || true
|
||||
rm -f "$BOT_PID_FILE"
|
||||
echo "Bot exited. Check config at http://localhost:7682. Retrying in 5s..."
|
||||
sleep 5
|
||||
echo "$BOT_PID" > /tmp/meshai.pid
|
||||
echo "Bot started (PID $BOT_PID)"
|
||||
|
||||
# Poll: wait for bot to exit OR restart signal
|
||||
while kill -0 $BOT_PID 2>/dev/null; do
|
||||
if [ -f /tmp/meshai_restart ]; then
|
||||
rm -f /tmp/meshai_restart
|
||||
echo "Restart signal received, restarting bot..."
|
||||
kill_bot $BOT_PID
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
wait $BOT_PID 2>/dev/null || true
|
||||
rm -f /tmp/meshai.pid
|
||||
echo "Bot exited. Restarting in 3s..."
|
||||
sleep 3
|
||||
done
|
||||
|
|
|
|||
|
|
@ -69,15 +69,14 @@ class Configurator:
|
|||
disabled_count = len(self.config.commands.disabled_commands)
|
||||
cmd_status = f"{disabled_count} disabled" if disabled_count else "all enabled"
|
||||
|
||||
table.add_row("1", "Bot Settings", f"@{self.config.bot.name}")
|
||||
table.add_row("1", "Bot Settings", self.config.bot.name)
|
||||
table.add_row("2", "Connection", f"{self.config.connection.type}")
|
||||
table.add_row("3", "LLM Backend", f"{self.config.llm.backend}/{self.config.llm.model}")
|
||||
table.add_row("4", "Response Settings", f"{self.config.response.max_length}ch max")
|
||||
table.add_row("5", "Channels", f"{self.config.channels.mode}")
|
||||
table.add_row("6", "History & Memory", f"{self.config.history.max_messages_per_user} msgs")
|
||||
table.add_row("7", "Commands", cmd_status)
|
||||
table.add_row("8", "Weather", f"{self.config.weather.primary}")
|
||||
table.add_row("9", "Setup Wizard", "[dim]First-time setup[/dim]")
|
||||
table.add_row("5", "History & Memory", f"{self.config.history.max_messages_per_user} msgs")
|
||||
table.add_row("6", "Commands", cmd_status)
|
||||
table.add_row("7", "Weather", f"{self.config.weather.primary}")
|
||||
table.add_row("8", "Setup Wizard", "[dim]First-time setup[/dim]")
|
||||
|
||||
console.print(table)
|
||||
console.print()
|
||||
|
|
@ -86,13 +85,13 @@ class Configurator:
|
|||
if self.modified:
|
||||
console.print("[yellow]* Unsaved changes[/yellow]")
|
||||
console.print()
|
||||
console.print("[white]10. Save[/white] [dim]Save config, stay in menu[/dim]")
|
||||
console.print("[green]11. Save & Restart Bot[/green] [dim]Apply changes now[/dim]")
|
||||
console.print("[white]12. Save & Exit[/white] [dim]Save, restart bot, exit[/dim]")
|
||||
console.print("[white]13. Exit without Saving[/white]")
|
||||
console.print("[white] 9. Save[/white] [dim]Save config, stay in menu[/dim]")
|
||||
console.print("[green]10. Save & Restart Bot[/green] [dim]Apply changes now[/dim]")
|
||||
console.print("[white]11. Save & Exit[/white] [dim]Save, restart bot, exit[/dim]")
|
||||
console.print("[white]12. Exit without Saving[/white]")
|
||||
console.print()
|
||||
|
||||
choice = IntPrompt.ask("Select option", default=11)
|
||||
choice = IntPrompt.ask("Select option", default=10)
|
||||
|
||||
if choice == 1:
|
||||
self._bot_settings()
|
||||
|
|
@ -103,23 +102,21 @@ class Configurator:
|
|||
elif choice == 4:
|
||||
self._response_settings()
|
||||
elif choice == 5:
|
||||
self._channel_settings()
|
||||
elif choice == 6:
|
||||
self._history_settings()
|
||||
elif choice == 7:
|
||||
elif choice == 6:
|
||||
self._command_settings()
|
||||
elif choice == 8:
|
||||
elif choice == 7:
|
||||
self._weather_settings()
|
||||
elif choice == 9:
|
||||
elif choice == 8:
|
||||
self._setup_wizard()
|
||||
elif choice == 10:
|
||||
elif choice == 9:
|
||||
self._save_only()
|
||||
elif choice == 11:
|
||||
elif choice == 10:
|
||||
self._save_and_restart()
|
||||
elif choice == 12:
|
||||
elif choice == 11:
|
||||
self._save_restart_exit()
|
||||
break
|
||||
elif choice == 13:
|
||||
elif choice == 12:
|
||||
break
|
||||
|
||||
def _show_header(self) -> None:
|
||||
|
|
@ -148,18 +145,13 @@ class Configurator:
|
|||
table.add_column("Setting", style="white")
|
||||
table.add_column("Value", style="green")
|
||||
|
||||
table.add_row("1", "Bot Name (@mention)", self.config.bot.name)
|
||||
table.add_row("1", "Bot Name", self.config.bot.name)
|
||||
table.add_row("2", "Owner", self.config.bot.owner or "[dim]not set[/dim]")
|
||||
table.add_row(
|
||||
"3",
|
||||
"Respond to @mentions",
|
||||
self._status_icon(self.config.bot.respond_to_mentions),
|
||||
"3", "Respond to DMs", self._status_icon(self.config.bot.respond_to_dms)
|
||||
)
|
||||
table.add_row(
|
||||
"4", "Respond to DMs", self._status_icon(self.config.bot.respond_to_dms)
|
||||
)
|
||||
table.add_row(
|
||||
"5", "Filter BBS Protocols", self._status_icon(self.config.bot.filter_bbs_protocols)
|
||||
"4", "Filter BBS Protocols", self._status_icon(self.config.bot.filter_bbs_protocols)
|
||||
)
|
||||
table.add_row("0", "Back", "")
|
||||
|
||||
|
|
@ -181,18 +173,11 @@ class Configurator:
|
|||
self.config.bot.owner = value
|
||||
self.modified = True
|
||||
elif choice == 3:
|
||||
value = Confirm.ask(
|
||||
"Respond to @mentions?", default=self.config.bot.respond_to_mentions
|
||||
)
|
||||
if value != self.config.bot.respond_to_mentions:
|
||||
self.config.bot.respond_to_mentions = value
|
||||
self.modified = True
|
||||
elif choice == 4:
|
||||
value = Confirm.ask("Respond to DMs?", default=self.config.bot.respond_to_dms)
|
||||
if value != self.config.bot.respond_to_dms:
|
||||
self.config.bot.respond_to_dms = value
|
||||
self.modified = True
|
||||
elif choice == 5:
|
||||
elif choice == 4:
|
||||
value = Confirm.ask("Filter BBS protocols?", default=self.config.bot.filter_bbs_protocols)
|
||||
if value != self.config.bot.filter_bbs_protocols:
|
||||
self.config.bot.filter_bbs_protocols = value
|
||||
|
|
@ -478,49 +463,6 @@ class Configurator:
|
|||
self.config.response.max_messages = value
|
||||
self.modified = True
|
||||
|
||||
def _channel_settings(self) -> None:
|
||||
"""Channel filtering settings submenu."""
|
||||
while True:
|
||||
self._clear()
|
||||
console.print("[bold]Channel Filtering[/bold]\n")
|
||||
|
||||
table = Table(box=box.ROUNDED)
|
||||
table.add_column("Option", style="cyan", width=4)
|
||||
table.add_column("Setting", style="white")
|
||||
table.add_column("Value", style="green")
|
||||
|
||||
whitelist_str = ", ".join(str(c) for c in self.config.channels.whitelist)
|
||||
table.add_row("1", "Mode", self.config.channels.mode)
|
||||
table.add_row("2", "Whitelist Channels", whitelist_str or "[dim]none[/dim]")
|
||||
table.add_row("0", "Back", "")
|
||||
|
||||
console.print(table)
|
||||
console.print()
|
||||
|
||||
choice = IntPrompt.ask("Select option", default=0)
|
||||
|
||||
if choice == 0:
|
||||
return
|
||||
elif choice == 1:
|
||||
console.print("\n[cyan]1.[/cyan] all - Respond on all channels")
|
||||
console.print("[cyan]2.[/cyan] whitelist - Only respond on specific channels")
|
||||
sel = IntPrompt.ask("Select", default=1 if self.config.channels.mode == "all" else 2)
|
||||
value = "all" if sel == 1 else "whitelist"
|
||||
if value != self.config.channels.mode:
|
||||
self.config.channels.mode = value
|
||||
self.modified = True
|
||||
elif choice == 2:
|
||||
value = Prompt.ask(
|
||||
"Whitelist (comma-separated)", default=whitelist_str
|
||||
)
|
||||
try:
|
||||
channels = [int(c.strip()) for c in value.split(",") if c.strip()]
|
||||
if channels != self.config.channels.whitelist:
|
||||
self.config.channels.whitelist = channels
|
||||
self.modified = True
|
||||
except ValueError:
|
||||
console.print("[red]Invalid input. Use comma-separated numbers.[/red]")
|
||||
|
||||
def _history_settings(self) -> None:
|
||||
"""History settings submenu."""
|
||||
while True:
|
||||
|
|
@ -603,7 +545,7 @@ class Configurator:
|
|||
|
||||
# Step 1: Bot identity
|
||||
console.print("[bold cyan]Step 1: Bot Identity[/bold cyan]")
|
||||
self.config.bot.name = Prompt.ask("Bot name (for @mentions)", default="ai")
|
||||
self.config.bot.name = Prompt.ask("Bot name", default="ai")
|
||||
self.config.bot.owner = Prompt.ask("Your name/callsign", default="")
|
||||
console.print()
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ class BotConfig:
|
|||
|
||||
name: str = "ai"
|
||||
owner: str = ""
|
||||
respond_to_mentions: bool = True
|
||||
respond_to_dms: bool = True
|
||||
filter_bbs_protocols: bool = True
|
||||
|
||||
|
|
@ -32,14 +31,6 @@ class ConnectionConfig:
|
|||
tcp_port: int = 4403
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChannelsConfig:
|
||||
"""Channel filtering settings."""
|
||||
|
||||
mode: str = "all" # all or whitelist
|
||||
whitelist: list[int] = field(default_factory=lambda: [0])
|
||||
|
||||
|
||||
@dataclass
|
||||
class ResponseConfig:
|
||||
"""Response behavior settings."""
|
||||
|
|
@ -134,7 +125,6 @@ class Config:
|
|||
|
||||
bot: BotConfig = field(default_factory=BotConfig)
|
||||
connection: ConnectionConfig = field(default_factory=ConnectionConfig)
|
||||
channels: ChannelsConfig = field(default_factory=ChannelsConfig)
|
||||
response: ResponseConfig = field(default_factory=ResponseConfig)
|
||||
history: HistoryConfig = field(default_factory=HistoryConfig)
|
||||
memory: MemoryConfig = field(default_factory=MemoryConfig)
|
||||
|
|
|
|||
|
|
@ -174,22 +174,12 @@ class MeshAI:
|
|||
if not response:
|
||||
return
|
||||
|
||||
# Send response
|
||||
if message.is_dm:
|
||||
await self.responder.send_response(
|
||||
text=response,
|
||||
destination=message.sender_id,
|
||||
channel=message.channel,
|
||||
)
|
||||
else:
|
||||
formatted = self.responder.format_channel_response(
|
||||
response, message.sender_name, mention_sender=True
|
||||
)
|
||||
await self.responder.send_response(
|
||||
text=formatted,
|
||||
destination=None,
|
||||
channel=message.channel,
|
||||
)
|
||||
# Send DM response
|
||||
await self.responder.send_response(
|
||||
text=response,
|
||||
destination=message.sender_id,
|
||||
channel=message.channel,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling message: {e}", exc_info=True)
|
||||
|
|
|
|||
|
|
@ -137,24 +137,3 @@ class Responder:
|
|||
return pos
|
||||
|
||||
return 0
|
||||
|
||||
def format_channel_response(
|
||||
self, text: str, sender_name: str, mention_sender: bool = False
|
||||
) -> str:
|
||||
"""Format response for channel context.
|
||||
|
||||
Args:
|
||||
text: Response text
|
||||
sender_name: Name of sender being replied to
|
||||
mention_sender: Whether to prefix with sender's name
|
||||
|
||||
Returns:
|
||||
Formatted response
|
||||
"""
|
||||
if mention_sender:
|
||||
# Check if adding prefix would exceed max length
|
||||
prefix = f"@{sender_name}: "
|
||||
if len(prefix) + len(text) <= self.config.max_length * self.config.max_messages:
|
||||
return prefix + text
|
||||
|
||||
return text
|
||||
|
|
|
|||
|
|
@ -68,13 +68,13 @@ class MessageRouter:
|
|||
self.dispatcher = dispatcher
|
||||
self.llm = llm_backend
|
||||
|
||||
# Compile mention pattern
|
||||
bot_name = re.escape(config.bot.name)
|
||||
self._mention_pattern = re.compile(rf"@{bot_name}\b", re.IGNORECASE)
|
||||
|
||||
def should_respond(self, message: MeshMessage) -> bool:
|
||||
"""Determine if we should respond to this message.
|
||||
|
||||
DM-only bot: ignores all public channel messages.
|
||||
Commands and conversational LLM responses both work in DMs.
|
||||
|
||||
Args:
|
||||
message: Incoming message
|
||||
|
||||
|
|
@ -85,35 +85,20 @@ class MessageRouter:
|
|||
if message.sender_id == self.connector.my_node_id:
|
||||
return False
|
||||
|
||||
# Only respond to DMs
|
||||
if not message.is_dm:
|
||||
return False
|
||||
|
||||
if not self.config.bot.respond_to_dms:
|
||||
return False
|
||||
|
||||
# Ignore advBBS protocol and notification messages
|
||||
if self.config.bot.filter_bbs_protocols:
|
||||
if any(message.text.startswith(p) for p in ADVBBS_PREFIXES):
|
||||
logger.debug(f"Ignoring advBBS message from {message.sender_id}: {message.text[:40]}...")
|
||||
return False
|
||||
|
||||
# Check if DM — conversational mode only, skip !commands
|
||||
# (let MeshMonitor or other bots handle bang commands in DMs)
|
||||
if message.is_dm:
|
||||
if self.dispatcher.is_command(message.text):
|
||||
return False
|
||||
return self.config.bot.respond_to_dms
|
||||
|
||||
# Check channel filtering
|
||||
if self.config.channels.mode == "whitelist":
|
||||
if message.channel not in self.config.channels.whitelist:
|
||||
return False
|
||||
|
||||
# Check for @mention
|
||||
if self.config.bot.respond_to_mentions:
|
||||
if self._mention_pattern.search(message.text):
|
||||
return True
|
||||
|
||||
# Check for bang command (always respond to commands)
|
||||
if self.dispatcher.is_command(message.text):
|
||||
return True
|
||||
|
||||
# Not a DM, no mention, no command - ignore
|
||||
return False
|
||||
return True
|
||||
|
||||
async def route(self, message: MeshMessage) -> RouteResult:
|
||||
"""Route a message and generate response.
|
||||
|
|
@ -200,11 +185,8 @@ class MessageRouter:
|
|||
logger.debug(f"Persisted summary for {user_id}")
|
||||
|
||||
def _clean_query(self, text: str) -> str:
|
||||
"""Remove @mention and check for prompt injection."""
|
||||
# Remove @botname mention
|
||||
cleaned = self._mention_pattern.sub("", text)
|
||||
# Clean up extra whitespace
|
||||
cleaned = " ".join(cleaned.split())
|
||||
"""Clean up query text and check for prompt injection."""
|
||||
cleaned = " ".join(text.split())
|
||||
cleaned = cleaned.strip()
|
||||
|
||||
# Check for prompt injection
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue