4 Commits
Author SHA1 Message Date
shuiyu67 6f4fa5076f fix: 修复文件管理器跨平台路径串台(Linux/Mac 风格路径在 Windows 下被拼成 C:/home/user/...)
- 新增 _resolve_cross_platform_path:Windows 下将 /home/<用户>/…、/Users/<用户>/…、/root/… 映射到当前用户家目录对应位置
- file_manager 全部 10 个操作的路径解析改用该归一化函数
- 更新 CHANGELOG v8.0.2 条目(覆盖 8.0.2 发行)
2026-08-07 19:53:16 +08:00
shuiyu67 0aaca9c63d chore: 发布 v8.0.2 小修复版本
- 版本号统一更新至 v8.0.2(README 中英 badge / about.txt / 中央 VERSION / TUI banner / LSP·MCP clientInfo.version)
- CHANGELOG 新增 v8.0.2 条目,记录两个 hotfix:
  - c81914a 加载动画在非 TTY 环境不再污染输出流(isatty 守卫)
  - 1c52482 非 TTY 环境保留一句随机情话保人味(过滤 URL 行)
2026-08-07 19:37:54 +08:00
shuiyu67 1c52482497 fix: 非 TTY 环境保留一句随机情话(修复后保人味)
上一版加载动画修复在非 TTY(聊天前端/管道)只输出静态提示,
导致聊天界面完全看不到情话、没了"人味"。

- 非 TTY 分支改为:请求开始时输出一句随机情话(💭 前缀),
  只打一次、不跑逐帧 \r 动画,零残帧污染
- love.txt 读取时过滤 http/https 行,避免把反战标语/网址当情话甩出
- 真 TTY 逐帧情话+spinner 动画、TUI 模式均不受影响
2026-08-07 19:35:08 +08:00
shuiyu67 c81914ab87 fix: 加载动画在非 TTY 环境不再污染输出流
_generate_response_with_animation 的 show_animation 用 print("\r...")
逐帧输出情话+spinner,仅真终端能原地覆盖;被聊天前端/管道/
日志镜像捕获时每帧(10/s)变成一行垃圾混进对话显示。

- 加 sys.stdout.isatty() 守卫:非 TTY 只输出一行静态
  "AI 正在思考中…",不跑逐帧 \r 动画
- 两处清行动画行的 print("\r" + " "*80 + "\r") 也加 is_tty 守卫
- TUI 模式(tui_output_callback)本就跳过动画,不受影响

已验证:非 TTY 输出单行静态提示、TTY 仍正常逐帧动画。
2026-08-07 19:29:50 +08:00
11 changed files with 83 additions and 20 deletions

No files matched your search

