perf: 启动输出瘦身 + FC 能力探测缓存,修复测试套件失效
自用体验(同时以人类和被接入 AI 的视角)暴露出三个问题,一并处理:
1. 启动太吵:117 行 / 16983 字符的开屏,人还没开始用就先刷屏
- 完整 ASCII 开机动画改为 --logo / --banner 按需召唤,默认两行紧凑标识
- 21 行命令说明折叠成一行「/help 全部命令 · /quit 退出 · ...」,
完整速查表挪进 /help(新增 show_quick_commands)
- 插件/引擎/工具的逐条加载日志改走 vprint,由 --verbose / -v /
XCLI_VERBOSE=1 召唤;工具加载汇总成一行
- 日志控制台 handler 降到 WARNING(文件仍留全量 INFO),并静音
httpx/httpcore/urllib3 等第三方心跳日志
=> 默认启动 10 行 / 475 字符,--verbose 仍可看到全部 36 行细节
2. 不支持 FC 的模型每次会话都白撞一发 500
讯飞 xophunyuan7bmt 不支持 Function Calling,请求带 tools 字段必报
500 Invalid Params,虽有降级重试兜底,但每个新会话都要先错一次。
- 新增 xcli_core/fc_tools 能力缓存(按 base_url::model 落盘 .fc_support.json)
- 降级重试成功后记住该模型不支持 FC,后续直接不发 tools
- 降级也失败时不写缓存,避免把别的故障误判成 FC 问题
- 新增 /fc [reset] 查看与清除缓存
3. diff 弹窗模式每次启动都要重新问一遍
- 首次选择后经 config.json 持久化,新增 /diff [popup|inline] 随时切换
测试:
- 新增 tests/test_startup_and_fc.py(27 项,含假 HTTP 验证降级与缓存链路)
- 修复 tests/test_plugins.py 长期失效:插件入口类早已由 Plugin 改名 Liugin,
8 处 fixture 未跟进导致 65 个用例直接 error;断言也未适配 ToolResult 返回值
与「❌」错误前缀。改为 plugin_class() / as_text() / is_error() 语义判断。
- 全量:217 passed(此前 152 passed + 65 error + 27 failed)
This commit is contained in:
1 parent
d2ac6896ad
commit
3a86da7444
12 files changed
+728
-167
No files matched your search
+11
-10
@@ -10,6 +10,7 @@ from .constants import (
|
||||
UNIFIED_TOOL_MANAGER_AVAILABLE, TEXTUAL_AVAILABLE,
|
||||
)
|
||||
from .config import get_system_config, set_system_config, logger
|
||||
from .verbose import vprint
|
||||
from .plugin_manager import LiuginManager
|
||||
from .process_protection import get_protection, enable_process_protection
|
||||
|
||||
@@ -70,11 +71,11 @@ class BaseAICLI:
|
||||
# 设置默认引擎,优先使用配置文件指定的引擎
|
||||
if default_engine_name in self.engines:
|
||||
self.current_engine = self.engines[default_engine_name]
|
||||
print(f"{Fore.GREEN}使用配置的默认AI引擎: {default_engine_name}{Style.RESET_ALL}")
|
||||
vprint(f"{Fore.GREEN}使用配置的默认AI引擎: {default_engine_name}{Style.RESET_ALL}")
|
||||
elif self.engines:
|
||||
first_engine_name = next(iter(self.engines))
|
||||
self.current_engine = self.engines[first_engine_name]
|
||||
print(f"{Fore.GREEN}使用默认AI引擎: {first_engine_name}{Style.RESET_ALL}")
|
||||
vprint(f"{Fore.GREEN}使用默认AI引擎: {first_engine_name}{Style.RESET_ALL}")
|
||||
else:
|
||||
print(f"{Fore.RED}警告: 没有可用的AI引擎,请检查ai_engines目录{Style.RESET_ALL}")
|
||||
self.current_engine = None
|
||||
@@ -153,7 +154,7 @@ class BaseAICLI:
|
||||
|
||||
def load_ai_engines(self):
|
||||
"""动态加载AI引擎插件"""
|
||||
print(f"{Fore.GREEN}正在加载AI引擎插件...{Style.RESET_ALL}")
|
||||
vprint(f"{Fore.GREEN}正在加载AI引擎插件...{Style.RESET_ALL}")
|
||||
ai_engines_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "ai_engines")
|
||||
if os.path.exists(ai_engines_dir):
|
||||
for filename in os.listdir(ai_engines_dir):
|
||||
@@ -171,7 +172,7 @@ class BaseAICLI:
|
||||
engine_key = getattr(engine_instance, 'name', engine_name)
|
||||
if engine_key not in self.engines:
|
||||
self.engines[engine_key] = engine_instance
|
||||
print(f"{Fore.GREEN}已加载AI引擎: {engine_key} ({class_name}){Style.RESET_ALL}")
|
||||
vprint(f"{Fore.GREEN}已加载AI引擎: {engine_key} ({class_name}){Style.RESET_ALL}")
|
||||
else:
|
||||
print(f"{Fore.YELLOW}AI引擎 {engine_key} 已存在,跳过加载{Style.RESET_ALL}")
|
||||
else:
|
||||
@@ -185,7 +186,7 @@ class BaseAICLI:
|
||||
except Exception as e:
|
||||
logger.error(f"加载AI引擎插件时发生未知错误 {filename}: {e}")
|
||||
print(f"{Fore.RED}加载AI引擎插件失败 {filename}: {e}{Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN}总共加载了 {len(self.engines)} 个AI引擎{Style.RESET_ALL}")
|
||||
vprint(f"{Fore.GREEN}总共加载了 {len(self.engines)} 个AI引擎{Style.RESET_ALL}")
|
||||
|
||||
def _find_engine_class(self, module, engine_name):
|
||||
"""查找AI引擎类,使用动态方式避免硬编码类名"""
|
||||
@@ -213,7 +214,7 @@ class BaseAICLI:
|
||||
|
||||
def load_liugins(self, liugins_dir=None, skills_dir=None):
|
||||
"""加载liugin和技能"""
|
||||
print(f"{Fore.GREEN}正在加载工具...{Style.RESET_ALL}")
|
||||
vprint(f"{Fore.GREEN}正在加载工具...{Style.RESET_ALL}")
|
||||
|
||||
if self._use_unified_manager:
|
||||
self.liugin_manager.initialize(liugins_dir or "plugins", skills_dir or "skills")
|
||||
@@ -242,19 +243,19 @@ class BaseAICLI:
|
||||
tool_info = plugin_instance.get_tool_info()
|
||||
tool_info['handler'] = plugin_instance.handle
|
||||
self.liugin_manager.tools.append(tool_info)
|
||||
print(f"{Fore.GREEN}已加载liugin: {liugin_name}{Style.RESET_ALL}")
|
||||
vprint(f"{Fore.GREEN}已加载liugin: {liugin_name}{Style.RESET_ALL}")
|
||||
except Exception as e:
|
||||
print(f"{Fore.RED}加载liugin失败 {filename}: {e}{Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN}总共加载了 {len(self.liugin_manager.tools)} 个工具插件{Style.RESET_ALL}")
|
||||
vprint(f"{Fore.GREEN}总共加载了 {len(self.liugin_manager.tools)} 个工具插件{Style.RESET_ALL}")
|
||||
|
||||
print(f"{Fore.GREEN}工具加载完成!{Style.RESET_ALL}")
|
||||
vprint(f"{Fore.GREEN}工具加载完成!{Style.RESET_ALL}")
|
||||
|
||||
# ── 命令注册 ──
|
||||
|
||||
def register_liugin_command(self, command, handler):
|
||||
"""注册插件命令"""
|
||||
self.liugin_commands[command] = handler
|
||||
print(f"{Fore.GREEN}已注册插件命令: {command}{Style.RESET_ALL}")
|
||||
vprint(f"{Fore.GREEN}已注册插件命令: {command}{Style.RESET_ALL}")
|
||||
|
||||
def unregister_liugin_command(self, command):
|
||||
"""注销插件命令"""
|
||||
|
||||
+96
-54
@@ -10,7 +10,7 @@ from typing import Optional
|
||||
from colorama import Fore, Style
|
||||
|
||||
from .constants import TEXTUAL_AVAILABLE, LOVE_FILE_PATH, DEFAULT_MAX_HISTORY, VERSION
|
||||
from .config import get_system_config, logger
|
||||
from .config import get_system_config, set_system_config, logger
|
||||
from .cli_base import BaseAICLI
|
||||
from .safety import get_safety, MODE_UNRESTRICTED, MODE_NORMAL, MODE_MANUAL
|
||||
from .cli_clawli import ClawliMixin
|
||||
@@ -1031,63 +1031,55 @@ multi 操作支持一次修改多处:
|
||||
"""运行 CLI"""
|
||||
import uuid
|
||||
self.user_id = str(uuid.getnode())
|
||||
print(f"{Fore.GREEN}小狸 Pro-CLI v{VERSION} 已启动!{Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN}用户ID: {self.user_id}{Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN}输入 '/help' 查看帮助信息{Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN}输入 '/quit' 退出程序{Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN}输入 '/tui' 切换到 TUI 模式{Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN}输入 '/engine list' 查看可用AI引擎{Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN}输入 '/engine switch <引擎名>' 切换AI引擎{Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN}输入 '/safe' 切换安全模式 (普通→人工→无限制){Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN}输入 '/notify' 切换任务完成通知 (开/关){Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN}输入 '/scheduler' 或 '/remind' 管理定时任务{Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN}输入 '/memory' 管理记忆系统 (日记/搜索/聊天记录){Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN}输入 '/file.read <文件名> [行数]' 直接读取文件内容{Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN}输入 '@文件路径' 自动读取文件内容并发送给AI{Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN}输入 '@图片路径' 自动分析图片并发送描述给AI{Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN}输入 '/image engines' 查看图像识别引擎{Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN}输入 '/chat save <名称>' 保存当前聊天记录{Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN}输入 '/chat list' 查看所有已保存的聊天记录{Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN}输入 '/chat open <名称>' 加载聊天记录{Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN}输入 '/remote' 查看远程连接帮助{Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN}输入 '/plugin' 管理插件市场 (安装/卸载/搜索){Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN}输入 '/protect' 查看进程保护状态{Style.RESET_ALL}")
|
||||
current_engine_name = getattr(self.current_engine, 'name', '未设置') if self.current_engine else '未设置'
|
||||
print(f"{Fore.GREEN}当前使用的AI引擎: {current_engine_name}{Style.RESET_ALL}")
|
||||
model_name = getattr(self.current_engine, 'model', '') if self.current_engine else ''
|
||||
engine_desc = f"{current_engine_name} ({model_name})" if model_name else current_engine_name
|
||||
|
||||
print(f"{Fore.GREEN}小狸 Pro-CLI v{VERSION} 已启动!{Style.RESET_ALL} "
|
||||
f"{Fore.WHITE}引擎: {engine_desc}{Style.RESET_ALL}")
|
||||
|
||||
# 一行速览,完整命令清单收进 /help
|
||||
print(f"{Fore.CYAN}/help 全部命令 · /quit 退出 · /tui 图形界面 · "
|
||||
f"@文件路径 读文件给 AI{Style.RESET_ALL}")
|
||||
|
||||
extras = []
|
||||
loaded_engines = list(self.engines.keys())
|
||||
if len(loaded_engines) > 1:
|
||||
print(f"{Fore.GREEN}已加载的AI引擎: {', '.join(loaded_engines)}{Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN}{'-' * 50}{Style.RESET_ALL}")
|
||||
|
||||
# 显示进程保护状态
|
||||
if hasattr(self, 'protection_results') and self.protection_results:
|
||||
extras.append(f"可切换引擎: {', '.join(loaded_engines)}")
|
||||
# 进程保护:只在有未生效项时才提示,全绿就闭嘴
|
||||
if getattr(self, 'protection_results', None):
|
||||
r = self.protection_results
|
||||
si_icon = '' if r.get('single_instance') else ''
|
||||
prio_icon = '' if r.get('priority') else ''
|
||||
ppl_icon = '' if r.get('ppl_protection') else ''
|
||||
wd_icon = '' if r.get('watchdog') else ''
|
||||
print(f"{Fore.CYAN} 进程保护: {si_icon}单实例 {prio_icon}优先级 {ppl_icon}PPL保护 {wd_icon}看门狗{Style.RESET_ALL}")
|
||||
failed = [n for k, n in (('single_instance', '单实例'), ('priority', '优先级'),
|
||||
('ppl_protection', 'PPL保护'), ('watchdog', '看门狗'))
|
||||
if not r.get(k)]
|
||||
if failed:
|
||||
extras.append(f"进程保护未生效: {', '.join(failed)}")
|
||||
if extras:
|
||||
print(f"{Fore.BLACK}{Style.BRIGHT}{' · '.join(extras)}{Style.RESET_ALL}")
|
||||
|
||||
# 询问 diff 弹窗模式
|
||||
print(f"{Fore.BLACK}{Style.BRIGHT}用户ID: {self.user_id}{Style.RESET_ALL}")
|
||||
|
||||
# 恢复 diff 显示模式(首次运行才询问)
|
||||
self._ask_diff_popup_mode()
|
||||
|
||||
self._run_cli_loop()
|
||||
|
||||
def _ask_diff_popup_mode(self):
|
||||
"""启动时询问 diff 显示模式"""
|
||||
# 找到 code_editor 插件
|
||||
code_editor = None
|
||||
def _get_code_editor_plugin(self):
|
||||
"""拿到 code_editor 插件实例(找不到返回 None)"""
|
||||
for tool in self.liugin_manager.tools:
|
||||
if tool.get('name') == 'code_editor':
|
||||
code_editor = tool.get('handler')
|
||||
break
|
||||
return getattr(tool.get('handler'), '__self__', None)
|
||||
return None
|
||||
|
||||
if code_editor is None:
|
||||
def _ask_diff_popup_mode(self):
|
||||
"""设置 diff 显示模式:已保存过就静默沿用,只有首次运行才交互询问"""
|
||||
plugin_instance = self._get_code_editor_plugin()
|
||||
if plugin_instance is None:
|
||||
return
|
||||
|
||||
# 找到插件实例(通过 handler 的 __self__)
|
||||
plugin_instance = getattr(code_editor, '__self__', None)
|
||||
if plugin_instance is None:
|
||||
saved = get_system_config('diff_popup_mode', None)
|
||||
if saved is not None:
|
||||
plugin_instance.diff_popup_mode = bool(saved)
|
||||
return
|
||||
|
||||
print(f"\n{Fore.CYAN} Diff 显示模式(AI 修改文件后的对比方式):{Style.RESET_ALL}")
|
||||
@@ -1095,19 +1087,23 @@ multi 操作支持一次修改多处:
|
||||
print(f" {Fore.WHITE}2{Style.RESET_ALL} - 主终端内显示(SSH 兼容,推荐)")
|
||||
|
||||
try:
|
||||
choice = input(f"\n{Fore.WHITE} 请选择 [1/2](默认 2): {Style.RESET_ALL}").strip()
|
||||
choice = input(f"\n{Fore.WHITE} 请选择 [1/2](默认 2,之后可用 /diff 改): {Style.RESET_ALL}").strip()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
choice = '2'
|
||||
|
||||
if choice == '1':
|
||||
plugin_instance.diff_popup_mode = True
|
||||
print(f"{Fore.GREEN} ✓ 已选择: 弹窗模式{Style.RESET_ALL}")
|
||||
else:
|
||||
plugin_instance.diff_popup_mode = False
|
||||
print(f"{Fore.GREEN} ✓ 已选择: 主终端内显示{Style.RESET_ALL}")
|
||||
|
||||
self._set_diff_popup_mode(choice == '1', plugin_instance)
|
||||
print()
|
||||
|
||||
def _set_diff_popup_mode(self, popup, plugin_instance=None):
|
||||
"""写入并持久化 diff 显示模式"""
|
||||
if plugin_instance is None:
|
||||
plugin_instance = self._get_code_editor_plugin()
|
||||
if plugin_instance is not None:
|
||||
plugin_instance.diff_popup_mode = bool(popup)
|
||||
set_system_config('diff_popup_mode', bool(popup))
|
||||
label = '弹窗模式' if popup else '主终端内显示'
|
||||
print(f"{Fore.GREEN} ✓ Diff 显示模式: {label}{Style.RESET_ALL}")
|
||||
|
||||
def _run_cli_loop(self):
|
||||
"""运行 CLI 主循环"""
|
||||
while True:
|
||||
@@ -1297,6 +1293,42 @@ multi 操作支持一次修改多处:
|
||||
print(f"{icon} 安全模式: {mode_name}")
|
||||
continue
|
||||
|
||||
if user_input.startswith('/diff'):
|
||||
parts = user_input.split()
|
||||
if self._get_code_editor_plugin() is None:
|
||||
print(f"{Fore.YELLOW}code_editor 插件未加载,无法设置 diff 模式{Style.RESET_ALL}")
|
||||
continue
|
||||
if len(parts) > 1:
|
||||
arg = parts[1].lower()
|
||||
if arg in ('popup', '弹窗', '1'):
|
||||
self._set_diff_popup_mode(True)
|
||||
elif arg in ('inline', '内联', '主终端', '2'):
|
||||
self._set_diff_popup_mode(False)
|
||||
else:
|
||||
print(f"{Fore.RED}用法: /diff [popup|inline]{Style.RESET_ALL}")
|
||||
else:
|
||||
cur = bool(get_system_config('diff_popup_mode', False))
|
||||
self._set_diff_popup_mode(not cur)
|
||||
continue
|
||||
|
||||
if user_input.startswith('/fc'):
|
||||
from .fc_tools import get_fc_cache, reset_fc_cache
|
||||
parts = user_input.split()
|
||||
if len(parts) > 1 and parts[1].lower() in ('reset', 'clear', '清除'):
|
||||
n = reset_fc_cache()
|
||||
print(f"{Fore.GREEN}已清除 {n} 条 FC 探测记录,下次请求会重新试探{Style.RESET_ALL}")
|
||||
else:
|
||||
cache = get_fc_cache()
|
||||
if not cache:
|
||||
print(f"{Fore.GREEN}FC 探测缓存为空:所有模型都按支持 function calling 处理{Style.RESET_ALL}")
|
||||
else:
|
||||
print(f"{Fore.CYAN}已探测到不支持 function calling 的模型:{Style.RESET_ALL}")
|
||||
for key, ok in cache.items():
|
||||
if ok is False:
|
||||
print(f" {Fore.YELLOW}✗{Style.RESET_ALL} {key}")
|
||||
print(f"{Fore.WHITE}这些请求会自动跳过 tools 字段;/fc reset 可清除重测{Style.RESET_ALL}")
|
||||
continue
|
||||
|
||||
if user_input.startswith('/notify'):
|
||||
parts = user_input.split()
|
||||
nm = get_notification_manager()
|
||||
@@ -1436,12 +1468,22 @@ def main():
|
||||
import codecs
|
||||
sys.stdout = codecs.getwriter('utf-8')(sys.stdout.buffer, 'strict')
|
||||
|
||||
for line in art_lines:
|
||||
def _safe_print(line):
|
||||
try:
|
||||
print(line, flush=True)
|
||||
except UnicodeEncodeError:
|
||||
print(line.encode('gbk', errors='ignore').decode('gbk'), flush=True)
|
||||
time.sleep(0.05)
|
||||
|
||||
# 完整开机动画(57 行 + 逐行 0.05s)默认不放,用 --logo 显式召唤
|
||||
if '--logo' in sys.argv or '--banner' in sys.argv:
|
||||
for line in art_lines:
|
||||
_safe_print(line)
|
||||
time.sleep(0.05)
|
||||
else:
|
||||
_safe_print(f"\n{Fore.CYAN} ▄▀▄ 小狸 Pro-CLI v{VERSION}{Style.RESET_ALL}")
|
||||
_safe_print(f"{Fore.CYAN} ▀▄▀ {Style.RESET_ALL}"
|
||||
f"{Fore.WHITE}AI 智能编程助手{Style.RESET_ALL}"
|
||||
f" {Fore.BLACK}{Style.BRIGHT}(--logo 看完整开机动画){Style.RESET_ALL}")
|
||||
|
||||
cli = AICLI()
|
||||
cli.run()
|
||||
@@ -376,8 +376,42 @@ class DisplayMixin:
|
||||
|
||||
# ── 帮助系统 ──
|
||||
|
||||
def show_quick_commands(self):
|
||||
"""常用命令速查表(启动时不再刷屏,改由 /help 展示)"""
|
||||
rows = [
|
||||
('/help [插件名]', '显示帮助信息'),
|
||||
('/quit', '退出程序'),
|
||||
('/tui', '切换到 TUI 图形模式'),
|
||||
('/engine list', '查看可用 AI 引擎'),
|
||||
('/engine switch <名>', '切换 AI 引擎'),
|
||||
('/fc [reset]', '查看/清除 FC 能力探测缓存'),
|
||||
('/safe', '切换安全模式 (普通→人工→无限制)'),
|
||||
('/diff', '切换 diff 显示模式 (弹窗/主终端)'),
|
||||
('/notify', '切换任务完成通知 (开/关)'),
|
||||
('/scheduler 或 /remind', '管理定时任务'),
|
||||
('/memory', '管理记忆系统 (日记/搜索/聊天记录)'),
|
||||
('/file.read <文件> [行数]', '直接读取文件内容'),
|
||||
('@文件路径', '自动读取文件内容并发送给 AI'),
|
||||
('@图片路径', '自动分析图片并发送描述给 AI'),
|
||||
('/image engines', '查看图像识别引擎'),
|
||||
('/chat save|list|open', '保存/查看/加载聊天记录'),
|
||||
('/remote', '查看远程连接帮助'),
|
||||
('/plugin', '管理插件市场 (安装/卸载/搜索)'),
|
||||
('/protect', '查看进程保护状态'),
|
||||
]
|
||||
width = max(len(cmd) for cmd, _ in rows)
|
||||
print(f"{Fore.GREEN}常用命令:{Style.RESET_ALL}")
|
||||
for cmd, desc in rows:
|
||||
print(f" {Fore.WHITE}{cmd.ljust(width)}{Style.RESET_ALL} {desc}")
|
||||
print()
|
||||
print(f"{Fore.GREEN}启动参数:{Style.RESET_ALL}")
|
||||
print(f" {Fore.WHITE}{'--verbose / -v'.ljust(width)}{Style.RESET_ALL} 打印引擎/插件/技能的逐条加载明细")
|
||||
print(f" {Fore.WHITE}{'--logo'.ljust(width)}{Style.RESET_ALL} 播放完整开机动画")
|
||||
print()
|
||||
|
||||
def show_help(self):
|
||||
"""显示帮助信息"""
|
||||
self.show_quick_commands()
|
||||
help_file_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "HELP.md")
|
||||
if os.path.exists(help_file_path):
|
||||
try:
|
||||
|
||||
+21
-5
@@ -76,13 +76,29 @@ def set_system_config(key, value):
|
||||
save_config(config)
|
||||
|
||||
|
||||
# 配置日志
|
||||
# 配置日志:文件留全量 INFO,控制台默认只报 WARNING 以上(--verbose 时放开到 INFO)
|
||||
try:
|
||||
from .verbose import is_verbose as _is_verbose
|
||||
except ImportError: # 被当作顶层模块单独导入时
|
||||
try:
|
||||
from verbose import is_verbose as _is_verbose
|
||||
except ImportError:
|
||||
def _is_verbose():
|
||||
return False
|
||||
|
||||
_file_handler = logging.FileHandler('xiaoli_cli.log', encoding='utf-8')
|
||||
_file_handler.setLevel(logging.INFO)
|
||||
_console_handler = logging.StreamHandler()
|
||||
_console_handler.setLevel(logging.INFO if _is_verbose() else logging.WARNING)
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler('xiaoli_cli.log', encoding='utf-8'),
|
||||
logging.StreamHandler() # 同时输出到控制台
|
||||
]
|
||||
handlers=[_file_handler, _console_handler]
|
||||
)
|
||||
|
||||
# 第三方库的心跳日志(httpx 每发一次请求就打一行)不上屏也不进日志文件
|
||||
for _noisy in ('httpx', 'httpcore', 'urllib3', 'requests', 'openai', 'PIL', 'markdown_it'):
|
||||
logging.getLogger(_noisy).setLevel(logging.WARNING)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -2,7 +2,9 @@
|
||||
Function Calling 工具格式转换
|
||||
将 MCP 插件定义转为 OpenAI FC 格式,收集所有可用工具
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import threading
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
|
||||
@@ -248,3 +250,81 @@ def _get_builtin_tools() -> List[Dict]:
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
# FC 能力探测缓存
|
||||
#
|
||||
# 部分渠道/模型(如讯飞星火经 one-api 转发的 xophunyuan* 系列)
|
||||
# 不支持 function calling,请求体里只要带上非空 tools 字段就会直接
|
||||
# 返回 500 RequestParamsError:Invalid Params。
|
||||
# 引擎捕获到这种错误后会去掉 tools 降级重试;本缓存把"该模型不支持 FC"
|
||||
# 这件事记下来,后续请求直接跳过 tools,避免每次冷启动都白撞一次 500。
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
|
||||
_FC_CACHE_FILE = os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".fc_support.json"
|
||||
)
|
||||
_fc_cache = None
|
||||
_fc_lock = threading.Lock()
|
||||
|
||||
|
||||
def _fc_key(base_url, model) -> str:
|
||||
"""缓存键:base_url + model(同一渠道下不同模型能力可能不同)"""
|
||||
return f"{(base_url or '').rstrip('/')}::{model or ''}"
|
||||
|
||||
|
||||
def _load_fc_cache() -> dict:
|
||||
global _fc_cache
|
||||
if _fc_cache is not None:
|
||||
return _fc_cache
|
||||
try:
|
||||
with open(_FC_CACHE_FILE, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
_fc_cache = data if isinstance(data, dict) else {}
|
||||
except (FileNotFoundError, json.JSONDecodeError, OSError, ValueError):
|
||||
_fc_cache = {}
|
||||
return _fc_cache
|
||||
|
||||
|
||||
def _save_fc_cache(cache: dict):
|
||||
try:
|
||||
with open(_FC_CACHE_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(cache, f, ensure_ascii=False, indent=2)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def supports_fc(base_url, model) -> bool:
|
||||
"""该 (base_url, model) 是否支持 FC。未探测过时默认 True(乐观,先试一次)"""
|
||||
return _load_fc_cache().get(_fc_key(base_url, model)) is not False
|
||||
|
||||
|
||||
def mark_fc_unsupported(base_url, model) -> bool:
|
||||
"""标记该 (base_url, model) 不支持 FC 并持久化。首次标记返回 True"""
|
||||
with _fc_lock:
|
||||
cache = _load_fc_cache()
|
||||
key = _fc_key(base_url, model)
|
||||
if cache.get(key) is False:
|
||||
return False
|
||||
cache[key] = False
|
||||
_save_fc_cache(cache)
|
||||
return True
|
||||
|
||||
|
||||
def reset_fc_cache(base_url=None, model=None) -> int:
|
||||
"""清除 FC 探测缓存。不传参数则全清,返回清除的条目数"""
|
||||
with _fc_lock:
|
||||
cache = _load_fc_cache()
|
||||
if base_url is None and model is None:
|
||||
count = len(cache)
|
||||
cache.clear()
|
||||
else:
|
||||
count = 1 if cache.pop(_fc_key(base_url, model), None) is not None else 0
|
||||
_save_fc_cache(cache)
|
||||
return count
|
||||
|
||||
|
||||
def get_fc_cache() -> dict:
|
||||
"""返回 FC 探测缓存副本(供 /fc 命令展示)"""
|
||||
return dict(_load_fc_cache())
|
||||
@@ -0,0 +1,24 @@
|
||||
"""
|
||||
启动详情开关
|
||||
|
||||
默认启动只报结论(加载了几个引擎/几个工具),不再逐条刷屏。
|
||||
需要看每个插件/技能的加载明细时,加 --verbose(或 -v),
|
||||
也可以设环境变量 XCLI_VERBOSE=1。
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
_TRUTHY = ('1', 'true', 'yes', 'on')
|
||||
|
||||
|
||||
def is_verbose() -> bool:
|
||||
"""是否输出启动明细"""
|
||||
if any(a in ('--verbose', '-v') for a in sys.argv[1:]):
|
||||
return True
|
||||
return str(os.environ.get('XCLI_VERBOSE', '')).lower() in _TRUTHY
|
||||
|
||||
|
||||
def vprint(*args, **kwargs):
|
||||
"""仅在 --verbose 时打印的 print"""
|
||||
if is_verbose():
|
||||
print(*args, **kwargs)
|
||||
Reference in new issue
Block a user