# modules/persistent_terminal.py - 持久化终端实例（修复版）

import asyncio
import subprocess
import os
import sys
import time
import signal
from pathlib import Path
from typing import Optional, Callable, Dict, List, Tuple
from datetime import datetime
import threading
import queue
from collections import deque
import shutil
import uuid
import codecs
from modules.host_sandbox_runner import (
    HostSandboxError,
    build_host_sandbox_shell_plan,
    host_sandbox_enabled,
)
try:
    from config import (
        OUTPUT_FORMATS,
        TERMINAL_OUTPUT_WAIT,
        TERMINAL_INPUT_MAX_CHARS,
        TERMINAL_SANDBOX_MODE,
        TERMINAL_SANDBOX_IMAGE,
        TERMINAL_SANDBOX_MOUNT_PATH,
        TERMINAL_SANDBOX_SHELL,
        TERMINAL_SANDBOX_NETWORK,
        TERMINAL_SANDBOX_CPUS,
        TERMINAL_SANDBOX_MEMORY,
        TERMINAL_SANDBOX_BINDS,
        TERMINAL_SANDBOX_BIN,
        TERMINAL_SANDBOX_NAME_PREFIX,
        TERMINAL_SANDBOX_ENV,
        TERMINAL_SANDBOX_REQUIRE,
    )
except ImportError:
    import sys
    from pathlib import Path
    project_root = Path(__file__).resolve().parents[1]
    if str(project_root) not in sys.path:
        sys.path.insert(0, str(project_root))
    from config import (
        OUTPUT_FORMATS,
        TERMINAL_OUTPUT_WAIT,
        TERMINAL_INPUT_MAX_CHARS,
        TERMINAL_SANDBOX_MODE,
        TERMINAL_SANDBOX_IMAGE,
        TERMINAL_SANDBOX_MOUNT_PATH,
        TERMINAL_SANDBOX_SHELL,
        TERMINAL_SANDBOX_NETWORK,
        TERMINAL_SANDBOX_CPUS,
        TERMINAL_SANDBOX_MEMORY,
        TERMINAL_SANDBOX_BINDS,
        TERMINAL_SANDBOX_BIN,
        TERMINAL_SANDBOX_NAME_PREFIX,
        TERMINAL_SANDBOX_ENV,
        TERMINAL_SANDBOX_REQUIRE,
    )

