Files
xiaoli-cli/xcli_core/cli_clawli.py
T
xiaoli-cli 04e2d26809 refactor: 拆分 ai_cli.py 为模块化 xcli_core 包
将 4130 行的单文件拆分为 13 个职责清晰的模块:
- constants.py: 常量 + 可选依赖导入
- sandbox.py: 安全代码沙箱 (AST检查器 + 子进程隔离)
- config.py: 配置管理 + 日志
- plugin_manager.py: LiuginManager 插件管理器
- cli_base.py: BaseAICLI 基类 (初始化/引擎/插件/命令)
- cli_clawli.py: ClawliMixin 远程模式
- cli_tools.py: ToolMixin 工具调用
- cli_code_exec.py: CodeExecMixin 代码沙箱执行
- cli_display.py: DisplayMixin 显示/输出/图片/帮助
- cli_history.py: HistoryMixin 聊天记录
- cli_core.py: AICLI 主类 + 对话循环 + main()
- tui.py: Textual TUI 界面
- __init__.py: 包导出汇总

ai_cli.py 保留为入口文件 (11行), 根目录整洁。
原逻辑完全不变, 仅做结构拆分。
2026-04-25 22:27:59 +08:00

321 lines
14 KiB
Python

import threading
import time
from colorama import Fore, Style
from .constants import CLAWLI_SERVER_AVAILABLE, WEBSOCKET_AVAILABLE
try:
import clawli_server
except ImportError:
clawli_server = None
try:
import websocket_server
except ImportError:
websocket_server = None
class ClawliMixin:
"""Clawli 远程模式相关方法"""
def _start_clawli_monitor(self):
"""启动 Clawli 消息监控线程"""
if self._clawli_monitor_running:
return
self._clawli_monitor_running = True
print(f"{Fore.CYAN}[Clawli] 消息监控线程已启动{Style.RESET_ALL}")
def monitor_loop():
while self._clawli_monitor_running and self.is_clawli_mode:
try:
if CLAWLI_SERVER_AVAILABLE and clawli_server:
messages = clawli_server.get_messages()
if messages:
print(f"[Clawli Monitor] 收到 {len(messages)} 条消息")
for msg in messages:
self._handle_clawli_message(msg)
time.sleep(0.1) # 100ms 检查间隔
except Exception as e:
print(f"[Clawli Monitor] 错误: {e}")
self._clawli_monitor_thread = threading.Thread(target=monitor_loop, daemon=True)
self._clawli_monitor_thread.start()
def _stop_clawli_monitor(self):
"""停止 Clawli 消息监控线程"""
self._clawli_monitor_running = False
if self._clawli_monitor_thread:
self._clawli_monitor_thread.join(timeout=1)
self._clawli_monitor_thread = None
def _handle_clawli_message(self, msg: dict):
"""处理来自手机端的消息"""
try:
msg_type = msg.get("type", "")
if msg_type == "message":
content = msg.get("content", "")
self._process_clawli_user_message(content)
elif msg_type == "file":
file_path = msg.get("path", "")
text = msg.get("text", "")
category = msg.get("category", "file")
if category == "image":
self._process_clawli_image(file_path, text)
else:
self._process_clawli_user_message(f"@{file_path}\n{text}")
except Exception as e:
print(f"[Clawli] 处理消息错误: {e}")
def _process_clawli_user_message(self, content: str):
"""处理 Clawli 用户消息"""
if not self.current_engine:
return
full_content = f"{content}\n\n[此消息由 Clawli 手机端发送]"
self.shared_conversation_history.append({
"role": "user",
"content": full_content
})
system_prompt = self._build_system_prompt(self.get_liugin_usage_prompts())
response = self.current_engine.generate_response(full_content, system_prompt=system_prompt)
self._process_clawli_response(response)
def _process_clawli_response(self, response: str):
"""处理 Clawli AI 响应"""
text_content, json_data = self._parse_mixed_response(response)
if json_data:
tool_calls = self._extract_tool_calls(json_data)
if tool_calls:
for tool_data in tool_calls:
tool_name = tool_data.get('tool', '未知工具')
tool_args = tool_data.get('args', '')
if CLAWLI_SERVER_AVAILABLE and clawli_server:
clawli_server.send_tool_status(tool_name, tool_args, "calling")
result = self.process_tool_call(tool_data)
result_text = result.get('result', '无结果')
if CLAWLI_SERVER_AVAILABLE and clawli_server:
clawli_server.send_tool_status(tool_name, tool_args, "success", result_text)
self.shared_conversation_history.append({
"role": "assistant",
"content": response
})
tool_result_str = "\n".join([f"工具执行结果: {r}" for r in [result.get('result', '')]])
self._process_clawli_user_message(f"{tool_result_str}\n请根据工具执行结果继续回答。")
return
final_response = text_content if text_content else response
self.shared_conversation_history.append({
"role": "assistant",
"content": final_response
})
if CLAWLI_SERVER_AVAILABLE and clawli_server:
clawli_server.send_message(final_response)
def _process_clawli_image(self, image_path: str, text: str):
"""处理 Clawli 图片消息"""
try:
image_engine = getattr(self, 'image_engine', None)
if not image_engine:
try:
from image_engine import get_image_engine
image_engine = get_image_engine()
except:
image_engine = None
if image_engine:
with open(image_path, 'rb') as f:
image_data = f.read()
result = image_engine.analyze(image_data, text or "请描述这张图片")
self._process_clawli_user_message(f"@{image_path}\n图片内容: {result}\n\n{text}")
else:
self._process_clawli_user_message(f"@{image_path}\n{text}")
except Exception as e:
self._process_clawli_user_message(f"@{image_path}\n图片处理失败: {e}\n{text}")
def _handle_remote_command(self, args):
"""处理远程连接命令"""
if not WEBSOCKET_AVAILABLE:
print(f"{Fore.RED}错误: websockets 库未安装{Style.RESET_ALL}")
print(f"{Fore.YELLOW}请运行: pip install websockets{Style.RESET_ALL}")
return
args = args.strip()
if not args or args == '':
print(f"\n{Fore.CYAN}{'='*55}{Style.RESET_ALL}")
print(f"{Fore.CYAN} 远程连接命令帮助{Style.RESET_ALL}")
print(f"{Fore.CYAN}{'='*55}{Style.RESET_ALL}")
print(f"\n{Fore.WHITE}本地模式 (局域网直连):{Style.RESET_ALL}")
print(f"{Fore.YELLOW} /remote start <端口> <密码>{Style.RESET_ALL}")
print(f"{Fore.WHITE} 端口: 监听端口 (默认9079){Style.RESET_ALL}")
print(f"{Fore.WHITE} 密码: PC密码 (安卓端需要输入){Style.RESET_ALL}")
print(f"\n{Fore.WHITE}代理模式 (无公网IP,通过中继服务器):{Style.RESET_ALL}")
print(f"{Fore.YELLOW} /remote proxy <服务器> <服务器密码> <PC端口> <PC密码>{Style.RESET_ALL}")
print(f"{Fore.WHITE} 服务器: 中继服务器地址:端口{Style.RESET_ALL}")
print(f"{Fore.WHITE} 服务器密码: 连接中继服务器的密码{Style.RESET_ALL}")
print(f"{Fore.WHITE} PC端口: 在服务器上使用的端口{Style.RESET_ALL}")
print(f"{Fore.WHITE} PC密码: 安卓端连接时需要验证{Style.RESET_ALL}")
print(f"\n{Fore.WHITE}其他命令:{Style.RESET_ALL}")
print(f"{Fore.WHITE} /remote stop - 停止服务{Style.RESET_ALL}")
print(f"{Fore.WHITE} /remote status - 查看状态{Style.RESET_ALL}")
print(f"{Fore.WHITE} /remote ip - 查看本机IP{Style.RESET_ALL}")
print(f"\n{Fore.CYAN}{'='*55}{Style.RESET_ALL}")
print(f"\n{Fore.GREEN}示例:{Style.RESET_ALL}")
print(f"{Fore.WHITE} /remote start 9079 mypassword{Style.RESET_ALL}")
print(f"{Fore.WHITE} /remote proxy relay.example.com:9079 serverpwd 12345 pcpwd{Style.RESET_ALL}")
print(f"{Fore.CYAN}{'='*55}{Style.RESET_ALL}\n")
return
parts = args.split()
cmd = parts[0].lower()
if cmd == 'start':
port = 9079
password = ""
if len(parts) == 2:
arg = parts[1]
if arg.isdigit():
port = int(arg)
print(f"{Fore.RED}错误: 请设置PC密码{Style.RESET_ALL}")
print(f"{Fore.YELLOW}用法: /remote start {port} <密码>{Style.RESET_ALL}")
return
else:
password = arg
elif len(parts) >= 3:
try:
port = int(parts[1])
password = parts[2]
except ValueError:
password = parts[1]
if not password:
print(f"{Fore.RED}错误: 请设置PC密码{Style.RESET_ALL}")
print(f"{Fore.YELLOW}用法: /remote start <端口> <密码>{Style.RESET_ALL}")
return
if len(password) < 4:
print(f"{Fore.RED}错误: 密码至少4个字符{Style.RESET_ALL}")
return
if CLAWLI_SERVER_AVAILABLE and clawli_server:
success = clawli_server.start_server("0.0.0.0", port, password)
if success:
self.is_clawli_mode = True
self._start_clawli_monitor()
elif websocket_server:
websocket_server.start_local_server(self, port, password)
elif cmd == 'proxy':
if len(parts) < 5:
print(f"{Fore.RED}参数不足{Style.RESET_ALL}")
print(f"{Fore.YELLOW}用法: /remote proxy <服务器> <服务器密码> <PC端口> <PC密码>{Style.RESET_ALL}")
print(f"{Fore.WHITE}示例: /remote proxy relay.example.com:9079 serverpwd 12345 pcpwd{Style.RESET_ALL}")
return
server_addr = parts[1]
server_password = parts[2]
pc_port = 0
pc_password = parts[4] if len(parts) > 4 else ""
try:
pc_port = int(parts[3])
if pc_port < 10000 or pc_port > 19999:
print(f"{Fore.YELLOW}建议端口范围: 10000-19999{Style.RESET_ALL}")
except ValueError:
print(f"{Fore.RED}无效的端口号: {parts[3]}{Style.RESET_ALL}")
return
if not pc_password:
print(f"{Fore.RED}错误: 请设置PC密码{Style.RESET_ALL}")
return
if len(pc_password) < 4:
print(f"{Fore.RED}错误: PC密码至少4个字符{Style.RESET_ALL}")
return
if ':' in server_addr:
server_host, port_str = server_addr.rsplit(':', 1)
try:
server_port = int(port_str)
except ValueError:
print(f"{Fore.RED}无效的服务器端口: {port_str}{Style.RESET_ALL}")
return
else:
server_host = server_addr
server_port = 9079
print(f"{Fore.CYAN}正在连接中继服务器...{Style.RESET_ALL}")
if websocket_server:
websocket_server.start_proxy_client(
self, server_host, server_port,
server_password, pc_port, pc_password
)
elif cmd == 'stop':
if CLAWLI_SERVER_AVAILABLE and clawli_server:
clawli_server.stop_server()
self.is_clawli_mode = False
elif websocket_server:
websocket_server.stop_all()
print(f"{Fore.GREEN}远程服务已停止{Style.RESET_ALL}")
elif cmd == 'status':
if CLAWLI_SERVER_AVAILABLE and clawli_server:
status = clawli_server.get_status()
elif websocket_server:
status = websocket_server.get_status()
else:
status = {}
if status.get('running'):
mode = status.get('mode', 'local')
if mode == 'proxy':
print(f"{Fore.GREEN}远程服务状态: 运行中 (代理模式){Style.RESET_ALL}")
print(f"{Fore.WHITE}服务器: {Fore.YELLOW}{status.get('server', '未知')}{Style.RESET_ALL}")
print(f"{Fore.WHITE}PC端口: {Fore.YELLOW}{status.get('port', '未知')}{Style.RESET_ALL}")
else:
print(f"{Fore.GREEN}远程服务状态: 运行中 (本地模式){Style.RESET_ALL}")
print(f"{Fore.WHITE}本机IP: {Fore.YELLOW}{status.get('local_ip', '未知')}{Style.RESET_ALL}")
print(f"{Fore.WHITE}端口: {Fore.YELLOW}{status.get('port', 9079)}{Style.RESET_ALL}")
print(f"{Fore.WHITE}已连接客户端: {Fore.YELLOW}{status.get('connected_clients', 0)}{Style.RESET_ALL}")
else:
print(f"{Fore.YELLOW}远程服务状态: 未启动{Style.RESET_ALL}")
print(f"{Fore.WHITE}使用 {Fore.CYAN}/remote start <端口> <密码>{Fore.WHITE} 启动本地服务{Style.RESET_ALL}")
print(f"{Fore.WHITE}使用 {Fore.CYAN}/remote proxy ...{Fore.WHITE} 连接代理服务器{Style.RESET_ALL}")
elif cmd == 'ip':
import socket
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
local_ip = s.getsockname()[0]
s.close()
print(f"{Fore.GREEN}本机IP地址: {Fore.YELLOW}{local_ip}{Style.RESET_ALL}")
print(f"{Fore.WHITE}在 Clawli 应用中输入此IP即可连接 (仅限局域网){Style.RESET_ALL}")
except Exception as e:
print(f"{Fore.RED}获取IP失败: {e}{Style.RESET_ALL}")
else:
print(f"{Fore.RED}未知命令: {cmd}{Style.RESET_ALL}")
print(f"{Fore.WHITE}可用命令: start, proxy, stop, status, ip{Style.RESET_ALL}")