+23
View File
@@ -4,6 +4,29 @@
--- ---
## v8.0.2 (2026-08-07) — 小修复版本
### 🐛 Bug 修复
- **加载动画在非 TTY 环境不再污染输出流**(提交 `c81914a`)
- `_generate_response_with_animation` 的 `show_animation` 用 `print("\r...")` 逐帧(约 10 帧/秒)输出 `love.txt` 情话 + spinner,仅真终端能原地覆盖;被聊天前端 / WebSocket 桥 / 管道 / 日志镜像捕获时每帧变成一行垃圾混进对话显示
- 加 `sys.stdout.isatty()` 守卫:非 TTY 只输出单行提示、不跑逐帧 `\r` 动画
- TUI 模式(`tui_output_callback`)本就跳过动画,不受影响
- **非 TTY 环境保留情话「人味」**(提交 `1c52482`)
- 上一版修复导致聊天界面完全看不到情话,改为:非 TTY 在请求开始输出一句随机情话(`💭` 前缀,只打一次、零残帧)
- `love.txt` 读取时过滤 `http/https` 行,避免把反战标语 / 网址当情话甩出
- 真 TTY 逐帧情话 + spinner 动画、TUI 模式均不受影响
- **修复文件管理器跨平台路径串台**(本次提交)
- 模型在 Windows 主机上常给出 Linux/Mac 风格绝对路径(如 `/home/user/Desktop`、`/Users/<谁>/Desktop`、`/root/Desktop`),`os.path.abspath` 在 Windows 下会错误地拼成 `C:\home\user\Desktop` 并报「路径不存在」
- 新增 `_resolve_cross_platform_path`:Windows 下把 `/home/<用户>/…`、`/Users/<用户>/…`、`/root/…` 统一映射到当前用户家目录对应位置(`~/Desktop` 等);Linux/Mac 下这些路径本身合法,原样保留
- `file_manager` 全部 10 个操作(list/read/write/append/copy/move/delete/search/info/mkdir)的路径解析均改用该归一化函数
### 🛠 工程
- 版本号统一更新至 v8.0.2(含 README 中英 badge、`about.txt`、`xcli_core/constants.py` 中央 `VERSION`、TUI banner、LSP/MCP `clientInfo.version`)
---
## v8.0.1 (2026-08-07) — Release ## v8.0.1 (2026-08-07) — Release
### ✨ 新增功能 ### ✨ 新增功能
+1 -1
View File
@@ -5,7 +5,7 @@
</p> </p>
<p align="center"> <p align="center">
<img src="https://img.shields.io/badge/version-8.0.1-blue" alt="version"> <img src="https://img.shields.io/badge/version-8.0.2-blue" alt="version">
<img src="https://img.shields.io/badge/python-3.10+-green" alt="python"> <img src="https://img.shields.io/badge/python-3.10+-green" alt="python">
<img src="https://img.shields.io/badge/license-MIT-orange" alt="license"> <img src="https://img.shields.io/badge/license-MIT-orange" alt="license">
<img src="https://img.shields.io/badge/tests-113%20passed-brightgreen" alt="tests"> <img src="https://img.shields.io/badge/tests-113%20passed-brightgreen" alt="tests">
+1 -1
View File
@@ -9,7 +9,7 @@
</p> </p>
<p align="center"> <p align="center">
<img src="https://img.shields.io/badge/version-8.0.1-blue" alt="version"> <img src="https://img.shields.io/badge/version-8.0.2-blue" alt="version">
<img src="https://img.shields.io/badge/python-3.10+-green" alt="python"> <img src="https://img.shields.io/badge/python-3.10+-green" alt="python">
<img src="https://img.shields.io/badge/license-MIT-orange" alt="license"> <img src="https://img.shields.io/badge/license-MIT-orange" alt="license">
<img src="https://img.shields.io/badge/tests-150+%20passed-brightgreen" alt="tests"> <img src="https://img.shields.io/badge/tests-150+%20passed-brightgreen" alt="tests">
+1 -1
View File
@@ -1,7 +1,7 @@
**产品标识** **产品标识**
- **产品名称**:小狸智能体 增强-终端(Xiaoli Pro-CLI) - **产品名称**:小狸智能体 增强-终端(Xiaoli Pro-CLI)
- **内部版本号**:20260801 - **内部版本号**:20260801
- **发布版本**:发行版8.0.1 - **发布版本**:发行版8.0.2
**技术规格** **技术规格**
- **主要语言**:简体中文(UTF-8) - **主要语言**:简体中文(UTF-8)
+1 -1
View File
@@ -268,7 +268,7 @@ class LspClient:
"workspace": {"workspaceFolders": True, "workspace": {"workspaceFolders": True,
"configuration": True}, "configuration": True},
}, },
"clientInfo": {"name": "xiaoli-cli", "version": "8.0.1"}, "clientInfo": {"name": "xiaoli-cli", "version": "8.0.2"},
}) })
if isinstance(result, dict): if isinstance(result, dict):
self.capabilities = result.get("capabilities", {}) or {} self.capabilities = result.get("capabilities", {}) or {}
+1 -1
View File
@@ -153,7 +153,7 @@ class McpClient:
result = self._send("initialize", { result = self._send("initialize", {
"protocolVersion": PROTOCOL_VERSION, "protocolVersion": PROTOCOL_VERSION,
"capabilities": {}, "capabilities": {},
"clientInfo": {"name": "xiaoli-cli", "version": "8.0.1"}, "clientInfo": {"name": "xiaoli-cli", "version": "8.0.2"},
}) })
# 通知 server 初始化完成(无响应,无需等待) # 通知 server 初始化完成(无响应,无需等待)
self._send("notifications/initialized", {}, is_notification=True) self._send("notifications/initialized", {}, is_notification=True)
+35 -10
View File
@@ -3,6 +3,7 @@
聚焦文件系统操作,代码编辑交给 code_editor 聚焦文件系统操作,代码编辑交给 code_editor
""" """
import os import os
import re
import json import json
import shutil import shutil
import sys import sys
@@ -10,6 +11,30 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import List from typing import List
# ── 跨平台路径归一化 ──────────────────────────────────────────────
# 模型常给出 Linux/Mac 风格绝对路径(如 /home/<user>/Desktop、
# /Users/<user>/Desktop、/root/Desktop)。在 Windows 下 os.path.abspath 会
# 错误地拼成 C:\home\user\Desktop。这里把这类路径映射到当前用户家目录
# 对应位置;Linux/Mac 下这些路径本身合法,原样保留。
_HOME_OR_USER_RE = re.compile(r'^/(?:home|Users)/[^/]+(/.*)?$')
_ROOT_RE = re.compile(r'^/root(/.*)?$')
def _resolve_cross_platform_path(path: str) -> str:
"""把模型给出的路径归一化到当前平台后再转绝对路径。"""
if os.name == 'nt' and path:
p = path.replace('\\', '/')
m = _HOME_OR_USER_RE.match(p)
if m:
rest = (m.group(1) or '').lstrip('/')
return os.path.join(os.path.expanduser('~'), rest)
m = _ROOT_RE.match(p)
if m:
rest = (m.group(1) or '').lstrip('/')
return os.path.join(os.path.expanduser('~'), rest)
return os.path.abspath(path)
# 添加项目根目录到 path # 添加项目根目录到 path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from xcli_core.tool_result import ToolResult, ErrorCode from xcli_core.tool_result import ToolResult, ErrorCode
@@ -158,7 +183,7 @@ class Liugin:
path = args[i] path = args[i]
i += 1 i += 1
path = os.path.abspath(path) path = _resolve_cross_platform_path(path)
if not os.path.isdir(path): if not os.path.isdir(path):
return ToolResult.not_found(f"目录: {path}") return ToolResult.not_found(f"目录: {path}")
@@ -193,7 +218,7 @@ class Liugin:
def _op_read(self, args: List[str]): def _op_read(self, args: List[str]):
if not args: if not args:
return ToolResult.missing("文件路径") return ToolResult.missing("文件路径")
file_path = os.path.abspath(args[0]) file_path = _resolve_cross_platform_path(args[0])
max_lines = None max_lines = None
i = 1 i = 1
while i < len(args): while i < len(args):
@@ -235,7 +260,7 @@ class Liugin:
def _op_write(self, args: List[str]): def _op_write(self, args: List[str]):
if len(args) < 2: if len(args) < 2:
return ToolResult.fail("格式: write <文件> -c <内容>", ErrorCode.MISSING_ARGS) return ToolResult.fail("格式: write <文件> -c <内容>", ErrorCode.MISSING_ARGS)
fp = os.path.abspath(args[0]) fp = _resolve_cross_platform_path(args[0])
content = None content = None
i = 1 i = 1
while i < len(args): while i < len(args):
@@ -256,7 +281,7 @@ class Liugin:
def _op_append(self, args: List[str]): def _op_append(self, args: List[str]):
if len(args) < 2: if len(args) < 2:
return ToolResult.fail("格式: append <文件> -c <内容>", ErrorCode.MISSING_ARGS) return ToolResult.fail("格式: append <文件> -c <内容>", ErrorCode.MISSING_ARGS)
fp = os.path.abspath(args[0]) fp = _resolve_cross_platform_path(args[0])
content = None content = None
i = 1 i = 1
while i < len(args): while i < len(args):
@@ -278,7 +303,7 @@ class Liugin:
def _op_copy(self, args: List[str]): def _op_copy(self, args: List[str]):
if len(args) < 2: if len(args) < 2:
return ToolResult.missing("源和目标路径") return ToolResult.missing("源和目标路径")
src, dst = os.path.abspath(args[0]), os.path.abspath(args[1]) src, dst = _resolve_cross_platform_path(args[0]), _resolve_cross_platform_path(args[1])
if not os.path.exists(src): if not os.path.exists(src):
return ToolResult.not_found(f"源: {src}") return ToolResult.not_found(f"源: {src}")
os.makedirs(os.path.dirname(dst) or ".", exist_ok=True) os.makedirs(os.path.dirname(dst) or ".", exist_ok=True)
@@ -291,7 +316,7 @@ class Liugin:
def _op_move(self, args: List[str]): def _op_move(self, args: List[str]):
if len(args) < 2: if len(args) < 2:
return ToolResult.missing("源和目标路径") return ToolResult.missing("源和目标路径")
src, dst = os.path.abspath(args[0]), os.path.abspath(args[1]) src, dst = _resolve_cross_platform_path(args[0]), _resolve_cross_platform_path(args[1])
if not os.path.exists(src): if not os.path.exists(src):
return ToolResult.not_found(f"源: {src}") return ToolResult.not_found(f"源: {src}")
os.makedirs(os.path.dirname(dst) or ".", exist_ok=True) os.makedirs(os.path.dirname(dst) or ".", exist_ok=True)
@@ -301,7 +326,7 @@ class Liugin:
def _op_delete(self, args: List[str]): def _op_delete(self, args: List[str]):
if not args: if not args:
return ToolResult.missing("路径") return ToolResult.missing("路径")
path = os.path.abspath(args[0]) path = _resolve_cross_platform_path(args[0])
recursive = "-r" in args recursive = "-r" in args
if not os.path.exists(path): if not os.path.exists(path):
return ToolResult.not_found(f"路径: {path}") return ToolResult.not_found(f"路径: {path}")
@@ -321,7 +346,7 @@ class Liugin:
def _op_search(self, args: List[str]): def _op_search(self, args: List[str]):
if not args: if not args:
return ToolResult.missing("搜索目录") return ToolResult.missing("搜索目录")
directory = os.path.abspath(args[0]) directory = _resolve_cross_platform_path(args[0])
pattern, content_search = None, None pattern, content_search = None, None
i = 1 i = 1
while i < len(args): while i < len(args):
@@ -363,7 +388,7 @@ class Liugin:
def _op_info(self, args: List[str]): def _op_info(self, args: List[str]):
if not args: if not args:
return ToolResult.missing("路径") return ToolResult.missing("路径")
path = os.path.abspath(args[0]) path = _resolve_cross_platform_path(args[0])
if not os.path.exists(path): if not os.path.exists(path):
return ToolResult.not_found(f"路径: {path}") return ToolResult.not_found(f"路径: {path}")
@@ -383,7 +408,7 @@ class Liugin:
def _op_mkdir(self, args: List[str]): def _op_mkdir(self, args: List[str]):
if not args: if not args:
return ToolResult.missing("目录路径") return ToolResult.missing("目录路径")
path = os.path.abspath(args[0]) path = _resolve_cross_platform_path(args[0])
if os.path.exists(path): if os.path.exists(path):
return ToolResult.fail(f"已存在: {path}", ErrorCode.INVALID_ARGS) return ToolResult.fail(f"已存在: {path}", ErrorCode.INVALID_ARGS)
os.makedirs(path, exist_ok=True) os.makedirs(path, exist_ok=True)
+16 -1
View File
@@ -963,12 +963,18 @@ multi 操作支持一次修改多处:
response_ready = threading.Event() response_ready = threading.Event()
response_result = [None] # 用列表存储,方便线程内修改 response_result = [None] # 用列表存储,方便线程内修改
# 是否真实交互终端:非 TTY(管道 / 日志镜像 / 聊天前端捕获 stdout)时,
# \r 无法原地覆盖,逐帧动画会污染输出流,必须禁用。
is_tty = sys.stdout.isatty()
love_sentences = [] love_sentences = []
love_file_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), LOVE_FILE_PATH) love_file_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), LOVE_FILE_PATH)
if os.path.exists(love_file_path): if os.path.exists(love_file_path):
try: try:
with open(love_file_path, 'r', encoding='utf-8') as f: with open(love_file_path, 'r', encoding='utf-8') as f:
love_sentences = [line.strip() for line in f.readlines() if line.strip()] love_sentences = [line.strip() for line in f.readlines()
if line.strip()
and not line.strip().lower().startswith(('http://', 'https://'))]
except Exception: except Exception:
pass pass
if not love_sentences: if not love_sentences:
@@ -1015,6 +1021,13 @@ multi 操作支持一次修改多处:
# ── 动画线程 ── # ── 动画线程 ──
def show_animation(): def show_animation():
# 非 TTY:不跑逐帧 \r 动画(避免残帧进入捕获流),但保留一句随机情话保人味
if not is_tty:
try:
print(f"💭 {random.choice(love_sentences)}", flush=True)
except Exception:
pass
return
last_change_time = time.time() last_change_time = time.time()
current_sentence = random.choice(love_sentences) current_sentence = random.choice(love_sentences)
while animation_running.is_set(): while animation_running.is_set():
@@ -1058,6 +1071,7 @@ multi 操作支持一次修改多处:
# ESC 被按下 → 取消 # ESC 被按下 → 取消
if esc_pressed.is_set(): if esc_pressed.is_set():
# 清除动画行 # 清除动画行
if is_tty:
print("\r" + " " * 80 + "\r", end="", flush=True) print("\r" + " " * 80 + "\r", end="", flush=True)
print(f"\n{Fore.YELLOW}⚡ AI 请求已取消 (ESC){Style.RESET_ALL}") print(f"\n{Fore.YELLOW}⚡ AI 请求已取消 (ESC){Style.RESET_ALL}")
return "[AI请求已取消]" return "[AI请求已取消]"
@@ -1066,6 +1080,7 @@ multi 操作支持一次修改多处:
response = response_result[0] response = response_result[0]
if response: if response:
# 清除动画行后再输出 # 清除动画行后再输出
if is_tty:
print("\r" + " " * 80 + "\r", end="", flush=True) print("\r" + " " * 80 + "\r", end="", flush=True)
self._typeprint(response, Fore.CYAN) self._typeprint(response, Fore.CYAN)
+1 -1
View File
@@ -7,7 +7,7 @@ LOVE_FILE_PATH = "love.txt"
DEFAULT_MAX_HISTORY = 999999 # 无限制对话历史 DEFAULT_MAX_HISTORY = 999999 # 无限制对话历史
# 版本号(v8.0 基于 v5.4.1 重新立项) # 版本号(v8.0 基于 v5.4.1 重新立项)
VERSION = "8.0.1" VERSION = "8.0.2"
VERSION_NAME = "小狸 Pro-CLI" VERSION_NAME = "小狸 Pro-CLI"
import os import os
+1 -1
View File
@@ -28,7 +28,7 @@ class RichTUI:
self.cli = cli self.cli = cli
# record=True 是关键:渲染过程被记录,可随时导出文本/SVG/HTML 自验 # record=True 是关键:渲染过程被记录,可随时导出文本/SVG/HTML 自验
self.console = Console(record=True, width=100) self.console = Console(record=True, width=100)
self.version = "8.0.1" self.version = "8.0.2"
self.engine = "" self.engine = ""
self.model = "" self.model = ""
self.mode = "普通" self.mode = "普通"
+2 -2
View File
@@ -667,7 +667,7 @@ if TEXTUAL_AVAILABLE:
"", "",
" ╔══════════════════════════════════════════════╗", " ╔══════════════════════════════════════════════╗",
" ║ ║", " ║ ║",
" ║ 小狸 Pro-CLI v8.0.1 ║", " ║ 小狸 Pro-CLI v8.0.2 ║",
" ║ 智能编程助手 · 动画增强版 ║", " ║ 智能编程助手 · 动画增强版 ║",
" ║ ║", " ║ ║",
" ╚══════════════════════════════════════════════╝", " ╚══════════════════════════════════════════════╝",
@@ -711,7 +711,7 @@ if TEXTUAL_AVAILABLE:
("", "msg-dim"), ("", "msg-dim"),
(" ╔══════════════════════════════════════════════╗", "msg-welcome"), (" ╔══════════════════════════════════════════════╗", "msg-welcome"),
(" ║ ║", "msg-welcome"), (" ║ ║", "msg-welcome"),
(" ║ 小狸 Pro-CLI v8.0.1 ║", "msg-welcome"), (" ║ 小狸 Pro-CLI v8.0.2 ║", "msg-welcome"),
(" ║ 智能编程助手 · 动画增强版 ║", "msg-welcome"), (" ║ 智能编程助手 · 动画增强版 ║", "msg-welcome"),
(" ║ ║", "msg-welcome"), (" ║ ║", "msg-welcome"),
(" ╚══════════════════════════════════════════════╝", "msg-welcome"), (" ╚══════════════════════════════════════════════╝", "msg-welcome"),