import tkinter as tk from tkinter import scrolledtext import subprocess import threading import sys import os import queue import time class ConsoleApp: def __init__(self, root, args): self.root = root self.args = args self.proc = None self.output_queue = queue.Queue() # 获取系统当前编码,尽量保证原样输出 self.encoding = sys.stdout.encoding or 'utf-8' # 记录最后一次收到输出的时间 self.last_output_time = time.time() root.title("AI 交互终端") root.geometry("900x600") # GUI 布局 # 输出区域 self.text_area = scrolledtext.ScrolledText(root, wrap=tk.WORD, font=("Consolas", 10)) self.text_area.pack(expand=True, fill='both', padx=5, pady=5) # 输入区域 input_frame = tk.Frame(root) input_frame.pack(fill='x', padx=5, pady=5) self.entry = tk.Entry(input_frame, font=("Consolas", 10)) self.entry.pack(side=tk.LEFT, expand=True, fill='x') self.entry.bind('', self.send_input) # 说话模式复选框 self.speak_mode = tk.BooleanVar(value=False) self.speak_check = tk.Checkbutton(input_frame, text="提示AI(说话)", variable=self.speak_mode, font=("Consolas", 9)) self.speak_check.pack(side=tk.RIGHT, padx=5) self.kill_btn = tk.Button(input_frame, text="强制终止进程", command=self.force_terminate, bg="red", fg="white") self.kill_btn.pack(side=tk.RIGHT, padx=5) # 状态栏 (用于显示无输出时长) status_frame = tk.Frame(root) status_frame.pack(fill='x', padx=5, pady=(0, 5)) self.time_label = tk.Label(status_frame, text="无输出时长: 00:00", font=("Consolas", 10), fg="gray") self.time_label.pack(side=tk.RIGHT) # 启动子进程 (合并 stderr 到 stdout) try: self.proc = subprocess.Popen( self.args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=0 # 无缓冲,确保实时性 ) except Exception as e: print(f"启动进程失败: {e}") sys.exit(1) # 启动读取子进程输出的线程 self.output_thread = threading.Thread(target=self.read_output, daemon=True) self.output_thread.start() # 启动读取当前脚本控制台输入的线程 (允许直接在终端输入) self.stdin_thread = threading.Thread(target=self.read_stdin, daemon=True) self.stdin_thread.start() # 窗口关闭事件 self.root.protocol("WM_DELETE_WINDOW", self.on_closing) # 开始轮询队列更新GUI self.update_gui() def read_output(self): """后台线程:实时读取子进程的输出""" fd = self.proc.stdout.fileno() while True: try: byte_data = os.read(fd, 1024) if not byte_data: break # 进程结束 # 只要收到数据,就更新最后输出时间 self.last_output_time = time.time() # 1. 实时输出到当前脚本的 stdout (不加任何前缀) sys.stdout.buffer.write(byte_data) sys.stdout.buffer.flush() # 2. 放入队列供 GUI 使用 try: text = byte_data.decode(self.encoding, errors='replace') except: text = byte_data.decode('utf-8', errors='replace') self.output_queue.put(text) except OSError: break # 管道已关闭 def read_stdin(self): """后台线程:实时读取宿主控制台的 stdin 并转发给子进程""" while True: line = sys.stdin.readline() if not line: break try: self.proc.stdin.write(line.encode(self.encoding, errors='replace')) self.proc.stdin.flush() except: break def send_input(self, event): """GUI 输入框回车事件""" text = self.entry.get() self.entry.delete(0, tk.END) if not text: return # 如果勾选了"提示AI",则直接输出到当前脚本的 stdout,不发给子进程 if self.speak_mode.get(): speak_text = text + '\n' # 输出到当前脚本控制台,供 AI 捕获 sys.stdout.buffer.write(speak_text.encode(self.encoding, errors='replace')) sys.stdout.buffer.flush() # 在 GUI 中回显,让用户知道说了什么(使用不同颜色区分) self.text_area.insert(tk.END, speak_text, 'speak_tag') self.text_area.tag_config('speak_tag', foreground="blue") self.text_area.see(tk.END) else: # 正常模式:发给子进程 text += '\n' try: self.proc.stdin.write(text.encode(self.encoding, errors='replace')) self.proc.stdin.flush() except Exception as e: self.output_queue.put(f"\n[输入发送失败: {e}]\n") def force_terminate(self): """强制终止子进程""" if self.proc and self.proc.poll() is None: print("用户终止了应用程序") # 输出到控制台 stdout self.output_queue.put("\n[用户终止了应用程序]\n") self.proc.kill() def on_closing(self): """关闭窗口时终止子进程""" if self.proc and self.proc.poll() is None: self.proc.kill() self.root.destroy() def update_gui(self): """主线程:从队列获取数据并更新GUI""" try: while True: text = self.output_queue.get_nowait() self.text_area.insert(tk.END, text) self.text_area.see(tk.END) except queue.Empty: pass # 计算并更新无输出时长 if self.proc and self.proc.poll() is None: idle_seconds = int(time.time() - self.last_output_time) mins = idle_seconds // 60 secs = idle_seconds % 60 self.time_label.config(text=f"无输出时长: {mins:02d}:{secs:02d}") else: self.time_label.config(text="进程已结束") # 检查进程是否已结束 if self.proc and self.proc.poll() is not None: if self.output_queue.empty(): self.kill_btn.config(state=tk.DISABLED, bg="grey", text="进程已结束") # 进程结束且输出已读取完毕,自动关闭程序 self.root.destroy() return # 停止轮询 # 每 200ms 轮询一次 (减少CPU占用,时间显示不需要50ms那么频繁) self.root.after(200, self.update_gui) if __name__ == "__main__": if len(sys.argv) < 2: print("用法: python ai_console.py [args...]") print("示例: python ai_console.py cmd") sys.exit(1) root = tk.Tk() app = ConsoleApp(root, sys.argv[1:]) root.mainloop()