Files
xiaoli-cli/xcli_core/config.py
T
shuiyu67 3a86da7444 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)
2026-08-05 15:57:38 +08:00

105 lines
3.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import re
import json
import logging
from colorama import Fore, Style
# 配置文件路径
CONFIG_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config.json")
# <input> 特殊语法:配置模型时可用它交互式询问用户并获取输入
# <input> → 用默认提示询问
# <input:提示语> → 用自定义提示语询问
_INPUT_PATTERN = re.compile(r"<input(?::([^>]*))?>")
def resolve_input_value(value, field_name="", default_prompt=None):
"""解析配置值中的 <input> 特殊语法,交互式询问用户并返回其输入。
用法(写在 config.json 的字段值里):
"model": "<input>" → 启动时提示"请输入model: "
"model": "<input:请输入模型名>" → 启动时提示自定义文案
也可以夹在字符串中间: "prefix<input>suffix"
不含 <input> 的值原样返回;用户直接回车/中断时返回空串,由调用方兜底。
"""
if not isinstance(value, str) or "<input" not in value:
return value
def _ask(match):
label = (match.group(1) or "").strip()
prompt = label or default_prompt or (f"请输入{field_name}" if field_name else "请输入")
prompt = prompt.rstrip()
if prompt and not prompt.endswith((":", ":", "?", "?", "!", "!", " ")):
prompt += ": "
try:
answer = input(f"{Fore.CYAN}{prompt}{Style.RESET_ALL}").strip()
return answer
except (EOFError, KeyboardInterrupt):
return ""
return _INPUT_PATTERN.sub(_ask, value)
def load_config():
"""加载配置文件"""
try:
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
except FileNotFoundError:
return {"api": {"engines": {}}, "system": {}, "plugins": {}}
except json.JSONDecodeError:
return {"api": {"engines": {}}, "system": {}, "plugins": {}}
def save_config(config):
"""保存配置文件"""
try:
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
json.dump(config, f, ensure_ascii=False, indent=2)
except Exception as e:
print(f"保存配置失败: {e}")
def get_system_config(key, default=None):
"""获取系统配置"""
config = load_config()
return config.get("system", {}).get(key, default)
def set_system_config(key, value):
"""设置系统配置"""
config = load_config()
if "system" not in config:
config["system"] = {}
config["system"][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=[_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__)