release: v6.1.0 - TUI 体验全面升级 + 背景自定义
TUI 界面改进: - 全面中文化: CommandPalette/HelpPanel/Footer/Chatbox 边框标题等所有用户可见英文汉化 - 辅助中心 (Ctrl+P/F9): 原"设置"重命名, 含 6 项可切换设置 - 独立帮助浮层 (? 键): 重写帮助页面逻辑, 全中文, 带滑入淡入动画 - 背景切换: 辅助中心 [5] 预设背景, [6] 自定义背景; 新增 /bg 命令支持图片提取主色调 - 换行修复: Ctrl+J 作为主推换行键 (Windows 终端不区分 Shift+Enter) - 引擎切换修复: 解决 call_from_thread 在主线程崩溃的 RuntimeError 技术改进: - 文本工具调用解析: 检测模型用 <tool_call> 标签返回的工具调用 - 网络工具优化: network_tools.py 和 ai_search.py 改进请求头/重试/编码 - 流式输出修复: append_chunk 的 refresh 改为 layout=True - 取消响应修复: 引擎层 _cancelled 标志 + TUI 层 worker 身份跟踪 融合 v6.0.0 工作: - 保留 terminal 模拟器/operit 桥接/深度思考/工作模式/记忆模式等 v6.0.0 全部功能 - 在此基础上叠加本会话的 TUI 改进
This commit is contained in:
1 parent
b5f638cffc
commit
de15b54b19
30 files changed
+3267
-934
No files matched your search
+23
-19
@@ -95,8 +95,8 @@ class BaseAICLI:
|
||||
self.load_liugins(liugins_dir, skills_dir)
|
||||
# 初始化共享对话历史
|
||||
self.shared_conversation_history = []
|
||||
# 设置最大历史记录数
|
||||
self.max_history = DEFAULT_MAX_HISTORY
|
||||
# 设置最大历史记录数 — 从 config 读取,未配置则用默认值
|
||||
self.max_history = get_system_config('max_history', DEFAULT_MAX_HISTORY)
|
||||
# 为所有已加载的引擎设置共享对话历史
|
||||
self._set_shared_conversation_history()
|
||||
# 初始化代码执行相关功能
|
||||
@@ -115,6 +115,14 @@ class BaseAICLI:
|
||||
self._clawli_monitor_running = False
|
||||
# TUI 输出回调(用于 TUI 模式下的实时输出)
|
||||
self.tui_output_callback = None
|
||||
# TUI 流式回调(用于 TUI 模式下逐字流式输出)
|
||||
self.tui_stream_callback = None
|
||||
# TUI 思考回调(用于 TUI 模式下深度思考内容单独显示)
|
||||
self.tui_thinking_callback = None
|
||||
# TUI 钩子:每次调用 AI 前触发(重新显示思考指示器等)
|
||||
self.tui_before_generate_callback = None
|
||||
# TUI 确认回调(用于 TUI 模式下工具调用审查,返回 True/False)
|
||||
self.tui_confirm_callback = None
|
||||
# 初始化用户ID(基于机器硬件动态生成)
|
||||
self.user_id = str(uuid.getnode())
|
||||
|
||||
@@ -319,6 +327,13 @@ class BaseAICLI:
|
||||
"""获取插件管理器"""
|
||||
return self.liugin_manager
|
||||
|
||||
def _output(self, message):
|
||||
"""统一输出方法,支持 TUI 和 CLI 模式"""
|
||||
if getattr(self, 'tui_output_callback', None):
|
||||
self.tui_output_callback(message)
|
||||
else:
|
||||
print(message)
|
||||
|
||||
def switch_engine(self, engine_name):
|
||||
"""切换AI引擎"""
|
||||
if engine_name in self.engines:
|
||||
@@ -333,35 +348,24 @@ class BaseAICLI:
|
||||
self.current_engine.max_history = self.max_history
|
||||
|
||||
if old_engine != self.current_engine and self.shared_conversation_history:
|
||||
print(f"{Fore.CYAN}已切换到AI引擎: {engine_name},正在传递对话历史...{Style.RESET_ALL}")
|
||||
self._output(f"{Fore.CYAN}已切换到AI引擎: {engine_name},正在传递对话历史...{Style.RESET_ALL}")
|
||||
|
||||
context_message = {
|
||||
"role": "system",
|
||||
"content": f"引擎切换通知:现在由{engine_name}引擎继续之前的对话。以下是之前的对话历史:\n"
|
||||
}
|
||||
self.shared_conversation_history.append(context_message)
|
||||
|
||||
if self.shared_conversation_history:
|
||||
print(f"{Fore.YELLOW}当前对话历史已传递给新引擎:{Style.RESET_ALL}")
|
||||
for msg in self.shared_conversation_history[-self.max_history:]:
|
||||
if isinstance(msg, dict) and "role" in msg and "content" in msg:
|
||||
role = msg["role"]
|
||||
content = msg["content"]
|
||||
if role == "user":
|
||||
print(f"{Fore.CYAN}用户: {content}{Style.RESET_ALL}")
|
||||
elif role == "assistant":
|
||||
print(f"{Fore.GREEN}AI: {content}{Style.RESET_ALL}")
|
||||
elif role == "tool":
|
||||
print(f"{Fore.MAGENTA}工具结果: {content}{Style.RESET_ALL}")
|
||||
self._output(f"{Fore.GREEN}已切换到AI引擎: {engine_name}{Style.RESET_ALL}")
|
||||
else:
|
||||
print(f"{Fore.GREEN}已切换到AI引擎: {engine_name}{Style.RESET_ALL}")
|
||||
self._output(f"{Fore.GREEN}已切换到AI引擎: {engine_name}{Style.RESET_ALL}")
|
||||
|
||||
try:
|
||||
set_system_config('default_engine', engine_name)
|
||||
print(f"{Fore.CYAN}已将 {engine_name} 设置为默认引擎{Style.RESET_ALL}")
|
||||
except Exception as e:
|
||||
print(f"{Fore.YELLOW}警告: 保存默认引擎配置失败: {e}{Style.RESET_ALL}")
|
||||
self._output(f"{Fore.YELLOW}警告: 保存默认引擎配置失败: {e}{Style.RESET_ALL}")
|
||||
else:
|
||||
print(f"{Fore.RED}未找到AI引擎: {engine_name}{Style.RESET_ALL}")
|
||||
self._output(f"{Fore.RED}未找到AI引擎: {engine_name}{Style.RESET_ALL}")
|
||||
|
||||
def _set_shared_conversation_history(self):
|
||||
"""为所有引擎设置共享对话历史"""
|
||||
|
||||
@@ -96,6 +96,7 @@ class ClawliMixin:
|
||||
tool_calls = self._extract_tool_calls(json_data)
|
||||
|
||||
if tool_calls:
|
||||
all_results = []
|
||||
for tool_data in tool_calls:
|
||||
tool_name = tool_data.get('tool', '未知工具')
|
||||
tool_args = tool_data.get('args', '')
|
||||
@@ -105,6 +106,7 @@ class ClawliMixin:
|
||||
|
||||
result = self.process_tool_call(tool_data)
|
||||
result_text = result.get('result', '无结果')
|
||||
all_results.append(result_text)
|
||||
|
||||
if CLAWLI_SERVER_AVAILABLE and clawli_server:
|
||||
clawli_server.send_tool_status(tool_name, tool_args, "success", result_text)
|
||||
@@ -114,7 +116,7 @@ class ClawliMixin:
|
||||
"content": response
|
||||
})
|
||||
|
||||
tool_result_str = "\n".join([f"工具执行结果: {r}" for r in [result.get('result', '')]])
|
||||
tool_result_str = "\n".join(f"工具执行结果: {r}" for r in all_results)
|
||||
self._process_clawli_user_message(f"{tool_result_str}\n请根据工具执行结果继续回答。")
|
||||
return
|
||||
|
||||
@@ -134,16 +136,13 @@ class ClawliMixin:
|
||||
image_engine = getattr(self, 'image_engine', None)
|
||||
if not image_engine:
|
||||
try:
|
||||
from image_engine import get_image_engine
|
||||
image_engine = get_image_engine()
|
||||
from image_engine import get_engine_manager
|
||||
image_engine = get_engine_manager()
|
||||
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 "请描述这张图片")
|
||||
result = image_engine.analyze(image_path, 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}")
|
||||
|
||||
+49
-48
@@ -1,60 +1,61 @@
|
||||
import os
|
||||
import time
|
||||
import json
|
||||
from colorama import Fore, Style
|
||||
|
||||
|
||||
class HistoryMixin:
|
||||
"""聊天记录管理相关方法"""
|
||||
"""聊天记录管理 — 统一委托给 MemoryManager"""
|
||||
|
||||
def save_chat_history(self, name):
|
||||
"""保存当前聊天记录到文件"""
|
||||
try:
|
||||
chat_data = {
|
||||
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"conversation": self.shared_conversation_history.copy()
|
||||
}
|
||||
file_path = os.path.join(self.chat_history_dir, f"{name}.json")
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(chat_data, f, ensure_ascii=False, indent=2, default=str)
|
||||
print(f"{Fore.GREEN}聊天记录已保存为: {name}{Style.RESET_ALL}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"{Fore.RED}保存聊天记录失败: {e}{Style.RESET_ALL}")
|
||||
"""保存当前聊天记录"""
|
||||
if not hasattr(self, 'memory_manager') or not self.memory_manager:
|
||||
print(f"{Fore.RED}记忆系统未初始化{Style.RESET_ALL}")
|
||||
return False
|
||||
self.memory_manager.auto_save_chat(
|
||||
self.shared_conversation_history,
|
||||
label=name
|
||||
)
|
||||
print(f"{Fore.GREEN}聊天记录已保存: {name}{Style.RESET_ALL}")
|
||||
return True
|
||||
|
||||
def list_chat_history(self):
|
||||
"""列出所有已保存的聊天记录"""
|
||||
try:
|
||||
chat_files = [f for f in os.listdir(self.chat_history_dir) if f.endswith('.json')]
|
||||
if not chat_files:
|
||||
print(f"{Fore.YELLOW}没有已保存的聊天记录{Style.RESET_ALL}")
|
||||
return
|
||||
print(f"{Fore.GREEN}已保存的聊天记录:{Style.RESET_ALL}")
|
||||
for i, file in enumerate(chat_files, 1):
|
||||
file_path = os.path.join(self.chat_history_dir, file)
|
||||
mod_time = os.path.getmtime(file_path)
|
||||
mod_time_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(mod_time))
|
||||
chat_name = file[:-5]
|
||||
print(f"{Fore.GREEN}{i}. {chat_name} (保存时间: {mod_time_str}){Style.RESET_ALL}")
|
||||
except Exception as e:
|
||||
print(f"{Fore.RED}列出聊天记录失败: {e}{Style.RESET_ALL}")
|
||||
"""列出所有聊天记录"""
|
||||
if not hasattr(self, 'memory_manager') or not self.memory_manager:
|
||||
print(f"{Fore.RED}记忆系统未初始化{Style.RESET_ALL}")
|
||||
return
|
||||
chats = self.memory_manager.list_chats(limit=30)
|
||||
if not chats:
|
||||
print(f"{Fore.YELLOW}没有已保存的聊天记录{Style.RESET_ALL}")
|
||||
return
|
||||
print(f"{Fore.GREEN}已保存的聊天记录:{Style.RESET_ALL}")
|
||||
for i, c in enumerate(chats, 1):
|
||||
print(f"{Fore.GREEN}{i}. {c['name']} ({c['time']}, {c['size_kb']}KB){Style.RESET_ALL}")
|
||||
|
||||
def load_chat_history(self, name):
|
||||
"""加载指定的聊天记录"""
|
||||
try:
|
||||
file_path = os.path.join(self.chat_history_dir, f"{name}.json")
|
||||
if not os.path.exists(file_path):
|
||||
print(f"{Fore.RED}聊天记录 '{name}' 不存在{Style.RESET_ALL}")
|
||||
return False
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
chat_data = json.load(f)
|
||||
self.shared_conversation_history = chat_data.get("conversation", [])
|
||||
self._set_shared_conversation_history()
|
||||
print(f"{Fore.GREEN}已加载聊天记录: {name}{Style.RESET_ALL}")
|
||||
if self.shared_conversation_history:
|
||||
print(f"{Fore.CYAN}加载了 {len(self.shared_conversation_history)} 条对话记录{Style.RESET_ALL}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"{Fore.RED}加载聊天记录失败: {e}{Style.RESET_ALL}")
|
||||
"""加载聊天记录(支持序号或文件名)"""
|
||||
if not hasattr(self, 'memory_manager') or not self.memory_manager:
|
||||
print(f"{Fore.RED}记忆系统未初始化{Style.RESET_ALL}")
|
||||
return False
|
||||
|
||||
# 支持序号加载
|
||||
if name.isdigit():
|
||||
chats = self.memory_manager.list_chats(limit=50)
|
||||
idx = int(name) - 1
|
||||
if 0 <= idx < len(chats):
|
||||
name = chats[idx]['name'].replace('.json', '')
|
||||
else:
|
||||
print(f"{Fore.RED}序号 {name} 超出范围{Style.RESET_ALL}")
|
||||
return False
|
||||
|
||||
conversation = self.memory_manager.load_chat(name)
|
||||
if conversation is None:
|
||||
# 尝试加 .json 后缀
|
||||
if not name.endswith('.json'):
|
||||
conversation = self.memory_manager.load_chat(name + '.json')
|
||||
if conversation is None:
|
||||
print(f"{Fore.RED}聊天记录 '{name}' 不存在{Style.RESET_ALL}")
|
||||
return False
|
||||
|
||||
self.shared_conversation_history = conversation
|
||||
self._set_shared_conversation_history()
|
||||
print(f"{Fore.GREEN}已加载聊天记录: {name}{Style.RESET_ALL}")
|
||||
print(f"{Fore.CYAN}加载了 {len(conversation)} 条对话记录{Style.RESET_ALL}")
|
||||
return True
|
||||
@@ -42,6 +42,60 @@ def set_system_config(key, value):
|
||||
save_config(config)
|
||||
|
||||
|
||||
# ── 引擎配置档案管理 ──
|
||||
# 存储结构: config["engine_profiles"][engine_name] = [ {name, base_url, api_key, model}, ... ]
|
||||
|
||||
def get_profiles(engine_name):
|
||||
"""获取指定引擎的所有配置档案"""
|
||||
config = load_config()
|
||||
return config.get("engine_profiles", {}).get(engine_name, [])
|
||||
|
||||
|
||||
def save_profile(engine_name, profile):
|
||||
"""保存/更新一个配置档案(按 name 去重,已存在则覆盖)"""
|
||||
config = load_config()
|
||||
if "engine_profiles" not in config:
|
||||
config["engine_profiles"] = {}
|
||||
profiles = config["engine_profiles"].setdefault(engine_name, [])
|
||||
# 按 name 去重
|
||||
for i, p in enumerate(profiles):
|
||||
if p.get("name") == profile.get("name"):
|
||||
profiles[i] = profile
|
||||
break
|
||||
else:
|
||||
profiles.append(profile)
|
||||
config["engine_profiles"][engine_name] = profiles
|
||||
save_config(config)
|
||||
|
||||
|
||||
def delete_profile(engine_name, profile_name):
|
||||
"""删除指定引擎的一个配置档案"""
|
||||
config = load_config()
|
||||
profiles = config.get("engine_profiles", {}).get(engine_name, [])
|
||||
profiles = [p for p in profiles if p.get("name") != profile_name]
|
||||
if "engine_profiles" not in config:
|
||||
config["engine_profiles"] = {}
|
||||
config["engine_profiles"][engine_name] = profiles
|
||||
save_config(config)
|
||||
|
||||
|
||||
def apply_profile(engine_name, profile):
|
||||
"""将档案应用到 config.json 的 api.engines 节点(下次启动或重载时生效)"""
|
||||
config = load_config()
|
||||
if "api" not in config:
|
||||
config["api"] = {}
|
||||
if "engines" not in config["api"]:
|
||||
config["api"]["engines"] = {}
|
||||
engine_cfg = config["api"]["engines"].setdefault(engine_name, {})
|
||||
if "base_url" in profile:
|
||||
engine_cfg["base_url"] = profile["base_url"]
|
||||
if "api_key" in profile:
|
||||
engine_cfg["api_key"] = profile["api_key"]
|
||||
if "model" in profile:
|
||||
engine_cfg["model"] = profile["model"]
|
||||
save_config(config)
|
||||
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
|
||||
@@ -162,6 +162,10 @@ def parse_fc_response(response_data: dict) -> Optional[List[Dict]]:
|
||||
except json.JSONDecodeError:
|
||||
args = {"raw": args_str}
|
||||
|
||||
# 确保 args 是 dict(LLM 可能返回非对象 JSON)
|
||||
if not isinstance(args, dict):
|
||||
args = {"raw": args}
|
||||
|
||||
# 统一为小狸内部格式: {"action": "use_tool", "tool": name, "args": "..." }
|
||||
# 如果 args 里有 args 字段,直接用;否则把整个 args 序列化
|
||||
if "args" in args and isinstance(args["args"], str):
|
||||
|
||||
@@ -109,17 +109,8 @@ class NotificationManager:
|
||||
pass
|
||||
|
||||
def _notify_windows(self, title: str, message: str):
|
||||
"""Windows 通知 — 优先使用 win10toast,降级到 PowerShell"""
|
||||
# 方法1: win10toast (pip install win10toast)
|
||||
try:
|
||||
from win10toast import ToastNotifier
|
||||
toaster = ToastNotifier()
|
||||
toaster.show_toast(title, message, duration=3, threaded=True)
|
||||
return
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# 方法2: PowerShell BurntToast 模块
|
||||
"""Windows 通知 — 使用 PowerShell(win10toast 在 Python 3.12+ 上不兼容)"""
|
||||
# 方法1: PowerShell BurntToast 模块
|
||||
try:
|
||||
ps_script = f'''
|
||||
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null
|
||||
@@ -138,7 +129,7 @@ class NotificationManager:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 方法3: PowerShell 简单弹窗 (最后降级)
|
||||
# 方法2: PowerShell 简单弹窗 (最后降级)
|
||||
try:
|
||||
escaped_msg = message.replace("'", "''")
|
||||
escaped_title = title.replace("'", "''")
|
||||
|
||||
@@ -107,24 +107,16 @@ class ProcessProtection:
|
||||
if sys.platform == 'darwin':
|
||||
# macOS: 使用 fcntl 文件锁
|
||||
import fcntl
|
||||
self._lock_fd = open(self._lock_file_path, 'w')
|
||||
self._lock_fd = open(self._lock_file_path, 'a+')
|
||||
self._lock_fd.seek(0)
|
||||
try:
|
||||
fcntl.flock(self._lock_fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except (IOError, OSError):
|
||||
# 文件已被锁定 — 已有实例在运行
|
||||
# 检查锁文件中的 PID 是否仍在运行
|
||||
try:
|
||||
old_pid = self._lock_fd.read().strip()
|
||||
if old_pid and self._is_process_running(int(old_pid)):
|
||||
self._lock_fd.close()
|
||||
self._lock_fd = None
|
||||
return False
|
||||
else:
|
||||
# 旧进程已死,清理残留锁
|
||||
self._lock_fd.seek(0)
|
||||
self._lock_fd.truncate()
|
||||
except (ValueError, OSError):
|
||||
pass
|
||||
# flock 失败 — 已有存活实例在运行
|
||||
# (flock 是进程级锁,进程退出时自动释放)
|
||||
self._lock_fd.close()
|
||||
self._lock_fd = None
|
||||
return False
|
||||
|
||||
self._lock_fd.seek(0)
|
||||
self._lock_fd.truncate()
|
||||
@@ -135,22 +127,15 @@ class ProcessProtection:
|
||||
else:
|
||||
# Linux: 使用 fcntl 文件锁
|
||||
import fcntl
|
||||
self._lock_fd = open(self._lock_file_path, 'w')
|
||||
self._lock_fd = open(self._lock_file_path, 'a+')
|
||||
self._lock_fd.seek(0)
|
||||
try:
|
||||
fcntl.flock(self._lock_fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except (IOError, OSError):
|
||||
# 检查旧进程是否仍在运行
|
||||
try:
|
||||
old_pid = self._lock_fd.read().strip()
|
||||
if old_pid and self._is_process_running(int(old_pid)):
|
||||
self._lock_fd.close()
|
||||
self._lock_fd = None
|
||||
return False
|
||||
else:
|
||||
self._lock_fd.seek(0)
|
||||
self._lock_fd.truncate()
|
||||
except (ValueError, OSError):
|
||||
pass
|
||||
# flock 失败 — 已有存活实例在运行
|
||||
self._lock_fd.close()
|
||||
self._lock_fd = None
|
||||
return False
|
||||
|
||||
self._lock_fd.seek(0)
|
||||
self._lock_fd.truncate()
|
||||
|
||||
+2093
-475
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user