xiaoli-cli 7.0.0: 重构底层架构 + 移除工具截断

- 重构 core/ 底层 (7文件): app/runner/engine/parser/history/safety/display
- FC原生调用为主,prompt-JSON保底
- 移除所有工具返回截断 (ai_search/git_tools/cmd_executor/browser_auto/network_tools/sub_agent/tool_search)
- 删除无用文件 (旧服务器/WebUI/彩蛋/启动器等21个文件)
- 清理 constants.py 死代码
- 版本号更新为 7.0.0
This commit is contained in:
shuiyu1123 committed 2026-07-05 13:17:27 +08:00
commit 78fea7ccdd
57 files changed
+18358

No files matched your search

+290
View File
@@ -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"
+491
View File
@@ -0,0 +1,491 @@
import os
import sys
import json
import requests
import threading
import time
from colorama import Fore, Style
# 延迟导入 ollama,避免程序启动时卡住
# 如果导入失败,ollama_client 将为 None
try:
import ollama
ollama_client = ollama
except Exception as e:
print(f"{Fore.YELLOW}警告: 无法导入 ollama 库: {e}{Style.RESET_ALL}")
print(f"{Fore.YELLOW}请确保已安装 ollama 包 (pip install ollama){Style.RESET_ALL}")
ollama_client = None
# 添加项目根目录到sys.path
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(current_dir)
sys.path.insert(0, project_root)
def _safe_get(obj, key, default=None):
"""兼容 dict 和 pydantic 对象的安全取值(新版 ollama 返回 pydantic 模型,不支持 .get())"""
if obj is None:
return default
# 1. 先尝试 dict 访问
try:
return obj[key]
except (KeyError, TypeError):
pass
# 2. 尝试属性访问(pydantic 模型)
val = getattr(obj, key, None)
if val is not None:
return val
# 3. 尝试 model_dump 后的 dict 访问
try:
return obj.model_dump()[key]
except (AttributeError, KeyError, TypeError):
return default
class OllamaAI:
"""基于Ollama的本地AI引擎"""
requires_api_key = False # Ollama是本地引擎,不需要API密钥
def get_help_info(self):
"""获取引擎帮助信息"""
help_text = """
Ollama AI引擎插件帮助信息
========================
引擎名称: Ollama
描述: 基于Ollama的本地AI引擎,支持多种大型语言模型
可用命令:
/engine.ollama models - 列出本地可用的模型
/engine.ollama remote-models - 显示可下载的模型列表
/engine.ollama search <关键词> - 搜索可下载的模型
/engine.ollama set <模型名> - 设置当前使用的模型
/engine.ollama pull <模型名> - 从Ollama库拉取模型
注意: 本引擎强制使用流式输出,AI回复会实时显示。
使用说明:
1. 确保Ollama服务已在本地运行
2. 使用/engine.switch ollama切换到此引擎
3. 使用上述命令管理模型和与AI交互
思考模式:
- 支持深度思考模型(如 QwQ、DeepSeek-R1)
- 思考过程会自动以灰色文本显示
"""
return help_text
def __init__(self):
self.name = "ollama"
self.cli = None # 引用CLI实例以访问插件
# 初始化对话历史
self.conversation_history = []
# 直接从 config.json 读取配置
config_file = os.path.join(project_root, "config.json")
try:
with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
engine_config = config.get("api", {}).get("engines", {}).get("ollama", {})
self.api_key = engine_config.get("api_key", "")
self.base_url = engine_config.get("base_url", "")
self.model = engine_config.get("model", "")
self.max_history = config.get("system", {}).get("max_history", 10)
except Exception as e:
# 如果读取失败,使用空值
self.api_key = ""
self.base_url = ""
self.model = ""
self.max_history = 10
# 共享对话历史引用(由CLI设置)
self.shared_conversation_history = None
# 取消标志
self._cancelled = False
# 直接启用思考标记,由主程序自动处理深度思考模型的响应
self.thinking_start_marker = "<thinking>"
self.thinking_end_marker = "</thinking>"
# 检测Ollama服务是否运行
self.is_service_running = self.check_ollama_service()
if self.is_service_running:
print(f"{Fore.GREEN}Ollama服务检测成功,引擎已启用{Style.RESET_ALL}")
else:
print(f"{Fore.RED}Ollama服务未运行,引擎将不可用{Style.RESET_ALL}")
def cancel(self):
"""取消正在进行的请求"""
self._cancelled = True
def apply_config(self, base_url=None, api_key=None, model=None):
"""应用新配置到当前引擎实例(内存),并持久化到 config.json"""
if base_url is not None:
self.base_url = base_url.rstrip('/')
if api_key is not None:
self.api_key = api_key
if model is not None:
self.model = model
# 持久化到 config.json
try:
import os, json
config_file = os.path.join(project_root, "config.json")
with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
engine_cfg = config.setdefault("api", {}).setdefault("engines", {}).setdefault("ollama", {})
engine_cfg["base_url"] = self.base_url
engine_cfg["api_key"] = self.api_key
engine_cfg["model"] = self.model
with open(config_file, 'w', encoding='utf-8') as f:
json.dump(config, f, ensure_ascii=False, indent=2)
except Exception as e:
logger.warning(f"持久化引擎配置失败: {e}")
def check_ollama_service(self):
"""检测Ollama服务是否运行"""
# 检查 ollama 库是否可用
if ollama_client is None:
return False
try:
# 尝试调用Ollama API获取版本信息
response = ollama_client.list()
# 如果没有异常,说明服务运行正常
return True
except Exception as e:
# 如果出现异常,说明服务未运行
return False
def generate_response(self, user_input, tool_results=None, system_prompt=None, stream_start_event=None):
"""
生成AI响应 - 使用Ollama API
Args:
user_input: 用户输入
tool_results: 工具执行结果
system_prompt: 系统提示词
"""
try:
# 检查Ollama服务是否运行
if not self.is_service_running:
return "错误:Ollama服务未运行,请启动Ollama服务后再使用此引擎."
# 检查模型是否已配置
if not self.model or not self.model.strip():
return ("错误:Ollama模型未配置。请执行以下操作:\n"
" 1. /engine.ollama models → 查看本地可用模型\n"
" 2. /engine.ollama set <模型名> → 设置要使用的模型\n"
" 或编辑 config.json 中 api.engines.ollama.model 字段")
# 如果有工具结果,将其作为上下文返回给AI处理
if tool_results:
tool_result_text = f"工具执行结果: {tool_results.get('result', '无结果')}"
messages = self._build_messages_with_history(system_prompt, tool_result_text)
else:
messages = self._build_messages_with_history(system_prompt, user_input)
# 构建调用参数
# 重置取消标志
self._cancelled = False
# 获取流式回调
stream_cb = None
thinking_cb = None
kwargs = {
"model": self.model,
"messages": messages,
"stream": True if stream_cb else False
}
# 添加采样参数(从引擎配置读取)
options = {}
for key in ("temperature", "top_k", "top_p", "num_predict", "num_ctx"):
val = self.engine_config.get(key)
if val is not None:
options[key] = val
if options:
kwargs["options"] = options
# 使用Ollama Python库调用API
response = ollama_client.chat(**kwargs)
# ── 非流式模式 ──
message = _safe_get(response, "message", {})
# 普通文本响应
content = _safe_get(message, "content", "") or ""
# 处理深度思考:Ollama 把 thinking 和 content 分开返回
# 合并到一起,用 <thinking> 标记包裹,让主程序处理
thinking_content = _safe_get(message, "thinking", "") or ""
if thinking_content:
thinking_content = thinking_content.encode('utf-8', 'replace').decode('utf-8')
content = content.encode('utf-8', 'replace').decode('utf-8')
return f"<thinking>{thinking_content}</thinking>{content}"
# 清理无效的 UTF-8 代理字符
content = content.encode('utf-8', 'replace').decode('utf-8')
return content
except Exception as e:
return f"Ollama API调用失败: {str(e)}"
def _build_messages_with_history(self, system_prompt, user_input):
"""构建包含历史记录的消息列表(支持 FC tool 消息)"""
messages = []
# 添加系统提示词
if system_prompt:
clean_prompt = system_prompt.encode('utf-8', 'replace').decode('utf-8')
messages.append({
"role": "system",
"content": clean_prompt
})
# 添加历史对话记录(使用共享历史)
if self.shared_conversation_history is not None:
for msg in self.shared_conversation_history[-self.max_history:]:
if not isinstance(msg, dict) or "role" not in msg:
continue
role = msg["role"]
# 普通 user/assistant 消息
if "content" in msg:
clean_content = msg["content"]
if isinstance(clean_content, str):
clean_content = clean_content.encode('utf-8', 'replace').decode('utf-8')
messages.append({"role": role, "content": clean_content})
# 清理用户输入
if user_input:
clean_input = user_input.encode('utf-8', 'replace').decode('utf-8')
messages.append({
"role": "user",
"content": clean_input
})
return messages
def _call_api_with_history(self, content, system_prompt=None):
"""使用对话历史调用API"""
if not self.is_service_running:
return "错误:Ollama服务未运行,请启动Ollama服务后再使用此引擎."
# 构建消息列表
messages = []
# 添加系统提示词(如果提供)
if system_prompt:
messages.append({
"role": "system",
"content": system_prompt
})
# 添加历史对话记录(使用共享历史)
if self.shared_conversation_history is not None:
# 确保历史记录格式正确
for msg in self.shared_conversation_history[-self.max_history:]:
if isinstance(msg, dict) and "role" in msg and "content" in msg:
messages.append(msg)
else:
# 如果格式不正确,跳过该条记录
continue
# 添加当前内容
messages.append({
"role": "tool",
"content": content
})
# 使用Ollama Python库调用API
response = ollama_client.chat(
model=self.model,
messages=messages,
stream=False
)
return response["message"]["content"]
def set_model(self, model_name):
"""设置要使用的模型"""
if not self.is_service_running:
print(f"{Fore.RED}错误:Ollama服务未运行,无法切换模型{Style.RESET_ALL}")
return
self.model = model_name
print(f"{Fore.GREEN}已切换Ollama模型为: {model_name}{Style.RESET_ALL}")
def list_models(self):
"""列出本地可用的模型"""
if not self.is_service_running:
return ["错误:Ollama服务未运行"]
try:
response = ollama_client.list()
models_list = _safe_get(response, "models", [])
result = []
for model in models_list:
name = _safe_get(model, "model", "") or _safe_get(model, "name", "")
if name:
result.append(name)
return result if result else ["未找到本地模型"]
except Exception as e:
return [f"获取模型列表失败: {str(e)}"]
def list_remote_models(self, limit=10):
"""获取可下载的模型列表(仅展示前N个)"""
if not self.is_service_running:
return ["错误:Ollama服务未运行"]
try:
# 使用Ollama Python库获取可下载的模型列表
# 注意:Ollama库本身没有直接获取可下载模型列表的API
# 这里返回一些常见的模型作为示例
# 实际应用中可能需要从Ollama官网或其他来源获取此信息
common_models = [
"llama3.1:latest",
"llama3:latest",
"mistral:latest",
"gemma2:latest",
"phi3:latest",
"qwen2:latest",
"command-r:latest",
"dolphin-mistral:latest",
"nous-hermes2:latest",
"llava:latest",
"codellama:latest",
"mxbai-embed-large:latest",
"all-minilm:latest",
"stable-diffusion:latest",
"moondream:latest"
]
return common_models[:limit]
except Exception as e:
return [f"获取远程模型列表失败: {str(e)}"]
def search_models(self, query, limit=10):
"""模糊搜索可下载的模型"""
if not self.is_service_running:
return ["错误:Ollama服务未运行"]
try:
# 使用Ollama Python库获取可下载的模型列表
# 注意:Ollama库本身没有直接获取可下载模型列表的API
all_models = [
"llama3.1:latest", "llama3:latest", "llama2:latest",
"mistral:latest", "mistral-openorca:latest",
"gemma2:latest", "gemma:latest",
"phi3:latest", "phi2:latest",
"qwen2:latest", "qwen:latest",
"command-r:latest", "command-r-plus:latest",
"dolphin-mistral:latest", "dolphin-llama3:latest",
"nous-hermes2:latest", "nous-hermes:latest",
"llava:latest", "bakllava:latest",
"codellama:latest", "codeup:latest",
"mxbai-embed-large:latest", "nomic-embed-text:latest",
"all-minilm:latest", "bge-m3:latest",
"stable-diffusion:latest", "moondream:latest",
"llama3-groq:latest", "eva-qwen2:latest"
]
# 进行模糊搜索(简单匹配,包含查询词的模型)
matched_models = [model for model in all_models if query.lower() in model.lower()]
# 限制返回数量
return matched_models[:limit]
except Exception as e:
return [f"搜索模型失败: {str(e)}"]
def pull_model(self, model_name):
"""从Ollama库拉取模型"""
if not self.is_service_running:
print(f"{Fore.RED}错误:Ollama服务未运行,无法拉取模型{Style.RESET_ALL}")
return False
try:
print(f"{Fore.YELLOW}正在拉取模型 {model_name}...{Style.RESET_ALL}")
# 使用Ollama Python库拉取模型
response = ollama_client.pull(model_name, stream=True)
# 逐块处理响应
for part in response:
status = _safe_get(part, "status", None)
if status:
print(f"{Fore.YELLOW}{status}{Style.RESET_ALL}")
# 检查是否完成
completed = _safe_get(part, "completed", None)
total = _safe_get(part, "total", None)
if completed is not None and total is not None:
if completed == total:
print(f"{Fore.GREEN}模型 {model_name} 拉取完成!{Style.RESET_ALL}")
print(f"{Fore.GREEN}模型 {model_name} 拉取成功!{Style.RESET_ALL}")
return True
except Exception as e:
print(f"{Fore.RED}拉取模型 {model_name} 失败: {str(e)}{Style.RESET_ALL}")
return False
def handle_command(self, command):
"""处理引擎特定命令"""
if not self.is_service_running:
print(f"{Fore.RED}Ollama服务未运行,无法执行命令{Style.RESET_ALL}")
return False
if command == "models":
# 显示本地可用模型
models = self.list_models()
print(f"{Fore.GREEN}本地可用的Ollama模型:{Style.RESET_ALL}")
for model in models:
status = " (当前使用)" if model == self.model else ""
print(f"{Fore.GREEN} - {model}{status}{Style.RESET_ALL}")
return True
elif command == "remote-models":
# 显示可下载的模型(前10个)
models = self.list_remote_models()
print(f"{Fore.GREEN}可下载的Ollama模型(前10个):{Style.RESET_ALL}")
for i, model in enumerate(models, 1):
print(f"{Fore.GREEN} {i}. {model}{Style.RESET_ALL}")
return True
elif command.startswith("search "):
# 搜索模型
query = command[7:].strip() # 移除 'search '
if query:
models = self.search_models(query)
if models:
print(f"{Fore.GREEN}搜索 '{query}' 的结果(前10个):{Style.RESET_ALL}")
for i, model in enumerate(models, 1):
print(f"{Fore.GREEN} {i}. {model}{Style.RESET_ALL}")
else:
print(f"{Fore.YELLOW}未找到与 '{query}' 匹配的模型{Style.RESET_ALL}")
return True
else:
print(f"{Fore.RED}请提供搜索关键词.用法: /engine.search <关键词>{Style.RESET_ALL}")
return False
elif command.startswith("set "):
# 设置模型
model_name = command[4:].strip() # 移除 'set '
if model_name:
self.set_model(model_name)
return True
else:
print(f"{Fore.RED}请提供模型名称.用法: /engine.set <模型名>{Style.RESET_ALL}")
return False
elif command.startswith("pull "):
# 拉取模型
model_name = command[5:].strip() # 移除 'pull '
if model_name:
success = self.pull_model(model_name)
return success
else:
print(f"{Fore.RED}请提供模型名称.用法: /engine.pull <模型名>{Style.RESET_ALL}")
return False
else:
print(f"{Fore.RED}未知的Ollama命令.可用命令: models, remote-models, search, set, pull{Style.RESET_ALL}")
return False
+603
View File
@@ -0,0 +1,603 @@
"""
OpenAI 兼容格式引擎
支持所有兼容 OpenAI API 格式的服务(如 DeepSeek、Grok、硅基流动、本地 vLLM 等)
"""
import os
import sys
import json
import requests
from colorama import Fore, Style
import logging
# 设置日志
logger = logging.getLogger(__name__)
# 添加项目根目录到sys.path
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(current_dir)
sys.path.insert(0, project_root)
class OpenaiAI:
"""OpenAI 兼容格式 AI 引擎 - 支持所有兼容 OpenAI API 的服务"""
requires_api_key = True
def get_help_info(self):
"""获取引擎帮助信息"""
help_text = """
OpenAI 兼容格式引擎帮助信息
============================
引擎名称: openai
描述: 通用 OpenAI API 兼容引擎,支持所有兼容 OpenAI 格式的服务
支持的服务:
- OpenAI 官方 API
- DeepSeek API
- Grok (xAI) API
- 硅基流动 (SiliconFlow) API
- 本地 vLLM / LiteLLM / OneAPI 等转发服务
- 任何兼容 /chat/completions 接口的服务
配置方式 (config.json):
{
"api": {
"engines": {
"openai": {
"api_key": "你的API密钥",
"base_url": "https://api.openai.com/v1",
"model": "gpt-4o"
}
}
}
}
可用命令:
/engine.openai models - 列出可用模型(需要服务端支持)
/engine.openai set <模型名> - 切换模型
/engine.openai info - 显示当前配置信息
/engine.openai think - 查看深度思考状态
/engine.openai think on - 开启深度思考
/engine.openai think off - 关闭深度思考
/engine.openai think high - 开启并设置推理强度 (low/medium/high)
使用说明:
1. 在 config.json 中配置 api_key、base_url、model
2. 使用 /engine switch openai 切换到此引擎
3. 直接输入自然语言与 AI 对话
注意:
- base_url 填写 API 的基础地址,无需包含 /chat/completions
- 例如 OpenAI 官方填 https://api.openai.com/v1
- 本地服务填 http://localhost:8000/v1
"""
return help_text
def __init__(self):
self.name = "openai"
self.cli = None
# 从 config.json 读取配置
config_file = os.path.join(project_root, "config.json")
try:
with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
engine_config = config.get("api", {}).get("engines", {}).get("openai", {})
self.engine_config = engine_config # 保存为属性
self.api_key = engine_config.get("api_key", "")
self.base_url = engine_config.get("base_url", "").rstrip('/')
self.model = engine_config.get("model", "")
self.max_history = config.get("system", {}).get("max_history", 10)
# 深度思考配置
self.deep_think = engine_config.get("deep_think", False)
self.reasoning_effort = engine_config.get("reasoning_effort", "medium")
except Exception as e:
logger.warning(f"读取配置文件失败: {e}")
self.api_key = ""
self.base_url = ""
self.model = ""
self.max_history = 10
self.deep_think = False
self.reasoning_effort = "medium"
# 初始化对话历史
self.conversation_history = []
self.shared_conversation_history = None
# 思考标记(兼容深度思考模型)
self.thinking_start_marker = "<think>"
self.thinking_end_marker = "</think>"
# 检查配置
if not self.api_key:
print(f"{Fore.YELLOW}警告: openai 引擎的 API 密钥未设置{Style.RESET_ALL}")
if not self.base_url:
print(f"{Fore.YELLOW}警告: openai 引擎的 base_url 未设置{Style.RESET_ALL}")
else:
print(f"{Fore.GREEN}OpenAI 兼容引擎已启用 (base_url: {self.base_url}){Style.RESET_ALL}")
def _get_headers(self):
"""构建请求头"""
headers = {
'Content-Type': 'application/json'
}
if self.api_key:
headers['Authorization'] = f'Bearer {self.api_key}'
return headers
def _get_api_url(self):
"""获取完整的 API 地址"""
url = self.base_url
if not url:
return ""
if not url.endswith('/chat/completions'):
url = url.rstrip('/') + '/chat/completions'
return url
def generate_response(self, user_input, tool_results=None, system_prompt=None):
"""
生成 AI 响应
Args:
user_input: 用户输入
tool_results: 工具执行结果
system_prompt: 系统提示词
"""
try:
if not self.base_url:
return "错误: openai 引擎的 base_url 未配置,请在 config.json 中设置"
# 处理工具结果
if tool_results:
tool_result_text = f"工具执行结果: {tool_results.get('result', '无结果')}"
messages = self._build_messages_with_history(system_prompt, tool_result_text)
else:
messages = self._build_messages_with_history(system_prompt, user_input)
# 构建请求体
body = {
"model": self.model,
"messages": messages,
"stream": False
}
# 添加采样参数(从引擎配置读取)
for key in ("temperature", "top_p"):
val = self.engine_config.get(key)
if val is not None:
body[key] = val
# top_k 部分 OpenAI 兼容 API 不支持,仅在配置明确时添加
top_k = self.engine_config.get("top_k")
if top_k is not None:
body["top_k"] = top_k
# ── 深度思考模式 ──
self._inject_thinking_prompt = None # 清除标记
if self.deep_think:
body = self._apply_deep_think(body)
# 不支持原生思考的模型,注入提示词
if self._inject_thinking_prompt:
thinking_hint = self._get_thinking_prompt(self._inject_thinking_prompt)
if thinking_hint and messages:
# 追加到系统提示词末尾
if messages[0].get("role") == "system":
messages[0]["content"] += thinking_hint
else:
messages.insert(0, {"role": "system", "content": thinking_hint})
body["messages"] = messages
# 发送请求(带重试)
api_url = self._get_api_url()
import time
max_retries = 3
response = None
for attempt in range(max_retries):
try:
response = requests.post(
url=api_url,
json=body,
headers=self._get_headers(),
timeout=None
)
if response.status_code < 500 or attempt == max_retries - 1:
break
print(f"{Fore.YELLOW}API 返回 {response.status_code},第 {attempt+1}/{max_retries} 次重试...{Style.RESET_ALL}")
time.sleep(2)
except requests.exceptions.ConnectionError:
if attempt == max_retries - 1:
return f"无法连接到 API 服务: {self.base_url},请检查服务是否运行"
print(f"{Fore.YELLOW}连接失败,第 {attempt+1}/{max_retries} 次重试...{Style.RESET_ALL}")
time.sleep(2)
if response is None:
return "API 调用失败(重试后仍无响应)"
if response.status_code == 200:
response_data = response.json()
if "choices" in response_data and len(response_data["choices"]) > 0:
message = response_data["choices"][0].get("message", {})
# 普通文本响应
ai_response = message.get("content", "") or ""
reasoning = message.get("reasoning_content", "") or ""
# 清理无效的 UTF-8 代理字符
if ai_response:
ai_response = ai_response.encode('utf-8', 'replace').decode('utf-8')
ai_response = self._process_thinking_content(ai_response)
# 处理 reasoning_content 字段(合并到返回值,用 <thinking> 标记包裹)
if reasoning:
reasoning = reasoning.encode('utf-8', 'replace').decode('utf-8')
print(f"{Fore.LIGHTBLACK_EX}[深度思考: {reasoning[:500]}{'...' if len(reasoning) > 500 else ''}]{Style.RESET_ALL}")
if ai_response:
return f"<thinking>{reasoning}</thinking>{ai_response}"
else:
return f"<thinking>{reasoning}</thinking>"
return ai_response or ""
else:
return f"API 返回格式错误: {response_data}"
elif response.status_code == 401:
return "API 认证失败,请检查 api_key 是否正确"
elif response.status_code == 404:
return f"API 端点不存在,请检查 base_url 是否正确: {api_url}"
elif response.status_code == 400:
return f"API 调用失败: {response.status_code} - {response.text}"
else:
return f"API 调用失败: {response.status_code} - {response.text}"
except requests.exceptions.ConnectionError:
return f"无法连接到 API 服务: {self.base_url},请检查服务是否运行"
except Exception as e:
return f"API 调用失败: {str(e)}"
def _build_messages_with_history(self, system_prompt, user_input):
"""构建包含历史记录的消息列表"""
messages = []
# 添加系统提示词
if system_prompt:
clean_prompt = system_prompt.encode('utf-8', 'replace').decode('utf-8')
messages.append({
"role": "system",
"content": clean_prompt
})
# 添加历史对话记录(使用共享历史)
if self.shared_conversation_history is not None:
for msg in self.shared_conversation_history[-self.max_history:]:
if not isinstance(msg, dict) or "role" not in msg:
continue
role = msg["role"]
# 普通 user/assistant 消息
if "content" in msg:
clean_content = msg["content"]
if isinstance(clean_content, str):
clean_content = clean_content.encode('utf-8', 'replace').decode('utf-8')
messages.append({"role": role, "content": clean_content})
# 添加当前用户输入
if user_input:
clean_input = user_input.encode('utf-8', 'replace').decode('utf-8')
messages.append({
"role": "user",
"content": clean_input
})
return messages
def _call_api_with_history(self, content, system_prompt=None):
"""使用对话历史调用 API"""
if not self.base_url:
return "错误: openai 引擎的 base_url 未配置"
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
if self.shared_conversation_history is not None:
for msg in self.shared_conversation_history[-self.max_history:]:
if isinstance(msg, dict) and "role" in msg and "content" in msg:
messages.append(msg)
messages.append({"role": "user", "content": content})
body = {
"model": self.model,
"messages": messages,
"stream": False
}
try:
response = requests.post(
url=self._get_api_url(),
json=body,
headers=self._get_headers(),
timeout=None
)
if response.status_code == 200:
response_data = response.json()
if "choices" in response_data and len(response_data["choices"]) > 0:
ai_response = response_data["choices"][0]["message"]["content"]
ai_response = ai_response.encode('utf-8', 'replace').decode('utf-8')
return self._process_thinking_content(ai_response)
return f"API 调用失败: {response.status_code} - {response.text}"
except Exception as e:
return f"API 调用失败: {str(e)}"
def _process_thinking_content(self, response):
"""处理思考内容,将思考部分以灰色文本显示"""
import re
if not self.thinking_start_marker or not self.thinking_end_marker:
return response
if self.thinking_start_marker in response and self.thinking_end_marker in response:
parts = response.split(self.thinking_start_marker)
if len(parts) > 1:
remaining = parts[1]
if self.thinking_end_marker in remaining:
thinking_part, reply_part = remaining.split(self.thinking_end_marker, 1)
thinking_content = thinking_part.strip()
reply_content = reply_part.strip()
if thinking_content:
print(f"{Fore.LIGHTBLACK_EX}[思考: {thinking_content}]{Style.RESET_ALL}")
# 如果回复内容为空,返回原始 response 而非空字符串(不丢内容)
return reply_content if reply_content else response
return response
def _apply_deep_think(self, body: dict) -> dict:
"""
通用深度思考适配。
原生支持的模型用 API 参数,不支持的用提示词模拟。
"""
model = (self.model or "").lower()
effort = self.reasoning_effort # low / medium / high
# ── 原生支持:API 参数 ──
# DeepSeek / MiMo
if "deepseek" in model or "mimo" in model:
body["enable_thinking"] = True
body.pop("temperature", None)
if effort:
body["reasoning_effort"] = effort
return body
# OpenAI o-series (o1, o3, o4-mini 等)
if model.startswith("o") and (model[1:2].isdigit() or model[1:3] in ("1-", "3-", "4-")):
body["reasoning"] = {"effort": effort}
return body
# Anthropic Claude (通过 OpenAI 兼容层)
if "claude" in model or "anthropic" in model:
body["thinking"] = {"type": "enabled", "budget_tokens": self._thinking_budget(effort)}
return body
# Grok
if "grok" in model:
body["thinking"] = {"enabled": True}
return body
# Gemini thinking 模型
if "gemini" in model and "thinking" in model:
return body # 模型名自带 thinking,不需要额外参数
# ── 不支持原生思考:用提示词模拟 ──
self._inject_thinking_prompt = effort
return body
@staticmethod
def _thinking_budget(effort: str) -> int:
"""思考 token 预算"""
return {"low": 2048, "medium": 8192, "high": 32768}.get(effort, 8192)
def _get_thinking_prompt(self, effort: str) -> str:
"""根据思考等级生成提示词(注入系统提示词末尾)"""
if effort == "low":
return ""
if effort == "medium":
return (
"\n\n【思考模式:中等】\n"
"回答前请先逐步思考:\n"
"1. 分析用户的核心需求\n"
"2. 列出关键因素和约束\n"
"3. 考虑可能的方案\n"
"4. 选择最优方案并解释理由\n"
"然后再给出最终回答。"
)
# high
return (
"\n\n【思考模式:深度推理】\n"
"你必须使用结构化深度推理框架回答问题。严格遵循以下步骤:\n\n"
"## 第一步:问题分解\n"
"- 将问题拆解为子问题\n"
"- 识别关键约束和边界条件\n"
"- 明确输入输出要求\n\n"
"## 第二步:多角度分析\n"
"- 从至少 3 个不同角度分析问题\n"
"- 评估每种方案的优缺点\n"
"- 考虑边界情况和异常场景\n\n"
"## 第三步:方案设计\n"
"- 选择最优方案并详细说明理由\n"
"- 给出具体的实现步骤\n"
"- 预判可能的问题并给出预防措施\n\n"
"## 第四步:验证与优化\n"
"- 检验方案是否满足所有约束\n"
"- 考虑是否有更优解\n"
"- 给出最终答案和注意事项\n\n"
"## 输出格式\n"
"先输出推理过程(用 [思考] 标记),再输出最终答案。\n"
"推理过程要详细但不啰嗦,最终答案要简洁明确。"
)
def set_model(self, model_name):
"""设置模型"""
self.model = model_name
print(f"{Fore.GREEN}已切换模型为: {model_name}{Style.RESET_ALL}")
def list_models(self):
"""列出可用模型(调用 API 的 /models 端点)"""
if not self.base_url:
return ["错误: base_url 未配置"]
try:
models_url = self.base_url.rstrip('/')
if models_url.endswith('/chat/completions'):
models_url = models_url[:-len('/chat/completions')]
models_url = models_url.rstrip('/') + '/models'
response = requests.get(
url=models_url,
headers=self._get_headers(),
timeout=10
)
if response.status_code == 200:
data = response.json()
if "data" in data:
models = [m.get("id", "unknown") for m in data["data"]]
return models
else:
return ["API 返回格式不标准,无法解析模型列表"]
else:
return [f"获取模型列表失败: {response.status_code}"]
except Exception as e:
return [f"获取模型列表失败: {str(e)}"]
def generate_with_tools(self, messages, tools=None, system_prompt=None):
"""
原生 Function Calling 模式
返回 dict: {"content": str, "tool_calls": list, "reasoning_content": str, "raw": dict}
"""
try:
from colorama import Fore, Style
except ImportError:
Fore = Style = type('D', (), {'LIGHTBLACK_EX': '', 'RESET_ALL': ''})()
import requests
import time
if system_prompt:
messages = [{"role": "system", "content": system_prompt}] + messages
body = {
"model": self.model,
"messages": messages,
"stream": False,
"temperature": self.engine_config.get("temperature", 0.7),
}
top_p = self.engine_config.get("top_p")
if top_p is not None:
body["top_p"] = top_p
top_k = self.engine_config.get("top_k")
if top_k is not None:
body["top_k"] = top_k
if tools:
body["tools"] = tools
body["tool_choice"] = "auto"
api_url = self._get_api_url()
max_retries = 3
for attempt in range(max_retries):
try:
response = requests.post(
url=api_url, json=body, headers=self._get_headers(), timeout=None
)
if response.status_code == 200:
response_data = response.json()
if "choices" in response_data and len(response_data["choices"]) > 0:
message = response_data["choices"][0].get("message", {})
content_text = message.get("content", "") or ""
reasoning = message.get("reasoning_content", "") or ""
tool_calls = message.get("tool_calls", [])
if reasoning:
print(f"{Fore.LIGHTBLACK_EX}[深度思考: {reasoning[:500]}{'...' if len(reasoning) > 500 else ''}]{Style.RESET_ALL}")
return {
"content": content_text,
"tool_calls": tool_calls,
"reasoning_content": reasoning,
"raw": response_data
}
else:
return {"content": f"API 返回格式错误", "tool_calls": [], "raw": response_data}
elif response.status_code >= 500 and attempt < max_retries - 1:
print(f"{Fore.YELLOW}API 返回 {response.status_code},第 {attempt+1}/{max_retries} 次重试...{Style.RESET_ALL}")
time.sleep(2)
continue
else:
return {"content": f"API 调用失败: {response.status_code}", "tool_calls": [], "raw": {}}
except requests.exceptions.ConnectionError:
if attempt < max_retries - 1:
print(f"{Fore.YELLOW}连接失败,第 {attempt+1}/{max_retries} 次重试...{Style.RESET_ALL}")
time.sleep(2)
continue
return {"content": f"无法连接到 API: {api_url}", "tool_calls": [], "raw": {}}
except Exception as e:
return {"content": f"引擎调用失败: {e}", "tool_calls": [], "raw": {}}
return {"content": f"API 调用失败(重试{max_retries}次后仍失败)", "tool_calls": [], "raw": {}}
def handle_command(self, command):
"""处理引擎特定命令"""
if command == "models":
models = self.list_models()
print(f"{Fore.GREEN}可用模型列表:{Style.RESET_ALL}")
for model in models:
status = " (当前使用)" if model == self.model else ""
print(f"{Fore.GREEN} - {model}{status}{Style.RESET_ALL}")
return True
elif command.startswith("set "):
model_name = command[4:].strip()
if model_name:
self.set_model(model_name)
return True
else:
print(f"{Fore.RED}请提供模型名称. 用法: /engine.openai set <模型名>{Style.RESET_ALL}")
return False
elif command == "info":
print(f"{Fore.GREEN}OpenAI 兼容引擎配置:{Style.RESET_ALL}")
print(f" base_url: {self.base_url}")
print(f" model: {self.model}")
print(f" api_key: {'已设置' if self.api_key else '未设置'}")
print(f" deep_think: {'开启' if self.deep_think else '关闭'}")
if self.deep_think:
print(f" reasoning: {self.reasoning_effort}")
return True
elif command.startswith("think"):
# /engine.openai think [on|off|low|medium|high]
arg = command.replace("think", "").strip().lower()
if arg in ("off", "0", "false"):
self.deep_think = False
print(f"{Fore.YELLOW}深度思考已关闭{Style.RESET_ALL}")
elif arg in ("on", "1", "true", ""):
self.deep_think = True
print(f"{Fore.GREEN}深度思考已开启 (reasoning_effort: {self.reasoning_effort}){Style.RESET_ALL}")
elif arg in ("low", "medium", "high"):
self.deep_think = True
self.reasoning_effort = arg
print(f"{Fore.GREEN}深度思考已开启 (reasoning_effort: {arg}){Style.RESET_ALL}")
else:
print(f"{Fore.RED}用法: /engine.openai think [on|off|low|medium|high]{Style.RESET_ALL}")
return True
else:
print(f"{Fore.RED}未知命令. 可用: models, set, info, think{Style.RESET_ALL}")
return False