Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f4fa5076f | ||
|
|
0aaca9c63d | ||
|
|
1c52482497 | ||
|
|
c81914ab87 |
No files matched your search
@@ -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
@@ -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">
|
||||||
|
|||||||
@@ -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,7 +1,7 @@
|
|||||||
**产品标识**
|
**产品标识**
|
||||||
- **产品名称**:小狸智能体 增强-终端(Xiaoli Pro-CLI)
|
- **产品名称**:小狸智能体 增强-终端(Xiaoli Pro-CLI)
|
||||||
- **内部版本号**:20260801
|
- **内部版本号**:20260801
|
||||||
- **发布版本**:发行版8.0.1
|
- **发布版本**:发行版8.0.2
|
||||||
|
|
||||||
**技术规格**
|
**技术规格**
|
||||||
- **主要语言**:简体中文(UTF-8)
|
- **主要语言**:简体中文(UTF-8)
|
||||||
|
|||||||
+1
-1
@@ -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
@@ -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
@@ -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)
|
||||||
|
|||||||
+18
-3
@@ -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,7 +1071,8 @@ multi 操作支持一次修改多处:
|
|||||||
# ESC 被按下 → 取消
|
# ESC 被按下 → 取消
|
||||||
if esc_pressed.is_set():
|
if esc_pressed.is_set():
|
||||||
# 清除动画行
|
# 清除动画行
|
||||||
print("\r" + " " * 80 + "\r", end="", flush=True)
|
if is_tty:
|
||||||
|
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,7 +1080,8 @@ multi 操作支持一次修改多处:
|
|||||||
response = response_result[0]
|
response = response_result[0]
|
||||||
if response:
|
if response:
|
||||||
# 清除动画行后再输出
|
# 清除动画行后再输出
|
||||||
print("\r" + " " * 80 + "\r", end="", flush=True)
|
if is_tty:
|
||||||
|
print("\r" + " " * 80 + "\r", end="", flush=True)
|
||||||
self._typeprint(response, Fore.CYAN)
|
self._typeprint(response, Fore.CYAN)
|
||||||
|
|
||||||
if response and self.shared_conversation_history is not None:
|
if response and self.shared_conversation_history is not None:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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"),
|
||||||
|
|||||||
Reference in new issue
Block a user