feat: 新增手动引擎 (manual) — 人类在另一个终端扮演 AI,消息通过文件传递
This commit is contained in:
1 parent
98c9e51dc2
commit
991d43fbfa
1 file changed
+290
@@ -0,0 +1,290 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
手动引擎 - 让人类在另一个终端扮演 AI
|
||||||
|
消息通过文件传递,另一个终端窗口显示消息并等待人工输入
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import threading
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# 添加项目根目录到sys.path
|
||||||
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
project_root = os.path.dirname(current_dir)
|
||||||
|
sys.path.insert(0, project_root)
|
||||||
|
|
||||||
|
# 通信用的临时目录
|
||||||
|
COMM_DIR = os.path.join(tempfile.gettempdir(), "xiaoli_manual_engine")
|
||||||
|
|
||||||
|
|
||||||
|
class ManualAI:
|
||||||
|
"""手动引擎 — 让人类在另一个终端扮演 AI"""
|
||||||
|
|
||||||
|
requires_api_key = False
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.name = "manual"
|
||||||
|
self.cli = None
|
||||||
|
self.conversation_history = []
|
||||||
|
self.max_history = 10
|
||||||
|
self._subprocess = None
|
||||||
|
self._session_id = str(int(time.time()))
|
||||||
|
self._started = False
|
||||||
|
|
||||||
|
# 确保通信目录存在
|
||||||
|
os.makedirs(COMM_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
def get_help_info(self):
|
||||||
|
return """手动引擎帮助信息
|
||||||
|
========================
|
||||||
|
|
||||||
|
引擎名称: manual
|
||||||
|
描述: 让人类在另一个终端窗口扮演 AI,消息通过文件传递
|
||||||
|
|
||||||
|
工作原理:
|
||||||
|
1. 切换到 manual 引擎时,自动打开一个新终端窗口
|
||||||
|
2. 用户在 CLI 中输入消息,消息会显示在新终端窗口中
|
||||||
|
3. 另一个终端窗口中的人阅读消息,手动输入回复并按回车
|
||||||
|
4. 回复传回 CLI,显示为 AI 的响应
|
||||||
|
|
||||||
|
使用说明:
|
||||||
|
1. /engine switch manual — 切换到手动引擎,自动打开另一个终端
|
||||||
|
2. 在 CLI 中正常对话,消息会转发到另一个终端
|
||||||
|
3. 在另一个终端中手动输入回复
|
||||||
|
4. /engine.manual status — 查看连接状态
|
||||||
|
5. /engine.manual restart — 重启另一个终端窗口
|
||||||
|
|
||||||
|
注意: 需要图形桌面环境才能自动打开新终端窗口。
|
||||||
|
如果是无头服务器,请手动运行通信脚本。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _get_paths(self):
|
||||||
|
"""获取通信文件路径"""
|
||||||
|
base = os.path.join(COMM_DIR, self._session_id)
|
||||||
|
return {
|
||||||
|
"msg_to_human": base + "_to_human.txt",
|
||||||
|
"msg_from_human": base + "_from_human.txt",
|
||||||
|
"ready": base + "_ready.flag",
|
||||||
|
"alive": base + "_alive.flag",
|
||||||
|
}
|
||||||
|
|
||||||
|
def _spawn_terminal(self):
|
||||||
|
"""在新终端窗口中启动人工回复脚本"""
|
||||||
|
paths = self._get_paths()
|
||||||
|
|
||||||
|
# 写一个临时脚本
|
||||||
|
script_content = self._generate_script(paths)
|
||||||
|
script_path = os.path.join(COMM_DIR, f"worker_{self._session_id}.py")
|
||||||
|
with open(script_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(script_content)
|
||||||
|
|
||||||
|
# 根据平台选择终端
|
||||||
|
import platform
|
||||||
|
system = platform.system()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if system == "Windows":
|
||||||
|
self._subprocess = subprocess.Popen(
|
||||||
|
["cmd", "/c", "start", "cmd", "/k", f"python \"{script_path}\""],
|
||||||
|
shell=True
|
||||||
|
)
|
||||||
|
elif system == "Darwin": # macOS
|
||||||
|
self._subprocess = subprocess.Popen([
|
||||||
|
"osascript", "-e",
|
||||||
|
f'tell app "Terminal" to do script "python3 \\"{script_path}\\""'
|
||||||
|
])
|
||||||
|
else: # Linux
|
||||||
|
# 尝试常见的终端模拟器
|
||||||
|
terminals = [
|
||||||
|
["x-terminal-emulator", "-e", f"python3 {script_path}"],
|
||||||
|
["gnome-terminal", "--", "python3", script_path],
|
||||||
|
["konsole", "-e", f"python3 {script_path}"],
|
||||||
|
["xfce4-terminal", "-e", f"python3 {script_path}"],
|
||||||
|
["xterm", "-e", f"python3 {script_path}"],
|
||||||
|
]
|
||||||
|
launched = False
|
||||||
|
for cmd in terminals:
|
||||||
|
try:
|
||||||
|
self._subprocess = subprocess.Popen(cmd)
|
||||||
|
launched = True
|
||||||
|
break
|
||||||
|
except FileNotFoundError:
|
||||||
|
continue
|
||||||
|
if not launched:
|
||||||
|
print(f"\033[33m[manual] 无法自动打开终端窗口,请手动运行:\033[0m")
|
||||||
|
print(f"\033[36m python3 {script_path}\033[0m")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 等待对方就绪
|
||||||
|
ready_path = paths["ready"]
|
||||||
|
for _ in range(30): # 最多等 15 秒
|
||||||
|
if os.path.exists(ready_path):
|
||||||
|
self._started = True
|
||||||
|
return True
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# 超时但不报错,可能终端启动慢
|
||||||
|
self._started = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\033[31m[manual] 启动终端失败: {e}\033[0m")
|
||||||
|
print(f"\033[33m[manual] 请手动运行: python3 {script_path}\033[0m")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _generate_script(self, paths):
|
||||||
|
"""生成人工回复脚本的内容"""
|
||||||
|
return f'''#!/usr/bin/env python3
|
||||||
|
"""小狸手动引擎 — 人工回复终端"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import signal
|
||||||
|
|
||||||
|
MSG_IN = r"{paths['msg_to_human']}"
|
||||||
|
MSG_OUT = r"{paths['msg_from_human']}"
|
||||||
|
READY = r"{paths['ready']}"
|
||||||
|
ALIVE = r"{paths['alive']}"
|
||||||
|
|
||||||
|
# 标记自己已就绪
|
||||||
|
with open(READY, 'w') as f:
|
||||||
|
f.write("ready")
|
||||||
|
|
||||||
|
# 定期刷新 alive 标记
|
||||||
|
def keep_alive():
|
||||||
|
try:
|
||||||
|
with open(ALIVE, 'w') as f:
|
||||||
|
f.write(str(time.time()))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
keep_alive()
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("\\033[36m╔══════════════════════════════════════════╗\\033[0m")
|
||||||
|
print("\\033[36m║ 小狸手动引擎 — 人工回复终端 ║\\033[0m")
|
||||||
|
print("\\033[36m║ 在这里输入回复,按回车发送 ║\\033[0m")
|
||||||
|
print("\\033[36m║ 输入 quit 退出 ║\\033[0m")
|
||||||
|
print("\\033[36m╚══════════════════════════════════════════╝\\033[0m")
|
||||||
|
print()
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
last_msg_time = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
keep_alive()
|
||||||
|
|
||||||
|
# 检查是否有新消息
|
||||||
|
try:
|
||||||
|
if os.path.exists(MSG_IN):
|
||||||
|
mtime = os.path.getmtime(MSG_IN)
|
||||||
|
if mtime > last_msg_time:
|
||||||
|
last_msg_time = mtime
|
||||||
|
with open(MSG_IN, 'r', encoding='utf-8') as f:
|
||||||
|
msg = f.read()
|
||||||
|
if msg:
|
||||||
|
print("\\033[33m[用户消息]\\033[0m")
|
||||||
|
print(msg)
|
||||||
|
print()
|
||||||
|
sys.stdout.flush()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 检查用户输入(非阻塞)
|
||||||
|
try:
|
||||||
|
import select
|
||||||
|
if select.select([sys.stdin], [], [], 0.3)[0]:
|
||||||
|
line = sys.stdin.readline().strip()
|
||||||
|
if line:
|
||||||
|
if line.lower() == "quit":
|
||||||
|
print("\\033[31m[已退出手动引擎]\\033[0m")
|
||||||
|
break
|
||||||
|
# 写回复
|
||||||
|
with open(MSG_OUT, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(line)
|
||||||
|
print(f"\\033[32m[已发送] {{line}}\\033[0m")
|
||||||
|
print()
|
||||||
|
sys.stdout.flush()
|
||||||
|
except:
|
||||||
|
# Windows 没有 select,用阻塞方式
|
||||||
|
try:
|
||||||
|
import msvcrt
|
||||||
|
if msvcrt.kbhit():
|
||||||
|
line = input().strip()
|
||||||
|
if line:
|
||||||
|
if line.lower() == "quit":
|
||||||
|
print("\\033[31m[已退出手动引擎]\\033[0m")
|
||||||
|
break
|
||||||
|
with open(MSG_OUT, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(line)
|
||||||
|
print(f"\\033[32m[已发送] {{line}}\\033[0m")
|
||||||
|
print()
|
||||||
|
sys.stdout.flush()
|
||||||
|
except:
|
||||||
|
time.sleep(0.3)
|
||||||
|
'''
|
||||||
|
|
||||||
|
def generate_response(self, user_input, tool_results=None, system_prompt=None):
|
||||||
|
"""生成响应 — 转发给另一个终端的人工"""
|
||||||
|
paths = self._get_paths()
|
||||||
|
|
||||||
|
# 如果还没启动终端,现在启动
|
||||||
|
if not self._started:
|
||||||
|
if not self._spawn_terminal():
|
||||||
|
return "[manual] 手动引擎未就绪,无法打开终端窗口。请手动启动通信脚本。"
|
||||||
|
|
||||||
|
# 清除旧的回复文件
|
||||||
|
from_human = paths["msg_from_human"]
|
||||||
|
if os.path.exists(from_human):
|
||||||
|
os.remove(from_human)
|
||||||
|
|
||||||
|
# 把用户消息写入文件
|
||||||
|
msg = user_input
|
||||||
|
if tool_results:
|
||||||
|
msg = f"[工具结果] {tool_results.get('result', '无结果')}\n\n{user_input}"
|
||||||
|
|
||||||
|
to_human = paths["msg_to_human"]
|
||||||
|
with open(to_human, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(msg)
|
||||||
|
|
||||||
|
print(f"\033[36m[manual] 消息已发送到人工终端,等待回复...\033[0m")
|
||||||
|
|
||||||
|
# 等待回复
|
||||||
|
timeout = 300 # 最多等 5 分钟
|
||||||
|
waited = 0
|
||||||
|
while waited < timeout:
|
||||||
|
if os.path.exists(from_human):
|
||||||
|
try:
|
||||||
|
with open(from_human, 'r', encoding='utf-8') as f:
|
||||||
|
response = f.read().strip()
|
||||||
|
if response:
|
||||||
|
# 清理回复文件
|
||||||
|
os.remove(from_human)
|
||||||
|
return response
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
time.sleep(0.5)
|
||||||
|
waited += 0.5
|
||||||
|
|
||||||
|
return "[manual] 等待人工回复超时(5分钟)"
|
||||||
|
|
||||||
|
def handle_command(self, command):
|
||||||
|
"""处理 manual 引擎专属命令"""
|
||||||
|
if command == "status":
|
||||||
|
paths = self._get_paths()
|
||||||
|
alive = os.path.exists(paths["alive"])
|
||||||
|
return f"会话ID: {self._session_id}\n终端状态: {'已连接' if alive else '未连接'}\n通信目录: {COMM_DIR}"
|
||||||
|
elif command == "restart":
|
||||||
|
self._started = False
|
||||||
|
self._session_id = str(int(time.time()))
|
||||||
|
if self._spawn_terminal():
|
||||||
|
return "手动引擎终端已重启"
|
||||||
|
else:
|
||||||
|
return "重启失败,无法打开终端"
|
||||||
|
else:
|
||||||
|
return f"未知命令: {command}\n可用: status, restart"
|
||||||
Reference in new issue
Block a user