class PersistentTerminal:
    """单个持久化终端实例"""
    
    def __init__(
        self,
        session_name: str,
        working_dir: str = None,
        shell_command: str = None,
        broadcast_callback: Callable = None,
        max_buffer_size: int = 20000,
        display_size: int = 5000,
        project_path: Optional[str] = None,
        sandbox_mode: Optional[str] = None,
        sandbox_options: Optional[Dict] = None,
    ):
        """
        初始化持久化终端
        
        Args:
            session_name: 会话名称
            working_dir: 工作目录
            shell_command: shell命令（None则自动选择）
            broadcast_callback: 广播回调函数（用于WebSocket）
            max_buffer_size: 最大缓冲区大小
            display_size: 显示大小限制
        """
        self.session_name = session_name
        self.working_dir = Path(working_dir).resolve() if working_dir else Path.cwd()
        self.project_path = Path(project_path).resolve() if project_path else self.working_dir
        self.host_shell_command = shell_command
        self.shell_command = shell_command
        self.broadcast = broadcast_callback
        self.max_buffer_size = max_buffer_size
        self.display_size = display_size
        
        # 进程相关
        self.process = None
        self.is_running = False
        self.start_time = None
        
        # 输出缓冲
        self.output_buffer = []
        self.command_history = []
        self.total_output_size = 0
        self.truncated_lines = 0
        self.output_history = deque()
        self._output_event_counter = 0
        self.last_output_time = None
        self.last_input_time = None
        self.last_input_text = ""
        self.echo_loop_detected = False
        self._consecutive_echo_matches = 0
        self.io_history = deque()
        self._io_history_max = 4000

        # 线程和队列
        self.output_queue = queue.Queue()
        self.reader_thread = None
        self.is_reading = False
        self._decoder = None
        
        # 状态标志
        self.is_interactive = False  # 是否在等待输入
        self.last_command = ""
        self.last_activity = time.time()
        
        # 系统特定设置
        self.is_windows = sys.platform == "win32"
        sandbox_defaults = {
            "image": TERMINAL_SANDBOX_IMAGE,
            "mount_path": TERMINAL_SANDBOX_MOUNT_PATH,
            "shell": TERMINAL_SANDBOX_SHELL,
            "network": TERMINAL_SANDBOX_NETWORK,
            "cpus": TERMINAL_SANDBOX_CPUS,
            "memory": TERMINAL_SANDBOX_MEMORY,
            "binds": list(TERMINAL_SANDBOX_BINDS),
            "bin": TERMINAL_SANDBOX_BIN,
            "name_prefix": TERMINAL_SANDBOX_NAME_PREFIX,
            "env": dict(TERMINAL_SANDBOX_ENV),
            "require": TERMINAL_SANDBOX_REQUIRE,
        }
        if sandbox_options:
            for key, value in sandbox_options.items():
                if key == "binds" and isinstance(value, list):
                    sandbox_defaults[key] = list(value)
                elif key == "env" and isinstance(value, dict):
                    sandbox_defaults[key] = dict(value)
                else:
                    sandbox_defaults[key] = value
        self.sandbox_mode = (sandbox_mode or TERMINAL_SANDBOX_MODE or "host").lower()
        self.sandbox_options = sandbox_defaults
        self.sandbox_required = bool(self.sandbox_options.get("require"))
        self.allow_direct_host_execution = bool(self.sandbox_options.get("allow_direct_host_execution", False))
        self.sandbox_container_name = None
        self.execution_mode = "host"
        self.using_container = False
        self._sandbox_bin_path = None
        self._owns_container = False
    
    def start(self) -> bool:
        """启动终端进程（支持容器沙箱）"""
        if self.is_running:
            return False

        try:
            process = None
            selected_mode = self.sandbox_mode
            if selected_mode == "docker":
                try:
                    process = self._start_docker_terminal()
                except Exception as exc:
                    message = f"容器终端启动失败: {exc}"
                    print(f"{OUTPUT_FORMATS['error']} {message}")
                    return False
            if process is None:
                process = self._start_host_terminal()
                selected_mode = "host"

            if not process:
                return False

            self.process = process
            self.is_running = True
            self.execution_mode = "docker" if self.using_container else "host"
            self.start_time = datetime.now()
            self.last_output_time = None
            self.last_input_time = None
            self.last_input_text = ""
            self.echo_loop_detected = False
            self._consecutive_echo_matches = 0

            # 启动输出读取线程
            self.is_reading = True
            self.reader_thread = threading.Thread(target=self._read_output)
            self.reader_thread.daemon = True
            self.reader_thread.start()

            # 宿主机Windows初始化
            if self.is_windows and not self.using_container:
                time.sleep(0.5)
                self.send_command("chcp 65001", timeout=1)
                time.sleep(0.5)
                self.send_command("cls", timeout=1)
                time.sleep(0.3)
                self.output_buffer.clear()
                self.total_output_size = 0

            # 广播终端启动事件
            if self.broadcast:
                self.broadcast('terminal_started', {
                    'session': self.session_name,
                    'working_dir': str(self.working_dir),
                    'shell': self.shell_command,
                    'mode': self.execution_mode,
                    'time': self.start_time.isoformat()
                })

            mode_label = "容器" if self.using_container else "宿主机"
            print(f"{OUTPUT_FORMATS['success']} 终端会话启动({mode_label}): {self.session_name}")
            return True

        except Exception as e:
            print(f"{OUTPUT_FORMATS['error']} 终端启动失败: {e}")
            self.is_running = False
            if self.using_container:
                self._stop_sandbox_container(force=True)
            return False

    def _start_host_terminal(self):
        """启动宿主机终端"""
        if self.allow_direct_host_execution:
            return self._start_plain_host_terminal()
        if not host_sandbox_enabled():
            raise RuntimeError("宿主机沙箱已禁用（HOST_SANDBOX_ENABLED=0），拒绝启动 host 终端。")
        self.using_container = False
        self._owns_container = False
        self.is_windows = sys.platform == "win32"

        env = os.environ.copy()
        env['PYTHONIOENCODING'] = 'utf-8'
        env['TERM'] = 'xterm-256color'
        env['LANG'] = 'en_US.UTF-8'
        env['LC_ALL'] = 'en_US.UTF-8'

        try:
            plan = build_host_sandbox_shell_plan(self.working_dir, env)
            cmd_args = plan.command
            pass_fds = ()
            seccomp_fd = None
            if plan.seccomp_bpf_path:
                seccomp_fd = os.open(plan.seccomp_bpf_path, os.O_RDONLY)
                cmd_args = [str(seccomp_fd) if token == "__SECCOMP_FD__" else token for token in cmd_args]
                pass_fds = (seccomp_fd,)
            try:
                process = subprocess.Popen(
                    cmd_args,
                    stdin=subprocess.PIPE,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.STDOUT,
                    cwd=plan.cwd or str(self.working_dir),
                    shell=False,
                    bufsize=0,
                    env=plan.env,
                    pass_fds=pass_fds
                )
            finally:
                if seccomp_fd is not None:
                    try:
                        os.close(seccomp_fd)
                    except OSError:
                        pass
            self.shell_command = "host-sandbox-shell"
            self.is_windows = False
            return process
        except HostSandboxError as exc:
            raise RuntimeError(str(exc))
        except FileNotFoundError:
            print(f"{OUTPUT_FORMATS['error']} 无法找到宿主机沙箱终端运行时")
            return None

    def _start_plain_host_terminal(self):
        self.using_container = False
        self._owns_container = False
        self.is_windows = sys.platform == "win32"
        shell_cmd = self.host_shell_command
        if self.is_windows:
            shell_cmd = shell_cmd or "cmd.exe"
        else:
            shell_cmd = shell_cmd or os.environ.get('SHELL', '/bin/bash')
        self.shell_command = shell_cmd
        env = os.environ.copy()
        env['PYTHONIOENCODING'] = 'utf-8'
        env['TERM'] = 'xterm-256color'
        env['LANG'] = 'en_US.UTF-8'
        env['LC_ALL'] = 'en_US.UTF-8'
        process = subprocess.Popen(
            shell_cmd,
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            cwd=str(self.working_dir),
            shell=False,
            bufsize=0,
            env=env
        )
        return process

    def _start_docker_terminal(self):
        """启动或连接容器化终端。"""
        docker_bin = self.sandbox_options.get("bin") or "docker"
        docker_path = shutil.which(docker_bin)
        if not docker_path:
            message = f"未找到容器运行时: {docker_bin}"
            if self.sandbox_required:
                raise RuntimeError(message)
            print(f"{OUTPUT_FORMATS['warning']} {message}")
            return None

        self._sandbox_bin_path = docker_path
        target_container = self.sandbox_options.get("container_name")
        if target_container:
            return self._start_existing_container_terminal(docker_path, target_container)
        return self._start_new_container_terminal(docker_path)

    def _start_existing_container_terminal(self, docker_path: str, container_name: str):
        """通过 docker exec 连接到已有容器。"""
        if not self._ensure_container_alive(docker_path, container_name):
            raise RuntimeError(f"目标容器未运行: {container_name}")

        mount_path = self.sandbox_options.get("mount_path") or "/workspace"
        container_workdir = self._resolve_container_workdir(mount_path)
        shell_path = self.sandbox_options.get("shell") or "/bin/bash"
        cmd = [
            docker_path,
            "exec",
            "-i",
        ]
        if container_workdir:
            cmd += ["-w", container_workdir]

        envs = {
            "PYTHONIOENCODING": "utf-8",
            "TERM": "xterm-256color",
        }
        for key, value in (self.sandbox_options.get("env") or {}).items():
            if value is not None:
                envs[key] = value
        for key, value in envs.items():
            cmd += ["-e", f"{key}={value}"]

        cmd.append(container_name)
        cmd.append(shell_path)
        if shell_path.endswith("sh"):
            cmd.append("-i")

        env = os.environ.copy()
        process = subprocess.Popen(
            cmd,
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            bufsize=0,
            env=env
        )

        self.sandbox_container_name = container_name
        self.shell_command = f"{shell_path} (attach:{container_name})"
        self.using_container = True
        self.is_windows = False
        self._owns_container = False
        return process

    def _start_new_container_terminal(self, docker_path: str):
        """启动全新的容器终端。"""
        image = self.sandbox_options.get("image")
        if not image:
            raise RuntimeError("TERMINAL_SANDBOX_IMAGE 未配置")

        mount_path = self.sandbox_options.get("mount_path") or "/workspace"
        working_dir = str(self.working_dir)
        if not self.working_dir.exists():
            self.working_dir.mkdir(parents=True, exist_ok=True)

        container_name = f"{self.sandbox_options.get('name_prefix', 'agent-term')}-{uuid.uuid4().hex[:10]}"
        cmd = [
            docker_path,
            "run",
            "--rm",
            "-i",
        ]
        cmd += [
            "--name",
            container_name,
            "-w",
            mount_path,
            "-v",
            f"{working_dir}:{mount_path}",
        ]

        network = self.sandbox_options.get("network")
        if network:
            cmd += ["--network", network]
        cpus = self.sandbox_options.get("cpus")
        if cpus:
            cmd += ["--cpus", cpus]
        memory = self.sandbox_options.get("memory")
        if memory:
            cmd += ["--memory", memory]

        for bind in self.sandbox_options.get("binds", []):
            bind = bind.strip()
            if bind:
                cmd += ["-v", bind]

        envs = {
            "PYTHONIOENCODING": "utf-8",
            "TERM": "xterm-256color",
        }
        for key, value in (self.sandbox_options.get("env") or {}).items():
            if value is not None:
                envs[key] = value
        for key, value in envs.items():
            cmd += ["-e", f"{key}={value}"]

        cmd.append(image)
        shell_path = self.sandbox_options.get("shell") or "/bin/bash"
        if shell_path:
            cmd.append(shell_path)
            if shell_path.endswith("sh"):
                cmd.append("-i")

        env = os.environ.copy()
        try:
            process = subprocess.Popen(
                cmd,
                stdin=subprocess.PIPE,
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                bufsize=0,
                env=env
            )
        except FileNotFoundError:
            message = f"无法执行容器运行时: {docker_path}"
            if self.sandbox_required:
                raise RuntimeError(message)
            print(f"{OUTPUT_FORMATS['warning']} {message}")
            return None

        self.sandbox_container_name = container_name
        self.shell_command = f"{shell_path} (sandbox:{image})"
        self.using_container = True
        self.is_windows = False
        self._owns_container = True
        return process

    def _resolve_container_workdir(self, mount_path: str) -> str:
        """推导容器内工作目录路径。"""
        mount_path = (mount_path or "/workspace").rstrip("/") or "/workspace"
        try:
            relative = self.working_dir.relative_to(self.project_path)
            if str(relative) == ".":
                return mount_path
            return f"{mount_path}/{relative.as_posix()}"
        except Exception:
            return mount_path

    def _ensure_container_alive(self, docker_path: str, container_name: str) -> bool:
        """确认目标容器正在运行。"""
        try:
            result = subprocess.run(
                [
                    docker_path,
                    "inspect",
                    "-f",
                    "{{.State.Running}}",
                    container_name,
                ],
                stdout=subprocess.PIPE,
                stderr=subprocess.DEVNULL,
                text=True,
                timeout=3,
                check=False,
            )
        except (OSError, subprocess.SubprocessError):
            return False
        return result.returncode == 0 and result.stdout.strip().lower() == "true"
    
    def _read_output(self):
        """后台线程：持续读取输出（修复版，正确处理编码）"""
        if self._decoder is None:
            encoding = 'gbk' if self.is_windows else 'utf-8'
            self._decoder = codecs.getincrementaldecoder(encoding)(errors='replace')
        while self.is_reading and self.process:
            try:
                # 读取任意字节块，不依赖换行符
                chunk = self.process.stdout.read(1024)
                if chunk:
                    text = self._decoder.decode(chunk)
                    if text:
                        self.output_queue.put(text)
                        self._process_output(text)
                elif self.process.poll() is not None:
                    # 进程已结束
                    self.is_running = False
                    break
                else:
                    # 没有输出，短暂休眠
                    time.sleep(0.01)
                    
            except Exception as e:
                # 不要因为单个错误而停止
                print(f"[Terminal] 读取输出警告: {e}")
                time.sleep(0.01)
                continue
    
    def _decode_output(self, data):
        """安全地解码输出"""
        # 如果已经是字符串，直接返回
        if isinstance(data, str):
            return data
        
        # 如果是字节，尝试解码
        if isinstance(data, bytes):
            # Windows系统尝试的编码顺序
            if self.is_windows:
                encodings = ['utf-8', 'gbk', 'gb2312', 'cp936', 'latin-1']
            else:
                encodings = ['utf-8', 'latin-1']
            
            for encoding in encodings:
                try:
                    return data.decode(encoding)
                except (UnicodeDecodeError, AttributeError):
                    continue
            
            # 如果所有编码都失败，使用替换模式
            return data.decode('utf-8', errors='replace')
        
        # 其他类型，转换为字符串
        return str(data)
    
    def _process_output(self, output: str):
        """处理输出行"""
        now = time.time()
        noisy_markers = (
            "bash: cannot set terminal process group",
            "bash: no job control in this shell",
        )
        for line in output.splitlines(keepends=True):
            if any(noise in line for noise in noisy_markers):
                continue
            self.output_buffer.append(line)
            self.total_output_size += len(line)
            now = time.time()
            self.last_output_time = now
            
            # 记录输出事件
            self._output_event_counter += 1
            self.output_history.append((self._output_event_counter, now, line))
            self._append_io_event('output', line, timestamp=now)

        # 控制输出历史长度
        if len(self.output_history) > 2000:
            self.output_history.popleft()

        # 检查是否需要截断
        if self.total_output_size > self.max_buffer_size:
            self._truncate_buffer()
        
        # 更新活动时间
        self.last_activity = now

        # 检测命令回显死循环
        cleaned_output = output.replace('\r', '').strip()
        cleaned_input = self.last_input_text.strip() if self.last_input_text else ""
        if cleaned_output and cleaned_input and cleaned_output == cleaned_input:
            self._consecutive_echo_matches += 1
        else:
            self._consecutive_echo_matches = 0
            if cleaned_output:
                self.echo_loop_detected = False
        if self._consecutive_echo_matches >= 1 and self.last_input_time:
            if now - self.last_input_time <= 2:
                self.echo_loop_detected = True
        
        # 检测交互式提示
        self._detect_interactive_prompt(output)
        
        # 广播输出
        if self.broadcast:
            self.broadcast('terminal_output', {
                'session': self.session_name,
                'data': output,
                'timestamp': time.time()
            })
    
    def _truncate_buffer(self):
        """截断缓冲区以保持在限制内"""
        # 保留最后的N个字符
        while self.total_output_size > self.max_buffer_size and self.output_buffer:
            removed = self.output_buffer.pop(0)
            self.total_output_size -= len(removed)
            self.truncated_lines += 1
            if self.output_history:
                self.output_history.popleft()
    
    def _detect_interactive_prompt(self, output: str):
        """检测是否在等待交互输入"""
        self.is_interactive = False
        # 常见的交互提示模式
        interactive_patterns = [
            "? ",  # 问题提示
            ": ",  # 输入提示
            "> ",  # 命令提示
            "$ ",  # shell提示
            "# ",  # root提示
            ">>> ",  # Python提示
            "... ",  # Python续行
            "(y/n)",  # 确认提示
            "[Y/n]",  # 确认提示
            "Password:",  # 密码提示
            "password:",  # 密码提示
            "Enter",  # 输入提示
            "选择",  # 中文选择
            "请输入",  # 中文输入
        ]
        
        output_lower = output.lower().strip()
        for pattern in interactive_patterns:
            if pattern.lower() in output_lower:
                self.is_interactive = True
                return
        
        # 如果输出以常见提示符结尾且没有换行，也认为是交互式
        if output and not output.endswith('\n'):
            last_chars = output.strip()[-3:]
            if last_chars in ['> ', '$ ', '# ', ': ']:
                self.is_interactive = True
    
    def _capture_history_marker(self) -> int:
        return self._output_event_counter

    def _get_output_since_marker(self, marker: int) -> str:
        if marker is None:
            return ''.join(item[2] for item in self.output_history)
        return ''.join(item[2] for item in self.output_history if item[0] > marker)

    def _append_io_event(self, event_type: str, data: str, timestamp: Optional[float] = None):
        """记录终端输入输出事件"""
        if timestamp is None:
            timestamp = time.time()
        self.io_history.append((event_type, timestamp, data))
        while len(self.io_history) > self._io_history_max:
            self.io_history.popleft()

    def _seconds_since_last_output(self) -> Optional[float]:
        if not self.last_output_time:
            return None
        return round(time.time() - self.last_output_time, 3)

    def send_command(
        self,
        command: str,
        timeout: float = None,
        timeout_cutoff: float = None,
        enforce_full_timeout: bool = False,
        sentinel: str = None,
    ) -> Dict:
        """
        发送命令到终端（统一编码处理）。

        Args:
            command: 要执行的命令文本
            timeout: 等待输出的最大秒数（可大于真实超时，用于等待收尾输出）
            timeout_cutoff: 将耗时大于此值视为超时，用于外层业务区分；默认为 timeout
            enforce_full_timeout: 若为 True，则不因空闲提前返回（除非捕获 sentinel）
            sentinel: 若提供，在输出中捕获到该标记即认为命令结束，并从输出中移除
        """
        if not self.is_running or not self.process:
            return {
                "success": False,
                "error": "终端未运行，请先打开终端会话。",
                "session": self.session_name
            }
        
        try:
            # 清空残留输出，防止上一条命令的输出干扰
            try:
                while True:
                    self.output_queue.get_nowait()
            except queue.Empty:
                pass

            marker = self._capture_history_marker()
            if timeout is None:
                timeout = TERMINAL_OUTPUT_WAIT
            else:
                try:
                    timeout = float(timeout)
                except (TypeError, ValueError):
                    timeout = TERMINAL_OUTPUT_WAIT
            if timeout < 0:
                timeout = 0
            start_time = time.time()
            command_text = command.rstrip('\n')
            # 记录命令
            self.command_history.append({
                "command": command_text,
                "timestamp": datetime.now().isoformat()
            })
            self.last_command = command_text
            self.is_interactive = False
            self.last_input_text = command_text
            self.last_input_time = time.time()
            self.echo_loop_detected = False
            self._consecutive_echo_matches = 0
            self._append_io_event('input', command_text + '\n', timestamp=self.last_input_time)

            # 广播输入事件
            if self.broadcast:
                self.broadcast('terminal_input', {
                    'session': self.session_name,
                    'data': command_text + '\n',
                    'timestamp': time.time()
                })
            
            # 确保命令有换行符
            to_send = command if command.endswith('\n') else command + '\n'
            
            # 发送命令（统一使用UTF-8编码）
            try:
                # 首先尝试UTF-8
                command_bytes = to_send.encode('utf-8')
            except UnicodeEncodeError:
                # 如果UTF-8失败，Windows系统尝试GBK
                if self.is_windows:
                    command_bytes = to_send.encode('gbk', errors='replace')
                else:
                    command_bytes = to_send.encode('utf-8', errors='replace')
            
            try:
                self.process.stdin.write(command_bytes)
                self.process.stdin.flush()
            except Exception:
                return {
                    "success": False,
                    "error": "终端已不可用或输入失败，请重新打开终端会话。",
                    "session": self.session_name
                }
            
            # 等待输出
            output, timed_out, marker_seen = self._wait_for_output(
                timeout=timeout,
                timeout_cutoff=timeout_cutoff,
                enforce_full_timeout=enforce_full_timeout,
                sentinel=sentinel,
                command_echo=command_text,
            )
            recent_output = self._get_output_since_marker(marker)
            if recent_output:
                if sentinel:
                    recent_output = recent_output.replace(sentinel, "")
                output = recent_output
            output = self._clean_output(output, command_text, sentinel)
            output_truncated = False
            if len(output) > TERMINAL_INPUT_MAX_CHARS:
                output = output[-TERMINAL_INPUT_MAX_CHARS:]
                output_truncated = True
            output_clean = output.strip()
            has_output = bool(output_clean)
            status = "completed"
            if timed_out:
                status = "timeout"
            elif not has_output:
                if self.echo_loop_detected:
                    status = "echo_loop"
                elif self.is_interactive:
                    status = "awaiting_input"
                else:
                    status = "no_output"
            else:
                if self.echo_loop_detected:
                    status = "output_with_echo"
            if marker_seen and status == "completed":
                # 明确捕获到结束标记，视为完成
                status = "completed"
            message_map = {
                "completed": "命令执行完成",
                "no_output": "未捕获输出，命令可能未产生结果",
                "awaiting_input": "命令已发送，终端等待进一步输入或仍在运行",
                "echo_loop": "检测到终端正在回显输入，命令可能未成功执行",
                "output_with_echo": "命令产生输出，但终端疑似重复回显",
                "timeout": f"输出等待达到上限（{int(timeout)}秒）"
            }
            if timeout >= 60:
                message = f"[已收集约{int(timeout)}秒内的输出]"
            else:
                message = message_map.get(status, "命令执行完成")
            if output_truncated:
                message += f"（输出已截断，保留末尾{TERMINAL_INPUT_MAX_CHARS}字符）"
            elapsed_ms = int((time.time() - start_time) * 1000)
            return {
                "success": status in {"completed", "output_with_echo"},
                "session": self.session_name,
                "command": command_text,
                "output": output,
                "message": message,
                "status": status,
                "truncated": output_truncated,
                "elapsed_ms": elapsed_ms,
                "timeout": timeout_cutoff or timeout
            }
                
        except Exception as e:
            error_msg = f"发送命令失败: {str(e)}"
            print(f"{OUTPUT_FORMATS['error']} {error_msg}")
            return {
                "success": False,
                "error": error_msg,
                "session": self.session_name
            }

    def _wait_for_output(
        self,
        timeout: float = 5,
        timeout_cutoff: Optional[float] = None,
        enforce_full_timeout: bool = False,
        sentinel: Optional[str] = None,
        command_echo: Optional[str] = None,
    ) -> Tuple[str, bool, bool]:
        """
        等待并收集输出，返回 (output, timed_out, marker_seen)。

        - 若提供 sentinel，捕获后立即返回（仍会吸干队列中的剩余片段）。
        - 若 enforce_full_timeout=True，则不因空闲提前返回；否则在输出后短暂空闲可提前返回。
        - timed_out 判定使用 timeout_cutoff（若未提供则与 timeout 相同）。
        """
        collected_output = []
        start_time = time.time()
        last_output_time = start_time
        output_seen = False
        marker_seen = False

        if timeout is None or timeout <= 0:
            timeout = 0

        if timeout == 0:
            try:
                while True:
                    output = self.output_queue.get_nowait()
                    if sentinel and sentinel in output:
                        output = output.replace(sentinel, "")
                        marker_seen = True
                    if output:
                        collected_output.append(output)
                # unreachable
            except queue.Empty:
                return ''.join(collected_output), False, marker_seen

        end_time = start_time + timeout
        cutoff = timeout_cutoff if timeout_cutoff is not None else timeout
        # 空闲提前返回仅在未强制等待且未使用结束标记时有效
        idle_threshold = None if enforce_full_timeout or sentinel else 1.5

        while True:
            now = time.time()
            if now >= end_time:
                break
            remaining = max(0.05, min(0.5, end_time - now))
            try:
                output = self.output_queue.get(timeout=remaining)
                if sentinel and sentinel in output:
                    # 避免把命令回显中的标记误判为完成信号
                    if command_echo and command_echo in output:
                        output = output.replace(sentinel, "")
                    else:
                        output = output.replace(sentinel, "")
                        marker_seen = True
                if output:
                    collected_output.append(output)
                    last_output_time = time.time()
                    output_seen = True
                # 尽量一次性收集当前批次，但受时间上限约束，避免无限循环
                while time.time() < end_time:
                    try:
                        extra = self.output_queue.get(timeout=0.01)
                        if sentinel and sentinel in extra:
                            if command_echo and command_echo in extra:
                                extra = extra.replace(sentinel, "")
                            else:
                                extra = extra.replace(sentinel, "")
                                marker_seen = True
                        if extra:
                            collected_output.append(extra)
                            last_output_time = time.time()
                            output_seen = True
                    except queue.Empty:
                        break
            except queue.Empty:
                pass

            if marker_seen:
                # 捕获到结束标记，立即返回
                break
            if idle_threshold and output_seen and (time.time() - last_output_time) > idle_threshold:
                break

        elapsed = time.time() - start_time
        timed_out = bool(cutoff and cutoff > 0 and elapsed >= cutoff)
        return ''.join(collected_output), timed_out, marker_seen

    @staticmethod
    def _clean_output(output: str, command_text: str, sentinel: Optional[str]) -> str:
        """
        移除封装命令回显和完成标记，保留纯净的命令输出。
        """
        if not output:
            return output
        lines = output.splitlines()
        cleaned = []
        for idx, line in enumerate(lines):
            # 去掉标记行
            if sentinel and sentinel in line:
                continue
            # 尝试剥离提示符
            for token in ("# ", "$ "):
                pos = line.find(token)
                if 0 <= pos <= 40 and "@" in line[:pos]:
                    line = line[pos + len(token):]
                    break
            # 去掉封装命令回显
            if idx == 0 and command_text:
                if line.strip() == command_text.strip():
                    continue
                # 包含 timeout/sh -c 的封装行也忽略
                if "timeout -k" in line and "sh -c" in line:
                    continue
            cleaned.append(line)
        # 保持末尾换行与原输出一致
        out = "\n".join(cleaned)
        if output.endswith("\n") and cleaned:
            out += "\n"
        return out
    
    def get_output(self, last_n_lines: int = 50) -> str:
        """
        获取终端输出
        
        Args:
            last_n_lines: 获取最后N行
            
        Returns:
            输出内容
        """
        if last_n_lines <= 0:
            return ''.join(self.output_buffer)
        
        # 获取最后N行
        lines = []
        for line in reversed(self.output_buffer):
            lines.insert(0, line)
            if len(lines) >= last_n_lines:
                break
        
        return ''.join(lines)
    
    def get_display_output(self) -> str:
        """获取用于显示的输出（截断到display_size）"""
        output = self.get_output()
        if len(output) > self.display_size:
            # 保留最后的display_size字符
            output = output[-self.display_size:]
            output = f"[输出已截断，显示最后{self.display_size}字符]\n{output}"
        return output

    def get_snapshot(self, last_n_lines: Optional[int], max_chars: Optional[int]) -> Dict:
        """获取终端快照，包含按顺序排列的输入/输出"""
        include_all_lines = last_n_lines is None or last_n_lines <= 0
        if not include_all_lines:
            last_n_lines = max(1, last_n_lines)

        segments: List[str] = []
        reset = "\x1b[0m"
        command_color = "\x1b[1;32m"
        for event_type, _, data in self.io_history:
            if event_type == 'input':
                # 显示输入命令并加上ANSI颜色，保持与实时终端一致
                input_text = data.rstrip('\n')
                decorated = f"{command_color}➜ {input_text}{reset}" if input_text.strip() else f"{command_color}➜{reset}"
                if not decorated.endswith('\n'):
                    decorated += '\n'
                segments.append(decorated)
            else:
                cleaned = data.replace('\r', '')
                segments.append(cleaned)

        full_text = ''.join(segments)
        if full_text:
            line_chunks = full_text.splitlines(keepends=True)
            total_lines = len(line_chunks)
            if include_all_lines:
                selected_lines = line_chunks
                lines_requested = total_lines
            else:
                count = min(last_n_lines, total_lines)
                selected_lines = line_chunks[-count:] if count else []
                lines_requested = last_n_lines
            output_text = ''.join(selected_lines)
        else:
            output_text = ''
            lines_requested = 0
            total_lines = 0

        truncated = False
        if max_chars is not None and max_chars > 0 and len(output_text) > max_chars:
            output_text = output_text[-max_chars:]
            truncated = True

        # 统计行数
        if output_text:
            lines_returned = output_text.count('\n') + (0 if output_text.endswith('\n') else 1)
        else:
            lines_returned = 0

        if include_all_lines:
            lines_requested = total_lines

        return {
            "success": True,
            "session": self.session_name,
            "output": output_text,
            "lines_requested": lines_requested,
            "lines_returned": lines_returned,
            "truncated": truncated,
            "is_interactive": self.is_interactive,
            "echo_loop_detected": self.echo_loop_detected,
            "seconds_since_last_output": self._seconds_since_last_output(),
            "last_command": self.last_command,
            "buffer_size": self.total_output_size,
            "timestamp": datetime.now().isoformat()
        }
    
    def get_status(self) -> Dict:
        """获取终端状态"""
        return {
            "session_name": self.session_name,
            "is_running": self.is_running,
            "working_dir": str(self.working_dir),
            "shell": self.shell_command,
            "execution_mode": self.execution_mode,
            "start_time": self.start_time.isoformat() if self.start_time else None,
            "is_interactive": self.is_interactive,
            "last_command": self.last_command,
            "command_count": len(self.command_history),
            "buffer_size": self.total_output_size,
            "truncated_lines": self.truncated_lines,
            "last_activity": datetime.fromtimestamp(self.last_activity).isoformat(),
            "uptime_seconds": (datetime.now() - self.start_time).total_seconds() if self.start_time else 0,
            "seconds_since_last_output": self._seconds_since_last_output(),
            "echo_loop_detected": self.echo_loop_detected
        }
    
    def close(self) -> bool:
        """关闭终端"""
        if not self.is_running:
            return False
        
        try:
            # 停止读取线程
            self.is_reading = False
            
            # 发送退出命令
            if self.process and self.process.poll() is None:
                exit_cmd = "exit\n"
                try:
                    self.process.stdin.write(exit_cmd.encode('utf-8'))
                    self.process.stdin.flush()
                except:
                    pass
                
                # 等待进程结束
                try:
                    self.process.wait(timeout=2)
                except subprocess.TimeoutExpired:
                    # 强制终止
                    self.process.terminate()
                    time.sleep(0.5)
                    if self.process.poll() is None:
                        self.process.kill()
            
            self.is_running = False
            
            # 等待读取线程结束
            if self.reader_thread and self.reader_thread.is_alive():
                self.reader_thread.join(timeout=1)
            
            # 广播终端关闭事件
            if self.broadcast:
                self.broadcast('terminal_closed', {
                    'session': self.session_name,
                    'time': datetime.now().isoformat()
                })
            
            if self.using_container:
                self._stop_sandbox_container()
                self.using_container = False
                self.execution_mode = "host"

            print(f"{OUTPUT_FORMATS['info']} 终端会话关闭: {self.session_name}")
            return True
            
        except Exception as e:
            print(f"{OUTPUT_FORMATS['error']} 关闭终端失败: {e}")
            return False

    def __del__(self):
        """析构函数，确保进程被关闭"""
        if hasattr(self, 'is_running') and self.is_running:
            self.close()

    def _stop_sandbox_container(self, force: bool = False):
        """确保容器终端被停止"""
        if not self._owns_container or not self.sandbox_container_name or not self._sandbox_bin_path:
            return
        try:
            subprocess.run(
                [self._sandbox_bin_path, "kill", self.sandbox_container_name],
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
                timeout=3,
                check=False
            )
        except Exception:
            if force:
                print(f"{OUTPUT_FORMATS['warning']} 强制终止容器 {self.sandbox_container_name} 失败，可能已退出。")
        finally:
            self.sandbox_container_name = None
            self._owns_container = False
