Merge pull request 'Filter ANSI output before Slack forwarding' (#3) from fix/slack-ansi-filter into main

Reviewed-on: #3
This commit was merged in pull request #3.
This commit is contained in:
2026-02-17 03:53:08 +09:00
3 changed files with 63 additions and 5 deletions

View File

@@ -8,6 +8,7 @@ import threading
import time
from lazy_enter.config import Config
from lazy_enter.output_filter import clean_terminal_output
from lazy_enter.pty_manager import PtyManager
from lazy_enter.slack_handler import SlackHandler
@@ -24,6 +25,7 @@ class Bridge:
self._output_thread: threading.Thread | None = None
self._running = False
self._channel: str = self.config.allowed_channel_id
self._last_sent_output: str = ""
self._last_input_at = time.monotonic()
self._last_output_at = time.monotonic()
self._input_idle_reported = False
@@ -79,6 +81,7 @@ class Bridge:
return
self._channel = channel
self._last_sent_output = ""
self.pty = PtyManager(self.config.default_shell)
self.pty.start()
self._running = True
@@ -97,6 +100,7 @@ class Bridge:
if self.pty:
self.pty.stop()
self.pty = None
self._last_sent_output = ""
self.slack.send_message(channel, ":stop_sign: 세션이 종료되었습니다.")
def _poll_output(self) -> None:
@@ -106,16 +110,20 @@ class Bridge:
now = time.monotonic()
output = self.pty.read_output(timeout=self.config.pty_read_timeout)
if output:
buffer += output
self._last_output_at = now
self._output_idle_reported = False
cleaned = clean_terminal_output(output)
if cleaned:
buffer += f"{cleaned}\n"
self._last_output_at = now
self._output_idle_reported = False
if buffer:
# 메시지 길이 제한 적용
message = buffer[: self.config.max_message_length]
message = buffer[: self.config.max_message_length].rstrip()
if len(buffer) > self.config.max_message_length:
message += "\n... (truncated)"
self.slack.send_message(self._channel, f"```\n{message}\n```")
if message and message != self._last_sent_output:
self.slack.send_message(self._channel, f"```\n{message}\n```")
self._last_sent_output = message
buffer = ""
output_idle = now - self._last_output_at

View File

@@ -0,0 +1,32 @@
"""PTY 출력에서 터미널 제어 시퀀스를 제거한다."""
from __future__ import annotations
import re
# ANSI CSI / OSC / DCS / 단일 ESC 시퀀스를 폭넓게 제거한다.
_ANSI_ESCAPE_RE = re.compile(
r"(?:\x1B\[[0-?]*[ -/]*[@-~])"
r"|(?:\x1B\][^\x1B\x07]*(?:\x07|\x1B\\))"
r"|(?:\x1BP[^\x1B]*(?:\x1B\\))"
r"|(?:\x1B[@-_])"
)
# ESC 문자가 유실된 상태로 남는 CSI 토큰(예: [38;2;153;153;153m) 제거.
_BROKEN_CSI_RE = re.compile(r"\[(?:[0-9;<=>?]|(?:\d+;))*\d*[A-Za-z]")
# C0 제어문자 중 개행/탭/캐리지리턴을 제외하고 제거.
_CONTROL_RE = re.compile(r"[\x00-\x08\x0B-\x1F\x7F]")
def clean_terminal_output(text: str) -> str:
"""Slack 전송용 텍스트를 정리한다."""
cleaned = _ANSI_ESCAPE_RE.sub("", text)
cleaned = _BROKEN_CSI_RE.sub("", cleaned)
cleaned = cleaned.replace("\r", "")
cleaned = _CONTROL_RE.sub("", cleaned)
# 제어 코드 제거 후 남는 빈 줄을 축소한다.
lines = [line.rstrip() for line in cleaned.splitlines()]
compact = "\n".join(lines).strip()
return compact

View File

@@ -0,0 +1,18 @@
"""output_filter 모듈 테스트."""
from lazy_enter.output_filter import clean_terminal_output
def test_clean_terminal_output_removes_ansi_sequences():
raw = "\x1b[?2004h\x1b[38;2;153;153;153mClaude Code\x1b[39m"
assert clean_terminal_output(raw) == "Claude Code"
def test_clean_terminal_output_removes_broken_csi_tokens():
raw = "[?2004h[38;2;153;153;153mhello[39m"
assert clean_terminal_output(raw) == "hello"
def test_clean_terminal_output_preserves_plain_text():
raw = "line1\nline2"
assert clean_terminal_output(raw) == "line1\nline2"